dukcapil/internal/service/master_service.go
2025-09-09 14:41:00 +07:00

634 lines
19 KiB
Go

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
}