Add letter and user id
This commit is contained in:
parent
1964fe50de
commit
f41daa63da
@ -46,6 +46,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
rbacHandler := handler.NewRBACHandler(services.rbacService)
|
rbacHandler := handler.NewRBACHandler(services.rbacService)
|
||||||
masterHandler := handler.NewMasterHandler(services.masterService)
|
masterHandler := handler.NewMasterHandler(services.masterService)
|
||||||
letterHandler := handler.NewLetterHandler(services.letterService)
|
letterHandler := handler.NewLetterHandler(services.letterService)
|
||||||
|
letterOutgoingHandler := handler.NewLetterOutgoingHandler(services.letterOutgoingService)
|
||||||
|
adminApprovalFlowHandler := handler.NewAdminApprovalFlowHandler(services.approvalFlowService)
|
||||||
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
@ -58,6 +60,8 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
rbacHandler,
|
rbacHandler,
|
||||||
masterHandler,
|
masterHandler,
|
||||||
letterHandler,
|
letterHandler,
|
||||||
|
letterOutgoingHandler,
|
||||||
|
adminApprovalFlowHandler,
|
||||||
dispositionRouteHandler,
|
dispositionRouteHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -126,6 +130,15 @@ type repositories struct {
|
|||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
|
// letter outgoing repos
|
||||||
|
letterOutgoingRepo *repository.LetterOutgoingRepository
|
||||||
|
letterOutgoingAttachmentRepo *repository.LetterOutgoingAttachmentRepository
|
||||||
|
letterOutgoingRecipientRepo *repository.LetterOutgoingRecipientRepository
|
||||||
|
letterOutgoingDiscussionRepo *repository.LetterOutgoingDiscussionRepository
|
||||||
|
letterOutgoingDiscussionAttachRepo *repository.LetterOutgoingDiscussionAttachmentRepository
|
||||||
|
letterOutgoingActivityLogRepo *repository.LetterOutgoingActivityLogRepository
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository
|
||||||
|
letterOutgoingApprovalRepo *repository.LetterOutgoingApprovalRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@ -151,6 +164,15 @@ func (a *App) initRepositories() *repositories {
|
|||||||
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
||||||
departmentRepo: repository.NewDepartmentRepository(a.db),
|
departmentRepo: repository.NewDepartmentRepository(a.db),
|
||||||
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
||||||
|
// letter outgoing repos
|
||||||
|
letterOutgoingRepo: repository.NewLetterOutgoingRepository(a.db),
|
||||||
|
letterOutgoingAttachmentRepo: repository.NewLetterOutgoingAttachmentRepository(a.db),
|
||||||
|
letterOutgoingRecipientRepo: repository.NewLetterOutgoingRecipientRepository(a.db),
|
||||||
|
letterOutgoingDiscussionRepo: repository.NewLetterOutgoingDiscussionRepository(a.db),
|
||||||
|
letterOutgoingDiscussionAttachRepo: repository.NewLetterOutgoingDiscussionAttachmentRepository(a.db),
|
||||||
|
letterOutgoingActivityLogRepo: repository.NewLetterOutgoingActivityLogRepository(a.db),
|
||||||
|
approvalFlowRepo: repository.NewApprovalFlowRepository(a.db),
|
||||||
|
letterOutgoingApprovalRepo: repository.NewLetterOutgoingApprovalRepository(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,6 +199,8 @@ type services struct {
|
|||||||
rbacService *service.RBACServiceImpl
|
rbacService *service.RBACServiceImpl
|
||||||
masterService *service.MasterServiceImpl
|
masterService *service.MasterServiceImpl
|
||||||
letterService *service.LetterServiceImpl
|
letterService *service.LetterServiceImpl
|
||||||
|
letterOutgoingService *service.LetterOutgoingServiceImpl
|
||||||
|
approvalFlowService *service.ApprovalFlowServiceImpl
|
||||||
dispositionRouteService *service.DispositionRouteServiceImpl
|
dispositionRouteService *service.DispositionRouteServiceImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +221,28 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
|
|
||||||
letterSvc := service.NewLetterService(processors.letterProcessor)
|
letterSvc := service.NewLetterService(processors.letterProcessor)
|
||||||
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
||||||
|
|
||||||
|
txManager := repository.NewTxManager(a.db)
|
||||||
|
letterOutgoingSvc := service.NewLetterOutgoingService(
|
||||||
|
a.db,
|
||||||
|
repos.letterOutgoingRepo,
|
||||||
|
repos.letterOutgoingAttachmentRepo,
|
||||||
|
repos.letterOutgoingRecipientRepo,
|
||||||
|
repos.letterOutgoingDiscussionRepo,
|
||||||
|
repos.letterOutgoingDiscussionAttachRepo,
|
||||||
|
repos.letterOutgoingActivityLogRepo,
|
||||||
|
repos.approvalFlowRepo,
|
||||||
|
repos.letterOutgoingApprovalRepo,
|
||||||
|
txManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
approvalFlowStepRepo := repository.NewApprovalFlowStepRepository(a.db)
|
||||||
|
approvalFlowSvc := service.NewApprovalFlowService(
|
||||||
|
a.db,
|
||||||
|
repos.approvalFlowRepo,
|
||||||
|
approvalFlowStepRepo,
|
||||||
|
txManager,
|
||||||
|
)
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
userService: userSvc,
|
userService: userSvc,
|
||||||
@ -205,6 +251,8 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
rbacService: rbacSvc,
|
rbacService: rbacSvc,
|
||||||
masterService: masterSvc,
|
masterService: masterSvc,
|
||||||
letterService: letterSvc,
|
letterService: letterSvc,
|
||||||
|
letterOutgoingService: letterOutgoingSvc,
|
||||||
|
approvalFlowService: approvalFlowSvc,
|
||||||
dispositionRouteService: dispRouteSvc,
|
dispositionRouteService: dispRouteSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
213
internal/contract/letter_outgoing_contract.go
Normal file
213
internal/contract/letter_outgoing_contract.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateOutgoingLetterRecipient struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Position *string `json:"position,omitempty"`
|
||||||
|
Institution *string `json:"institution,omitempty"`
|
||||||
|
IsPrimary bool `json:"is_primary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateOutgoingLetterAttachment struct {
|
||||||
|
FileURL string `json:"file_url" validate:"required"`
|
||||||
|
FileName string `json:"file_name" validate:"required"`
|
||||||
|
FileType string `json:"file_type" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateOutgoingLetterRequest struct {
|
||||||
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
|
Subject string `json:"subject" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||||
|
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||||
|
IssueDate time.Time `json:"issue_date" validate:"required"`
|
||||||
|
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
||||||
|
Recipients []CreateOutgoingLetterRecipient `json:"recipients,omitempty"`
|
||||||
|
Attachments []CreateOutgoingLetterAttachment `json:"attachments,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutgoingLetterRecipientResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Position *string `json:"position,omitempty"`
|
||||||
|
Institution *string `json:"institution,omitempty"`
|
||||||
|
IsPrimary bool `json:"is_primary"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutgoingLetterAttachmentResponse 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 OutgoingLetterApprovalResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
StepOrder int `json:"step_order"`
|
||||||
|
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Remarks *string `json:"remarks,omitempty"`
|
||||||
|
ActedAt *time.Time `json:"acted_at,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutgoingLetterResponse 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"`
|
||||||
|
Priority *PriorityResponse `json:"priority,omitempty"`
|
||||||
|
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||||
|
ReceiverInstitution *InstitutionResponse `json:"receiver_institution,omitempty"`
|
||||||
|
IssueDate time.Time `json:"issue_date"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Recipients []OutgoingLetterRecipientResponse `json:"recipients,omitempty"`
|
||||||
|
Attachments []OutgoingLetterAttachmentResponse `json:"attachments,omitempty"`
|
||||||
|
Approvals []OutgoingLetterApprovalResponse `json:"approvals,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateOutgoingLetterRequest struct {
|
||||||
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
|
Subject *string `json:"subject,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||||
|
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||||
|
IssueDate *time.Time `json:"issue_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOutgoingLettersRequest struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
Query *string `json:"query,omitempty"`
|
||||||
|
CreatedBy *uuid.UUID `json:"created_by,omitempty"`
|
||||||
|
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||||
|
FromDate *time.Time `json:"from_date,omitempty"`
|
||||||
|
ToDate *time.Time `json:"to_date,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOutgoingLettersResponse struct {
|
||||||
|
Items []*OutgoingLetterResponse `json:"items"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApproveLetterRequest struct {
|
||||||
|
Remarks *string `json:"remarks,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RejectLetterRequest struct {
|
||||||
|
Reason string `json:"reason" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddRecipientsRequest struct {
|
||||||
|
Recipients []CreateOutgoingLetterRecipient `json:"recipients" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRecipientRequest struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
Position *string `json:"position,omitempty"`
|
||||||
|
Institution *string `json:"institution,omitempty"`
|
||||||
|
IsPrimary bool `json:"is_primary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddAttachmentsRequest struct {
|
||||||
|
Attachments []CreateOutgoingLetterAttachment `json:"attachments" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDiscussionAttachment struct {
|
||||||
|
FileURL string `json:"file_url" validate:"required"`
|
||||||
|
FileName string `json:"file_name" validate:"required"`
|
||||||
|
FileType string `json:"file_type" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDiscussionRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Message string `json:"message" validate:"required"`
|
||||||
|
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
||||||
|
Attachments []CreateDiscussionAttachment `json:"attachments,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDiscussionRequest struct {
|
||||||
|
Message string `json:"message" validate:"required"`
|
||||||
|
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscussionResponse struct {
|
||||||
|
ID uuid.UUID `json:"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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalFlowRequest struct {
|
||||||
|
DepartmentID uuid.UUID `json:"department_id" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Steps []ApprovalFlowStepRequest `json:"steps" validate:"required,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalFlowStepRequest struct {
|
||||||
|
StepOrder int `json:"step_order" validate:"required,min=1"`
|
||||||
|
ParallelGroup int `json:"parallel_group" validate:"min=1"`
|
||||||
|
ApproverRoleID *uuid.UUID `json:"approver_role_id,omitempty"`
|
||||||
|
ApproverUserID *uuid.UUID `json:"approver_user_id,omitempty"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalFlowResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
DepartmentID uuid.UUID `json:"department_id"`
|
||||||
|
Department *DepartmentResponse `json:"department,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
Steps []ApprovalFlowStepResponse `json:"steps,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalFlowStepResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
StepOrder int `json:"step_order"`
|
||||||
|
ParallelGroup int `json:"parallel_group"`
|
||||||
|
ApproverRoleID *uuid.UUID `json:"approver_role_id,omitempty"`
|
||||||
|
ApproverRole *RoleResponse `json:"approver_role,omitempty"`
|
||||||
|
ApproverUserID *uuid.UUID `json:"approver_user_id,omitempty"`
|
||||||
|
ApproverUser *UserResponse `json:"approver_user,omitempty"`
|
||||||
|
Required bool `json:"required"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListApprovalFlowsRequest struct {
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Offset int `json:"offset"`
|
||||||
|
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListApprovalFlowsResponse struct {
|
||||||
|
Items []*ApprovalFlowResponse `json:"items"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
68
internal/entities/approval_flow.go
Normal file
68
internal/entities/approval_flow.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalFlow struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Department *Department `gorm:"foreignKey:DepartmentID" json:"department,omitempty"`
|
||||||
|
Steps []ApprovalFlowStep `gorm:"foreignKey:FlowID" json:"steps,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ApprovalFlow) TableName() string { return "approval_flows" }
|
||||||
|
|
||||||
|
type ApprovalFlowStep struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
FlowID uuid.UUID `gorm:"type:uuid;not null" json:"flow_id"`
|
||||||
|
StepOrder int `gorm:"not null" json:"step_order"`
|
||||||
|
ParallelGroup int `gorm:"default:1" json:"parallel_group"`
|
||||||
|
ApproverRoleID *uuid.UUID `json:"approver_role_id,omitempty"`
|
||||||
|
ApproverUserID *uuid.UUID `json:"approver_user_id,omitempty"`
|
||||||
|
Required bool `gorm:"default:true" json:"required"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
ApprovalFlow *ApprovalFlow `gorm:"foreignKey:FlowID" json:"approval_flow,omitempty"`
|
||||||
|
ApproverRole *Role `gorm:"foreignKey:ApproverRoleID" json:"approver_role,omitempty"`
|
||||||
|
ApproverUser *User `gorm:"foreignKey:ApproverUserID" json:"approver_user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ApprovalFlowStep) TableName() string { return "approval_flow_steps" }
|
||||||
|
|
||||||
|
type ApprovalStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApprovalStatusPending ApprovalStatus = "pending"
|
||||||
|
ApprovalStatusApproved ApprovalStatus = "approved"
|
||||||
|
ApprovalStatusRejected ApprovalStatus = "rejected"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoingApproval 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"`
|
||||||
|
StepID uuid.UUID `gorm:"type:uuid;not null" json:"step_id"`
|
||||||
|
ApproverID *uuid.UUID `json:"approver_id,omitempty"`
|
||||||
|
Status ApprovalStatus `gorm:"not null;default:'pending'" json:"status"`
|
||||||
|
Remarks *string `json:"remarks,omitempty"`
|
||||||
|
ActedAt *time.Time `json:"acted_at,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Letter *LetterOutgoing `gorm:"foreignKey:LetterID" json:"letter,omitempty"`
|
||||||
|
Step *ApprovalFlowStep `gorm:"foreignKey:StepID" json:"step,omitempty"`
|
||||||
|
Approver *User `gorm:"foreignKey:ApproverID" json:"approver,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterOutgoingApproval) TableName() string { return "letter_outgoing_approvals" }
|
||||||
105
internal/entities/letter_outgoing.go
Normal file
105
internal/entities/letter_outgoing.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoingStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LetterOutgoingStatusDraft LetterOutgoingStatus = "draft"
|
||||||
|
LetterOutgoingStatusPendingApproval LetterOutgoingStatus = "pending_approval"
|
||||||
|
LetterOutgoingStatusApproved LetterOutgoingStatus = "approved"
|
||||||
|
LetterOutgoingStatusSent LetterOutgoingStatus = "sent"
|
||||||
|
LetterOutgoingStatusArchived LetterOutgoingStatus = "archived"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoing 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"`
|
||||||
|
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||||
|
IssueDate time.Time `json:"issue_date"`
|
||||||
|
Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"`
|
||||||
|
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
||||||
|
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"`
|
||||||
|
DeletedAt *time.Time `gorm:"index" json:"deleted_at,omitempty"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Priority *Priority `gorm:"foreignKey:PriorityID" json:"priority,omitempty"`
|
||||||
|
ReceiverInstitution *Institution `gorm:"foreignKey:ReceiverInstitutionID" json:"receiver_institution,omitempty"`
|
||||||
|
Creator *User `gorm:"foreignKey:CreatedBy" json:"creator,omitempty"`
|
||||||
|
ApprovalFlow *ApprovalFlow `gorm:"foreignKey:ApprovalFlowID" json:"approval_flow,omitempty"`
|
||||||
|
Recipients []LetterOutgoingRecipient `gorm:"foreignKey:LetterID" json:"recipients,omitempty"`
|
||||||
|
Attachments []LetterOutgoingAttachment `gorm:"foreignKey:LetterID" json:"attachments,omitempty"`
|
||||||
|
Approvals []LetterOutgoingApproval `gorm:"foreignKey:LetterID" json:"approvals,omitempty"`
|
||||||
|
Discussions []LetterOutgoingDiscussion `gorm:"foreignKey:LetterID" json:"discussions,omitempty"`
|
||||||
|
ActivityLogs []LetterOutgoingActivityLog `gorm:"foreignKey:LetterID" json:"activity_logs,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterOutgoing) TableName() string { return "letters_outgoing" }
|
||||||
|
|
||||||
|
type LetterOutgoingRecipient 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"`
|
||||||
|
RecipientName string `gorm:"not null" json:"recipient_name"`
|
||||||
|
RecipientEmail *string `json:"recipient_email,omitempty"`
|
||||||
|
RecipientPosition *string `json:"recipient_position,omitempty"`
|
||||||
|
RecipientInstitution *string `json:"recipient_institution,omitempty"`
|
||||||
|
IsPrimary bool `gorm:"default:false" json:"is_primary"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterOutgoingRecipient) TableName() string { return "letter_outgoing_recipients" }
|
||||||
|
|
||||||
|
type LetterOutgoingAttachment 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 (LetterOutgoingAttachment) TableName() string { return "letter_outgoing_attachments" }
|
||||||
|
|
||||||
|
type LetterOutgoingDiscussion 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:"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"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
Attachments []LetterOutgoingDiscussionAttachment `gorm:"foreignKey:DiscussionID" json:"attachments,omitempty"`
|
||||||
|
Replies []LetterOutgoingDiscussion `gorm:"foreignKey:ParentID" json:"replies,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterOutgoingDiscussion) TableName() string { return "letter_outgoing_discussions" }
|
||||||
|
|
||||||
|
type LetterOutgoingDiscussionAttachment struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
DiscussionID uuid.UUID `gorm:"type:uuid;not null" json:"discussion_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 (LetterOutgoingDiscussionAttachment) TableName() string {
|
||||||
|
return "letter_outgoing_discussion_attachments"
|
||||||
|
}
|
||||||
46
internal/entities/letter_outgoing_activity_log.go
Normal file
46
internal/entities/letter_outgoing_activity_log.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoingActivityLog 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"`
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
Letter *LetterOutgoing `gorm:"foreignKey:LetterID" json:"letter,omitempty"`
|
||||||
|
ActorUser *User `gorm:"foreignKey:ActorUserID" json:"actor_user,omitempty"`
|
||||||
|
ActorDepartment *Department `gorm:"foreignKey:ActorDepartmentID" json:"actor_department,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterOutgoingActivityLog) TableName() string { return "letter_outgoing_activity_logs" }
|
||||||
|
|
||||||
|
// Action types for letter outgoing activity logs
|
||||||
|
const (
|
||||||
|
LetterOutgoingActionCreated = "created"
|
||||||
|
LetterOutgoingActionUpdated = "updated"
|
||||||
|
LetterOutgoingActionDeleted = "deleted"
|
||||||
|
LetterOutgoingActionStatusChanged = "status_changed"
|
||||||
|
LetterOutgoingActionSubmittedApproval = "submitted_for_approval"
|
||||||
|
LetterOutgoingActionApproved = "approved"
|
||||||
|
LetterOutgoingActionRejected = "rejected"
|
||||||
|
LetterOutgoingActionSent = "sent"
|
||||||
|
LetterOutgoingActionArchived = "archived"
|
||||||
|
LetterOutgoingActionAttachmentAdded = "attachment_added"
|
||||||
|
LetterOutgoingActionAttachmentRemoved = "attachment_removed"
|
||||||
|
LetterOutgoingActionRecipientAdded = "recipient_added"
|
||||||
|
LetterOutgoingActionRecipientRemoved = "recipient_removed"
|
||||||
|
LetterOutgoingActionDiscussionAdded = "discussion_added"
|
||||||
|
)
|
||||||
344
internal/handler/admin_approval_flow_handler.go
Normal file
344
internal/handler/admin_approval_flow_handler.go
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalFlowService interface {
|
||||||
|
CreateApprovalFlow(ctx context.Context, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
||||||
|
GetApprovalFlow(ctx context.Context, id uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
||||||
|
GetApprovalFlowByDepartment(ctx context.Context, departmentID uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
||||||
|
UpdateApprovalFlow(ctx context.Context, id uuid.UUID, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
||||||
|
DeleteApprovalFlow(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListApprovalFlows(ctx context.Context, req *contract.ListApprovalFlowsRequest) (*contract.ListApprovalFlowsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminApprovalFlowHandler struct {
|
||||||
|
svc ApprovalFlowService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAdminApprovalFlowHandler(svc ApprovalFlowService) *AdminApprovalFlowHandler {
|
||||||
|
return &AdminApprovalFlowHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) CreateApprovalFlow(c *gin.Context) {
|
||||||
|
var req contract.ApprovalFlowRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that at least one step is provided
|
||||||
|
if len(req.Steps) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "at least one approval step is required", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each step has either a role or user as approver
|
||||||
|
for i, step := range req.Steps {
|
||||||
|
if step.ApproverRoleID == nil && step.ApproverUserID == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
||||||
|
Error: "step " + strconv.Itoa(i+1) + " must have either approver_role_id or approver_user_id",
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if step.ApproverRoleID != nil && step.ApproverUserID != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
||||||
|
Error: "step " + strconv.Itoa(i+1) + " cannot have both approver_role_id and approver_user_id",
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.CreateApprovalFlow(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) GetApprovalFlow(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) GetApprovalFlowByDepartment(c *gin.Context) {
|
||||||
|
departmentID, err := uuid.Parse(c.Param("department_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid department_id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.GetApprovalFlowByDepartment(c.Request.Context(), departmentID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) UpdateApprovalFlow(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.ApprovalFlowRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that at least one step is provided
|
||||||
|
if len(req.Steps) == 0 {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "at least one approval step is required", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each step has either a role or user as approver
|
||||||
|
for i, step := range req.Steps {
|
||||||
|
if step.ApproverRoleID == nil && step.ApproverUserID == nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
||||||
|
Error: "step " + strconv.Itoa(i+1) + " must have either approver_role_id or approver_user_id",
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if step.ApproverRoleID != nil && step.ApproverUserID != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{
|
||||||
|
Error: "step " + strconv.Itoa(i+1) + " cannot have both approver_role_id and approver_user_id",
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.UpdateApprovalFlow(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) DeleteApprovalFlow(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.DeleteApprovalFlow(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "approval flow deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) ListApprovalFlows(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
|
departmentIDStr := c.Query("department_id")
|
||||||
|
isActiveStr := c.Query("is_active")
|
||||||
|
|
||||||
|
var departmentID *uuid.UUID
|
||||||
|
var isActive *bool
|
||||||
|
|
||||||
|
if departmentIDStr != "" {
|
||||||
|
if id, err := uuid.Parse(departmentIDStr); err == nil {
|
||||||
|
departmentID = &id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActiveStr != "" {
|
||||||
|
if isActiveStr == "true" {
|
||||||
|
active := true
|
||||||
|
isActive = &active
|
||||||
|
} else if isActiveStr == "false" {
|
||||||
|
active := false
|
||||||
|
isActive = &active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &contract.ListApprovalFlowsRequest{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
DepartmentID: departmentID,
|
||||||
|
IsActive: isActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.ListApprovalFlows(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) ActivateApprovalFlow(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current flow
|
||||||
|
flow, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the IsActive field
|
||||||
|
req := &contract.ApprovalFlowRequest{
|
||||||
|
DepartmentID: flow.DepartmentID,
|
||||||
|
Name: flow.Name,
|
||||||
|
Description: flow.Description,
|
||||||
|
IsActive: true,
|
||||||
|
Steps: make([]contract.ApprovalFlowStepRequest, len(flow.Steps)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy existing steps
|
||||||
|
for i, step := range flow.Steps {
|
||||||
|
req.Steps[i] = contract.ApprovalFlowStepRequest{
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
ParallelGroup: step.ParallelGroup,
|
||||||
|
ApproverRoleID: step.ApproverRoleID,
|
||||||
|
ApproverUserID: step.ApproverUserID,
|
||||||
|
Required: step.Required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.UpdateApprovalFlow(c.Request.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) DeactivateApprovalFlow(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current flow
|
||||||
|
flow, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update only the IsActive field
|
||||||
|
req := &contract.ApprovalFlowRequest{
|
||||||
|
DepartmentID: flow.DepartmentID,
|
||||||
|
Name: flow.Name,
|
||||||
|
Description: flow.Description,
|
||||||
|
IsActive: false,
|
||||||
|
Steps: make([]contract.ApprovalFlowStepRequest, len(flow.Steps)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy existing steps
|
||||||
|
for i, step := range flow.Steps {
|
||||||
|
req.Steps[i] = contract.ApprovalFlowStepRequest{
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
ParallelGroup: step.ParallelGroup,
|
||||||
|
ApproverRoleID: step.ApproverRoleID,
|
||||||
|
ApproverUserID: step.ApproverUserID,
|
||||||
|
Required: step.Required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.UpdateApprovalFlow(c.Request.Context(), id, req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *AdminApprovalFlowHandler) CloneApprovalFlow(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneReq struct {
|
||||||
|
DepartmentID uuid.UUID `json:"department_id" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&cloneReq); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the source flow
|
||||||
|
sourceFlow, err := h.svc.GetApprovalFlow(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new flow request with cloned data
|
||||||
|
req := &contract.ApprovalFlowRequest{
|
||||||
|
DepartmentID: cloneReq.DepartmentID,
|
||||||
|
Name: cloneReq.Name,
|
||||||
|
Description: sourceFlow.Description,
|
||||||
|
IsActive: false, // New cloned flow starts as inactive
|
||||||
|
Steps: make([]contract.ApprovalFlowStepRequest, len(sourceFlow.Steps)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy steps from source flow
|
||||||
|
for i, step := range sourceFlow.Steps {
|
||||||
|
req.Steps[i] = contract.ApprovalFlowStepRequest{
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
ParallelGroup: step.ParallelGroup,
|
||||||
|
ApproverRoleID: step.ApproverRoleID,
|
||||||
|
ApproverUserID: step.ApproverUserID,
|
||||||
|
Required: step.Required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.CreateApprovalFlow(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
420
internal/handler/letter_outgoing_handler.go
Normal file
420
internal/handler/letter_outgoing_handler.go
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoingService interface {
|
||||||
|
CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||||
|
GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error)
|
||||||
|
ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error)
|
||||||
|
UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||||
|
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
SubmitForApproval(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error
|
||||||
|
RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error
|
||||||
|
SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
|
||||||
|
AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error
|
||||||
|
UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error
|
||||||
|
RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error
|
||||||
|
|
||||||
|
AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
|
||||||
|
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
||||||
|
|
||||||
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
|
||||||
|
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
|
||||||
|
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingHandler struct {
|
||||||
|
svc LetterOutgoingService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterOutgoingHandler(svc LetterOutgoingService) *LetterOutgoingHandler {
|
||||||
|
return &LetterOutgoingHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) CreateOutgoingLetter(c *gin.Context) {
|
||||||
|
var req contract.CreateOutgoingLetterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.CreateOutgoingLetter(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) GetOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.GetOutgoingLetterByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) ListOutgoingLetters(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
|
status := c.Query("status")
|
||||||
|
query := c.Query("q")
|
||||||
|
createdByStr := c.Query("created_by")
|
||||||
|
receiverInstitutionStr := c.Query("receiver_institution_id")
|
||||||
|
|
||||||
|
var statusPtr *string
|
||||||
|
var queryPtr *string
|
||||||
|
var createdByPtr *uuid.UUID
|
||||||
|
var receiverInstitutionPtr *uuid.UUID
|
||||||
|
|
||||||
|
if status != "" {
|
||||||
|
statusPtr = &status
|
||||||
|
}
|
||||||
|
if query != "" {
|
||||||
|
queryPtr = &query
|
||||||
|
}
|
||||||
|
if createdByStr != "" {
|
||||||
|
if createdBy, err := uuid.Parse(createdByStr); err == nil {
|
||||||
|
createdByPtr = &createdBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if receiverInstitutionStr != "" {
|
||||||
|
if receiverInstitution, err := uuid.Parse(receiverInstitutionStr); err == nil {
|
||||||
|
receiverInstitutionPtr = &receiverInstitution
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &contract.ListOutgoingLettersRequest{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: offset,
|
||||||
|
Status: statusPtr,
|
||||||
|
Query: queryPtr,
|
||||||
|
CreatedBy: createdByPtr,
|
||||||
|
ReceiverInstitutionID: receiverInstitutionPtr,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.ListOutgoingLetters(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) UpdateOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdateOutgoingLetterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.UpdateOutgoingLetter(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) DeleteOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.DeleteOutgoingLetter(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) SubmitForApproval(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.SubmitForApproval(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "submitted for approval"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) ApproveOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.ApproveLetterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.ApproveOutgoingLetter(c.Request.Context(), id, &req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "approved"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) RejectOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.RejectLetterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RejectOutgoingLetter(c.Request.Context(), id, &req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "rejected"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) SendOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.SendOutgoingLetter(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "sent"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) ArchiveOutgoingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.ArchiveOutgoingLetter(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "archived"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) AddRecipients(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.AddRecipientsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.AddRecipients(c.Request.Context(), id, &req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "recipients added"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) UpdateRecipient(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipientID, err := uuid.Parse(c.Param("recipient_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid recipient id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdateRecipientRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.UpdateRecipient(c.Request.Context(), id, recipientID, &req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "recipient updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) RemoveRecipient(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
recipientID, err := uuid.Parse(c.Param("recipient_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid recipient id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RemoveRecipient(c.Request.Context(), id, recipientID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "recipient removed"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) AddAttachments(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.AddAttachmentsRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.AddAttachments(c.Request.Context(), id, &req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachments added"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) RemoveAttachment(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentID, err := uuid.Parse(c.Param("attachment_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid attachment id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.RemoveAttachment(c.Request.Context(), id, attachmentID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "attachment removed"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) CreateDiscussion(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid letter id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.CreateDiscussionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.CreateDiscussion(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) UpdateDiscussion(c *gin.Context) {
|
||||||
|
discussionID, err := uuid.Parse(c.Param("discussion_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid discussion id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdateDiscussionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.UpdateDiscussion(c.Request.Context(), discussionID, &req); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "discussion updated"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterOutgoingHandler) DeleteDiscussion(c *gin.Context) {
|
||||||
|
discussionID, err := uuid.Parse(c.Param("discussion_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid discussion id", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svc.DeleteDiscussion(c.Request.Context(), discussionID); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "discussion deleted"})
|
||||||
|
}
|
||||||
@ -296,6 +296,33 @@ func (h *UserHandler) ListTitles(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, titles)
|
c.JSON(http.StatusOK, titles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) GetUserProfile(c *gin.Context) {
|
||||||
|
userIDStr := c.Param("id")
|
||||||
|
userID, err := uuid.Parse(userIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::GetUserProfile -> Invalid user ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid user ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validationError, validationErrorCode := h.userValidator.ValidateUserID(userID)
|
||||||
|
if validationError != nil {
|
||||||
|
logger.FromContext(c).WithError(validationError).Error("UserHandler::GetUserProfile -> user ID validation failed")
|
||||||
|
h.sendValidationErrorResponse(c, validationError.Error(), validationErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile, err := h.userService.GetProfile(c.Request.Context(), userID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::GetUserProfile -> Failed to get user profile from service")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("UserHandler::GetUserProfile -> Successfully retrieved user profile for user ID = %s", userID)
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(profile))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
|
func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
|
||||||
search := c.Query("search")
|
search := c.Query("search")
|
||||||
limitStr := c.DefaultQuery("limit", "50")
|
limitStr := c.DefaultQuery("limit", "50")
|
||||||
@ -307,7 +334,7 @@ func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
|
|||||||
if limit > 100 {
|
if limit > 100 {
|
||||||
limit = 100
|
limit = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchPtr *string
|
var searchPtr *string
|
||||||
if search != "" {
|
if search != "" {
|
||||||
searchPtr = &search
|
searchPtr = &search
|
||||||
|
|||||||
229
internal/repository/approval_flow_repository.go
Normal file
229
internal/repository/approval_flow_repository.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalFlowRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewApprovalFlowRepository(db *gorm.DB) *ApprovalFlowRepository {
|
||||||
|
return &ApprovalFlowRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowRepository) Create(ctx context.Context, e *entities.ApprovalFlow) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowRepository) Get(ctx context.Context, id uuid.UUID) (*entities.ApprovalFlow, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.ApprovalFlow
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("Steps", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("step_order ASC, parallel_group ASC")
|
||||||
|
}).
|
||||||
|
Preload("Steps.ApproverRole").
|
||||||
|
Preload("Steps.ApproverUser").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowRepository) GetByDepartment(ctx context.Context, departmentID uuid.UUID) (*entities.ApprovalFlow, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.ApprovalFlow
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("Steps", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("step_order ASC, parallel_group ASC")
|
||||||
|
}).
|
||||||
|
Preload("Steps.ApproverRole").
|
||||||
|
Preload("Steps.ApproverUser").
|
||||||
|
Where("department_id = ? AND is_active = true", departmentID).
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowRepository) Update(ctx context.Context, e *entities.ApprovalFlow) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.ApprovalFlow{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.ApprovalFlow{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListApprovalFlowsFilter struct {
|
||||||
|
DepartmentID *uuid.UUID
|
||||||
|
IsActive *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowRepository) List(ctx context.Context, filter ListApprovalFlowsFilter, limit, offset int) ([]entities.ApprovalFlow, int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
query := db.WithContext(ctx).Model(&entities.ApprovalFlow{})
|
||||||
|
|
||||||
|
if filter.DepartmentID != nil {
|
||||||
|
query = query.Where("department_id = ?", *filter.DepartmentID)
|
||||||
|
}
|
||||||
|
if filter.IsActive != nil {
|
||||||
|
query = query.Where("is_active = ?", *filter.IsActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []entities.ApprovalFlow
|
||||||
|
if err := query.
|
||||||
|
Preload("Department").
|
||||||
|
Preload("Steps", func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Order("step_order ASC, parallel_group ASC")
|
||||||
|
}).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalFlowStepRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewApprovalFlowStepRepository(db *gorm.DB) *ApprovalFlowStepRepository {
|
||||||
|
return &ApprovalFlowStepRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowStepRepository) Create(ctx context.Context, e *entities.ApprovalFlowStep) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowStepRepository) CreateBulk(ctx context.Context, list []entities.ApprovalFlowStep) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowStepRepository) Update(ctx context.Context, e *entities.ApprovalFlowStep) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.ApprovalFlowStep{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowStepRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.ApprovalFlowStep{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowStepRepository) DeleteByFlow(ctx context.Context, flowID uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("flow_id = ?", flowID).Delete(&entities.ApprovalFlowStep{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ApprovalFlowStepRepository) ListByFlow(ctx context.Context, flowID uuid.UUID) ([]entities.ApprovalFlowStep, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.ApprovalFlowStep
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("ApproverRole").
|
||||||
|
Preload("ApproverUser").
|
||||||
|
Where("flow_id = ?", flowID).
|
||||||
|
Order("step_order ASC, parallel_group ASC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingApprovalRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingApprovalRepository(db *gorm.DB) *LetterOutgoingApprovalRepository {
|
||||||
|
return &LetterOutgoingApprovalRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) Create(ctx context.Context, e *entities.LetterOutgoingApproval) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingApproval) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingApproval, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterOutgoingApproval
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Letter").
|
||||||
|
Preload("Step").
|
||||||
|
Preload("Approver").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) GetByLetterAndStep(ctx context.Context, letterID, stepID uuid.UUID) (*entities.LetterOutgoingApproval, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterOutgoingApproval
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_id = ? AND step_id = ?", letterID, stepID).
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) Update(ctx context.Context, e *entities.LetterOutgoingApproval) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterOutgoingApproval{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingApproval, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterOutgoingApproval
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Step.ApproverRole").
|
||||||
|
Preload("Step.ApproverUser").
|
||||||
|
Preload("Approver").
|
||||||
|
Where("letter_id = ?", letterID).
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingApprovalRepository) GetPendingApprovals(ctx context.Context, userID uuid.UUID) ([]entities.LetterOutgoingApproval, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterOutgoingApproval
|
||||||
|
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Letter").
|
||||||
|
Preload("Step").
|
||||||
|
Joins("JOIN approval_flow_steps afs ON afs.id = letter_outgoing_approvals.step_id").
|
||||||
|
Where("letter_outgoing_approvals.status = ? AND (afs.approver_user_id = ? OR afs.approver_role_id IN (SELECT role_id FROM user_roles WHERE user_id = ?))",
|
||||||
|
entities.ApprovalStatusPending, userID, userID).
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
275
internal/repository/letter_outgoing_repository.go
Normal file
275
internal/repository/letter_outgoing_repository.go
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoingRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingRepository(db *gorm.DB) *LetterOutgoingRepository {
|
||||||
|
return &LetterOutgoingRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) Create(ctx context.Context, e *entities.LetterOutgoing) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterOutgoing
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("Priority").
|
||||||
|
Preload("ReceiverInstitution").
|
||||||
|
Preload("Creator").
|
||||||
|
Preload("ApprovalFlow").
|
||||||
|
Preload("Recipients").
|
||||||
|
Preload("Attachments").
|
||||||
|
Preload("Approvals.Step").
|
||||||
|
Preload("Approvals.Approver").
|
||||||
|
Where("id = ? AND deleted_at IS NULL", id).
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) Update(ctx context.Context, e *entities.LetterOutgoing) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) SoftDelete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("deleted_at", now).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOutgoingLettersFilter struct {
|
||||||
|
Status *string
|
||||||
|
Query *string
|
||||||
|
CreatedBy *uuid.UUID
|
||||||
|
ReceiverInstitutionID *uuid.UUID
|
||||||
|
FromDate *time.Time
|
||||||
|
ToDate *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).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 ? OR letter_number ILIKE ?", q, q, q)
|
||||||
|
}
|
||||||
|
if filter.CreatedBy != nil {
|
||||||
|
query = query.Where("created_by = ?", *filter.CreatedBy)
|
||||||
|
}
|
||||||
|
if filter.ReceiverInstitutionID != nil {
|
||||||
|
query = query.Where("receiver_institution_id = ?", *filter.ReceiverInstitutionID)
|
||||||
|
}
|
||||||
|
if filter.FromDate != nil {
|
||||||
|
query = query.Where("issue_date >= ?", *filter.FromDate)
|
||||||
|
}
|
||||||
|
if filter.ToDate != nil {
|
||||||
|
query = query.Where("issue_date <= ?", *filter.ToDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var list []entities.LetterOutgoing
|
||||||
|
if err := query.
|
||||||
|
Preload("Priority").
|
||||||
|
Preload("ReceiverInstitution").
|
||||||
|
Preload("Creator").
|
||||||
|
Order("created_at DESC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.LetterOutgoingStatus) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("status", status).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingAttachmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingAttachmentRepository(db *gorm.DB) *LetterOutgoingAttachmentRepository {
|
||||||
|
return &LetterOutgoingAttachmentRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingAttachmentRepository) Create(ctx context.Context, e *entities.LetterOutgoingAttachment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingAttachment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingAttachment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterOutgoingAttachment
|
||||||
|
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingAttachmentRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingAttachment{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingRecipientRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingRecipientRepository(db *gorm.DB) *LetterOutgoingRecipientRepository {
|
||||||
|
return &LetterOutgoingRecipientRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) Create(ctx context.Context, e *entities.LetterOutgoingRecipient) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingRecipient) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingRecipient, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterOutgoingRecipient
|
||||||
|
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("is_primary DESC, created_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) Update(ctx context.Context, e *entities.LetterOutgoingRecipient) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterOutgoingRecipient{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingRecipient{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingRecipientRepository) DeleteByLetter(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("letter_id = ?", letterID).Delete(&entities.LetterOutgoingRecipient{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingDiscussionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingDiscussionRepository(db *gorm.DB) *LetterOutgoingDiscussionRepository {
|
||||||
|
return &LetterOutgoingDiscussionRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingDiscussionRepository) Create(ctx context.Context, e *entities.LetterOutgoingDiscussion) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingDiscussionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoingDiscussion, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterOutgoingDiscussion
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Attachments").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&e).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingDiscussionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingDiscussion, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterOutgoingDiscussion
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("User").
|
||||||
|
Preload("Attachments").
|
||||||
|
Preload("Replies.User").
|
||||||
|
Where("letter_id = ? AND parent_id IS NULL", letterID).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingDiscussionRepository) Update(ctx context.Context, e *entities.LetterOutgoingDiscussion) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
now := time.Now()
|
||||||
|
e.EditedAt = &now
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterOutgoingDiscussion{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingDiscussionRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Where("id = ?", id).Delete(&entities.LetterOutgoingDiscussion{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingDiscussionAttachmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingDiscussionAttachmentRepository(db *gorm.DB) *LetterOutgoingDiscussionAttachmentRepository {
|
||||||
|
return &LetterOutgoingDiscussionAttachmentRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingDiscussionAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterOutgoingDiscussionAttachment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingActivityLogRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterOutgoingActivityLogRepository(db *gorm.DB) *LetterOutgoingActivityLogRepository {
|
||||||
|
return &LetterOutgoingActivityLogRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingActivityLogRepository) Create(ctx context.Context, e *entities.LetterOutgoingActivityLog) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterOutgoingActivityLogRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterOutgoingActivityLog, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterOutgoingActivityLog
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("ActorUser").
|
||||||
|
Preload("ActorDepartment").
|
||||||
|
Where("letter_id = ?", letterID).
|
||||||
|
Order("occurred_at DESC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ type HealthHandler interface {
|
|||||||
type UserHandler interface {
|
type UserHandler interface {
|
||||||
ListUsers(c *gin.Context)
|
ListUsers(c *gin.Context)
|
||||||
GetProfile(c *gin.Context)
|
GetProfile(c *gin.Context)
|
||||||
|
GetUserProfile(c *gin.Context)
|
||||||
UpdateProfile(c *gin.Context)
|
UpdateProfile(c *gin.Context)
|
||||||
ChangePassword(c *gin.Context)
|
ChangePassword(c *gin.Context)
|
||||||
ListTitles(c *gin.Context)
|
ListTitles(c *gin.Context)
|
||||||
@ -70,6 +71,43 @@ type LetterHandler interface {
|
|||||||
UpdateDiscussion(c *gin.Context)
|
UpdateDiscussion(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingHandler interface {
|
||||||
|
CreateOutgoingLetter(c *gin.Context)
|
||||||
|
GetOutgoingLetter(c *gin.Context)
|
||||||
|
ListOutgoingLetters(c *gin.Context)
|
||||||
|
UpdateOutgoingLetter(c *gin.Context)
|
||||||
|
DeleteOutgoingLetter(c *gin.Context)
|
||||||
|
|
||||||
|
SubmitForApproval(c *gin.Context)
|
||||||
|
ApproveOutgoingLetter(c *gin.Context)
|
||||||
|
RejectOutgoingLetter(c *gin.Context)
|
||||||
|
SendOutgoingLetter(c *gin.Context)
|
||||||
|
ArchiveOutgoingLetter(c *gin.Context)
|
||||||
|
|
||||||
|
AddRecipients(c *gin.Context)
|
||||||
|
UpdateRecipient(c *gin.Context)
|
||||||
|
RemoveRecipient(c *gin.Context)
|
||||||
|
|
||||||
|
AddAttachments(c *gin.Context)
|
||||||
|
RemoveAttachment(c *gin.Context)
|
||||||
|
|
||||||
|
CreateDiscussion(c *gin.Context)
|
||||||
|
UpdateDiscussion(c *gin.Context)
|
||||||
|
DeleteDiscussion(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminApprovalFlowHandler interface {
|
||||||
|
CreateApprovalFlow(c *gin.Context)
|
||||||
|
GetApprovalFlow(c *gin.Context)
|
||||||
|
GetApprovalFlowByDepartment(c *gin.Context)
|
||||||
|
UpdateApprovalFlow(c *gin.Context)
|
||||||
|
DeleteApprovalFlow(c *gin.Context)
|
||||||
|
ListApprovalFlows(c *gin.Context)
|
||||||
|
ActivateApprovalFlow(c *gin.Context)
|
||||||
|
DeactivateApprovalFlow(c *gin.Context)
|
||||||
|
CloneApprovalFlow(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
type DispositionRouteHandler interface {
|
type DispositionRouteHandler interface {
|
||||||
Create(c *gin.Context)
|
Create(c *gin.Context)
|
||||||
Update(c *gin.Context)
|
Update(c *gin.Context)
|
||||||
|
|||||||
@ -8,16 +8,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
authHandler AuthHandler
|
authHandler AuthHandler
|
||||||
healthHandler HealthHandler
|
healthHandler HealthHandler
|
||||||
authMiddleware AuthMiddleware
|
authMiddleware AuthMiddleware
|
||||||
userHandler UserHandler
|
userHandler UserHandler
|
||||||
fileHandler FileHandler
|
fileHandler FileHandler
|
||||||
rbacHandler RBACHandler
|
rbacHandler RBACHandler
|
||||||
masterHandler MasterHandler
|
masterHandler MasterHandler
|
||||||
letterHandler LetterHandler
|
letterHandler LetterHandler
|
||||||
dispRouteHandler DispositionRouteHandler
|
letterOutgoingHandler LetterOutgoingHandler
|
||||||
|
adminApprovalFlowHandler AdminApprovalFlowHandler
|
||||||
|
dispRouteHandler DispositionRouteHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(
|
func NewRouter(
|
||||||
@ -30,19 +32,23 @@ func NewRouter(
|
|||||||
rbacHandler RBACHandler,
|
rbacHandler RBACHandler,
|
||||||
masterHandler MasterHandler,
|
masterHandler MasterHandler,
|
||||||
letterHandler LetterHandler,
|
letterHandler LetterHandler,
|
||||||
|
letterOutgoingHandler LetterOutgoingHandler,
|
||||||
|
adminApprovalFlowHandler AdminApprovalFlowHandler,
|
||||||
dispRouteHandler DispositionRouteHandler,
|
dispRouteHandler DispositionRouteHandler,
|
||||||
) *Router {
|
) *Router {
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
authHandler: authHandler,
|
authHandler: authHandler,
|
||||||
authMiddleware: authMiddleware,
|
authMiddleware: authMiddleware,
|
||||||
healthHandler: healthHandler,
|
healthHandler: healthHandler,
|
||||||
userHandler: userHandler,
|
userHandler: userHandler,
|
||||||
fileHandler: fileHandler,
|
fileHandler: fileHandler,
|
||||||
rbacHandler: rbacHandler,
|
rbacHandler: rbacHandler,
|
||||||
masterHandler: masterHandler,
|
masterHandler: masterHandler,
|
||||||
letterHandler: letterHandler,
|
letterHandler: letterHandler,
|
||||||
dispRouteHandler: dispRouteHandler,
|
letterOutgoingHandler: letterOutgoingHandler,
|
||||||
|
adminApprovalFlowHandler: adminApprovalFlowHandler,
|
||||||
|
dispRouteHandler: dispRouteHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +85,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
{
|
{
|
||||||
users.GET("", r.authMiddleware.RequirePermissions("user.read"), r.userHandler.ListUsers)
|
users.GET("", r.authMiddleware.RequirePermissions("user.read"), r.userHandler.ListUsers)
|
||||||
users.GET("/profile", r.userHandler.GetProfile)
|
users.GET("/profile", r.userHandler.GetProfile)
|
||||||
|
users.GET("/:id/profile", r.userHandler.GetUserProfile)
|
||||||
users.PUT("/profile", r.userHandler.UpdateProfile)
|
users.PUT("/profile", r.userHandler.UpdateProfile)
|
||||||
users.PUT(":id/password", r.userHandler.ChangePassword)
|
users.PUT(":id/password", r.userHandler.ChangePassword)
|
||||||
users.GET("/titles", r.userHandler.ListTitles)
|
users.GET("/titles", r.userHandler.ListTitles)
|
||||||
@ -139,6 +146,29 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter)
|
lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter)
|
||||||
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
||||||
|
|
||||||
|
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
|
||||||
|
lettersch.GET("/outgoing/:id", r.letterOutgoingHandler.GetOutgoingLetter)
|
||||||
|
lettersch.GET("/outgoing", r.letterOutgoingHandler.ListOutgoingLetters)
|
||||||
|
lettersch.PUT("/outgoing/:id", r.letterOutgoingHandler.UpdateOutgoingLetter)
|
||||||
|
lettersch.DELETE("/outgoing/:id", r.letterOutgoingHandler.DeleteOutgoingLetter)
|
||||||
|
|
||||||
|
lettersch.POST("/outgoing/:id/submit", r.letterOutgoingHandler.SubmitForApproval)
|
||||||
|
lettersch.POST("/outgoing/:id/approve", r.letterOutgoingHandler.ApproveOutgoingLetter)
|
||||||
|
lettersch.POST("/outgoing/:id/reject", r.letterOutgoingHandler.RejectOutgoingLetter)
|
||||||
|
lettersch.POST("/outgoing/:id/send", r.letterOutgoingHandler.SendOutgoingLetter)
|
||||||
|
lettersch.POST("/outgoing/:id/archive", r.letterOutgoingHandler.ArchiveOutgoingLetter)
|
||||||
|
|
||||||
|
lettersch.POST("/outgoing/:id/recipients", r.letterOutgoingHandler.AddRecipients)
|
||||||
|
lettersch.PUT("/outgoing/:id/recipients/:recipient_id", r.letterOutgoingHandler.UpdateRecipient)
|
||||||
|
lettersch.DELETE("/outgoing/:id/recipients/:recipient_id", r.letterOutgoingHandler.RemoveRecipient)
|
||||||
|
|
||||||
|
lettersch.POST("/outgoing/:id/attachments", r.letterOutgoingHandler.AddAttachments)
|
||||||
|
lettersch.DELETE("/outgoing/:id/attachments/:attachment_id", r.letterOutgoingHandler.RemoveAttachment)
|
||||||
|
|
||||||
|
lettersch.POST("/outgoing/:id/discussions", r.letterOutgoingHandler.CreateDiscussion)
|
||||||
|
lettersch.PUT("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.UpdateDiscussion)
|
||||||
|
lettersch.DELETE("/outgoing/discussions/:discussion_id", r.letterOutgoingHandler.DeleteDiscussion)
|
||||||
|
|
||||||
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
||||||
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
|
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
|
||||||
|
|
||||||
@ -155,5 +185,23 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
droutes.GET("department", r.dispRouteHandler.ListByFromDept)
|
droutes.GET("department", r.dispRouteHandler.ListByFromDept)
|
||||||
droutes.PUT(":id/active", r.dispRouteHandler.SetActive)
|
droutes.PUT(":id/active", r.dispRouteHandler.SetActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
admin := v1.Group("/admin")
|
||||||
|
admin.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
approvalFlows := admin.Group("/approval-flows")
|
||||||
|
approvalFlows.Use(r.authMiddleware.RequirePermissions("admin.approval_flow"))
|
||||||
|
{
|
||||||
|
approvalFlows.POST("", r.adminApprovalFlowHandler.CreateApprovalFlow)
|
||||||
|
approvalFlows.GET("", r.adminApprovalFlowHandler.ListApprovalFlows)
|
||||||
|
approvalFlows.GET("/:id", r.adminApprovalFlowHandler.GetApprovalFlow)
|
||||||
|
approvalFlows.GET("/department/:department_id", r.adminApprovalFlowHandler.GetApprovalFlowByDepartment)
|
||||||
|
approvalFlows.PUT("/:id", r.adminApprovalFlowHandler.UpdateApprovalFlow)
|
||||||
|
approvalFlows.DELETE("/:id", r.adminApprovalFlowHandler.DeleteApprovalFlow)
|
||||||
|
approvalFlows.POST("/:id/activate", r.adminApprovalFlowHandler.ActivateApprovalFlow)
|
||||||
|
approvalFlows.POST("/:id/deactivate", r.adminApprovalFlowHandler.DeactivateApprovalFlow)
|
||||||
|
approvalFlows.POST("/:id/clone", r.adminApprovalFlowHandler.CloneApprovalFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
247
internal/service/approval_flow_service.go
Normal file
247
internal/service/approval_flow_service.go
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApprovalFlowService interface {
|
||||||
|
CreateApprovalFlow(ctx context.Context, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
||||||
|
GetApprovalFlow(ctx context.Context, id uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
||||||
|
GetApprovalFlowByDepartment(ctx context.Context, departmentID uuid.UUID) (*contract.ApprovalFlowResponse, error)
|
||||||
|
UpdateApprovalFlow(ctx context.Context, id uuid.UUID, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error)
|
||||||
|
DeleteApprovalFlow(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListApprovalFlows(ctx context.Context, req *contract.ListApprovalFlowsRequest) (*contract.ListApprovalFlowsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApprovalFlowServiceImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
flowRepo *repository.ApprovalFlowRepository
|
||||||
|
stepRepo *repository.ApprovalFlowStepRepository
|
||||||
|
txManager *repository.TxManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApprovalFlowService(
|
||||||
|
db *gorm.DB,
|
||||||
|
flowRepo *repository.ApprovalFlowRepository,
|
||||||
|
stepRepo *repository.ApprovalFlowStepRepository,
|
||||||
|
txManager *repository.TxManager,
|
||||||
|
) *ApprovalFlowServiceImpl {
|
||||||
|
return &ApprovalFlowServiceImpl{
|
||||||
|
db: db,
|
||||||
|
flowRepo: flowRepo,
|
||||||
|
stepRepo: stepRepo,
|
||||||
|
txManager: txManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ApprovalFlowServiceImpl) CreateApprovalFlow(ctx context.Context, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error) {
|
||||||
|
flow := &entities.ApprovalFlow{
|
||||||
|
DepartmentID: req.DepartmentID,
|
||||||
|
Name: req.Name,
|
||||||
|
Description: req.Description,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.flowRepo.Create(txCtx, flow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Steps) > 0 {
|
||||||
|
steps := make([]entities.ApprovalFlowStep, len(req.Steps))
|
||||||
|
for i, stepReq := range req.Steps {
|
||||||
|
if stepReq.ApproverRoleID == nil && stepReq.ApproverUserID == nil {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
steps[i] = entities.ApprovalFlowStep{
|
||||||
|
FlowID: flow.ID,
|
||||||
|
StepOrder: stepReq.StepOrder,
|
||||||
|
ParallelGroup: stepReq.ParallelGroup,
|
||||||
|
ApproverRoleID: stepReq.ApproverRoleID,
|
||||||
|
ApproverUserID: stepReq.ApproverUserID,
|
||||||
|
Required: stepReq.Required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.stepRepo.CreateBulk(txCtx, steps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.flowRepo.Get(ctx, flow.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformApprovalFlowToResponse(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ApprovalFlowServiceImpl) GetApprovalFlow(ctx context.Context, id uuid.UUID) (*contract.ApprovalFlowResponse, error) {
|
||||||
|
flow, err := s.flowRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformApprovalFlowToResponse(flow), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ApprovalFlowServiceImpl) GetApprovalFlowByDepartment(ctx context.Context, departmentID uuid.UUID) (*contract.ApprovalFlowResponse, error) {
|
||||||
|
flow, err := s.flowRepo.GetByDepartment(ctx, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformApprovalFlowToResponse(flow), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ApprovalFlowServiceImpl) UpdateApprovalFlow(ctx context.Context, id uuid.UUID, req *contract.ApprovalFlowRequest) (*contract.ApprovalFlowResponse, error) {
|
||||||
|
flow, err := s.flowRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
flow.DepartmentID = req.DepartmentID
|
||||||
|
flow.Name = req.Name
|
||||||
|
flow.Description = req.Description
|
||||||
|
flow.IsActive = req.IsActive
|
||||||
|
|
||||||
|
err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.flowRepo.Update(txCtx, flow); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.stepRepo.DeleteByFlow(txCtx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Steps) > 0 {
|
||||||
|
steps := make([]entities.ApprovalFlowStep, len(req.Steps))
|
||||||
|
for i, stepReq := range req.Steps {
|
||||||
|
if stepReq.ApproverRoleID == nil && stepReq.ApproverUserID == nil {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
steps[i] = entities.ApprovalFlowStep{
|
||||||
|
FlowID: flow.ID,
|
||||||
|
StepOrder: stepReq.StepOrder,
|
||||||
|
ParallelGroup: stepReq.ParallelGroup,
|
||||||
|
ApproverRoleID: stepReq.ApproverRoleID,
|
||||||
|
ApproverUserID: stepReq.ApproverUserID,
|
||||||
|
Required: stepReq.Required,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.stepRepo.CreateBulk(txCtx, steps); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.flowRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformApprovalFlowToResponse(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ApprovalFlowServiceImpl) DeleteApprovalFlow(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.flowRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ApprovalFlowServiceImpl) ListApprovalFlows(ctx context.Context, req *contract.ListApprovalFlowsRequest) (*contract.ListApprovalFlowsResponse, error) {
|
||||||
|
filter := repository.ListApprovalFlowsFilter{
|
||||||
|
DepartmentID: req.DepartmentID,
|
||||||
|
IsActive: req.IsActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
flows, total, err := s.flowRepo.List(ctx, filter, req.Limit, req.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]*contract.ApprovalFlowResponse, len(flows))
|
||||||
|
for i, flow := range flows {
|
||||||
|
items[i] = transformApprovalFlowToResponse(&flow)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ListApprovalFlowsResponse{
|
||||||
|
Items: items,
|
||||||
|
Total: total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformApprovalFlowToResponse(flow *entities.ApprovalFlow) *contract.ApprovalFlowResponse {
|
||||||
|
resp := &contract.ApprovalFlowResponse{
|
||||||
|
ID: flow.ID,
|
||||||
|
DepartmentID: flow.DepartmentID,
|
||||||
|
Name: flow.Name,
|
||||||
|
Description: flow.Description,
|
||||||
|
IsActive: flow.IsActive,
|
||||||
|
CreatedAt: flow.CreatedAt,
|
||||||
|
UpdatedAt: flow.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if flow.Department != nil {
|
||||||
|
resp.Department = &contract.DepartmentResponse{
|
||||||
|
ID: flow.Department.ID,
|
||||||
|
Name: flow.Department.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flow.Steps) > 0 {
|
||||||
|
resp.Steps = make([]contract.ApprovalFlowStepResponse, len(flow.Steps))
|
||||||
|
for i, step := range flow.Steps {
|
||||||
|
stepResp := contract.ApprovalFlowStepResponse{
|
||||||
|
ID: step.ID,
|
||||||
|
StepOrder: step.StepOrder,
|
||||||
|
ParallelGroup: step.ParallelGroup,
|
||||||
|
ApproverRoleID: step.ApproverRoleID,
|
||||||
|
ApproverUserID: step.ApproverUserID,
|
||||||
|
Required: step.Required,
|
||||||
|
CreatedAt: step.CreatedAt,
|
||||||
|
UpdatedAt: step.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if step.ApproverRole != nil {
|
||||||
|
stepResp.ApproverRole = &contract.RoleResponse{
|
||||||
|
ID: step.ApproverRole.ID,
|
||||||
|
Name: step.ApproverRole.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if step.ApproverUser != nil {
|
||||||
|
stepResp.ApproverUser = &contract.UserResponse{
|
||||||
|
ID: step.ApproverUser.ID,
|
||||||
|
Name: step.ApproverUser.Name,
|
||||||
|
Email: step.ApproverUser.Email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Steps[i] = stepResp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
836
internal/service/letter_outgoing_service.go
Normal file
836
internal/service/letter_outgoing_service.go
Normal file
@ -0,0 +1,836 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterOutgoingService interface {
|
||||||
|
CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||||
|
GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error)
|
||||||
|
ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error)
|
||||||
|
UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||||
|
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
SubmitForApproval(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error
|
||||||
|
RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error
|
||||||
|
SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
||||||
|
|
||||||
|
AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error
|
||||||
|
UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error
|
||||||
|
RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error
|
||||||
|
|
||||||
|
AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
|
||||||
|
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
||||||
|
|
||||||
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
|
||||||
|
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
|
||||||
|
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterOutgoingServiceImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
letterRepo *repository.LetterOutgoingRepository
|
||||||
|
attachmentRepo *repository.LetterOutgoingAttachmentRepository
|
||||||
|
recipientRepo *repository.LetterOutgoingRecipientRepository
|
||||||
|
discussionRepo *repository.LetterOutgoingDiscussionRepository
|
||||||
|
discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository
|
||||||
|
activityLogRepo *repository.LetterOutgoingActivityLogRepository
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository
|
||||||
|
approvalRepo *repository.LetterOutgoingApprovalRepository
|
||||||
|
txManager *repository.TxManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterOutgoingService(
|
||||||
|
db *gorm.DB,
|
||||||
|
letterRepo *repository.LetterOutgoingRepository,
|
||||||
|
attachmentRepo *repository.LetterOutgoingAttachmentRepository,
|
||||||
|
recipientRepo *repository.LetterOutgoingRecipientRepository,
|
||||||
|
discussionRepo *repository.LetterOutgoingDiscussionRepository,
|
||||||
|
discussionAttachmentRepo *repository.LetterOutgoingDiscussionAttachmentRepository,
|
||||||
|
activityLogRepo *repository.LetterOutgoingActivityLogRepository,
|
||||||
|
approvalFlowRepo *repository.ApprovalFlowRepository,
|
||||||
|
approvalRepo *repository.LetterOutgoingApprovalRepository,
|
||||||
|
txManager *repository.TxManager,
|
||||||
|
) *LetterOutgoingServiceImpl {
|
||||||
|
return &LetterOutgoingServiceImpl{
|
||||||
|
db: db,
|
||||||
|
letterRepo: letterRepo,
|
||||||
|
attachmentRepo: attachmentRepo,
|
||||||
|
recipientRepo: recipientRepo,
|
||||||
|
discussionRepo: discussionRepo,
|
||||||
|
discussionAttachmentRepo: discussionAttachmentRepo,
|
||||||
|
activityLogRepo: activityLogRepo,
|
||||||
|
approvalFlowRepo: approvalFlowRepo,
|
||||||
|
approvalRepo: approvalRepo,
|
||||||
|
txManager: txManager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter := &entities.LetterOutgoing{
|
||||||
|
Subject: req.Subject,
|
||||||
|
Description: req.Description,
|
||||||
|
PriorityID: req.PriorityID,
|
||||||
|
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||||
|
IssueDate: req.IssueDate,
|
||||||
|
Status: entities.LetterOutgoingStatusDraft,
|
||||||
|
ApprovalFlowID: req.ApprovalFlowID,
|
||||||
|
CreatedBy: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ReferenceNumber != nil {
|
||||||
|
letter.ReferenceNumber = req.ReferenceNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.letterRepo.Create(txCtx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Recipients) > 0 {
|
||||||
|
recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients))
|
||||||
|
for i, r := range req.Recipients {
|
||||||
|
recipients[i] = entities.LetterOutgoingRecipient{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
RecipientName: r.Name,
|
||||||
|
RecipientEmail: r.Email,
|
||||||
|
RecipientPosition: r.Position,
|
||||||
|
RecipientInstitution: r.Institution,
|
||||||
|
IsPrimary: r.IsPrimary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Attachments) > 0 {
|
||||||
|
attachments := make([]entities.LetterOutgoingAttachment, len(req.Attachments))
|
||||||
|
for i, a := range req.Attachments {
|
||||||
|
attachments[i] = entities.LetterOutgoingAttachment{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
FileURL: a.FileURL,
|
||||||
|
FileName: a.FileName,
|
||||||
|
FileType: a.FileType,
|
||||||
|
UploadedBy: &userID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.attachmentRepo.CreateBulk(txCtx, attachments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
ActionType: entities.LetterOutgoingActionCreated,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.letterRepo.Get(ctx, letter.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformLetterToResponse(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error) {
|
||||||
|
letter, err := s.letterRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformLetterToResponse(letter), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) {
|
||||||
|
filter := repository.ListOutgoingLettersFilter{
|
||||||
|
Status: req.Status,
|
||||||
|
Query: req.Query,
|
||||||
|
CreatedBy: req.CreatedBy,
|
||||||
|
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||||
|
FromDate: req.FromDate,
|
||||||
|
ToDate: req.ToDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
letters, total, err := s.letterRepo.List(ctx, filter, req.Limit, req.Offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]*contract.OutgoingLetterResponse, len(letters))
|
||||||
|
for i, letter := range letters {
|
||||||
|
items[i] = transformLetterToResponse(&letter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ListOutgoingLettersResponse{
|
||||||
|
Items: items,
|
||||||
|
Total: total,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return nil, gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Subject != nil {
|
||||||
|
letter.Subject = *req.Subject
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
letter.Description = req.Description
|
||||||
|
}
|
||||||
|
if req.PriorityID != nil {
|
||||||
|
letter.PriorityID = req.PriorityID
|
||||||
|
}
|
||||||
|
if req.ReceiverInstitutionID != nil {
|
||||||
|
letter.ReceiverInstitutionID = req.ReceiverInstitutionID
|
||||||
|
}
|
||||||
|
if req.IssueDate != nil {
|
||||||
|
letter.IssueDate = *req.IssueDate
|
||||||
|
}
|
||||||
|
if req.ReferenceNumber != nil {
|
||||||
|
letter.ReferenceNumber = req.ReferenceNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.letterRepo.Update(txCtx, letter); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
ActionType: entities.LetterOutgoingActionUpdated,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.letterRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformLetterToResponse(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.letterRepo.SoftDelete(txCtx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letter.ID,
|
||||||
|
ActionType: entities.LetterOutgoingActionDeleted,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) SubmitForApproval(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.ApprovalFlowID == nil {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
flow, err := s.approvalFlowRepo.Get(ctx, *letter.ApprovalFlowID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
approvals := make([]entities.LetterOutgoingApproval, len(flow.Steps))
|
||||||
|
for i, step := range flow.Steps {
|
||||||
|
approvals[i] = entities.LetterOutgoingApproval{
|
||||||
|
LetterID: letterID,
|
||||||
|
StepID: step.ID,
|
||||||
|
Status: entities.ApprovalStatusPending,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.approvalRepo.CreateBulk(txCtx, approvals); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusPendingApproval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionSubmittedApproval,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
FromStatus: ptr(string(entities.LetterOutgoingStatusDraft)),
|
||||||
|
ToStatus: ptr(string(entities.LetterOutgoingStatusPendingApproval)),
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusPendingApproval {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := s.approvalRepo.ListByLetter(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentApproval *entities.LetterOutgoingApproval
|
||||||
|
for i := range approvals {
|
||||||
|
if approvals[i].Status == entities.ApprovalStatusPending {
|
||||||
|
step := approvals[i].Step
|
||||||
|
if (step.ApproverUserID != nil && *step.ApproverUserID == userID) ||
|
||||||
|
(step.ApproverRoleID != nil && userHasRole(ctx, *step.ApproverRoleID)) {
|
||||||
|
currentApproval = &approvals[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentApproval == nil {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
now := time.Now()
|
||||||
|
currentApproval.Status = entities.ApprovalStatusApproved
|
||||||
|
currentApproval.ApproverID = &userID
|
||||||
|
currentApproval.ActedAt = &now
|
||||||
|
currentApproval.Remarks = req.Remarks
|
||||||
|
|
||||||
|
if err := s.approvalRepo.Update(txCtx, currentApproval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
allApproved := true
|
||||||
|
for _, approval := range approvals {
|
||||||
|
if approval.ID != currentApproval.ID && approval.Status == entities.ApprovalStatusPending {
|
||||||
|
allApproved = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allApproved {
|
||||||
|
if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusApproved); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionApproved,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
TargetID: ¤tApproval.ID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusPendingApproval {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
approvals, err := s.approvalRepo.ListByLetter(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentApproval *entities.LetterOutgoingApproval
|
||||||
|
for i := range approvals {
|
||||||
|
if approvals[i].Status == entities.ApprovalStatusPending {
|
||||||
|
step := approvals[i].Step
|
||||||
|
if (step.ApproverUserID != nil && *step.ApproverUserID == userID) ||
|
||||||
|
(step.ApproverRoleID != nil && userHasRole(ctx, *step.ApproverRoleID)) {
|
||||||
|
currentApproval = &approvals[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentApproval == nil {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
now := time.Now()
|
||||||
|
currentApproval.Status = entities.ApprovalStatusRejected
|
||||||
|
currentApproval.ApproverID = &userID
|
||||||
|
currentApproval.ActedAt = &now
|
||||||
|
currentApproval.Remarks = &req.Reason
|
||||||
|
|
||||||
|
if err := s.approvalRepo.Update(txCtx, currentApproval); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusDraft); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionRejected,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
TargetID: ¤tApproval.ID,
|
||||||
|
FromStatus: ptr(string(entities.LetterOutgoingStatusPendingApproval)),
|
||||||
|
ToStatus: ptr(string(entities.LetterOutgoingStatusDraft)),
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusApproved {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusSent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionSent,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
FromStatus: ptr(string(entities.LetterOutgoingStatusApproved)),
|
||||||
|
ToStatus: ptr(string(entities.LetterOutgoingStatusSent)),
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusSent {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.letterRepo.UpdateStatus(txCtx, letterID, entities.LetterOutgoingStatusArchived); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionArchived,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
FromStatus: ptr(string(entities.LetterOutgoingStatusSent)),
|
||||||
|
ToStatus: ptr(string(entities.LetterOutgoingStatusArchived)),
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients))
|
||||||
|
for i, r := range req.Recipients {
|
||||||
|
recipients[i] = entities.LetterOutgoingRecipient{
|
||||||
|
LetterID: letterID,
|
||||||
|
RecipientName: r.Name,
|
||||||
|
RecipientEmail: r.Email,
|
||||||
|
RecipientPosition: r.Position,
|
||||||
|
RecipientInstitution: r.Institution,
|
||||||
|
IsPrimary: r.IsPrimary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionRecipientAdded,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error {
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient := &entities.LetterOutgoingRecipient{
|
||||||
|
ID: recipientID,
|
||||||
|
RecipientName: req.Name,
|
||||||
|
RecipientEmail: req.Email,
|
||||||
|
RecipientPosition: req.Position,
|
||||||
|
RecipientInstitution: req.Institution,
|
||||||
|
IsPrimary: req.IsPrimary,
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.recipientRepo.Update(ctx, recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.recipientRepo.Delete(txCtx, recipientID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionRecipientRemoved,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
TargetID: &recipientID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments := make([]entities.LetterOutgoingAttachment, len(req.Attachments))
|
||||||
|
for i, a := range req.Attachments {
|
||||||
|
attachments[i] = entities.LetterOutgoingAttachment{
|
||||||
|
LetterID: letterID,
|
||||||
|
FileURL: a.FileURL,
|
||||||
|
FileName: a.FileName,
|
||||||
|
FileType: a.FileType,
|
||||||
|
UploadedBy: &userID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.attachmentRepo.CreateBulk(txCtx, attachments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionAttachmentAdded,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
letter, err := s.letterRepo.Get(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.attachmentRepo.Delete(txCtx, attachmentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionAttachmentRemoved,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
TargetID: &attachmentID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) {
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
|
||||||
|
discussion := &entities.LetterOutgoingDiscussion{
|
||||||
|
LetterID: letterID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
UserID: userID,
|
||||||
|
Message: req.Message,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Mentions != nil {
|
||||||
|
discussion.Mentions = req.Mentions
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := s.discussionRepo.Create(txCtx, discussion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Attachments) > 0 {
|
||||||
|
attachments := make([]entities.LetterOutgoingDiscussionAttachment, len(req.Attachments))
|
||||||
|
for i, a := range req.Attachments {
|
||||||
|
attachments[i] = entities.LetterOutgoingDiscussionAttachment{
|
||||||
|
DiscussionID: discussion.ID,
|
||||||
|
FileURL: a.FileURL,
|
||||||
|
FileName: a.FileName,
|
||||||
|
FileType: a.FileType,
|
||||||
|
UploadedBy: &userID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.discussionAttachmentRepo.CreateBulk(txCtx, attachments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activityLog := &entities.LetterOutgoingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: entities.LetterOutgoingActionDiscussionAdded,
|
||||||
|
ActorUserID: &userID,
|
||||||
|
TargetID: &discussion.ID,
|
||||||
|
}
|
||||||
|
if err := s.activityLogRepo.Create(txCtx, activityLog); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.discussionRepo.Get(ctx, discussion.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformDiscussionToResponse(result), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error {
|
||||||
|
discussion, err := s.discussionRepo.Get(ctx, discussionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
if discussion.UserID != userID {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
discussion.Message = req.Message
|
||||||
|
if req.Mentions != nil {
|
||||||
|
discussion.Mentions = req.Mentions
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.discussionRepo.Update(ctx, discussion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error {
|
||||||
|
discussion, err := s.discussionRepo.Get(ctx, discussionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userID := getUserIDFromContext(ctx)
|
||||||
|
if discussion.UserID != userID {
|
||||||
|
return gorm.ErrInvalidData
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.discussionRepo.Delete(ctx, discussionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserIDFromContext(ctx context.Context) uuid.UUID {
|
||||||
|
return uuid.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
func userHasRole(ctx context.Context, roleID uuid.UUID) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptr(s string) *string {
|
||||||
|
return &s
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.OutgoingLetterResponse {
|
||||||
|
return &contract.OutgoingLetterResponse{
|
||||||
|
ID: letter.ID,
|
||||||
|
LetterNumber: letter.LetterNumber,
|
||||||
|
ReferenceNumber: letter.ReferenceNumber,
|
||||||
|
Subject: letter.Subject,
|
||||||
|
Description: letter.Description,
|
||||||
|
PriorityID: letter.PriorityID,
|
||||||
|
ReceiverInstitutionID: letter.ReceiverInstitutionID,
|
||||||
|
IssueDate: letter.IssueDate,
|
||||||
|
Status: string(letter.Status),
|
||||||
|
ApprovalFlowID: letter.ApprovalFlowID,
|
||||||
|
CreatedBy: letter.CreatedBy,
|
||||||
|
CreatedAt: letter.CreatedAt,
|
||||||
|
UpdatedAt: letter.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformDiscussionToResponse(discussion *entities.LetterOutgoingDiscussion) *contract.DiscussionResponse {
|
||||||
|
return &contract.DiscussionResponse{
|
||||||
|
ID: discussion.ID,
|
||||||
|
UserID: discussion.UserID,
|
||||||
|
Message: discussion.Message,
|
||||||
|
CreatedAt: discussion.CreatedAt,
|
||||||
|
UpdatedAt: discussion.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
24
migrations/000013_letters_outgoing_suite.down.sql
Normal file
24
migrations/000013_letters_outgoing_suite.down.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Drop triggers first
|
||||||
|
DROP TRIGGER IF EXISTS trg_letter_outgoing_discussions_updated_at ON letter_outgoing_discussions;
|
||||||
|
DROP TRIGGER IF EXISTS trg_approval_flow_steps_updated_at ON approval_flow_steps;
|
||||||
|
DROP TRIGGER IF EXISTS trg_approval_flows_updated_at ON approval_flows;
|
||||||
|
DROP TRIGGER IF EXISTS trg_letters_outgoing_updated_at ON letters_outgoing;
|
||||||
|
|
||||||
|
-- Drop tables in reverse order (due to foreign key constraints)
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_activity_logs;
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_discussion_attachments;
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_discussions;
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_approvals;
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_attachments;
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_labels;
|
||||||
|
DROP TABLE IF EXISTS letter_outgoing_recipients;
|
||||||
|
DROP TABLE IF EXISTS letters_outgoing;
|
||||||
|
DROP TABLE IF EXISTS approval_flow_steps;
|
||||||
|
DROP TABLE IF EXISTS approval_flows;
|
||||||
|
|
||||||
|
-- Drop sequence
|
||||||
|
DROP SEQUENCE IF EXISTS letters_outgoing_seq;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
199
migrations/000013_letters_outgoing_suite.up.sql
Normal file
199
migrations/000013_letters_outgoing_suite.up.sql
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- SEQUENCE FOR LETTER NUMBER
|
||||||
|
-- =======================
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS letters_outgoing_seq;
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- APPROVAL FLOWS
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS approval_flows (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE RESTRICT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
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 INDEX IF NOT EXISTS idx_approval_flows_department ON approval_flows(department_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_approval_flows_active ON approval_flows(is_active);
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_approval_flows_updated_at
|
||||||
|
BEFORE UPDATE ON approval_flows
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- APPROVAL FLOW STEPS
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS approval_flow_steps (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
flow_id UUID NOT NULL REFERENCES approval_flows(id) ON DELETE CASCADE,
|
||||||
|
step_order INT NOT NULL,
|
||||||
|
parallel_group INT DEFAULT 1,
|
||||||
|
approver_role_id UUID REFERENCES roles(id) ON DELETE SET NULL,
|
||||||
|
approver_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
required BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
UNIQUE(flow_id, step_order, parallel_group, approver_role_id, approver_user_id),
|
||||||
|
CHECK ((approver_role_id IS NOT NULL) OR (approver_user_id IS NOT NULL))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_approval_flow_steps_flow ON approval_flow_steps(flow_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_approval_flow_steps_order ON approval_flow_steps(flow_id, step_order);
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_approval_flow_steps_updated_at
|
||||||
|
BEFORE UPDATE ON approval_flow_steps
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTERS OUTGOING
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letters_outgoing (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_number TEXT NOT NULL UNIQUE DEFAULT ('OUT-' || lpad(nextval('letters_outgoing_seq')::text, 8, '0')),
|
||||||
|
reference_number TEXT,
|
||||||
|
subject TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
priority_id UUID REFERENCES priorities(id) ON DELETE SET NULL,
|
||||||
|
receiver_institution_id UUID REFERENCES institutions(id) ON DELETE SET NULL,
|
||||||
|
issue_date DATE NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','pending_approval','approved','sent','archived')),
|
||||||
|
approval_flow_id UUID REFERENCES approval_flows(id) ON DELETE SET NULL,
|
||||||
|
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_outgoing_status ON letters_outgoing(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_issue_date ON letters_outgoing(issue_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_approval_flow ON letters_outgoing(approval_flow_id);
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_letters_outgoing_updated_at
|
||||||
|
BEFORE UPDATE ON letters_outgoing
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING RECIPIENTS
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_recipients (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
|
||||||
|
recipient_name TEXT NOT NULL,
|
||||||
|
recipient_email TEXT,
|
||||||
|
recipient_position TEXT,
|
||||||
|
recipient_institution TEXT,
|
||||||
|
is_primary BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_letter ON letter_outgoing_recipients(letter_id);
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING LABELS (M:N)
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_labels (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_id UUID NOT NULL REFERENCES letters_outgoing(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_outgoing_labels_letter ON letter_outgoing_labels(letter_id);
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING ATTACHMENTS
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_attachments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_id UUID NOT NULL REFERENCES letters_outgoing(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_outgoing_attachments_letter ON letter_outgoing_attachments(letter_id);
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING APPROVALS
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_approvals (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
|
||||||
|
step_id UUID NOT NULL REFERENCES approval_flow_steps(id) ON DELETE CASCADE,
|
||||||
|
approver_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
|
||||||
|
remarks TEXT,
|
||||||
|
acted_at TIMESTAMP WITHOUT TIME ZONE,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_letter ON letter_outgoing_approvals(letter_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_step ON letter_outgoing_approvals(step_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_approvals_status ON letter_outgoing_approvals(status);
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING DISCUSSIONS (Threaded)
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_discussions (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_id UUID NOT NULL REFERENCES letters_outgoing(id) ON DELETE CASCADE,
|
||||||
|
parent_id UUID REFERENCES letter_outgoing_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_outgoing_discussions_letter ON letter_outgoing_discussions(letter_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_discussions_parent ON letter_outgoing_discussions(parent_id);
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_letter_outgoing_discussions_updated_at
|
||||||
|
BEFORE UPDATE ON letter_outgoing_discussions
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING DISCUSSION ATTACHMENTS
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_discussion_attachments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
discussion_id UUID NOT NULL REFERENCES letter_outgoing_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_outgoing_discussion_attachments_discussion ON letter_outgoing_discussion_attachments(discussion_id);
|
||||||
|
|
||||||
|
-- =======================
|
||||||
|
-- LETTER OUTGOING ACTIVITY LOGS (Immutable)
|
||||||
|
-- =======================
|
||||||
|
CREATE TABLE IF NOT EXISTS letter_outgoing_activity_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
letter_id UUID NOT NULL REFERENCES letters_outgoing(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_outgoing_activity_logs_letter ON letter_outgoing_activity_logs(letter_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_activity_logs_action ON letter_outgoing_activity_logs(action_type);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
Loading…
x
Reference in New Issue
Block a user