diff --git a/internal/app/app.go b/internal/app/app.go index 2155bdd..331ec88 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -44,6 +44,9 @@ func (a *App) Initialize(cfg *config.Config) error { healthHandler := handler.NewHealthHandler() fileHandler := handler.NewFileHandler(services.fileService) rbacHandler := handler.NewRBACHandler(services.rbacService) + masterHandler := handler.NewMasterHandler(services.masterService) + letterHandler := handler.NewLetterHandler(services.letterService) + dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService) a.router = router.NewRouter( cfg, @@ -53,6 +56,9 @@ func (a *App) Initialize(cfg *config.Config) error { handler.NewUserHandler(services.userService, validator.NewUserValidator()), fileHandler, rbacHandler, + masterHandler, + letterHandler, + dispositionRouteHandler, ) return nil @@ -98,36 +104,70 @@ func (a *App) Shutdown() { } type repositories struct { - userRepo *repository.UserRepositoryImpl - userProfileRepo *repository.UserProfileRepository - titleRepo *repository.TitleRepository - rbacRepo *repository.RBACRepository + userRepo *repository.UserRepositoryImpl + userProfileRepo *repository.UserProfileRepository + titleRepo *repository.TitleRepository + rbacRepo *repository.RBACRepository + labelRepo *repository.LabelRepository + priorityRepo *repository.PriorityRepository + institutionRepo *repository.InstitutionRepository + dispRepo *repository.DispositionActionRepository + letterRepo *repository.LetterIncomingRepository + letterAttachRepo *repository.LetterIncomingAttachmentRepository + activityLogRepo *repository.LetterIncomingActivityLogRepository + dispositionRouteRepo *repository.DispositionRouteRepository + // new repos + letterDispositionRepo *repository.LetterDispositionRepository + letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository + dispositionNoteRepo *repository.DispositionNoteRepository + letterDiscussionRepo *repository.LetterDiscussionRepository } func (a *App) initRepositories() *repositories { return &repositories{ - userRepo: repository.NewUserRepository(a.db), - userProfileRepo: repository.NewUserProfileRepository(a.db), - titleRepo: repository.NewTitleRepository(a.db), - rbacRepo: repository.NewRBACRepository(a.db), + userRepo: repository.NewUserRepository(a.db), + userProfileRepo: repository.NewUserProfileRepository(a.db), + titleRepo: repository.NewTitleRepository(a.db), + rbacRepo: repository.NewRBACRepository(a.db), + labelRepo: repository.NewLabelRepository(a.db), + priorityRepo: repository.NewPriorityRepository(a.db), + institutionRepo: repository.NewInstitutionRepository(a.db), + dispRepo: repository.NewDispositionActionRepository(a.db), + letterRepo: repository.NewLetterIncomingRepository(a.db), + letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db), + activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db), + dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db), + letterDispositionRepo: repository.NewLetterDispositionRepository(a.db), + letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db), + dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db), + letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db), } } type processors struct { - userProcessor *processor.UserProcessorImpl + userProcessor *processor.UserProcessorImpl + letterProcessor *processor.LetterProcessorImpl + activityLogger *processor.ActivityLogProcessorImpl } func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors { + txMgr := repository.NewTxManager(a.db) + activity := processor.NewActivityLogProcessor(repos.activityLogRepo) return &processors{ - userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo), + userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo), + letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo), + activityLogger: activity, } } type services struct { - userService *service.UserServiceImpl - authService *service.AuthServiceImpl - fileService *service.FileServiceImpl - rbacService *service.RBACServiceImpl + userService *service.UserServiceImpl + authService *service.AuthServiceImpl + fileService *service.FileServiceImpl + rbacService *service.RBACServiceImpl + masterService *service.MasterServiceImpl + letterService *service.LetterServiceImpl + dispositionRouteService *service.DispositionRouteServiceImpl } func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { @@ -137,18 +177,25 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo) - // File storage client and service fileCfg := cfg.S3Config s3Client := client.NewFileClient(fileCfg) fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents") rbacSvc := service.NewRBACService(repos.rbacRepo) + masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo) + + letterSvc := service.NewLetterService(processors.letterProcessor) + dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo) + return &services{ - userService: userSvc, - authService: authService, - fileService: fileSvc, - rbacService: rbacSvc, + userService: userSvc, + authService: authService, + fileService: fileSvc, + rbacService: rbacSvc, + masterService: masterSvc, + letterService: letterSvc, + dispositionRouteService: dispRouteSvc, } } diff --git a/internal/contract/common.go b/internal/contract/common.go index 11484c8..2bd94a6 100644 --- a/internal/contract/common.go +++ b/internal/contract/common.go @@ -47,3 +47,118 @@ type HealthResponse struct { Timestamp time.Time `json:"timestamp"` Version string `json:"version"` } + +type LabelResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Color *string `json:"color,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateLabelRequest struct { + Name string `json:"name"` + Color *string `json:"color,omitempty"` +} + +type UpdateLabelRequest struct { + Name *string `json:"name,omitempty"` + Color *string `json:"color,omitempty"` +} + +type ListLabelsResponse struct { + Labels []LabelResponse `json:"labels"` +} + +type PriorityResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Level int `json:"level"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreatePriorityRequest struct { + Name string `json:"name"` + Level int `json:"level"` +} + +type UpdatePriorityRequest struct { + Name *string `json:"name,omitempty"` + Level *int `json:"level,omitempty"` +} + +type ListPrioritiesResponse struct { + Priorities []PriorityResponse `json:"priorities"` +} + +type InstitutionResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Address *string `json:"address,omitempty"` + ContactPerson *string `json:"contact_person,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateInstitutionRequest struct { + Name string `json:"name"` + Type string `json:"type"` + Address *string `json:"address,omitempty"` + ContactPerson *string `json:"contact_person,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` +} + +type UpdateInstitutionRequest struct { + Name *string `json:"name,omitempty"` + Type *string `json:"type,omitempty"` + Address *string `json:"address,omitempty"` + ContactPerson *string `json:"contact_person,omitempty"` + Phone *string `json:"phone,omitempty"` + Email *string `json:"email,omitempty"` +} + +type ListInstitutionsResponse struct { + Institutions []InstitutionResponse `json:"institutions"` +} + +type DispositionActionResponse struct { + ID string `json:"id"` + Code string `json:"code"` + Label string `json:"label"` + Description *string `json:"description,omitempty"` + RequiresNote bool `json:"requires_note"` + GroupName *string `json:"group_name,omitempty"` + SortOrder *int `json:"sort_order,omitempty"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateDispositionActionRequest struct { + Code string `json:"code"` + Label string `json:"label"` + Description *string `json:"description,omitempty"` + RequiresNote *bool `json:"requires_note,omitempty"` + GroupName *string `json:"group_name,omitempty"` + SortOrder *int `json:"sort_order,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type UpdateDispositionActionRequest struct { + Code *string `json:"code,omitempty"` + Label *string `json:"label,omitempty"` + Description *string `json:"description,omitempty"` + RequiresNote *bool `json:"requires_note,omitempty"` + GroupName *string `json:"group_name,omitempty"` + SortOrder *int `json:"sort_order,omitempty"` + IsActive *bool `json:"is_active,omitempty"` +} + +type ListDispositionActionsResponse struct { + Actions []DispositionActionResponse `json:"actions"` +} diff --git a/internal/contract/disposition_route_contract.go b/internal/contract/disposition_route_contract.go new file mode 100644 index 0000000..b8eecf2 --- /dev/null +++ b/internal/contract/disposition_route_contract.go @@ -0,0 +1,33 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type DispositionRouteResponse struct { + ID uuid.UUID `json:"id"` + FromDepartmentID uuid.UUID `json:"from_department_id"` + ToDepartmentID uuid.UUID `json:"to_department_id"` + IsActive bool `json:"is_active"` + AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateDispositionRouteRequest struct { + FromDepartmentID uuid.UUID `json:"from_department_id"` + ToDepartmentID uuid.UUID `json:"to_department_id"` + IsActive *bool `json:"is_active,omitempty"` + AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"` +} + +type UpdateDispositionRouteRequest struct { + IsActive *bool `json:"is_active,omitempty"` + AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"` +} + +type ListDispositionRoutesResponse struct { + Routes []DispositionRouteResponse `json:"routes"` +} diff --git a/internal/contract/letter_contract.go b/internal/contract/letter_contract.go new file mode 100644 index 0000000..65c5e8f --- /dev/null +++ b/internal/contract/letter_contract.go @@ -0,0 +1,122 @@ +package contract + +import ( + "time" + + "github.com/google/uuid" +) + +type CreateIncomingLetterAttachment struct { + FileURL string `json:"file_url"` + FileName string `json:"file_name"` + FileType string `json:"file_type"` +} + +type CreateIncomingLetterRequest struct { + ReferenceNumber *string `json:"reference_number,omitempty"` + Subject string `json:"subject"` + Description *string `json:"description,omitempty"` + PriorityID *uuid.UUID `json:"priority_id,omitempty"` + SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"` + ReceivedDate time.Time `json:"received_date"` + DueDate *time.Time `json:"due_date,omitempty"` + Attachments []CreateIncomingLetterAttachment `json:"attachments,omitempty"` +} + +type IncomingLetterAttachmentResponse struct { + ID uuid.UUID `json:"id"` + FileURL string `json:"file_url"` + FileName string `json:"file_name"` + FileType string `json:"file_type"` + UploadedAt time.Time `json:"uploaded_at"` +} + +type IncomingLetterResponse struct { + ID uuid.UUID `json:"id"` + LetterNumber string `json:"letter_number"` + ReferenceNumber *string `json:"reference_number,omitempty"` + Subject string `json:"subject"` + Description *string `json:"description,omitempty"` + PriorityID *uuid.UUID `json:"priority_id,omitempty"` + SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"` + ReceivedDate time.Time `json:"received_date"` + DueDate *time.Time `json:"due_date,omitempty"` + Status string `json:"status"` + CreatedBy uuid.UUID `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Attachments []IncomingLetterAttachmentResponse `json:"attachments"` +} + +type UpdateIncomingLetterRequest struct { + ReferenceNumber *string `json:"reference_number,omitempty"` + Subject *string `json:"subject,omitempty"` + Description *string `json:"description,omitempty"` + PriorityID *uuid.UUID `json:"priority_id,omitempty"` + SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"` + ReceivedDate *time.Time `json:"received_date,omitempty"` + DueDate *time.Time `json:"due_date,omitempty"` + Status *string `json:"status,omitempty"` +} + +type ListIncomingLettersRequest struct { + Page int `json:"page"` + Limit int `json:"limit"` + Status *string `json:"status,omitempty"` + Query *string `json:"query,omitempty"` +} + +type ListIncomingLettersResponse struct { + Letters []IncomingLetterResponse `json:"letters"` + Pagination PaginationResponse `json:"pagination"` +} + +type CreateDispositionActionSelection struct { + ActionID uuid.UUID `json:"action_id"` + Note *string `json:"note,omitempty"` +} + +type CreateLetterDispositionRequest struct { + LetterID uuid.UUID `json:"letter_id"` + ToDepartmentIDs []uuid.UUID `json:"to_department_ids"` + Notes *string `json:"notes,omitempty"` + SelectedActions []CreateDispositionActionSelection `json:"selected_actions,omitempty"` +} + +type DispositionResponse struct { + ID uuid.UUID `json:"id"` + LetterID uuid.UUID `json:"letter_id"` + FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"` + ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"` + Notes *string `json:"notes,omitempty"` + Status string `json:"status"` + CreatedBy uuid.UUID `json:"created_by"` + CreatedAt time.Time `json:"created_at"` +} + +type ListDispositionsResponse struct { + Dispositions []DispositionResponse `json:"dispositions"` +} + +type CreateLetterDiscussionRequest struct { + ParentID *uuid.UUID `json:"parent_id,omitempty"` + Message string `json:"message"` + Mentions map[string]interface{} `json:"mentions,omitempty"` +} + +type UpdateLetterDiscussionRequest struct { + Message string `json:"message"` + Mentions map[string]interface{} `json:"mentions,omitempty"` +} + +type LetterDiscussionResponse struct { + ID uuid.UUID `json:"id"` + LetterID uuid.UUID `json:"letter_id"` + ParentID *uuid.UUID `json:"parent_id,omitempty"` + UserID uuid.UUID `json:"user_id"` + Message string `json:"message"` + Mentions map[string]interface{} `json:"mentions,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + EditedAt *time.Time `json:"edited_at,omitempty"` +} diff --git a/internal/entities/disposition_action.go b/internal/entities/disposition_action.go new file mode 100644 index 0000000..30844a6 --- /dev/null +++ b/internal/entities/disposition_action.go @@ -0,0 +1,22 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type DispositionAction struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Code string `gorm:"uniqueIndex;not null" json:"code"` + Label string `gorm:"not null" json:"label"` + Description *string `json:"description,omitempty"` + RequiresNote bool `gorm:"not null;default:false" json:"requires_note"` + GroupName *string `json:"group_name,omitempty"` + SortOrder *int `json:"sort_order,omitempty"` + IsActive bool `gorm:"not null;default:true" json:"is_active"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (DispositionAction) TableName() string { return "disposition_actions" } diff --git a/internal/entities/disposition_route.go b/internal/entities/disposition_route.go new file mode 100644 index 0000000..1c8bfc7 --- /dev/null +++ b/internal/entities/disposition_route.go @@ -0,0 +1,19 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type DispositionRoute struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + FromDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"from_department_id"` + ToDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"to_department_id"` + IsActive bool `gorm:"not null;default:true" json:"is_active"` + AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (DispositionRoute) TableName() string { return "disposition_routes" } diff --git a/internal/entities/institution.go b/internal/entities/institution.go new file mode 100644 index 0000000..ee4bf85 --- /dev/null +++ b/internal/entities/institution.go @@ -0,0 +1,30 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type InstitutionType string + +const ( + InstGovernment InstitutionType = "government" + InstPrivate InstitutionType = "private" + InstNGO InstitutionType = "ngo" + InstIndividual InstitutionType = "individual" +) + +type Institution struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"not null;size:255" json:"name"` + Type InstitutionType `gorm:"not null;size:32" json:"type"` + Address *string `json:"address,omitempty"` + ContactPerson *string `gorm:"size:255" json:"contact_person,omitempty"` + Phone *string `gorm:"size:50" json:"phone,omitempty"` + Email *string `gorm:"size:255" json:"email,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (Institution) TableName() string { return "institutions" } diff --git a/internal/entities/label.go b/internal/entities/label.go new file mode 100644 index 0000000..fce6195 --- /dev/null +++ b/internal/entities/label.go @@ -0,0 +1,17 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type Label struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"not null;size:255" json:"name"` + Color *string `gorm:"size:16" json:"color,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (Label) TableName() string { return "labels" } diff --git a/internal/entities/letter_discussion.go b/internal/entities/letter_discussion.go new file mode 100644 index 0000000..6e27112 --- /dev/null +++ b/internal/entities/letter_discussion.go @@ -0,0 +1,21 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type LetterDiscussion struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` + ParentID *uuid.UUID `json:"parent_id,omitempty"` + UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` + Message string `gorm:"not null" json:"message"` + Mentions JSONB `gorm:"type:jsonb" json:"mentions,omitempty"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + EditedAt *time.Time `json:"edited_at,omitempty"` +} + +func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" } diff --git a/internal/entities/letter_disposition.go b/internal/entities/letter_disposition.go new file mode 100644 index 0000000..002bf3d --- /dev/null +++ b/internal/entities/letter_disposition.go @@ -0,0 +1,55 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type LetterDispositionStatus string + +const ( + DispositionPending LetterDispositionStatus = "pending" + DispositionRead LetterDispositionStatus = "read" + DispositionRejected LetterDispositionStatus = "rejected" + DispositionCompleted LetterDispositionStatus = "completed" +) + +type LetterDisposition struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` + FromUserID *uuid.UUID `json:"from_user_id,omitempty"` + FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"` + ToUserID *uuid.UUID `json:"to_user_id,omitempty"` + ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"` + Notes *string `json:"notes,omitempty"` + Status LetterDispositionStatus `gorm:"not null;default:'pending'" json:"status"` + CreatedBy uuid.UUID `gorm:"not null" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + ReadAt *time.Time `json:"read_at,omitempty"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (LetterDisposition) TableName() string { return "letter_dispositions" } + +type DispositionNote struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"` + UserID *uuid.UUID `json:"user_id,omitempty"` + Note string `gorm:"not null" json:"note"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` +} + +func (DispositionNote) TableName() string { return "disposition_notes" } + +type LetterDispositionActionSelection struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"` + ActionID uuid.UUID `gorm:"type:uuid;not null" json:"action_id"` + Note *string `json:"note,omitempty"` + CreatedBy uuid.UUID `gorm:"not null" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` +} + +func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" } diff --git a/internal/entities/letter_incoming.go b/internal/entities/letter_incoming.go new file mode 100644 index 0000000..76319f0 --- /dev/null +++ b/internal/entities/letter_incoming.go @@ -0,0 +1,45 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type LetterIncomingStatus string + +const ( + LetterIncomingStatusNew LetterIncomingStatus = "new" + LetterIncomingStatusInProgress LetterIncomingStatus = "in_progress" + LetterIncomingStatusCompleted LetterIncomingStatus = "completed" +) + +type LetterIncoming struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + LetterNumber string `gorm:"uniqueIndex;not null" json:"letter_number"` + ReferenceNumber *string `json:"reference_number,omitempty"` + Subject string `gorm:"not null" json:"subject"` + Description *string `json:"description,omitempty"` + PriorityID *uuid.UUID `json:"priority_id,omitempty"` + SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"` + ReceivedDate time.Time `json:"received_date"` + DueDate *time.Time `json:"due_date,omitempty"` + Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"` + CreatedBy uuid.UUID `gorm:"not null" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (LetterIncoming) TableName() string { return "letters_incoming" } + +type LetterIncomingAttachment struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` + FileURL string `gorm:"not null" json:"file_url"` + FileName string `gorm:"not null" json:"file_name"` + FileType string `gorm:"not null" json:"file_type"` + UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"` + UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"` +} + +func (LetterIncomingAttachment) TableName() string { return "letter_incoming_attachments" } diff --git a/internal/entities/letter_incoming_activity_log.go b/internal/entities/letter_incoming_activity_log.go new file mode 100644 index 0000000..005a658 --- /dev/null +++ b/internal/entities/letter_incoming_activity_log.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type LetterIncomingActivityLog struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"` + ActionType string `gorm:"not null" json:"action_type"` + ActorUserID *uuid.UUID `json:"actor_user_id,omitempty"` + ActorDepartmentID *uuid.UUID `json:"actor_department_id,omitempty"` + TargetType *string `json:"target_type,omitempty"` + TargetID *uuid.UUID `json:"target_id,omitempty"` + FromStatus *string `json:"from_status,omitempty"` + ToStatus *string `json:"to_status,omitempty"` + Context JSONB `gorm:"type:jsonb" json:"context,omitempty"` + OccurredAt time.Time `gorm:"autoCreateTime" json:"occurred_at"` +} + +func (LetterIncomingActivityLog) TableName() string { return "letter_incoming_activity_logs" } diff --git a/internal/entities/priority.go b/internal/entities/priority.go new file mode 100644 index 0000000..8158935 --- /dev/null +++ b/internal/entities/priority.go @@ -0,0 +1,17 @@ +package entities + +import ( + "time" + + "github.com/google/uuid" +) + +type Priority struct { + ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"` + Name string `gorm:"not null;size:255" json:"name"` + Level int `gorm:"not null" json:"level"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` +} + +func (Priority) TableName() string { return "priorities" } diff --git a/internal/handler/disposition_route_handler.go b/internal/handler/disposition_route_handler.go new file mode 100644 index 0000000..1305258 --- /dev/null +++ b/internal/handler/disposition_route_handler.go @@ -0,0 +1,100 @@ +package handler + +import ( + "context" + + "eslogad-be/internal/contract" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type DispositionRouteService interface { + Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) + Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) + Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error) + ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error) + SetActive(ctx context.Context, id uuid.UUID, active bool) error +} + +type DispositionRouteHandler struct{ svc DispositionRouteService } + +func NewDispositionRouteHandler(svc DispositionRouteService) *DispositionRouteHandler { + return &DispositionRouteHandler{svc: svc} +} + +func (h *DispositionRouteHandler) Create(c *gin.Context) { + var req contract.CreateDispositionRouteRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.Create(c.Request.Context(), &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(201, contract.BuildSuccessResponse(resp)) +} + +func (h *DispositionRouteHandler) Update(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + var req contract.UpdateDispositionRouteRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.Update(c.Request.Context(), id, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *DispositionRouteHandler) Get(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + resp, err := h.svc.Get(c.Request.Context(), id) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) { + fromID, err := uuid.Parse(c.Param("from_department_id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid from_department_id", Code: 400}) + return + } + resp, err := h.svc.ListByFromDept(c.Request.Context(), fromID) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *DispositionRouteHandler) SetActive(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + toggle := c.Query("active") + active := toggle != "false" + if err := h.svc.SetActive(c.Request.Context(), id, active); err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, &contract.SuccessResponse{Message: "updated"}) +} diff --git a/internal/handler/letter_handler.go b/internal/handler/letter_handler.go new file mode 100644 index 0000000..05635b7 --- /dev/null +++ b/internal/handler/letter_handler.go @@ -0,0 +1,183 @@ +package handler + +import ( + "context" + "net/http" + "strconv" + + "eslogad-be/internal/contract" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type LetterService interface { + CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) + GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) + ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) + UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) + SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error + + CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) + ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) + + CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) + UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) +} + +type LetterHandler struct{ svc LetterService } + +func NewLetterHandler(svc LetterService) *LetterHandler { return &LetterHandler{svc: svc} } + +func (h *LetterHandler) CreateIncomingLetter(c *gin.Context) { + var req contract.CreateIncomingLetterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest}) + return + } + resp, err := h.svc.CreateIncomingLetter(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) GetIncomingLetter(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + resp, err := h.svc.GetIncomingLetterByID(c.Request.Context(), id) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) ListIncomingLetters(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) + status := c.Query("status") + query := c.Query("q") + var statusPtr *string + var queryPtr *string + if status != "" { + statusPtr = &status + } + if query != "" { + queryPtr = &query + } + req := &contract.ListIncomingLettersRequest{Page: page, Limit: limit, Status: statusPtr, Query: queryPtr} + resp, err := h.svc.ListIncomingLetters(c.Request.Context(), req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) UpdateIncomingLetter(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + var req contract.UpdateIncomingLetterRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.UpdateIncomingLetter(c.Request.Context(), id, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + if err := h.svc.SoftDeleteIncomingLetter(c.Request.Context(), id); err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, &contract.SuccessResponse{Message: "deleted"}) +} + +func (h *LetterHandler) CreateDispositions(c *gin.Context) { + var req contract.CreateLetterDispositionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.CreateDispositions(c.Request.Context(), &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(201, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) ListDispositionsByLetter(c *gin.Context) { + letterID, err := uuid.Parse(c.Param("letter_id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400}) + return + } + resp, err := h.svc.ListDispositionsByLetter(c.Request.Context(), letterID) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) CreateDiscussion(c *gin.Context) { + letterID, err := uuid.Parse(c.Param("letter_id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400}) + return + } + var req contract.CreateLetterDiscussionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.CreateDiscussion(c.Request.Context(), letterID, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(201, contract.BuildSuccessResponse(resp)) +} + +func (h *LetterHandler) UpdateDiscussion(c *gin.Context) { + letterID, err := uuid.Parse(c.Param("letter_id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400}) + return + } + discussionID, err := uuid.Parse(c.Param("discussion_id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid discussion_id", Code: 400}) + return + } + var req contract.UpdateLetterDiscussionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.UpdateDiscussion(c.Request.Context(), letterID, discussionID, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} diff --git a/internal/handler/master_handler.go b/internal/handler/master_handler.go new file mode 100644 index 0000000..3c5f1e8 --- /dev/null +++ b/internal/handler/master_handler.go @@ -0,0 +1,252 @@ +package handler + +import ( + "context" + "net/http" + + "eslogad-be/internal/contract" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" +) + +type MasterService interface { + CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) + UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) + DeleteLabel(ctx context.Context, id uuid.UUID) error + ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) + + CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) + UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) + DeletePriority(ctx context.Context, id uuid.UUID) error + ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) + + CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) + UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) + DeleteInstitution(ctx context.Context, id uuid.UUID) error + ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error) + + CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) + UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) + DeleteDispositionAction(ctx context.Context, id uuid.UUID) error + ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) +} + +type MasterHandler struct{ svc MasterService } + +func NewMasterHandler(svc MasterService) *MasterHandler { return &MasterHandler{svc: svc} } + +func (h *MasterHandler) CreateLabel(c *gin.Context) { + var req contract.CreateLabelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.CreateLabel(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp)) +} + +func (h *MasterHandler) UpdateLabel(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + var req contract.UpdateLabelRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.UpdateLabel(c.Request.Context(), id, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} +func (h *MasterHandler) DeleteLabel(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + if err := h.svc.DeleteLabel(c.Request.Context(), id); err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, &contract.SuccessResponse{Message: "deleted"}) +} +func (h *MasterHandler) ListLabels(c *gin.Context) { + resp, err := h.svc.ListLabels(c.Request.Context()) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +// Priorities +func (h *MasterHandler) CreatePriority(c *gin.Context) { + var req contract.CreatePriorityRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.CreatePriority(c.Request.Context(), &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(201, contract.BuildSuccessResponse(resp)) +} +func (h *MasterHandler) UpdatePriority(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + var req contract.UpdatePriorityRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.UpdatePriority(c.Request.Context(), id, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} +func (h *MasterHandler) DeletePriority(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + if err := h.svc.DeletePriority(c.Request.Context(), id); err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, &contract.SuccessResponse{Message: "deleted"}) +} +func (h *MasterHandler) ListPriorities(c *gin.Context) { + resp, err := h.svc.ListPriorities(c.Request.Context()) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +// Institutions +func (h *MasterHandler) CreateInstitution(c *gin.Context) { + var req contract.CreateInstitutionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.CreateInstitution(c.Request.Context(), &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(201, contract.BuildSuccessResponse(resp)) +} + +func (h *MasterHandler) UpdateInstitution(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + var req contract.UpdateInstitutionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.UpdateInstitution(c.Request.Context(), id, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +func (h *MasterHandler) DeleteInstitution(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + if err := h.svc.DeleteInstitution(c.Request.Context(), id); err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, &contract.SuccessResponse{Message: "deleted"}) +} + +func (h *MasterHandler) ListInstitutions(c *gin.Context) { + resp, err := h.svc.ListInstitutions(c.Request.Context()) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} + +// Disposition Actions +func (h *MasterHandler) CreateDispositionAction(c *gin.Context) { + var req contract.CreateDispositionActionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.CreateDispositionAction(c.Request.Context(), &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(201, contract.BuildSuccessResponse(resp)) +} +func (h *MasterHandler) UpdateDispositionAction(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + var req contract.UpdateDispositionActionRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400}) + return + } + resp, err := h.svc.UpdateDispositionAction(c.Request.Context(), id, &req) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} +func (h *MasterHandler) DeleteDispositionAction(c *gin.Context) { + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400}) + return + } + if err := h.svc.DeleteDispositionAction(c.Request.Context(), id); err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, &contract.SuccessResponse{Message: "deleted"}) +} +func (h *MasterHandler) ListDispositionActions(c *gin.Context) { + resp, err := h.svc.ListDispositionActions(c.Request.Context()) + if err != nil { + c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500}) + return + } + c.JSON(200, contract.BuildSuccessResponse(resp)) +} diff --git a/internal/processor/activity_log_processor.go b/internal/processor/activity_log_processor.go new file mode 100644 index 0000000..0196c5b --- /dev/null +++ b/internal/processor/activity_log_processor.go @@ -0,0 +1,37 @@ +package processor + +import ( + "context" + + "eslogad-be/internal/entities" + "eslogad-be/internal/repository" + + "github.com/google/uuid" +) + +type ActivityLogProcessorImpl struct { + repo *repository.LetterIncomingActivityLogRepository +} + +func NewActivityLogProcessor(repo *repository.LetterIncomingActivityLogRepository) *ActivityLogProcessorImpl { + return &ActivityLogProcessorImpl{repo: repo} +} + +func (p *ActivityLogProcessorImpl) Log(ctx context.Context, letterID uuid.UUID, actionType string, actorUserID *uuid.UUID, actorDepartmentID *uuid.UUID, targetType *string, targetID *uuid.UUID, fromStatus *string, toStatus *string, contextData map[string]interface{}) error { + ctxJSON := entities.JSONB{} + for k, v := range contextData { + ctxJSON[k] = v + } + entry := &entities.LetterIncomingActivityLog{ + LetterID: letterID, + ActionType: actionType, + ActorUserID: actorUserID, + ActorDepartmentID: actorDepartmentID, + TargetType: targetType, + TargetID: targetID, + FromStatus: fromStatus, + ToStatus: toStatus, + Context: ctxJSON, + } + return p.repo.Create(ctx, entry) +} diff --git a/internal/processor/letter_processor.go b/internal/processor/letter_processor.go new file mode 100644 index 0000000..02ac1f9 --- /dev/null +++ b/internal/processor/letter_processor.go @@ -0,0 +1,319 @@ +package processor + +import ( + "context" + "time" + + "eslogad-be/internal/appcontext" + "eslogad-be/internal/contract" + "eslogad-be/internal/entities" + "eslogad-be/internal/repository" + "eslogad-be/internal/transformer" + + "github.com/google/uuid" +) + +type LetterProcessorImpl struct { + letterRepo *repository.LetterIncomingRepository + attachRepo *repository.LetterIncomingAttachmentRepository + txManager *repository.TxManager + activity *ActivityLogProcessorImpl + // new repos for dispositions + dispositionRepo *repository.LetterDispositionRepository + dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository + dispositionNoteRepo *repository.DispositionNoteRepository + // discussion repo + discussionRepo *repository.LetterDiscussionRepository +} + +func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository) *LetterProcessorImpl { + return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo} +} + +func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { + var result *contract.IncomingLetterResponse + err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + userID := appcontext.FromGinContext(txCtx).UserID + entity := &entities.LetterIncoming{ + ReferenceNumber: req.ReferenceNumber, + Subject: req.Subject, + Description: req.Description, + PriorityID: req.PriorityID, + SenderInstitutionID: req.SenderInstitutionID, + ReceivedDate: req.ReceivedDate, + DueDate: req.DueDate, + Status: entities.LetterIncomingStatusNew, + CreatedBy: userID, + } + if err := p.letterRepo.Create(txCtx, entity); err != nil { + return err + } + + if p.activity != nil { + action := "letter.created" + if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil { + return err + } + } + + attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments)) + for _, a := range req.Attachments { + attachments = append(attachments, entities.LetterIncomingAttachment{ + LetterID: entity.ID, + FileURL: a.FileURL, + FileName: a.FileName, + FileType: a.FileType, + UploadedBy: &userID, + }) + } + if len(attachments) > 0 { + if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil { + return err + } + if p.activity != nil { + action := "attachment.uploaded" + for _, a := range attachments { + ctxMap := map[string]interface{}{"file_name": a.FileName, "file_type": a.FileType} + if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, ctxMap); err != nil { + return err + } + } + } + } + + savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID) + result = transformer.LetterEntityToContract(entity, savedAttachments) + return nil + }) + + if err != nil { + return nil, err + } + return result, nil +} + +func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) { + entity, err := p.letterRepo.Get(ctx, id) + if err != nil { + return nil, err + } + atts, _ := p.attachRepo.ListByLetter(ctx, id) + return transformer.LetterEntityToContract(entity, atts), nil +} + +func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) { + page, limit := req.Page, req.Limit + if page <= 0 { + page = 1 + } + if limit <= 0 { + limit = 10 + } + filter := repository.ListIncomingLettersFilter{Status: req.Status, Query: req.Query} + list, total, err := p.letterRepo.List(ctx, filter, limit, (page-1)*limit) + if err != nil { + return nil, err + } + respList := make([]contract.IncomingLetterResponse, 0, len(list)) + for _, e := range list { + atts, _ := p.attachRepo.ListByLetter(ctx, e.ID) + resp := transformer.LetterEntityToContract(&e, atts) + respList = append(respList, *resp) + } + return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil +} + +func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { + var out *contract.IncomingLetterResponse + err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + entity, err := p.letterRepo.Get(txCtx, id) + if err != nil { + return err + } + fromStatus := string(entity.Status) + if req.ReferenceNumber != nil { + entity.ReferenceNumber = req.ReferenceNumber + } + if req.Subject != nil { + entity.Subject = *req.Subject + } + if req.Description != nil { + entity.Description = req.Description + } + if req.PriorityID != nil { + entity.PriorityID = req.PriorityID + } + if req.SenderInstitutionID != nil { + entity.SenderInstitutionID = req.SenderInstitutionID + } + if req.ReceivedDate != nil { + entity.ReceivedDate = *req.ReceivedDate + } + if req.DueDate != nil { + entity.DueDate = req.DueDate + } + if req.Status != nil { + entity.Status = entities.LetterIncomingStatus(*req.Status) + } + if err := p.letterRepo.Update(txCtx, entity); err != nil { + return err + } + toStatus := string(entity.Status) + if p.activity != nil && fromStatus != toStatus { + userID := appcontext.FromGinContext(txCtx).UserID + action := "status.changed" + if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, &fromStatus, &toStatus, map[string]interface{}{}); err != nil { + return err + } + } + atts, _ := p.attachRepo.ListByLetter(txCtx, id) + out = transformer.LetterEntityToContract(entity, atts) + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error { + return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + if err := p.letterRepo.SoftDelete(txCtx, id); err != nil { + return err + } + if p.activity != nil { + userID := appcontext.FromGinContext(txCtx).UserID + action := "letter.deleted" + if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil { + return err + } + } + return nil + }) +} + +func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) { + var out *contract.ListDispositionsResponse + err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + userID := appcontext.FromGinContext(txCtx).UserID + created := make([]entities.LetterDisposition, 0, len(req.ToDepartmentIDs)) + for _, toDept := range req.ToDepartmentIDs { + disp := entities.LetterDisposition{ + LetterID: req.LetterID, + FromDepartmentID: nil, + ToDepartmentID: &toDept, + Notes: req.Notes, + Status: entities.DispositionPending, + CreatedBy: userID, + } + if err := p.dispositionRepo.Create(txCtx, &disp); err != nil { + return err + } + created = append(created, disp) + + if len(req.SelectedActions) > 0 { + selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions)) + for _, sel := range req.SelectedActions { + selections = append(selections, entities.LetterDispositionActionSelection{ + DispositionID: disp.ID, + ActionID: sel.ActionID, + Note: sel.Note, + CreatedBy: userID, + }) + } + if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil { + return err + } + } + + if p.activity != nil { + action := "disposition.created" + for _, d := range created { + ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID} + if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil { + return err + } + } + } + } + + out = &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(created)} + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) { + list, err := p.dispositionRepo.ListByLetter(ctx, letterID) + if err != nil { + return nil, err + } + return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil +} + +func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { + var out *contract.LetterDiscussionResponse + err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + userID := appcontext.FromGinContext(txCtx).UserID + mentions := entities.JSONB(nil) + if req.Mentions != nil { + mentions = entities.JSONB(req.Mentions) + } + disc := &entities.LetterDiscussion{LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions} + if err := p.discussionRepo.Create(txCtx, disc); err != nil { + return err + } + if p.activity != nil { + action := "discussion.created" + tgt := "discussion" + ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID} + if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil { + return err + } + } + out = transformer.DiscussionEntityToContract(disc) + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} + +func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { + var out *contract.LetterDiscussionResponse + err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error { + disc, err := p.discussionRepo.Get(txCtx, discussionID) + if err != nil { + return err + } + oldMessage := disc.Message + disc.Message = req.Message + if req.Mentions != nil { + disc.Mentions = entities.JSONB(req.Mentions) + } + now := time.Now() + disc.EditedAt = &now + if err := p.discussionRepo.Update(txCtx, disc); err != nil { + return err + } + if p.activity != nil { + userID := appcontext.FromGinContext(txCtx).UserID + action := "discussion.updated" + tgt := "discussion" + ctxMap := map[string]interface{}{"old_message": oldMessage, "new_message": req.Message} + if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil { + return err + } + } + out = transformer.DiscussionEntityToContract(disc) + return nil + }) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/internal/repository/disposition_route_repository.go b/internal/repository/disposition_route_repository.go new file mode 100644 index 0000000..2a59980 --- /dev/null +++ b/internal/repository/disposition_route_repository.go @@ -0,0 +1,45 @@ +package repository + +import ( + "context" + + "eslogad-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type DispositionRouteRepository struct{ db *gorm.DB } + +func NewDispositionRouteRepository(db *gorm.DB) *DispositionRouteRepository { + return &DispositionRouteRepository{db: db} +} + +func (r *DispositionRouteRepository) Create(ctx context.Context, e *entities.DispositionRoute) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(e).Error +} +func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", e.ID).Updates(e).Error +} +func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) { + db := DBFromContext(ctx, r.db) + var e entities.DispositionRoute + if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} +func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) { + db := DBFromContext(ctx, r.db) + var list []entities.DispositionRoute + if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).Order("to_department_id").Find(&list).Error; err != nil { + return nil, err + } + return list, nil +} +func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error +} diff --git a/internal/repository/letter_repository.go b/internal/repository/letter_repository.go new file mode 100644 index 0000000..6c0d58e --- /dev/null +++ b/internal/repository/letter_repository.go @@ -0,0 +1,180 @@ +package repository + +import ( + "context" + "time" + + "eslogad-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type LetterIncomingRepository struct{ db *gorm.DB } + +func NewLetterIncomingRepository(db *gorm.DB) *LetterIncomingRepository { + return &LetterIncomingRepository{db: db} +} + +func (r *LetterIncomingRepository) Create(ctx context.Context, e *entities.LetterIncoming) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(e).Error +} +func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) { + db := DBFromContext(ctx, r.db) + var e entities.LetterIncoming + if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} + +func (r *LetterIncomingRepository) Update(ctx context.Context, e *entities.LetterIncoming) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error +} + +func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL", id).Error +} + +type ListIncomingLettersFilter struct { + Status *string + Query *string +} + +func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) { + db := DBFromContext(ctx, r.db) + query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL") + if filter.Status != nil { + query = query.Where("status = ?", *filter.Status) + } + if filter.Query != nil { + q := "%" + *filter.Query + "%" + query = query.Where("subject ILIKE ? OR reference_number ILIKE ?", q, q) + } + var total int64 + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + var list []entities.LetterIncoming + if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil { + return nil, 0, err + } + return list, total, nil +} + +type LetterIncomingAttachmentRepository struct{ db *gorm.DB } + +func NewLetterIncomingAttachmentRepository(db *gorm.DB) *LetterIncomingAttachmentRepository { + return &LetterIncomingAttachmentRepository{db: db} +} + +func (r *LetterIncomingAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingAttachment) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(&list).Error +} +func (r *LetterIncomingAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingAttachment, error) { + db := DBFromContext(ctx, r.db) + var list []entities.LetterIncomingAttachment + if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil { + return nil, err + } + return list, nil +} + +type LetterIncomingActivityLogRepository struct{ db *gorm.DB } + +func NewLetterIncomingActivityLogRepository(db *gorm.DB) *LetterIncomingActivityLogRepository { + return &LetterIncomingActivityLogRepository{db: db} +} + +func (r *LetterIncomingActivityLogRepository) Create(ctx context.Context, e *entities.LetterIncomingActivityLog) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(e).Error +} + +func (r *LetterIncomingActivityLogRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingActivityLog, error) { + db := DBFromContext(ctx, r.db) + var list []entities.LetterIncomingActivityLog + if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("occurred_at ASC").Find(&list).Error; err != nil { + return nil, err + } + return list, nil +} + +type LetterDispositionRepository struct{ db *gorm.DB } + +func NewLetterDispositionRepository(db *gorm.DB) *LetterDispositionRepository { + return &LetterDispositionRepository{db: db} +} +func (r *LetterDispositionRepository) Create(ctx context.Context, e *entities.LetterDisposition) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(e).Error +} +func (r *LetterDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDisposition, error) { + db := DBFromContext(ctx, r.db) + var list []entities.LetterDisposition + if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("created_at ASC").Find(&list).Error; err != nil { + return nil, err + } + return list, nil +} + +type DispositionNoteRepository struct{ db *gorm.DB } + +func NewDispositionNoteRepository(db *gorm.DB) *DispositionNoteRepository { + return &DispositionNoteRepository{db: db} +} +func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.DispositionNote) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(e).Error +} + +type LetterDispositionActionSelectionRepository struct{ db *gorm.DB } + +func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository { + return &LetterDispositionActionSelectionRepository{db: db} +} +func (r *LetterDispositionActionSelectionRepository) CreateBulk(ctx context.Context, list []entities.LetterDispositionActionSelection) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(&list).Error +} +func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterDispositionActionSelection, error) { + db := DBFromContext(ctx, r.db) + var list []entities.LetterDispositionActionSelection + if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil { + return nil, err + } + return list, nil +} + +type LetterDiscussionRepository struct{ db *gorm.DB } + +func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository { + return &LetterDiscussionRepository{db: db} +} +func (r *LetterDiscussionRepository) Create(ctx context.Context, e *entities.LetterDiscussion) error { + db := DBFromContext(ctx, r.db) + return db.WithContext(ctx).Create(e).Error +} +func (r *LetterDiscussionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterDiscussion, error) { + db := DBFromContext(ctx, r.db) + var e entities.LetterDiscussion + if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} +func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.LetterDiscussion) error { + db := DBFromContext(ctx, r.db) + // ensure edited_at is set when updating + if e.EditedAt == nil { + now := time.Now() + e.EditedAt = &now + } + return db.WithContext(ctx).Model(&entities.LetterDiscussion{}). + Where("id = ?", e.ID). + Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error +} diff --git a/internal/repository/master_repository.go b/internal/repository/master_repository.go new file mode 100644 index 0000000..d5376e8 --- /dev/null +++ b/internal/repository/master_repository.go @@ -0,0 +1,114 @@ +package repository + +import ( + "context" + + "eslogad-be/internal/entities" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +type LabelRepository struct{ db *gorm.DB } + +func NewLabelRepository(db *gorm.DB) *LabelRepository { return &LabelRepository{db: db} } +func (r *LabelRepository) Create(ctx context.Context, e *entities.Label) error { + return r.db.WithContext(ctx).Create(e).Error +} +func (r *LabelRepository) Update(ctx context.Context, e *entities.Label) error { + return r.db.WithContext(ctx).Model(&entities.Label{}).Where("id = ?", e.ID).Updates(e).Error +} +func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Label{}, "id = ?", id).Error +} +func (r *LabelRepository) List(ctx context.Context) ([]entities.Label, error) { + var list []entities.Label + err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error + return list, err +} +func (r *LabelRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Label, error) { + var e entities.Label + if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} + +type PriorityRepository struct{ db *gorm.DB } + +func NewPriorityRepository(db *gorm.DB) *PriorityRepository { return &PriorityRepository{db: db} } +func (r *PriorityRepository) Create(ctx context.Context, e *entities.Priority) error { + return r.db.WithContext(ctx).Create(e).Error +} +func (r *PriorityRepository) Update(ctx context.Context, e *entities.Priority) error { + return r.db.WithContext(ctx).Model(&entities.Priority{}).Where("id = ?", e.ID).Updates(e).Error +} +func (r *PriorityRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Priority{}, "id = ?", id).Error +} +func (r *PriorityRepository) List(ctx context.Context) ([]entities.Priority, error) { + var list []entities.Priority + err := r.db.WithContext(ctx).Order("level ASC").Find(&list).Error + return list, err +} +func (r *PriorityRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Priority, error) { + var e entities.Priority + if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} + +type InstitutionRepository struct{ db *gorm.DB } + +func NewInstitutionRepository(db *gorm.DB) *InstitutionRepository { + return &InstitutionRepository{db: db} +} +func (r *InstitutionRepository) Create(ctx context.Context, e *entities.Institution) error { + return r.db.WithContext(ctx).Create(e).Error +} +func (r *InstitutionRepository) Update(ctx context.Context, e *entities.Institution) error { + return r.db.WithContext(ctx).Model(&entities.Institution{}).Where("id = ?", e.ID).Updates(e).Error +} +func (r *InstitutionRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.Institution{}, "id = ?", id).Error +} +func (r *InstitutionRepository) List(ctx context.Context) ([]entities.Institution, error) { + var list []entities.Institution + err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error + return list, err +} +func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) { + var e entities.Institution + if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} + +type DispositionActionRepository struct{ db *gorm.DB } + +func NewDispositionActionRepository(db *gorm.DB) *DispositionActionRepository { + return &DispositionActionRepository{db: db} +} +func (r *DispositionActionRepository) Create(ctx context.Context, e *entities.DispositionAction) error { + return r.db.WithContext(ctx).Create(e).Error +} +func (r *DispositionActionRepository) Update(ctx context.Context, e *entities.DispositionAction) error { + return r.db.WithContext(ctx).Model(&entities.DispositionAction{}).Where("id = ?", e.ID).Updates(e).Error +} +func (r *DispositionActionRepository) Delete(ctx context.Context, id uuid.UUID) error { + return r.db.WithContext(ctx).Delete(&entities.DispositionAction{}, "id = ?", id).Error +} +func (r *DispositionActionRepository) List(ctx context.Context) ([]entities.DispositionAction, error) { + var list []entities.DispositionAction + err := r.db.WithContext(ctx).Order("sort_order NULLS LAST, label ASC").Find(&list).Error + return list, err +} +func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionAction, error) { + var e entities.DispositionAction + if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil { + return nil, err + } + return &e, nil +} diff --git a/internal/repository/tx_manager.go b/internal/repository/tx_manager.go new file mode 100644 index 0000000..94a19e8 --- /dev/null +++ b/internal/repository/tx_manager.go @@ -0,0 +1,35 @@ +package repository + +import ( + "context" + + "gorm.io/gorm" +) + +type txKeyType struct{} + +var txKey = txKeyType{} + +// DBFromContext returns the transactional *gorm.DB from context if present; otherwise returns base. +func DBFromContext(ctx context.Context, base *gorm.DB) *gorm.DB { + if v := ctx.Value(txKey); v != nil { + if tx, ok := v.(*gorm.DB); ok && tx != nil { + return tx + } + } + return base +} + +type TxManager struct { + db *gorm.DB +} + +func NewTxManager(db *gorm.DB) *TxManager { return &TxManager{db: db} } + +// WithTransaction runs fn inside a DB transaction, injecting the *gorm.DB tx into ctx. +func (m *TxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error { + return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + ctxTx := context.WithValue(ctx, txKey, tx) + return fn(ctxTx) + }) +} diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index 26d49fa..b79c48a 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -30,3 +30,48 @@ type RBACHandler interface { DeleteRole(c *gin.Context) ListRoles(c *gin.Context) } + +type MasterHandler interface { + // labels + CreateLabel(c *gin.Context) + UpdateLabel(c *gin.Context) + DeleteLabel(c *gin.Context) + ListLabels(c *gin.Context) + // priorities + CreatePriority(c *gin.Context) + UpdatePriority(c *gin.Context) + DeletePriority(c *gin.Context) + ListPriorities(c *gin.Context) + // institutions + CreateInstitution(c *gin.Context) + UpdateInstitution(c *gin.Context) + DeleteInstitution(c *gin.Context) + ListInstitutions(c *gin.Context) + // disposition actions + CreateDispositionAction(c *gin.Context) + UpdateDispositionAction(c *gin.Context) + DeleteDispositionAction(c *gin.Context) + ListDispositionActions(c *gin.Context) +} + +type LetterHandler interface { + CreateIncomingLetter(c *gin.Context) + GetIncomingLetter(c *gin.Context) + ListIncomingLetters(c *gin.Context) + UpdateIncomingLetter(c *gin.Context) + DeleteIncomingLetter(c *gin.Context) + + CreateDispositions(c *gin.Context) + ListDispositionsByLetter(c *gin.Context) + + CreateDiscussion(c *gin.Context) + UpdateDiscussion(c *gin.Context) +} + +type DispositionRouteHandler interface { + Create(c *gin.Context) + Update(c *gin.Context) + Get(c *gin.Context) + ListByFromDept(c *gin.Context) + SetActive(c *gin.Context) +} diff --git a/internal/router/router.go b/internal/router/router.go index c57222b..c2c1de0 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -8,13 +8,16 @@ import ( ) type Router struct { - config *config.Config - authHandler AuthHandler - healthHandler HealthHandler - authMiddleware AuthMiddleware - userHandler UserHandler - fileHandler FileHandler - rbacHandler RBACHandler + config *config.Config + authHandler AuthHandler + healthHandler HealthHandler + authMiddleware AuthMiddleware + userHandler UserHandler + fileHandler FileHandler + rbacHandler RBACHandler + masterHandler MasterHandler + letterHandler LetterHandler + dispRouteHandler DispositionRouteHandler } func NewRouter( @@ -25,15 +28,21 @@ func NewRouter( userHandler UserHandler, fileHandler FileHandler, rbacHandler RBACHandler, + masterHandler MasterHandler, + letterHandler LetterHandler, + dispRouteHandler DispositionRouteHandler, ) *Router { return &Router{ - config: cfg, - authHandler: authHandler, - authMiddleware: authMiddleware, - healthHandler: healthHandler, - userHandler: userHandler, - fileHandler: fileHandler, - rbacHandler: rbacHandler, + config: cfg, + authHandler: authHandler, + authMiddleware: authMiddleware, + healthHandler: healthHandler, + userHandler: userHandler, + fileHandler: fileHandler, + rbacHandler: rbacHandler, + masterHandler: masterHandler, + letterHandler: letterHandler, + dispRouteHandler: dispRouteHandler, } } @@ -88,10 +97,61 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { rbac.POST("/permissions", r.rbacHandler.CreatePermission) rbac.PUT("/permissions/:id", r.rbacHandler.UpdatePermission) rbac.DELETE("/permissions/:id", r.rbacHandler.DeletePermission) + rbac.GET("/roles", r.rbacHandler.ListRoles) rbac.POST("/roles", r.rbacHandler.CreateRole) rbac.PUT("/roles/:id", r.rbacHandler.UpdateRole) rbac.DELETE("/roles/:id", r.rbacHandler.DeleteRole) } + + master := v1.Group("/master") + master.Use(r.authMiddleware.RequireAuth()) + { + master.GET("/labels", r.masterHandler.ListLabels) + master.POST("/labels", r.masterHandler.CreateLabel) + master.PUT("/labels/:id", r.masterHandler.UpdateLabel) + master.DELETE("/labels/:id", r.masterHandler.DeleteLabel) + + master.GET("/priorities", r.masterHandler.ListPriorities) + master.POST("/priorities", r.masterHandler.CreatePriority) + master.PUT("/priorities/:id", r.masterHandler.UpdatePriority) + master.DELETE("/priorities/:id", r.masterHandler.DeletePriority) + + master.GET("/institutions", r.masterHandler.ListInstitutions) + master.POST("/institutions", r.masterHandler.CreateInstitution) + master.PUT("/institutions/:id", r.masterHandler.UpdateInstitution) + master.DELETE("/institutions/:id", r.masterHandler.DeleteInstitution) + + master.GET("/disposition-actions", r.masterHandler.ListDispositionActions) + master.POST("/disposition-actions", r.masterHandler.CreateDispositionAction) + master.PUT("/disposition-actions/:id", r.masterHandler.UpdateDispositionAction) + master.DELETE("/disposition-actions/:id", r.masterHandler.DeleteDispositionAction) + } + + lettersch := v1.Group("/letters") + lettersch.Use(r.authMiddleware.RequireAuth()) + { + lettersch.POST("/incoming", r.letterHandler.CreateIncomingLetter) + lettersch.GET("/incoming/:id", r.letterHandler.GetIncomingLetter) + lettersch.GET("/incoming", r.letterHandler.ListIncomingLetters) + lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter) + lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter) + + lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions) + lettersch.GET("/dispositions/:letter_id", r.letterHandler.ListDispositionsByLetter) + + lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion) + lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion) + } + + droutes := v1.Group("/disposition-routes") + droutes.Use(r.authMiddleware.RequireAuth()) + { + droutes.POST("", r.dispRouteHandler.Create) + droutes.GET(":id", r.dispRouteHandler.Get) + droutes.PUT(":id", r.dispRouteHandler.Update) + droutes.GET("from/:from_department_id", r.dispRouteHandler.ListByFromDept) + droutes.PUT(":id/active", r.dispRouteHandler.SetActive) + } } } diff --git a/internal/service/disposition_route_service.go b/internal/service/disposition_route_service.go new file mode 100644 index 0000000..21f43d8 --- /dev/null +++ b/internal/service/disposition_route_service.go @@ -0,0 +1,70 @@ +package service + +import ( + "context" + + "eslogad-be/internal/contract" + "eslogad-be/internal/entities" + "eslogad-be/internal/repository" + "eslogad-be/internal/transformer" + + "github.com/google/uuid" +) + +type DispositionRouteServiceImpl struct { + repo *repository.DispositionRouteRepository +} + +func NewDispositionRouteService(repo *repository.DispositionRouteRepository) *DispositionRouteServiceImpl { + return &DispositionRouteServiceImpl{repo: repo} +} + +func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) { + entity := &entities.DispositionRoute{FromDepartmentID: req.FromDepartmentID, ToDepartmentID: req.ToDepartmentID} + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } + if req.AllowedActions != nil { + entity.AllowedActions = entities.JSONB(*req.AllowedActions) + } + if err := s.repo.Create(ctx, entity); err != nil { + return nil, err + } + resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0] + return &resp, nil +} +func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) { + entity, err := s.repo.Get(ctx, id) + if err != nil { + return nil, err + } + if req.IsActive != nil { + entity.IsActive = *req.IsActive + } + if req.AllowedActions != nil { + entity.AllowedActions = entities.JSONB(*req.AllowedActions) + } + if err := s.repo.Update(ctx, entity); err != nil { + return nil, err + } + resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0] + return &resp, nil +} +func (s *DispositionRouteServiceImpl) Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error) { + entity, err := s.repo.Get(ctx, id) + if err != nil { + return nil, err + } + resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0] + return &resp, nil +} +func (s *DispositionRouteServiceImpl) ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error) { + list, err := s.repo.ListByFromDept(ctx, from) + if err != nil { + return nil, err + } + return &contract.ListDispositionRoutesResponse{Routes: transformer.DispositionRoutesToContract(list)}, nil +} +func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error { + return s.repo.SetActive(ctx, id, active) +} diff --git a/internal/service/letter_service.go b/internal/service/letter_service.go new file mode 100644 index 0000000..ec6ebeb --- /dev/null +++ b/internal/service/letter_service.go @@ -0,0 +1,63 @@ +package service + +import ( + "context" + + "eslogad-be/internal/contract" + + "github.com/google/uuid" +) + +type LetterProcessor interface { + CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) + GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) + ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) + UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) + SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error + + CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) + ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) + + CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) + UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) +} + +type LetterServiceImpl struct { + processor LetterProcessor +} + +func NewLetterService(processor LetterProcessor) *LetterServiceImpl { + return &LetterServiceImpl{processor: processor} +} + +func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { + return s.processor.CreateIncomingLetter(ctx, req) +} +func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) { + return s.processor.GetIncomingLetterByID(ctx, id) +} +func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) { + return s.processor.ListIncomingLetters(ctx, req) +} +func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) { + return s.processor.UpdateIncomingLetter(ctx, id, req) +} +func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error { + return s.processor.SoftDeleteIncomingLetter(ctx, id) +} + +func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) { + return s.processor.CreateDispositions(ctx, req) +} + +func (s *LetterServiceImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) { + return s.processor.ListDispositionsByLetter(ctx, letterID) +} + +func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { + return s.processor.CreateDiscussion(ctx, letterID, req) +} + +func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { + return s.processor.UpdateDiscussion(ctx, letterID, discussionID, req) +} diff --git a/internal/service/master_service.go b/internal/service/master_service.go new file mode 100644 index 0000000..b4b22ce --- /dev/null +++ b/internal/service/master_service.go @@ -0,0 +1,214 @@ +package service + +import ( + "context" + + "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 +} + +func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository) *MasterServiceImpl { + return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp} +} + +// 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) (*contract.ListInstitutionsResponse, error) { + list, err := s.institutionRepo.List(ctx) + 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 +} diff --git a/internal/transformer/common_transformer.go b/internal/transformer/common_transformer.go index 2e8d524..61ff598 100644 --- a/internal/transformer/common_transformer.go +++ b/internal/transformer/common_transformer.go @@ -190,3 +190,66 @@ func RoleWithPermissionsToContract(role entities.Role, perms []entities.Permissi UpdatedAt: role.UpdatedAt, } } + +func LabelsToContract(list []entities.Label) []contract.LabelResponse { + out := make([]contract.LabelResponse, 0, len(list)) + for _, e := range list { + out = append(out, contract.LabelResponse{ID: e.ID.String(), Name: e.Name, Color: e.Color, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt}) + } + return out +} + +func PrioritiesToContract(list []entities.Priority) []contract.PriorityResponse { + out := make([]contract.PriorityResponse, 0, len(list)) + for _, e := range list { + out = append(out, contract.PriorityResponse{ID: e.ID.String(), Name: e.Name, Level: e.Level, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt}) + } + return out +} + +func InstitutionsToContract(list []entities.Institution) []contract.InstitutionResponse { + out := make([]contract.InstitutionResponse, 0, len(list)) + for _, e := range list { + out = append(out, contract.InstitutionResponse{ID: e.ID.String(), Name: e.Name, Type: string(e.Type), Address: e.Address, ContactPerson: e.ContactPerson, Phone: e.Phone, Email: e.Email, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt}) + } + return out +} + +func DispositionActionsToContract(list []entities.DispositionAction) []contract.DispositionActionResponse { + out := make([]contract.DispositionActionResponse, 0, len(list)) + for _, e := range list { + out = append(out, contract.DispositionActionResponse{ + ID: e.ID.String(), + Code: e.Code, + Label: e.Label, + Description: e.Description, + RequiresNote: e.RequiresNote, + GroupName: e.GroupName, + SortOrder: e.SortOrder, + IsActive: e.IsActive, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + }) + } + return out +} + +func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.DispositionRouteResponse { + out := make([]contract.DispositionRouteResponse, 0, len(list)) + for _, e := range list { + var allowed map[string]interface{} + if e.AllowedActions != nil { + allowed = map[string]interface{}(e.AllowedActions) + } + out = append(out, contract.DispositionRouteResponse{ + ID: e.ID, + FromDepartmentID: e.FromDepartmentID, + ToDepartmentID: e.ToDepartmentID, + IsActive: e.IsActive, + AllowedActions: allowed, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + }) + } + return out +} diff --git a/internal/transformer/letter_transformer.go b/internal/transformer/letter_transformer.go new file mode 100644 index 0000000..f6354cf --- /dev/null +++ b/internal/transformer/letter_transformer.go @@ -0,0 +1,70 @@ +package transformer + +import ( + "eslogad-be/internal/contract" + "eslogad-be/internal/entities" +) + +func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment) *contract.IncomingLetterResponse { + resp := &contract.IncomingLetterResponse{ + ID: e.ID, + LetterNumber: e.LetterNumber, + ReferenceNumber: e.ReferenceNumber, + Subject: e.Subject, + Description: e.Description, + PriorityID: e.PriorityID, + SenderInstitutionID: e.SenderInstitutionID, + ReceivedDate: e.ReceivedDate, + DueDate: e.DueDate, + Status: string(e.Status), + CreatedBy: e.CreatedBy, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)), + } + for _, a := range attachments { + resp.Attachments = append(resp.Attachments, contract.IncomingLetterAttachmentResponse{ + ID: a.ID, + FileURL: a.FileURL, + FileName: a.FileName, + FileType: a.FileType, + UploadedAt: a.UploadedAt, + }) + } + return resp +} + +func DispositionsToContract(list []entities.LetterDisposition) []contract.DispositionResponse { + out := make([]contract.DispositionResponse, 0, len(list)) + for _, d := range list { + out = append(out, contract.DispositionResponse{ + ID: d.ID, + LetterID: d.LetterID, + FromDepartmentID: d.FromDepartmentID, + ToDepartmentID: d.ToDepartmentID, + Notes: d.Notes, + Status: string(d.Status), + CreatedBy: d.CreatedBy, + CreatedAt: d.CreatedAt, + }) + } + return out +} + +func DiscussionEntityToContract(e *entities.LetterDiscussion) *contract.LetterDiscussionResponse { + var mentions map[string]interface{} + if e.Mentions != nil { + mentions = map[string]interface{}(e.Mentions) + } + return &contract.LetterDiscussionResponse{ + ID: e.ID, + LetterID: e.LetterID, + ParentID: e.ParentID, + UserID: e.UserID, + Message: e.Message, + Mentions: mentions, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + EditedAt: e.EditedAt, + } +} diff --git a/migrations/000006_labels_priorities_institutions.down.sql b/migrations/000006_labels_priorities_institutions.down.sql new file mode 100644 index 0000000..a720293 --- /dev/null +++ b/migrations/000006_labels_priorities_institutions.down.sql @@ -0,0 +1,7 @@ +BEGIN; + +DROP TABLE IF EXISTS institutions; +DROP TABLE IF EXISTS priorities; +DROP TABLE IF EXISTS labels; + +COMMIT; \ No newline at end of file diff --git a/migrations/000006_labels_priorities_institutions.up.sql b/migrations/000006_labels_priorities_institutions.up.sql new file mode 100644 index 0000000..6bf3307 --- /dev/null +++ b/migrations/000006_labels_priorities_institutions.up.sql @@ -0,0 +1,52 @@ +BEGIN; + +-- ======================= +-- LABELS +-- ======================= +CREATE TABLE IF NOT EXISTS labels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + color VARCHAR(16), -- HEX color code (e.g., #FF0000) + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER trg_labels_updated_at + BEFORE UPDATE ON labels + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ======================= +-- PRIORITIES +-- ======================= +CREATE TABLE IF NOT EXISTS priorities ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + level INT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER trg_priorities_updated_at + BEFORE UPDATE ON priorities + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ======================= +-- INSTITUTIONS +-- ======================= +CREATE TABLE IF NOT EXISTS institutions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + type TEXT NOT NULL CHECK (type IN ('government','private','ngo','individual')), + address TEXT, + contact_person VARCHAR(255), + phone VARCHAR(50), + email VARCHAR(255), + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER trg_institutions_updated_at + BEFORE UPDATE ON institutions + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +COMMIT; \ No newline at end of file diff --git a/migrations/000007_disposition_actions.down.sql b/migrations/000007_disposition_actions.down.sql new file mode 100644 index 0000000..8a9a41c --- /dev/null +++ b/migrations/000007_disposition_actions.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS disposition_actions; + +COMMIT; \ No newline at end of file diff --git a/migrations/000007_disposition_actions.up.sql b/migrations/000007_disposition_actions.up.sql new file mode 100644 index 0000000..dc8b68e --- /dev/null +++ b/migrations/000007_disposition_actions.up.sql @@ -0,0 +1,23 @@ +BEGIN; + +-- ======================= +-- DISPOSITION ACTIONS +-- ======================= +CREATE TABLE IF NOT EXISTS disposition_actions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code TEXT UNIQUE NOT NULL, + label TEXT NOT NULL, + description TEXT, + requires_note BOOLEAN NOT NULL DEFAULT FALSE, + group_name TEXT, + sort_order INT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER trg_disposition_actions_updated_at + BEFORE UPDATE ON disposition_actions + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +COMMIT; \ No newline at end of file diff --git a/migrations/000008_letters_incoming_suite.down.sql b/migrations/000008_letters_incoming_suite.down.sql new file mode 100644 index 0000000..b6f9a24 --- /dev/null +++ b/migrations/000008_letters_incoming_suite.down.sql @@ -0,0 +1,16 @@ +BEGIN; + +DROP TABLE IF EXISTS letter_incoming_activity_logs; +DROP TABLE IF EXISTS letter_incoming_discussion_attachments; +DROP TABLE IF EXISTS letter_incoming_discussions; +DROP TABLE IF EXISTS letter_disposition_actions; +DROP TABLE IF EXISTS disposition_notes; +DROP TABLE IF EXISTS letter_dispositions; +DROP TABLE IF EXISTS letter_incoming_attachments; +DROP TABLE IF EXISTS letter_incoming_labels; +DROP TABLE IF EXISTS letter_incoming_recipients; +DROP TABLE IF EXISTS letters_incoming; + +DROP SEQUENCE IF EXISTS letters_incoming_seq; + +COMMIT; \ No newline at end of file diff --git a/migrations/000008_letters_incoming_suite.up.sql b/migrations/000008_letters_incoming_suite.up.sql new file mode 100644 index 0000000..e580021 --- /dev/null +++ b/migrations/000008_letters_incoming_suite.up.sql @@ -0,0 +1,189 @@ +BEGIN; + +-- ======================= +-- SEQUENCE FOR LETTER NUMBER +-- ======================= +CREATE SEQUENCE IF NOT EXISTS letters_incoming_seq; + +-- ======================= +-- LETTERS INCOMING +-- ======================= +CREATE TABLE IF NOT EXISTS letters_incoming ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_number TEXT NOT NULL UNIQUE DEFAULT ('IN-' || lpad(nextval('letters_incoming_seq')::text, 8, '0')), + reference_number TEXT, + subject TEXT NOT NULL, + description TEXT, + priority_id UUID REFERENCES priorities(id) ON DELETE SET NULL, + sender_institution_id UUID REFERENCES institutions(id) ON DELETE SET NULL, + received_date DATE NOT NULL, + due_date DATE, + status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','in_progress','completed')), + created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE INDEX IF NOT EXISTS idx_letters_incoming_status ON letters_incoming(status); +CREATE INDEX IF NOT EXISTS idx_letters_incoming_received_date ON letters_incoming(received_date); + +CREATE TRIGGER trg_letters_incoming_updated_at + BEFORE UPDATE ON letters_incoming + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ======================= +-- LETTER INCOMING RECIPIENTS +-- ======================= +CREATE TABLE IF NOT EXISTS letter_incoming_recipients ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE, + recipient_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + recipient_department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','read','completed')), + read_at TIMESTAMP WITHOUT TIME ZONE, + completed_at TIMESTAMP WITHOUT TIME ZONE, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_letter ON letter_incoming_recipients(letter_id); + +-- ======================= +-- LETTER INCOMING LABELS (M:N) +-- ======================= +CREATE TABLE IF NOT EXISTS letter_incoming_labels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE, + label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE (letter_id, label_id) +); + +CREATE INDEX IF NOT EXISTS idx_letter_incoming_labels_letter ON letter_incoming_labels(letter_id); + +-- ======================= +-- LETTER INCOMING ATTACHMENTS +-- ======================= +CREATE TABLE IF NOT EXISTS letter_incoming_attachments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_name TEXT NOT NULL, + file_type TEXT NOT NULL, + uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL, + uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_letter_incoming_attachments_letter ON letter_incoming_attachments(letter_id); + +-- ======================= +-- LETTER DISPOSITIONS +-- ======================= +CREATE TABLE IF NOT EXISTS letter_dispositions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE, + from_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + from_department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + to_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + to_department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + notes TEXT, + status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','read','rejected','completed')), + created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + read_at TIMESTAMP WITHOUT TIME ZONE, + completed_at TIMESTAMP WITHOUT TIME ZONE, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_letter_dispositions_letter ON letter_dispositions(letter_id); + +CREATE TRIGGER trg_letter_dispositions_updated_at + BEFORE UPDATE ON letter_dispositions + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ======================= +-- DISPOSITION NOTES +-- ======================= +CREATE TABLE IF NOT EXISTS disposition_notes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + note TEXT NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_disposition_notes_disposition ON disposition_notes(disposition_id); + +-- ======================= +-- LETTER DISPOSITION ACTIONS (Selections) +-- ======================= +CREATE TABLE IF NOT EXISTS letter_disposition_actions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE, + action_id UUID NOT NULL REFERENCES disposition_actions(id) ON DELETE RESTRICT, + note TEXT, + created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + UNIQUE (disposition_id, action_id) +); + +CREATE INDEX IF NOT EXISTS idx_letter_disposition_actions_disposition ON letter_disposition_actions(disposition_id); + +-- ======================= +-- LETTER INCOMING DISCUSSIONS (Threaded) +-- ======================= +CREATE TABLE IF NOT EXISTS letter_incoming_discussions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE, + parent_id UUID REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + message TEXT NOT NULL, + mentions JSONB, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + edited_at TIMESTAMP WITHOUT TIME ZONE +); + +CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_letter ON letter_incoming_discussions(letter_id); +CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_parent ON letter_incoming_discussions(parent_id); + +CREATE TRIGGER trg_letter_incoming_discussions_updated_at + BEFORE UPDATE ON letter_incoming_discussions + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +-- ======================= +-- LETTER INCOMING DISCUSSION ATTACHMENTS +-- ======================= +CREATE TABLE IF NOT EXISTS letter_incoming_discussion_attachments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + discussion_id UUID NOT NULL REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE, + file_url TEXT NOT NULL, + file_name TEXT NOT NULL, + file_type TEXT NOT NULL, + uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL, + uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussion_attachments_discussion ON letter_incoming_discussion_attachments(discussion_id); + +-- ======================= +-- LETTER INCOMING ACTIVITY LOGS (Immutable) +-- ======================= +CREATE TABLE IF NOT EXISTS letter_incoming_activity_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE, + action_type TEXT NOT NULL, + actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + actor_department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + target_type TEXT, + target_id UUID, + from_status TEXT, + to_status TEXT, + context JSONB, + occurred_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_letter ON letter_incoming_activity_logs(letter_id); +CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_action ON letter_incoming_activity_logs(action_type); + +COMMIT; \ No newline at end of file diff --git a/migrations/000009_disposition_routes.down.sql b/migrations/000009_disposition_routes.down.sql new file mode 100644 index 0000000..edfdb46 --- /dev/null +++ b/migrations/000009_disposition_routes.down.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE IF EXISTS disposition_routes; + +COMMIT; \ No newline at end of file diff --git a/migrations/000009_disposition_routes.up.sql b/migrations/000009_disposition_routes.up.sql new file mode 100644 index 0000000..ef987ea --- /dev/null +++ b/migrations/000009_disposition_routes.up.sql @@ -0,0 +1,27 @@ +BEGIN; + +-- ======================= +-- DISPOSITION ROUTES +-- ======================= +CREATE TABLE IF NOT EXISTS disposition_routes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + from_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE, + to_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + allowed_actions JSONB, + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_disposition_routes_from_dept ON disposition_routes(from_department_id); + +-- Prevent duplicate active routes from -> to +CREATE UNIQUE INDEX IF NOT EXISTS uq_disposition_routes_active + ON disposition_routes(from_department_id, to_department_id) + WHERE is_active = TRUE; + +CREATE TRIGGER trg_disposition_routes_updated_at + BEFORE UPDATE ON disposition_routes + FOR EACH ROW EXECUTE FUNCTION set_updated_at(); + +COMMIT; \ No newline at end of file