package service import ( "context" "sort" "strings" "eslogad-be/config" "eslogad-be/internal/contract" "eslogad-be/internal/entities" "eslogad-be/internal/repository" "eslogad-be/internal/transformer" "github.com/google/uuid" ) type MasterServiceImpl struct { labelRepo *repository.LabelRepository priorityRepo *repository.PriorityRepository institutionRepo *repository.InstitutionRepository dispRepo *repository.DispositionActionRepository departmentRepo *repository.DepartmentRepository config *config.Config } func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository, department *repository.DepartmentRepository, cfg *config.Config) *MasterServiceImpl { return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp, departmentRepo: department, config: cfg} } // Labels func (s *MasterServiceImpl) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) { entity := &entities.Label{Name: req.Name, Color: req.Color} if err := s.labelRepo.Create(ctx, entity); err != nil { return nil, err } resp := transformer.LabelsToContract([]entities.Label{*entity})[0] return &resp, nil } func (s *MasterServiceImpl) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) { entity := &entities.Label{ID: id} if req.Name != nil { entity.Name = *req.Name } if req.Color != nil { entity.Color = req.Color } if err := s.labelRepo.Update(ctx, entity); err != nil { return nil, err } e, err := s.labelRepo.Get(ctx, id) if err != nil { return nil, err } resp := transformer.LabelsToContract([]entities.Label{*e})[0] return &resp, nil } func (s *MasterServiceImpl) DeleteLabel(ctx context.Context, id uuid.UUID) error { return s.labelRepo.Delete(ctx, id) } func (s *MasterServiceImpl) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) { list, err := s.labelRepo.List(ctx) if err != nil { return nil, err } return &contract.ListLabelsResponse{Labels: transformer.LabelsToContract(list)}, nil } // Priorities func (s *MasterServiceImpl) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) { entity := &entities.Priority{Name: req.Name, Level: req.Level} if err := s.priorityRepo.Create(ctx, entity); err != nil { return nil, err } resp := transformer.PrioritiesToContract([]entities.Priority{*entity})[0] return &resp, nil } func (s *MasterServiceImpl) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) { entity := &entities.Priority{ID: id} if req.Name != nil { entity.Name = *req.Name } if req.Level != nil { entity.Level = *req.Level } if err := s.priorityRepo.Update(ctx, entity); err != nil { return nil, err } e, err := s.priorityRepo.Get(ctx, id) if err != nil { return nil, err } resp := transformer.PrioritiesToContract([]entities.Priority{*e})[0] return &resp, nil } func (s *MasterServiceImpl) DeletePriority(ctx context.Context, id uuid.UUID) error { return s.priorityRepo.Delete(ctx, id) } func (s *MasterServiceImpl) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) { list, err := s.priorityRepo.List(ctx) if err != nil { return nil, err } return &contract.ListPrioritiesResponse{Priorities: transformer.PrioritiesToContract(list)}, nil } // Institutions func (s *MasterServiceImpl) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) { entity := &entities.Institution{Name: req.Name, Type: entities.InstitutionType(req.Type), Address: req.Address, ContactPerson: req.ContactPerson, Phone: req.Phone, Email: req.Email} if err := s.institutionRepo.Create(ctx, entity); err != nil { return nil, err } resp := transformer.InstitutionsToContract([]entities.Institution{*entity})[0] return &resp, nil } func (s *MasterServiceImpl) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) { entity := &entities.Institution{ID: id} if req.Name != nil { entity.Name = *req.Name } if req.Type != nil { entity.Type = entities.InstitutionType(*req.Type) } if req.Address != nil { entity.Address = req.Address } if req.ContactPerson != nil { entity.ContactPerson = req.ContactPerson } if req.Phone != nil { entity.Phone = req.Phone } if req.Email != nil { entity.Email = req.Email } if err := s.institutionRepo.Update(ctx, entity); err != nil { return nil, err } e, err := s.institutionRepo.Get(ctx, id) if err != nil { return nil, err } resp := transformer.InstitutionsToContract([]entities.Institution{*e})[0] return &resp, nil } func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error { return s.institutionRepo.Delete(ctx, id) } func (s *MasterServiceImpl) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) { list, err := s.institutionRepo.ListWithSearch(ctx, req.Search) if err != nil { return nil, err } return &contract.ListInstitutionsResponse{Institutions: transformer.InstitutionsToContract(list)}, nil } // Disposition Actions func (s *MasterServiceImpl) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) { entity := &entities.DispositionAction{Code: req.Code, Label: req.Label, Description: req.Description} if req.RequiresNote != nil { entity.RequiresNote = *req.RequiresNote } if req.GroupName != nil { entity.GroupName = req.GroupName } if req.SortOrder != nil { entity.SortOrder = req.SortOrder } if req.IsActive != nil { entity.IsActive = *req.IsActive } if err := s.dispRepo.Create(ctx, entity); err != nil { return nil, err } resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*entity})[0] return &resp, nil } func (s *MasterServiceImpl) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) { entity := &entities.DispositionAction{ID: id} if req.Code != nil { entity.Code = *req.Code } if req.Label != nil { entity.Label = *req.Label } if req.Description != nil { entity.Description = req.Description } if req.RequiresNote != nil { entity.RequiresNote = *req.RequiresNote } if req.GroupName != nil { entity.GroupName = req.GroupName } if req.SortOrder != nil { entity.SortOrder = req.SortOrder } if req.IsActive != nil { entity.IsActive = *req.IsActive } if err := s.dispRepo.Update(ctx, entity); err != nil { return nil, err } e, err := s.dispRepo.Get(ctx, id) if err != nil { return nil, err } resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*e})[0] return &resp, nil } func (s *MasterServiceImpl) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error { return s.dispRepo.Delete(ctx, id) } func (s *MasterServiceImpl) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) { list, err := s.dispRepo.List(ctx) if err != nil { return nil, err } return &contract.ListDispositionActionsResponse{Actions: transformer.DispositionActionsToContract(list)}, nil } // Departments func (s *MasterServiceImpl) CreateDepartment(ctx context.Context, req *contract.CreateDepartmentRequest) (*contract.GetDepartmentResponse, error) { // Build the path based on parent var path string if req.ParentID != nil { // Get parent department to build the path parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID) if err != nil { return nil, err } // Build path as parent.path + code path = parent.Path + "." + req.Code } else { // Root level department, just use the code as path path = req.Code } entity := &entities.Department{ Name: req.Name, Code: req.Code, Path: path, } if err := s.departmentRepo.Create(ctx, entity); err != nil { return nil, err } // Get parent name if parent exists var parentName *string if req.ParentID != nil { if parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID); err == nil { parentName = &parent.Name } } return &contract.GetDepartmentResponse{ ID: entity.ID, Name: entity.Name, Code: entity.Code, Path: entity.Path, ParentID: req.ParentID, ParentName: parentName, CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt, }, nil } func (s *MasterServiceImpl) GetDepartment(ctx context.Context, id uuid.UUID) (*contract.GetDepartmentResponse, error) { entity, err := s.departmentRepo.Get(ctx, id) if err != nil { return nil, err } // Derive parent_id and parent_name from path var parentID *uuid.UUID var parentName *string parts := strings.Split(entity.Path, ".") if len(parts) > 1 { // Has parent, try to find it parentPath := strings.Join(parts[:len(parts)-1], ".") if parent, err := s.departmentRepo.GetByPath(ctx, parentPath); err == nil { parentID = &parent.ID parentName = &parent.Name } } return &contract.GetDepartmentResponse{ ID: entity.ID, Name: entity.Name, Code: entity.Code, Path: entity.Path, ParentID: parentID, ParentName: parentName, CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt, }, nil } func (s *MasterServiceImpl) UpdateDepartment(ctx context.Context, id uuid.UUID, req *contract.UpdateDepartmentRequest) (*contract.GetDepartmentResponse, error) { entity, err := s.departmentRepo.Get(ctx, id) if err != nil { return nil, err } // Store the old path before changes oldPath := entity.Path if req.Name != nil { entity.Name = *req.Name } if req.Code != nil { entity.Code = *req.Code } // Rebuild path if parent is being changed or code is being changed if req.ParentID != nil || req.Code != nil { // Determine the code to use (new code if provided, otherwise existing) code := entity.Code if req.Code != nil { code = *req.Code } // Build the new path based on parent var path string if req.ParentID != nil { if *req.ParentID == uuid.Nil { // Moving to root level path = code } else { // Get parent department to build the path parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID) if err != nil { return nil, err } // Build path as parent.path + code path = parent.Path + "." + code } } else if req.Code != nil { // Code changed but parent not specified, rebuild path with current parent // Extract parent path from current path parts := strings.Split(entity.Path, ".") if len(parts) > 1 { // Has parent, rebuild with new code parentPath := strings.Join(parts[:len(parts)-1], ".") path = parentPath + "." + code } else { // Root level, just use new code path = code } } if path != "" { entity.Path = path } } // Update the department if err := s.departmentRepo.Update(ctx, entity); err != nil { return nil, err } // If the path changed, update all children paths if oldPath != entity.Path { if err := s.departmentRepo.UpdateChildrenPaths(ctx, oldPath, entity.Path); err != nil { // Log the error but don't fail the operation // You might want to handle this differently based on your requirements // For now, we'll continue since the parent update succeeded } } // Derive parent_id and parent_name from path for response var parentID *uuid.UUID var parentName *string if req.ParentID != nil { parentID = req.ParentID // Get parent name if parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID); err == nil { parentName = &parent.Name } } else { // Derive from path if not provided in request parts := strings.Split(entity.Path, ".") if len(parts) > 1 { parentPath := strings.Join(parts[:len(parts)-1], ".") if parent, err := s.departmentRepo.GetByPath(ctx, parentPath); err == nil { parentID = &parent.ID parentName = &parent.Name } } } return &contract.GetDepartmentResponse{ ID: entity.ID, Name: entity.Name, Code: entity.Code, Path: entity.Path, ParentID: parentID, ParentName: parentName, CreatedAt: entity.CreatedAt, UpdatedAt: entity.UpdatedAt, }, nil } func (s *MasterServiceImpl) DeleteDepartment(ctx context.Context, id uuid.UUID) error { return s.departmentRepo.Delete(ctx, id) } func (s *MasterServiceImpl) GetOrganizationalChartByID(ctx context.Context, departmentID uuid.UUID) (*contract.OrganizationalChartResponse, error) { // First get the department to find its path department, err := s.departmentRepo.Get(ctx, departmentID) if err != nil { return nil, err } // Now get the organizational chart starting from this department's path return s.GetOrganizationalChart(ctx, department.Path) } func (s *MasterServiceImpl) GetOrganizationalChart(ctx context.Context, rootPath string) (*contract.OrganizationalChartResponse, error) { var departments []entities.Department var err error // Get config values parentPath := s.config.Department.ParentPath excludedPaths := s.config.Department.ExcludedPaths if rootPath == "" { // Get all departments with parent filter departments, err = s.departmentRepo.GetAllWithParentFilter(ctx, parentPath, excludedPaths) } else { // Get departments under specific path departments, err = s.departmentRepo.GetByPathPrefix(ctx, rootPath) // Filter out excluded paths manually for specific path queries filteredDepts := make([]entities.Department, 0) for _, dept := range departments { excluded := false for _, excludedPath := range excludedPaths { if strings.Contains(dept.Path, excludedPath) { excluded = true break } } if !excluded { filteredDepts = append(filteredDepts, dept) } } departments = filteredDepts } if err != nil { return nil, err } // Build the tree structure nodeMap := make(map[string]*contract.DepartmentNode) roots := make([]*contract.DepartmentNode, 0) // Calculate base level offset based on parent path baseLevelOffset := 0 if parentPath != "" { baseLevelOffset = len(strings.Split(parentPath, ".")) - 1 } // First pass: create all nodes including missing parents for _, dept := range departments { pathParts := strings.Split(dept.Path, ".") // Create any missing parent nodes for i := 1; i <= len(pathParts); i++ { currentPath := strings.Join(pathParts[:i], ".") if _, exists := nodeMap[currentPath]; !exists { // Calculate level for this path adjustedLevel := i - baseLevelOffset if adjustedLevel < 1 { adjustedLevel = 1 } // Create node (placeholder for missing parents, real data for existing) var node *contract.DepartmentNode if currentPath == dept.Path { // This is the actual department node = &contract.DepartmentNode{ ID: dept.ID, Name: dept.Name, Code: dept.Code, Path: dept.Path, Level: adjustedLevel, Children: make([]*contract.DepartmentNode, 0), } } else { // This is a missing parent - create placeholder // Extract the last segment as the name lastSegment := pathParts[i-1] node = &contract.DepartmentNode{ ID: uuid.Nil, // Use nil UUID for placeholder Name: strings.ToUpper(strings.ReplaceAll(lastSegment, "_", " ")), Code: lastSegment, Path: currentPath, Level: adjustedLevel, Children: make([]*contract.DepartmentNode, 0), } } nodeMap[currentPath] = node } } } // Second pass: build the tree relationships // Only process nodes that actually exist in the database (not placeholders) processedPaths := make(map[string]bool) for _, dept := range departments { if processedPaths[dept.Path] { continue } processedPaths[dept.Path] = true node := nodeMap[dept.Path] pathParts := strings.Split(dept.Path, ".") // Check if this should be a root node isRoot := false if rootPath != "" && dept.Path == rootPath { // Explicitly requested root isRoot = true } else if rootPath == "" && parentPath != "" && dept.Path == parentPath { // The configured parent path is the root when showing all isRoot = true } else if len(pathParts) == 1 { // Single segment path isRoot = true } else { // Find parent path parentPathStr := strings.Join(pathParts[:len(pathParts)-1], ".") if parent, exists := nodeMap[parentPathStr]; exists { // Check if this child is already added alreadyAdded := false for _, child := range parent.Children { if child.Path == node.Path { alreadyAdded = true break } } if !alreadyAdded { parent.Children = append(parent.Children, node) } } else { // Parent doesn't exist - this is an orphaned node // Only include it as a root if it's a direct child of the parent path if parentPath != "" { // Check if this is a direct child of the configured parent expectedParent := parentPath actualParent := strings.Join(pathParts[:len(pathParts)-1], ".") if actualParent != expectedParent { // This is an orphaned node - skip it continue } } isRoot = true } } if isRoot { // Check for duplicates in roots alreadyInRoots := false for _, r := range roots { if r.Path == node.Path { alreadyInRoots = true break } } if !alreadyInRoots { roots = append(roots, node) } } } // Sort children at each level var sortChildren func([]*contract.DepartmentNode) sortChildren = func(nodes []*contract.DepartmentNode) { for _, node := range nodes { if len(node.Children) > 0 { // Sort children by name sort.Slice(node.Children, func(i, j int) bool { return node.Children[i].Name < node.Children[j].Name }) sortChildren(node.Children) } } } // Sort root nodes sort.Slice(roots, func(i, j int) bool { return roots[i].Name < roots[j].Name }) sortChildren(roots) return &contract.OrganizationalChartResponse{ Chart: roots, TotalNodes: len(departments), }, nil } func (s *MasterServiceImpl) ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error) { // Set default values if not provided page := req.Page if page < 1 { page = 1 } limit := req.Limit if limit < 1 { limit = 10 } if limit > 100 { limit = 100 // Max limit to prevent performance issues } offset := (page - 1) * limit // Use filtered list with parent path from config parentPath := s.config.Department.ParentPath excludedPaths := s.config.Department.ExcludedPaths list, total, err := s.departmentRepo.ListWithParentFilter(ctx, req.Search, limit, offset, parentPath, excludedPaths) if err != nil { return nil, err } return &contract.ListDepartmentsResponse{ Departments: transformer.DepartmentsToContract(list), Total: total, Page: page, Limit: limit, }, nil }