package service import ( "context" "encoding/json" "errors" "fmt" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" applogger "github.com/gochat/gochat/pkg/logger" ) // DashboardAppService implements business logic for DashboardApp CRUD. // Reference: Chatwoot app/controllers/api/v1/accounts/dashboard_apps_controller.rb type DashboardAppService struct { repo *repository.DashboardAppRepo } func NewDashboardAppService(repo *repository.DashboardAppRepo) *DashboardAppService { return &DashboardAppService{repo: repo} } // CreateDashboardAppRequest is the DTO for creating a dashboard app. // Chatwoot permits: :title, content: [:url, :type] // GoChat extensions: description, icon, url, kind, active type CreateDashboardAppRequest struct { Title string `json:"title" validate:"required,min=2"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` URL string `json:"url,omitempty"` Kind string `json:"kind,omitempty"` // frame, link Content json.RawMessage `json:"content,omitempty"` Active *bool `json:"active,omitempty"` // pointer so nil = default true } // DashboardAppCreateWrapper wraps the create request under "dashboard_app" key. // Chatwoot: params.require(:dashboard_app).permit(:title, content: [:url, :type]) type DashboardAppCreateWrapper struct { DashboardApp CreateDashboardAppRequest `json:"dashboard_app"` } // UpdateDashboardAppRequest is the DTO for updating a dashboard app. type UpdateDashboardAppRequest struct { Title string `json:"title,omitempty"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` URL string `json:"url,omitempty"` Kind string `json:"kind,omitempty"` Content json.RawMessage `json:"content,omitempty"` Active *bool `json:"active,omitempty"` } // DashboardAppUpdateWrapper wraps the update request under "dashboard_app" key. // Chatwoot: params.require(:dashboard_app).permit(:title, content: [:url, :type]) type DashboardAppUpdateWrapper struct { DashboardApp UpdateDashboardAppRequest `json:"dashboard_app"` } func (s *DashboardAppService) Create(ctx context.Context, accountID uint, userID *uint, req *CreateDashboardAppRequest) (*model.DashboardApp, error) { // Validate content if provided if len(req.Content) > 0 && string(req.Content) != "" { if err := model.ValidateContent(req.Content); err != nil { return nil, err } } content := req.Content if len(content) == 0 || string(content) == "" { content = json.RawMessage(`[]`) } active := model.BoolPtr(true) if req.Active != nil { active = req.Active } kind := req.Kind if kind == "" { kind = "frame" } app := &model.DashboardApp{ AccountID: accountID, UserID: userID, Title: req.Title, Description: req.Description, Icon: req.Icon, URL: req.URL, Kind: kind, Content: content, Active: active, } if err := s.repo.Create(ctx, app); err != nil { applogger.L().Errorf("Create dashboard app: %v", err) return nil, err } return app, nil } func (s *DashboardAppService) GetByID(ctx context.Context, id uint) (*model.DashboardApp, error) { app, err := s.repo.GetByID(ctx, id) if err != nil { applogger.L().Errorf("Get dashboard app: %v", err) return nil, err } return app, nil } func (s *DashboardAppService) Update(ctx context.Context, id uint, req *UpdateDashboardAppRequest) (*model.DashboardApp, error) { app, err := s.repo.GetByID(ctx, id) if err != nil { applogger.L().Errorf("Get dashboard app for update: %v", err) return nil, err } // Validate content if provided if len(req.Content) > 0 && string(req.Content) != "" { if err := model.ValidateContent(req.Content); err != nil { return nil, err } } if req.Title != "" { app.Title = req.Title } if req.Description != "" { app.Description = req.Description } if req.Icon != "" { app.Icon = req.Icon } if req.URL != "" { app.URL = req.URL } if req.Kind != "" { app.Kind = req.Kind } if len(req.Content) > 0 && string(req.Content) != "" { app.Content = req.Content } if req.Active != nil { app.Active = req.Active } if err := s.repo.Update(ctx, app); err != nil { applogger.L().Errorf("Update dashboard app: %v", err) return nil, err } return app, nil } func (s *DashboardAppService) Delete(ctx context.Context, id uint) error { app, err := s.repo.GetByID(ctx, id) if err != nil { applogger.L().Errorf("Delete dashboard app: find failed: %v", err) return err } if app == nil { return fmt.Errorf("dashboard app not found") } if err := s.repo.Delete(ctx, id); err != nil { applogger.L().Errorf("Delete dashboard app: %v", err) return err } return nil } // ListByAccount returns all dashboard apps for an account. // Chatwoot: Current.account.dashboard_apps — no pagination func (s *DashboardAppService) ListByAccount(ctx context.Context, accountID uint) ([]model.DashboardApp, error) { apps, err := s.repo.FindAllByAccountID(ctx, accountID) if err != nil { applogger.L().Errorf("List dashboard apps by account: %v", err) return nil, err } return apps, nil } // ListByAccountPaginated returns paginated dashboard apps for an account. func (s *DashboardAppService) ListByAccountPaginated(ctx context.Context, accountID uint, page, perPage int) ([]model.DashboardApp, int64, error) { offset := (page - 1) * perPage apps, count, err := s.repo.FindByAccountID(ctx, accountID, offset, perPage) if err != nil { applogger.L().Errorf("List dashboard apps by account: %v", err) return nil, 0, err } return apps, count, nil } // ListActiveByAccount returns only active dashboard apps for an account. // ListActiveByAccount returns all active dashboard apps for an account (no pagination). // GoChat extension — Chatwoot has no separate active list endpoint. func (s *DashboardAppService) ListActiveByAccount(ctx context.Context, accountID uint) ([]model.DashboardApp, error) { apps, err := s.repo.FindAllActiveByAccountID(ctx, accountID) if err != nil { applogger.L().Errorf("List active dashboard apps by account: %v", err) return nil, err } return apps, nil } // Search searches dashboard apps by title keyword within an account. // GoChat extension — Chatwoot has no search endpoint. func (s *DashboardAppService) Search(ctx context.Context, accountID uint, query string) ([]model.DashboardApp, error) { apps, err := s.repo.SearchAll(ctx, accountID, query) if err != nil { applogger.L().Errorf("Search dashboard apps: %v", err) return nil, err } return apps, nil } // SearchPaginated returns paginated search results. func (s *DashboardAppService) SearchPaginated(ctx context.Context, accountID uint, query string, page, perPage int) ([]model.DashboardApp, int64, error) { offset := (page - 1) * perPage apps, count, err := s.repo.Search(ctx, accountID, query, offset, perPage) if err != nil { applogger.L().Errorf("Search dashboard apps: %v", err) return nil, 0, err } return apps, count, nil } // ========== Widget Management ========== // AddWidgetRequest is the DTO for adding a widget to a DashboardApp. type AddWidgetRequest struct { Type string `json:"type" validate:"required,eq=frame"` URL string `json:"url" validate:"required,url"` ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` } // UpdateWidgetRequest is the DTO for updating a widget in a DashboardApp. type UpdateWidgetRequest struct { Type string `json:"type,omitempty"` URL string `json:"url,omitempty"` ID string `json:"id,omitempty"` Name string `json:"name,omitempty"` } // AddWidget adds a new widget to the DashboardApp's content array. func (s *DashboardAppService) AddWidget(ctx context.Context, dashboardAppID uint, req *AddWidgetRequest) (*model.DashboardApp, error) { app, err := s.repo.GetByID(ctx, dashboardAppID) if err != nil { return nil, err } widget := model.DashboardWidget{ Type: req.Type, URL: req.URL, ID: req.ID, Name: req.Name, } // Validate the widget if widget.Type != "frame" { return nil, errors.New("widget type must be 'frame'") } if !model.IsValidHTTPURL(widget.URL) { return nil, errors.New("widget url must be http/https URI") } // Parse existing content var widgets []model.DashboardWidget if len(app.Content) > 0 && string(app.Content) != "" && string(app.Content) != "[]" { if err := json.Unmarshal(app.Content, &widgets); err != nil { return nil, err } } widgets = append(widgets, widget) newContent, err := json.Marshal(widgets) if err != nil { return nil, err } app.Content = json.RawMessage(newContent) if err := s.repo.Update(ctx, app); err != nil { return nil, err } return app, nil } // UpdateWidget updates a specific widget (by index) in the DashboardApp's content array. func (s *DashboardAppService) UpdateWidget(ctx context.Context, dashboardAppID uint, widgetIndex int, req *UpdateWidgetRequest) (*model.DashboardApp, error) { app, err := s.repo.GetByID(ctx, dashboardAppID) if err != nil { return nil, err } var widgets []model.DashboardWidget if len(app.Content) > 0 && string(app.Content) != "" && string(app.Content) != "[]" { if err := json.Unmarshal(app.Content, &widgets); err != nil { return nil, err } } if widgetIndex < 0 || widgetIndex >= len(widgets) { return nil, errors.New("widget index out of range") } w := widgets[widgetIndex] if req.Type != "" { if req.Type != "frame" { return nil, errors.New("widget type must be 'frame'") } w.Type = req.Type } if req.URL != "" { if !model.IsValidHTTPURL(req.URL) { return nil, errors.New("widget url must be http/https URI") } w.URL = req.URL } if req.ID != "" { w.ID = req.ID } if req.Name != "" { w.Name = req.Name } widgets[widgetIndex] = w newContent, err := json.Marshal(widgets) if err != nil { return nil, err } app.Content = json.RawMessage(newContent) if err := s.repo.Update(ctx, app); err != nil { return nil, err } return app, nil } // RemoveWidget removes a widget (by index) from the DashboardApp's content array. func (s *DashboardAppService) RemoveWidget(ctx context.Context, dashboardAppID uint, widgetIndex int) (*model.DashboardApp, error) { app, err := s.repo.GetByID(ctx, dashboardAppID) if err != nil { return nil, err } var widgets []model.DashboardWidget if len(app.Content) > 0 && string(app.Content) != "" && string(app.Content) != "[]" { if err := json.Unmarshal(app.Content, &widgets); err != nil { return nil, err } } if widgetIndex < 0 || widgetIndex >= len(widgets) { return nil, errors.New("widget index out of range") } widgets = append(widgets[:widgetIndex], widgets[widgetIndex+1:]...) newContent, err := json.Marshal(widgets) if err != nil { return nil, err } app.Content = json.RawMessage(newContent) if err := s.repo.Update(ctx, app); err != nil { return nil, err } return app, nil } // GetWidgets returns all widgets from a DashboardApp's content array. func (s *DashboardAppService) GetWidgets(ctx context.Context, dashboardAppID uint) ([]model.DashboardWidget, error) { app, err := s.repo.GetByID(ctx, dashboardAppID) if err != nil { return nil, err } var widgets []model.DashboardWidget if len(app.Content) > 0 && string(app.Content) != "" && string(app.Content) != "[]" { if err := json.Unmarshal(app.Content, &widgets); err != nil { return nil, err } } return widgets, nil }