package services import ( "encoding/json" "fmt" "strings" "time" "agent-desk/internal/models" "agent-desk/internal/pkg/dto" "agent-desk/internal/pkg/dto/request" "agent-desk/internal/pkg/dto/response" "agent-desk/internal/pkg/enums" "agent-desk/internal/pkg/errorsx" "agent-desk/internal/pkg/i18nx" "agent-desk/internal/pkg/utils" "agent-desk/internal/repositories" "github.com/mlogclub/simple/sqls" ) var SystemConfigService = newSystemConfigService() func newSystemConfigService() *systemConfigService { return &systemConfigService{} } type systemConfigService struct { } const ( systemConfigGroupSupportCenter = "support" systemConfigKeySupportNavMenu = "navigationMenu" // The branding group is what lets an operator rename a deployment without // editing configuration and restarting. It is deliberately separate from the // support group: the name is read on every page load by both the visitor // portal and the dashboard, while the navigation menu is a portal concern. systemConfigGroupBranding = "branding" systemConfigKeyBrandingCompanyName = "companyName" systemConfigKeyBrandingCompanyLogo = "companyLogoUrl" ) type configValidator interface { Validate(raw json.RawMessage) (json.RawMessage, []response.ConfigFieldError, error) } type systemConfigDefinition struct { GroupCode string Key string TitleKey string DescriptionKey string DefaultValue any Validator configValidator } type SystemConfigValidationError struct { errors []response.ConfigFieldError } func (e *SystemConfigValidationError) Error() string { return e.Message(i18nx.DefaultLocale) } func (e *SystemConfigValidationError) Message(locale string) string { if len(e.errors) == 0 { return i18nx.Getf(locale, "error.supportConfig.validationFailed") } fieldErrorMessage := e.errors[0].Message if e.errors[0].MessageKey != "" { fieldErrorMessage = i18nx.Getf(locale, e.errors[0].MessageKey) } return fmt.Sprintf("%s: %s", i18nx.Getf(locale, "error.supportConfig.validationFailed"), fieldErrorMessage) } func (e *SystemConfigValidationError) FieldErrors() []response.ConfigFieldError { if e == nil { return nil } return e.errors } func (e *SystemConfigValidationError) FieldErrorsLocale(locale string) []response.ConfigFieldError { if e == nil { return nil } return localizeConfigFieldErrors(e.errors, locale) } var systemConfigDefinitions = map[string]map[string]systemConfigDefinition{ systemConfigGroupSupportCenter: { systemConfigKeySupportNavMenu: { GroupCode: systemConfigGroupSupportCenter, Key: systemConfigKeySupportNavMenu, TitleKey: "systemConfig.support.navigationMenu.title", DescriptionKey: "systemConfig.support.navigationMenu.description", DefaultValue: defaultSupportNavigationMenu(), Validator: supportNavigationMenuValidator{}, }, }, systemConfigGroupBranding: { systemConfigKeyBrandingCompanyName: { GroupCode: systemConfigGroupBranding, Key: systemConfigKeyBrandingCompanyName, TitleKey: "systemConfig.branding.companyName.title", DescriptionKey: "systemConfig.branding.companyName.description", DefaultValue: "", Validator: brandingCompanyNameValidator{}, }, systemConfigKeyBrandingCompanyLogo: { GroupCode: systemConfigGroupBranding, Key: systemConfigKeyBrandingCompanyLogo, TitleKey: "systemConfig.branding.companyLogoUrl.title", DescriptionKey: "systemConfig.branding.companyLogoUrl.description", DefaultValue: "", Validator: brandingCompanyLogoURLValidator{}, }, }, } func (s *systemConfigService) Get(id int64) *models.SystemConfig { return repositories.SystemConfigRepository.Get(sqls.DB(), id) } func (s *systemConfigService) Find(cnd *sqls.Cnd) []models.SystemConfig { return repositories.SystemConfigRepository.Find(sqls.DB(), cnd) } func (s *systemConfigService) FindByGroupCode(groupCode string) []models.SystemConfig { return repositories.SystemConfigRepository.FindByGroupCode(sqls.DB(), groupCode) } func (s *systemConfigService) GetByGroupAndKey(groupCode, key string) *models.SystemConfig { return repositories.SystemConfigRepository.FindByGroupAndKey(sqls.DB(), groupCode, key) } func (s *systemConfigService) FindOne(cnd *sqls.Cnd) *models.SystemConfig { return repositories.SystemConfigRepository.FindOne(sqls.DB(), cnd) } func (s *systemConfigService) FindPageByCnd(cnd *sqls.Cnd) (list []models.SystemConfig, paging *sqls.Paging) { return repositories.SystemConfigRepository.FindPageByCnd(sqls.DB(), cnd) } func (s *systemConfigService) GetPublicSupportConfig() response.PublicSupportConfigResponse { return response.PublicSupportConfigResponse{ NavigationMenu: s.enabledSupportNavigationMenu(), } } func (s *systemConfigService) GetDashboardSupportConfig() response.DashboardSupportConfigResponse { return response.DashboardSupportConfigResponse{ NavigationMenu: s.supportNavigationMenu(), } } func (s *systemConfigService) SaveSupportConfig(payload map[string]json.RawMessage, operator *dto.AuthPrincipal) (response.DashboardSupportConfigResponse, error) { if err := s.SaveGroupConfig(systemConfigGroupSupportCenter, payload, operator); err != nil { return response.DashboardSupportConfigResponse{}, err } return s.GetDashboardSupportConfig(), nil } func (s *systemConfigService) SaveGroupConfig(groupCode string, payload map[string]json.RawMessage, operator *dto.AuthPrincipal) error { definitions := systemConfigDefinitions[groupCode] if len(definitions) == 0 { return errorsx.InvalidParamI18n("error.supportConfig.groupUnsupported") } if len(payload) == 0 { return errorsx.InvalidParamI18n("error.supportConfig.emptyPayload") } values := make(map[string]json.RawMessage, len(payload)) for key, raw := range payload { definition, ok := definitions[key] if !ok { return errorsx.InvalidParamI18n("error.supportConfig.keyUnsupported", key) } normalized := raw if definition.Validator != nil { next, fieldErrors, err := definition.Validator.Validate(raw) if err != nil { return err } if len(fieldErrors) > 0 { return &SystemConfigValidationError{errors: fieldErrors} } normalized = next } values[key] = normalized } return sqls.WithTransaction(func(ctx *sqls.TxContext) error { now := time.Now() auditFields := utils.BuildAuditFields(operator) for key, raw := range values { definition := definitions[key] existing := repositories.SystemConfigRepository.FindByGroupAndKey(ctx.Tx, groupCode, key) if existing == nil { item := &models.SystemConfig{ ConfigKey: key, ConfigValue: string(raw), GroupCode: groupCode, Title: definition.Title(), Description: definition.Description(), Status: enums.StatusOk, AuditFields: auditFields, } if err := repositories.SystemConfigRepository.Create(ctx.Tx, item); err != nil { return err } continue } columns := map[string]any{ "config_value": string(raw), "group_code": groupCode, "title": definition.Title(), "description": definition.Description(), "status": enums.StatusOk, "updated_at": now, "update_user_id": auditFields.UpdateUserID, "update_user_name": auditFields.UpdateUserName, } if err := repositories.SystemConfigRepository.Updates(ctx.Tx, existing.ID, columns); err != nil { return err } } return nil }) } func (s *systemConfigService) UpdateSupportNavigationMenu(items []request.SupportNavigationMenuItemRequest, operator *dto.AuthPrincipal) ([]response.SupportNavigationMenuItemResponse, error) { raw, err := json.Marshal(items) if err != nil { return nil, err } _, err = s.SaveSupportConfig(map[string]json.RawMessage{ systemConfigKeySupportNavMenu: raw, }, operator) if err != nil { return nil, err } return s.supportNavigationMenu(), nil } func (s *systemConfigService) enabledSupportNavigationMenu() []response.SupportNavigationMenuItemResponse { items := s.supportNavigationMenu() enabled := make([]response.SupportNavigationMenuItemResponse, 0, len(items)) for _, item := range items { if item.Visible { item.Children = visibleSupportNavigationChildren(item.Children) enabled = append(enabled, item) } } if len(enabled) == 0 { return defaultSupportNavigationMenu() } return enabled } func (s *systemConfigService) supportNavigationMenu() []response.SupportNavigationMenuItemResponse { item := repositories.SystemConfigRepository.FindByGroupAndKey(sqls.DB(), systemConfigGroupSupportCenter, systemConfigKeySupportNavMenu) if item == nil || strings.TrimSpace(item.ConfigValue) == "" { return defaultSupportNavigationMenu() } var list []response.SupportNavigationMenuItemResponse if err := json.Unmarshal([]byte(item.ConfigValue), &list); err != nil { return defaultSupportNavigationMenu() } if len(list) == 0 { return defaultSupportNavigationMenu() } return sortSupportNavigationMenu(list) } func sortSupportNavigationMenu(items []response.SupportNavigationMenuItemResponse) []response.SupportNavigationMenuItemResponse { ret := append([]response.SupportNavigationMenuItemResponse(nil), items...) for i := 0; i < len(ret)-1; i++ { for j := i + 1; j < len(ret); j++ { if ret[j].SortNo < ret[i].SortNo || (ret[j].SortNo == ret[i].SortNo && ret[j].ID < ret[i].ID) { ret[i], ret[j] = ret[j], ret[i] } } } return ret } func visibleSupportNavigationChildren(items []response.SupportNavigationMenuItemResponse) []response.SupportNavigationMenuItemResponse { if len(items) == 0 { return nil } visible := make([]response.SupportNavigationMenuItemResponse, 0, len(items)) for _, item := range sortSupportNavigationMenu(items) { if item.Visible { visible = append(visible, item) } } return visible } func (d systemConfigDefinition) Title() string { return i18nx.Get(d.TitleKey) } func (d systemConfigDefinition) Description() string { return i18nx.Get(d.DescriptionKey) }