Update department etc
This commit is contained in:
parent
d869d83d4b
commit
0399c87736
@ -31,6 +31,7 @@ type Config struct {
|
|||||||
S3Config S3Config `mapstructure:"s3"`
|
S3Config S3Config `mapstructure:"s3"`
|
||||||
OnlyOffice OnlyOffice `mapstructure:"onlyoffice"`
|
OnlyOffice OnlyOffice `mapstructure:"onlyoffice"`
|
||||||
Novu Novu `mapstructure:"novu"`
|
Novu Novu `mapstructure:"novu"`
|
||||||
|
Department Department `mapstructure:"department"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -93,3 +94,8 @@ type Novu struct {
|
|||||||
BaseURL string `mapstructure:"base_url"`
|
BaseURL string `mapstructure:"base_url"`
|
||||||
IncomingLetterWorkflowID string `mapstructure:"incoming_letter_workflow_id"`
|
IncomingLetterWorkflowID string `mapstructure:"incoming_letter_workflow_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Department struct {
|
||||||
|
ParentPath string `mapstructure:"parent_path"`
|
||||||
|
ExcludedPaths []string `mapstructure:"excluded_paths"`
|
||||||
|
}
|
||||||
|
|||||||
BIN
eslogad-backend
BIN
eslogad-backend
Binary file not shown.
@ -41,4 +41,10 @@ novu:
|
|||||||
api_key: 'f7de60a16abf825996191bf69ea8054a' # Add your Novu API key here
|
api_key: 'f7de60a16abf825996191bf69ea8054a' # Add your Novu API key here
|
||||||
application_id: 'cDeX8L5VWe-r' # Add your Novu Application ID here
|
application_id: 'cDeX8L5VWe-r' # Add your Novu Application ID here
|
||||||
base_url: 'https://novu-api.apskel.org' # Optional: defaults to https://api.novu.co
|
base_url: 'https://novu-api.apskel.org' # Optional: defaults to https://api.novu.co
|
||||||
incoming_letter_workflow_id: 'notification-dashbpard'
|
incoming_letter_workflow_id: 'notification-dashbpard'
|
||||||
|
|
||||||
|
department:
|
||||||
|
parent_path: 'eslogad.aslog' # Parent path for departments to be included in API
|
||||||
|
excluded_paths: # Paths to exclude from department APIs
|
||||||
|
- 'superadmin'
|
||||||
|
- 'system'
|
||||||
@ -338,7 +338,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
|
|
||||||
rbacSvc := service.NewRBACService(repos.rbacRepo)
|
rbacSvc := service.NewRBACService(repos.rbacRepo)
|
||||||
|
|
||||||
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo, repos.departmentRepo)
|
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo, repos.departmentRepo, cfg)
|
||||||
|
|
||||||
txManager := repository.NewTxManager(a.db)
|
txManager := repository.NewTxManager(a.db)
|
||||||
letterSvc := service.NewLetterService(
|
letterSvc := service.NewLetterService(
|
||||||
@ -349,6 +349,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
processors.activityLogger,
|
processors.activityLogger,
|
||||||
processors.letterDispositionProcessor,
|
processors.letterDispositionProcessor,
|
||||||
processors.notificationProcessor,
|
processors.notificationProcessor,
|
||||||
|
processors.activityLogger,
|
||||||
)
|
)
|
||||||
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
||||||
letterOutgoingSvc := service.NewLetterOutgoingService(processors.letterOutgoingProcessor)
|
letterOutgoingSvc := service.NewLetterOutgoingService(processors.letterOutgoingProcessor)
|
||||||
|
|||||||
@ -16,18 +16,18 @@ type AnalyticsDashboardRequest struct {
|
|||||||
|
|
||||||
// AnalyticsDashboardResponse represents the complete analytics dashboard
|
// AnalyticsDashboardResponse represents the complete analytics dashboard
|
||||||
type AnalyticsDashboardResponse struct {
|
type AnalyticsDashboardResponse struct {
|
||||||
Summary LetterSummaryStats `json:"summary"`
|
Summary LetterSummaryStats `json:"summary"`
|
||||||
PriorityDistribution []PriorityDistribution `json:"priority_distribution"`
|
PriorityDistribution []PriorityDistribution `json:"priority_distribution"`
|
||||||
DepartmentStats []DepartmentStats `json:"department_stats"`
|
DepartmentStats []DepartmentStats `json:"department_stats"`
|
||||||
MonthlyTrend []MonthlyTrend `json:"monthly_trend"`
|
MonthlyTrend []MonthlyTrend `json:"monthly_trend"`
|
||||||
DepartmentsStats []SimpleDepartmentStats `json:"departments_stats,omitempty"`
|
DepartmentsStats []SimpleDepartmentStats `json:"departments_stats,omitempty"`
|
||||||
InstitutionStats []InstitutionStats `json:"institution_stats"`
|
InstitutionStats []InstitutionStats `json:"institution_stats"`
|
||||||
DailyActivity []DailyActivity `json:"daily_activity"`
|
DailyActivity []DailyActivity `json:"daily_activity"`
|
||||||
StatusDistribution []StatusDistribution `json:"status_distribution,omitempty"`
|
StatusDistribution []StatusDistribution `json:"status_distribution,omitempty"`
|
||||||
TopSenders []TopUserStats `json:"top_senders,omitempty"`
|
TopSenders []TopUserStats `json:"top_senders,omitempty"`
|
||||||
TopRecipients []TopUserStats `json:"top_recipients,omitempty"`
|
TopRecipients []TopUserStats `json:"top_recipients,omitempty"`
|
||||||
ApprovalMetrics *ApprovalMetrics `json:"approval_metrics,omitempty"`
|
ApprovalMetrics *ApprovalMetrics `json:"approval_metrics,omitempty"`
|
||||||
ResponseTimeStats *ResponseTimeStats `json:"response_time_stats,omitempty"`
|
ResponseTimeStats *ResponseTimeStats `json:"response_time_stats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LetterSummaryStats represents overall summary statistics
|
// LetterSummaryStats represents overall summary statistics
|
||||||
@ -36,6 +36,7 @@ type LetterSummaryStats struct {
|
|||||||
TotalOutgoing int64 `json:"total_outgoing"`
|
TotalOutgoing int64 `json:"total_outgoing"`
|
||||||
WeekOverWeekGrowth float64 `json:"week_over_week_growth"`
|
WeekOverWeekGrowth float64 `json:"week_over_week_growth"`
|
||||||
MonthOverMonthGrowth float64 `json:"month_over_month_growth"`
|
MonthOverMonthGrowth float64 `json:"month_over_month_growth"`
|
||||||
|
TotalThisWeek float64 `json:"total_this_week"`
|
||||||
TotalPending int64 `json:"total_pending,omitempty"`
|
TotalPending int64 `json:"total_pending,omitempty"`
|
||||||
TotalApproved int64 `json:"total_approved,omitempty"`
|
TotalApproved int64 `json:"total_approved,omitempty"`
|
||||||
TotalRejected int64 `json:"total_rejected,omitempty"`
|
TotalRejected int64 `json:"total_rejected,omitempty"`
|
||||||
@ -54,24 +55,24 @@ type StatusDistribution struct {
|
|||||||
|
|
||||||
// PriorityDistribution represents letter distribution by priority
|
// PriorityDistribution represents letter distribution by priority
|
||||||
type PriorityDistribution struct {
|
type PriorityDistribution struct {
|
||||||
PriorityID string `json:"priority_id"`
|
PriorityID string `json:"priority_id"`
|
||||||
PriorityName string `json:"priority_name"`
|
PriorityName string `json:"priority_name"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
Percentage float64 `json:"percentage"`
|
Percentage float64 `json:"percentage"`
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DepartmentStats represents statistics per department
|
// DepartmentStats represents statistics per department
|
||||||
type DepartmentStats struct {
|
type DepartmentStats struct {
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
DepartmentID uuid.UUID `json:"department_id"`
|
||||||
DepartmentName string `json:"department_name"`
|
DepartmentName string `json:"department_name"`
|
||||||
DepartmentCode string `json:"department_code"`
|
DepartmentCode string `json:"department_code"`
|
||||||
IncomingCount int64 `json:"incoming_count"`
|
IncomingCount int64 `json:"incoming_count"`
|
||||||
OutgoingCount int64 `json:"outgoing_count"`
|
OutgoingCount int64 `json:"outgoing_count"`
|
||||||
PendingCount int64 `json:"pending_count"`
|
PendingCount int64 `json:"pending_count"`
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
||||||
CompletionRate float64 `json:"completion_rate"`
|
CompletionRate float64 `json:"completion_rate"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonthlyTrend represents monthly trend data
|
// MonthlyTrend represents monthly trend data
|
||||||
@ -86,12 +87,12 @@ type MonthlyTrend struct {
|
|||||||
|
|
||||||
// TopUserStats represents top users by letter activity
|
// TopUserStats represents top users by letter activity
|
||||||
type TopUserStats struct {
|
type TopUserStats struct {
|
||||||
UserID uuid.UUID `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
UserName string `json:"user_name"`
|
UserName string `json:"user_name"`
|
||||||
UserEmail string `json:"user_email"`
|
UserEmail string `json:"user_email"`
|
||||||
Department string `json:"department"`
|
Department string `json:"department"`
|
||||||
LetterCount int64 `json:"letter_count"`
|
LetterCount int64 `json:"letter_count"`
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstitutionStats represents statistics per institution
|
// InstitutionStats represents statistics per institution
|
||||||
@ -107,50 +108,50 @@ type InstitutionStats struct {
|
|||||||
|
|
||||||
// ApprovalMetrics represents approval-related metrics
|
// ApprovalMetrics represents approval-related metrics
|
||||||
type ApprovalMetrics struct {
|
type ApprovalMetrics struct {
|
||||||
TotalSubmitted int64 `json:"total_submitted"`
|
TotalSubmitted int64 `json:"total_submitted"`
|
||||||
TotalApproved int64 `json:"total_approved"`
|
TotalApproved int64 `json:"total_approved"`
|
||||||
TotalRejected int64 `json:"total_rejected"`
|
TotalRejected int64 `json:"total_rejected"`
|
||||||
TotalPending int64 `json:"total_pending"`
|
TotalPending int64 `json:"total_pending"`
|
||||||
ApprovalRate float64 `json:"approval_rate"`
|
ApprovalRate float64 `json:"approval_rate"`
|
||||||
RejectionRate float64 `json:"rejection_rate"`
|
RejectionRate float64 `json:"rejection_rate"`
|
||||||
AvgApprovalTime float64 `json:"avg_approval_time_hours"`
|
AvgApprovalTime float64 `json:"avg_approval_time_hours"`
|
||||||
AvgApprovalSteps float64 `json:"avg_approval_steps"`
|
AvgApprovalSteps float64 `json:"avg_approval_steps"`
|
||||||
BottleneckSteps []BottleneckStep `json:"bottleneck_steps"`
|
BottleneckSteps []BottleneckStep `json:"bottleneck_steps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BottleneckStep represents approval steps that cause delays
|
// BottleneckStep represents approval steps that cause delays
|
||||||
type BottleneckStep struct {
|
type BottleneckStep struct {
|
||||||
StepOrder int `json:"step_order"`
|
StepOrder int `json:"step_order"`
|
||||||
ApproverName string `json:"approver_name"`
|
ApproverName string `json:"approver_name"`
|
||||||
AvgProcessTime float64 `json:"avg_process_time_hours"`
|
AvgProcessTime float64 `json:"avg_process_time_hours"`
|
||||||
PendingCount int64 `json:"pending_count"`
|
PendingCount int64 `json:"pending_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponseTimeStats represents response time statistics
|
// ResponseTimeStats represents response time statistics
|
||||||
type ResponseTimeStats struct {
|
type ResponseTimeStats struct {
|
||||||
MinResponseTime float64 `json:"min_response_time_hours"`
|
MinResponseTime float64 `json:"min_response_time_hours"`
|
||||||
MaxResponseTime float64 `json:"max_response_time_hours"`
|
MaxResponseTime float64 `json:"max_response_time_hours"`
|
||||||
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
AvgResponseTime float64 `json:"avg_response_time_hours"`
|
||||||
MedianResponseTime float64 `json:"median_response_time_hours"`
|
MedianResponseTime float64 `json:"median_response_time_hours"`
|
||||||
P95ResponseTime float64 `json:"p95_response_time_hours"`
|
P95ResponseTime float64 `json:"p95_response_time_hours"`
|
||||||
P99ResponseTime float64 `json:"p99_response_time_hours"`
|
P99ResponseTime float64 `json:"p99_response_time_hours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SimpleDepartmentStats represents simplified department statistics
|
// SimpleDepartmentStats represents simplified department statistics
|
||||||
type SimpleDepartmentStats struct {
|
type SimpleDepartmentStats struct {
|
||||||
DepartmentID uuid.UUID `json:"department_id"`
|
DepartmentID uuid.UUID `json:"department_id"`
|
||||||
Department string `json:"department"`
|
Department string `json:"department"`
|
||||||
LetterCount int64 `json:"letter_count"`
|
LetterCount int64 `json:"letter_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DailyActivity represents daily activity data
|
// DailyActivity represents daily activity data
|
||||||
type DailyActivity struct {
|
type DailyActivity struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
DayOfWeek string `json:"day_of_week"`
|
DayOfWeek string `json:"day_of_week"`
|
||||||
IncomingCount int64 `json:"incoming_count"`
|
IncomingCount int64 `json:"incoming_count"`
|
||||||
OutgoingCount int64 `json:"outgoing_count"`
|
OutgoingCount int64 `json:"outgoing_count"`
|
||||||
ApprovedCount int64 `json:"approved_count,omitempty"`
|
ApprovedCount int64 `json:"approved_count,omitempty"`
|
||||||
RejectedCount int64 `json:"rejected_count,omitempty"`
|
RejectedCount int64 `json:"rejected_count,omitempty"`
|
||||||
HourlyDistribution []HourlyActivity `json:"hourly_distribution,omitempty"`
|
HourlyDistribution []HourlyActivity `json:"hourly_distribution,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,4 +183,4 @@ type OutgoingLetterVolume struct {
|
|||||||
ThisMonth int64 `json:"this_month"`
|
ThisMonth int64 `json:"this_month"`
|
||||||
ThisYear int64 `json:"this_year"`
|
ThisYear int64 `json:"this_year"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,18 +7,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateUserRequest struct {
|
type CreateUserRequest struct {
|
||||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
Password string `json:"password" validate:"required,min=6"`
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"`
|
RoleID *uuid.UUID `json:"role_id,omitempty" validate:"required"`
|
||||||
|
DepartmentIDs []uuid.UUID `json:"department_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserRequest struct {
|
type UpdateUserRequest struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
Email *string `json:"email,omitempty" validate:"omitempty,email"`
|
||||||
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter"`
|
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
Permissions *map[string]interface{} `json:"permissions,omitempty"`
|
Permissions *map[string]interface{} `json:"permissions,omitempty"`
|
||||||
|
DepartmentIDs *[]uuid.UUID `json:"department_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangePasswordRequest struct {
|
type ChangePasswordRequest struct {
|
||||||
@ -96,6 +98,43 @@ type ListDepartmentsResponse struct {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateDepartmentRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
Code string `json:"code" validate:"required,min=1,max=50"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDepartmentRequest struct {
|
||||||
|
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||||
|
Code *string `json:"code,omitempty" validate:"omitempty,min=1,max=50"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetDepartmentResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
ParentName *string `json:"parent_name,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DepartmentNode struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
Children []*DepartmentNode `json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizationalChartResponse struct {
|
||||||
|
Chart []*DepartmentNode `json:"chart"`
|
||||||
|
TotalNodes int `json:"total_nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
type UserProfileResponse struct {
|
type UserProfileResponse struct {
|
||||||
UserID uuid.UUID `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
FullName string `json:"full_name"`
|
FullName string `json:"full_name"`
|
||||||
|
|||||||
@ -286,7 +286,6 @@ func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract department ID from context
|
|
||||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
req.FromDepartment = appCtx.DepartmentID
|
req.FromDepartment = appCtx.DepartmentID
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MasterService interface {
|
type MasterService interface {
|
||||||
@ -31,7 +32,13 @@ type MasterService interface {
|
|||||||
DeleteDispositionAction(ctx context.Context, id uuid.UUID) error
|
DeleteDispositionAction(ctx context.Context, id uuid.UUID) error
|
||||||
ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error)
|
ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error)
|
||||||
|
|
||||||
|
CreateDepartment(ctx context.Context, req *contract.CreateDepartmentRequest) (*contract.GetDepartmentResponse, error)
|
||||||
|
GetDepartment(ctx context.Context, id uuid.UUID) (*contract.GetDepartmentResponse, error)
|
||||||
|
UpdateDepartment(ctx context.Context, id uuid.UUID, req *contract.UpdateDepartmentRequest) (*contract.GetDepartmentResponse, error)
|
||||||
|
DeleteDepartment(ctx context.Context, id uuid.UUID) error
|
||||||
ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error)
|
ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error)
|
||||||
|
GetOrganizationalChart(ctx context.Context, rootPath string) (*contract.OrganizationalChartResponse, error)
|
||||||
|
GetOrganizationalChartByID(ctx context.Context, departmentID uuid.UUID) (*contract.OrganizationalChartResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MasterHandler struct{ svc MasterService }
|
type MasterHandler struct{ svc MasterService }
|
||||||
@ -260,15 +267,87 @@ func (h *MasterHandler) ListDispositionActions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Departments
|
// Departments
|
||||||
|
func (h *MasterHandler) CreateDepartment(c *gin.Context) {
|
||||||
|
var req contract.CreateDepartmentRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateDepartment(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) GetDepartment(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.GetDepartment(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) UpdateDepartment(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateDepartmentRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateDepartment(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) DeleteDepartment(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeleteDepartment(c.Request.Context(), id); err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "department deleted successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *MasterHandler) ListDepartments(c *gin.Context) {
|
func (h *MasterHandler) ListDepartments(c *gin.Context) {
|
||||||
var req contract.ListDepartmentsRequest
|
var req contract.ListDepartmentsRequest
|
||||||
|
|
||||||
// Parse query parameters
|
// Parse query parameters
|
||||||
if err := c.ShouldBindQuery(&req); err != nil {
|
if err := c.ShouldBindQuery(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: 400})
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: 400})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := h.svc.ListDepartments(c.Request.Context(), &req)
|
resp, err := h.svc.ListDepartments(c.Request.Context(), &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
@ -276,3 +355,34 @@ func (h *MasterHandler) ListDepartments(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) GetOrganizationalChart(c *gin.Context) {
|
||||||
|
// Get optional root path from query parameter
|
||||||
|
rootPath := c.Query("root_path")
|
||||||
|
|
||||||
|
resp, err := h.svc.GetOrganizationalChart(c.Request.Context(), rootPath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) GetOrganizationalChartByID(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid department id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.svc.GetOrganizationalChartByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, &contract.ErrorResponse{Error: "department not found", Code: 404})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|||||||
@ -353,7 +353,8 @@ func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.FromContext(c).Infof("UserHandler::GetActiveUsersForMention -> Successfully retrieved %d active users", len(users))
|
logger.FromContext(c).Infof("UserHandler::GetActiveUsersForMention -> Successfully retrieved %d active users", len(users))
|
||||||
c.JSON(http.StatusOK, response)
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package processor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"eslogad-be/internal/appcontext"
|
"eslogad-be/internal/appcontext"
|
||||||
@ -309,77 +310,129 @@ func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id u
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateDispositions creates a new disposition with modular helper functions
|
||||||
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
||||||
var out *contract.ListDispositionsResponse
|
// Transaction should be handled at service layer
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
// The context passed here should already contain the transaction if needed
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
|
|
||||||
existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(txCtx, req.LetterID, req.FromDepartment)
|
// Step 1: Update existing disposition departments
|
||||||
if err == nil && len(existingDispDepts) > 0 {
|
if err := p.updateExistingDispositionDepartments(ctx, req.LetterID, req.FromDepartment); err != nil {
|
||||||
for _, existingDispDept := range existingDispDepts {
|
return nil, err
|
||||||
if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
|
}
|
||||||
existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
|
|
||||||
if err := p.dispositionDeptRepo.Update(txCtx, &existingDispDept); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
disp := entities.LetterIncomingDisposition{
|
// Step 2: Create the main disposition
|
||||||
LetterID: req.LetterID,
|
disp, err := p.createMainDisposition(ctx, req)
|
||||||
DepartmentID: &req.FromDepartment,
|
|
||||||
Notes: req.Notes,
|
|
||||||
CreatedBy: userID,
|
|
||||||
}
|
|
||||||
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var dispDepartments []entities.LetterIncomingDispositionDepartment
|
|
||||||
for _, toDept := range req.ToDepartmentIDs {
|
|
||||||
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
|
||||||
LetterIncomingDispositionID: disp.ID,
|
|
||||||
LetterIncomingID: req.LetterID,
|
|
||||||
DepartmentID: toDept,
|
|
||||||
Status: entities.DispositionDepartmentStatusPending,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.dispositionDeptRepo.CreateBulk(txCtx, dispDepartments); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(req.SelectedActions) > 0 {
|
|
||||||
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
|
||||||
for _, sel := range req.SelectedActions {
|
|
||||||
selections = append(selections, entities.LetterDispositionActionSelection{
|
|
||||||
DispositionID: disp.ID,
|
|
||||||
ActionID: sel.ActionID,
|
|
||||||
Note: sel.Note,
|
|
||||||
CreatedBy: userID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.activity != nil {
|
|
||||||
action := "disposition.created"
|
|
||||||
ctxMap := map[string]interface{}{"to_department_id": dispDepartments}
|
|
||||||
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &disp.ID, nil, nil, ctxMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out = &contract.ListDispositionsResponse{Dispositions: []contract.DispositionResponse{transformer.DispoToContract(disp)}}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return out, nil
|
|
||||||
|
// Step 3: Create disposition departments for target departments
|
||||||
|
dispDepartments, err := p.createDispositionDepartments(ctx, disp.ID, req.LetterID, req.ToDepartmentIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Create action selections if provided
|
||||||
|
if err := p.createActionSelections(ctx, disp.ID, req.SelectedActions, req.CreatedBy); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: Build and return the response
|
||||||
|
return p.buildDispositionResponse(disp, dispDepartments, req.ToDepartmentIDs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateExistingDispositionDepartments updates the status of existing disposition departments
|
||||||
|
func (p *LetterProcessorImpl) updateExistingDispositionDepartments(ctx context.Context, letterID uuid.UUID, fromDepartment uuid.UUID) error {
|
||||||
|
existingDispDepts, err := p.dispositionDeptRepo.GetByLetterAndDepartment(ctx, letterID, fromDepartment)
|
||||||
|
if err != nil {
|
||||||
|
// If no existing departments found, that's ok
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, existingDispDept := range existingDispDepts {
|
||||||
|
if existingDispDept.Status == entities.DispositionDepartmentStatusPending {
|
||||||
|
existingDispDept.Status = entities.DispositionDepartmentStatusDispositioned
|
||||||
|
if err := p.dispositionDeptRepo.Update(ctx, &existingDispDept); err != nil {
|
||||||
|
return fmt.Errorf("failed to update existing disposition department: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMainDisposition creates the primary disposition record
|
||||||
|
func (p *LetterProcessorImpl) createMainDisposition(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*entities.LetterIncomingDisposition, error) {
|
||||||
|
disp := &entities.LetterIncomingDisposition{
|
||||||
|
LetterID: req.LetterID,
|
||||||
|
DepartmentID: &req.FromDepartment,
|
||||||
|
Notes: req.Notes,
|
||||||
|
CreatedBy: req.CreatedBy, // Should be set by service layer
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.dispositionRepo.Create(ctx, disp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create disposition: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return disp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDispositionDepartments creates disposition department records for target departments
|
||||||
|
func (p *LetterProcessorImpl) createDispositionDepartments(ctx context.Context, dispositionID, letterID uuid.UUID, toDepartmentIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
if len(toDepartmentIDs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dispDepartments := make([]entities.LetterIncomingDispositionDepartment, 0, len(toDepartmentIDs))
|
||||||
|
for _, toDept := range toDepartmentIDs {
|
||||||
|
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
||||||
|
LetterIncomingDispositionID: dispositionID,
|
||||||
|
LetterIncomingID: letterID,
|
||||||
|
DepartmentID: toDept,
|
||||||
|
Status: entities.DispositionDepartmentStatusPending,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.dispositionDeptRepo.CreateBulk(ctx, dispDepartments); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create disposition departments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dispDepartments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createActionSelections creates action selection records for the disposition
|
||||||
|
func (p *LetterProcessorImpl) createActionSelections(ctx context.Context, dispositionID uuid.UUID, selectedActions []contract.CreateDispositionActionSelection, createdBy uuid.UUID) error {
|
||||||
|
if len(selectedActions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selections := make([]entities.LetterDispositionActionSelection, 0, len(selectedActions))
|
||||||
|
for _, sel := range selectedActions {
|
||||||
|
selections = append(selections, entities.LetterDispositionActionSelection{
|
||||||
|
DispositionID: dispositionID,
|
||||||
|
ActionID: sel.ActionID,
|
||||||
|
Note: sel.Note,
|
||||||
|
CreatedBy: createdBy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.dispositionActionSelRepo.CreateBulk(ctx, selections); err != nil {
|
||||||
|
return fmt.Errorf("failed to create action selections: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDispositionResponse builds the response for the created disposition
|
||||||
|
func (p *LetterProcessorImpl) buildDispositionResponse(disp *entities.LetterIncomingDisposition, dispDepartments []entities.LetterIncomingDispositionDepartment, toDepartmentIDs []uuid.UUID) *contract.ListDispositionsResponse {
|
||||||
|
response := &contract.ListDispositionsResponse{
|
||||||
|
Dispositions: []contract.DispositionResponse{transformer.DispoToContract(*disp)},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The toDepartmentIDs are available in the dispDepartments for service layer logging
|
||||||
|
// No need to store them in the response as DispositionResponse doesn't have this field
|
||||||
|
|
||||||
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
|
func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
|
||||||
@ -449,67 +502,54 @@ func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
var out *contract.LetterDiscussionResponse
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
mentions := entities.JSONB(nil)
|
||||||
mentions := entities.JSONB(nil)
|
if req.Mentions != nil {
|
||||||
if req.Mentions != nil {
|
mentions = entities.JSONB(req.Mentions)
|
||||||
mentions = entities.JSONB(req.Mentions)
|
|
||||||
}
|
|
||||||
disc := &entities.LetterDiscussion{ID: uuid.New(), LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions}
|
|
||||||
if err := p.discussionRepo.Create(txCtx, disc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if p.activity != nil {
|
|
||||||
action := "discussion.created"
|
|
||||||
tgt := "discussion"
|
|
||||||
ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID}
|
|
||||||
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = transformer.DiscussionEntityToContract(disc)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return out, nil
|
|
||||||
|
disc := &entities.LetterDiscussion{
|
||||||
|
ID: uuid.New(),
|
||||||
|
LetterID: letterID,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
UserID: userID,
|
||||||
|
Message: req.Message,
|
||||||
|
Mentions: mentions,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.discussionRepo.Create(ctx, disc); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create discussion: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activity logging should be handled at service layer
|
||||||
|
return transformer.DiscussionEntityToContract(disc), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, string, error) {
|
||||||
var out *contract.LetterDiscussionResponse
|
// Transaction should be handled at service layer
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
disc, err := p.discussionRepo.Get(ctx, discussionID)
|
||||||
disc, err := p.discussionRepo.Get(txCtx, discussionID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldMessage := disc.Message
|
|
||||||
disc.Message = req.Message
|
|
||||||
if req.Mentions != nil {
|
|
||||||
disc.Mentions = entities.JSONB(req.Mentions)
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
disc.EditedAt = &now
|
|
||||||
if err := p.discussionRepo.Update(txCtx, disc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if p.activity != nil {
|
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
|
||||||
action := "discussion.updated"
|
|
||||||
tgt := "discussion"
|
|
||||||
ctxMap := map[string]interface{}{"old_message": oldMessage, "new_message": req.Message}
|
|
||||||
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = transformer.DiscussionEntityToContract(disc)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, "", fmt.Errorf("failed to get discussion: %w", err)
|
||||||
}
|
}
|
||||||
return out, nil
|
|
||||||
|
// Store old message for activity logging
|
||||||
|
oldMessage := disc.Message
|
||||||
|
|
||||||
|
// Update discussion fields
|
||||||
|
disc.Message = req.Message
|
||||||
|
if req.Mentions != nil {
|
||||||
|
disc.Mentions = entities.JSONB(req.Mentions)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
disc.EditedAt = &now
|
||||||
|
|
||||||
|
if err := p.discussionRepo.Update(ctx, disc); err != nil {
|
||||||
|
return nil, "", fmt.Errorf("failed to update discussion: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return both the updated discussion and old message for service layer logging
|
||||||
|
return transformer.DiscussionEntityToContract(disc), oldMessage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []contract.CreateIncomingLetterAttachment, userID uuid.UUID) error {
|
func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uuid.UUID, attachments []contract.CreateIncomingLetterAttachment, userID uuid.UUID) error {
|
||||||
|
|||||||
@ -78,12 +78,29 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign departments if provided
|
||||||
|
if len(req.DepartmentIDs) > 0 {
|
||||||
|
departments := make([]entities.Department, len(req.DepartmentIDs))
|
||||||
|
for i, deptID := range req.DepartmentIDs {
|
||||||
|
departments[i] = entities.Department{ID: deptID}
|
||||||
|
}
|
||||||
|
if err := p.userRepo.UpdateDepartments(ctx, userEntity.ID, departments); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to assign departments: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p.novuProcessor != nil {
|
if p.novuProcessor != nil {
|
||||||
if err := p.novuProcessor.CreateSubscriber(ctx, userEntity); err != nil {
|
if err := p.novuProcessor.CreateSubscriber(ctx, userEntity); err != nil {
|
||||||
_ = err
|
_ = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the user with departments for response
|
||||||
|
userWithDepts, _ := p.userRepo.GetByIDWithDepartments(ctx, userEntity.ID)
|
||||||
|
if userWithDepts != nil {
|
||||||
|
userEntity = userWithDepts
|
||||||
|
}
|
||||||
|
|
||||||
return transformer.EntityToContract(userEntity), nil
|
return transformer.EntityToContract(userEntity), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,6 +124,17 @@ func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *c
|
|||||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update departments if provided
|
||||||
|
if req.DepartmentIDs != nil {
|
||||||
|
departments := make([]entities.Department, len(*req.DepartmentIDs))
|
||||||
|
for i, deptID := range *req.DepartmentIDs {
|
||||||
|
departments[i] = entities.Department{ID: deptID}
|
||||||
|
}
|
||||||
|
if err := p.userRepo.UpdateDepartments(ctx, updated.ID, departments); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to update departments: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update Novu subscriber
|
// Update Novu subscriber
|
||||||
if p.novuProcessor != nil {
|
if p.novuProcessor != nil {
|
||||||
if err := p.novuProcessor.UpdateSubscriber(ctx, updated); err != nil {
|
if err := p.novuProcessor.UpdateSubscriber(ctx, updated); err != nil {
|
||||||
@ -114,6 +142,12 @@ func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch the user with departments for response
|
||||||
|
userWithDepts, _ := p.userRepo.GetByIDWithDepartments(ctx, updated.ID)
|
||||||
|
if userWithDepts != nil {
|
||||||
|
updated = userWithDepts
|
||||||
|
}
|
||||||
|
|
||||||
return transformer.EntityToContract(updated), nil
|
return transformer.EntityToContract(updated), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,18 +218,8 @@ func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*
|
|||||||
return transformer.EntityToContract(user), nil
|
return transformer.EntityToContract(user), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error) {
|
func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]contract.UserResponse, int, error) {
|
||||||
page := req.Page
|
users, totalCount, err := p.userRepo.ListWithFilters(ctx, search, roleCode, isActive, limit, offset)
|
||||||
if page <= 0 {
|
|
||||||
page = 1
|
|
||||||
}
|
|
||||||
limit := req.Limit
|
|
||||||
if limit <= 0 {
|
|
||||||
limit = 10
|
|
||||||
}
|
|
||||||
offset := (page - 1) * limit
|
|
||||||
|
|
||||||
users, totalCount, err := p.userRepo.ListWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||||
}
|
}
|
||||||
@ -333,12 +357,7 @@ func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.U
|
|||||||
|
|
||||||
// GetActiveUsersForMention retrieves active users for mention purposes with optional username search
|
// GetActiveUsersForMention retrieves active users for mention purposes with optional username search
|
||||||
func (p *UserProcessorImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
func (p *UserProcessorImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
||||||
if limit <= 0 {
|
// Limit validation is handled in the service layer
|
||||||
limit = 50 // Default limit for mention suggestions
|
|
||||||
}
|
|
||||||
if limit > 100 {
|
|
||||||
limit = 100 // Max limit for mention suggestions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set isActive to true to only get active users
|
// Set isActive to true to only get active users
|
||||||
isActive := true
|
isActive := true
|
||||||
|
|||||||
@ -1,249 +0,0 @@
|
|||||||
package processor
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"eslogad-be/internal/entities"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockUserRepository is a mock implementation of UserRepository
|
|
||||||
type MockUserRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) Create(ctx context.Context, user *entities.User) error {
|
|
||||||
args := m.Called(ctx, user)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Get(0).(*entities.User), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
|
||||||
args := m.Called(ctx, email)
|
|
||||||
return args.Get(0).(*entities.User), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
|
|
||||||
args := m.Called(ctx, role)
|
|
||||||
return args.Get(0).([]*entities.User), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
|
|
||||||
args := m.Called(ctx, organizationID)
|
|
||||||
return args.Get(0).([]*entities.User), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) Update(ctx context.Context, user *entities.User) error {
|
|
||||||
args := m.Called(ctx, user)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
||||||
args := m.Called(ctx, id)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
|
|
||||||
args := m.Called(ctx, id, passwordHash)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
|
|
||||||
args := m.Called(ctx, id, isActive)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.User, int64, error) {
|
|
||||||
args := m.Called(ctx, filters, limit, offset)
|
|
||||||
return args.Get(0).([]*entities.User), args.Get(1).(int64), args.Error(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
|
||||||
args := m.Called(ctx, filters)
|
|
||||||
return args.Get(0).(int64), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
|
|
||||||
args := m.Called(ctx, userID)
|
|
||||||
return args.Get(0).([]entities.Role), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
|
|
||||||
args := m.Called(ctx, userID)
|
|
||||||
return args.Get(0).([]entities.Permission), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error) {
|
|
||||||
args := m.Called(ctx, userID)
|
|
||||||
return args.Get(0).([]entities.Department), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
|
|
||||||
args := m.Called(ctx, userIDs)
|
|
||||||
return args.Get(0).(map[uuid.UUID][]entities.Role), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserRepository) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
|
|
||||||
args := m.Called(ctx, search, roleCode, isActive, limit, offset)
|
|
||||||
return args.Get(0).([]*entities.User), args.Get(1).(int64), args.Error(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockUserProfileRepository is a mock implementation of UserProfileRepository
|
|
||||||
type MockUserProfileRepository struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserProfileRepository) GetByUserID(ctx context.Context, userID uuid.UUID) (*entities.UserProfile, error) {
|
|
||||||
args := m.Called(ctx, userID)
|
|
||||||
return args.Get(0).(*entities.UserProfile), args.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserProfileRepository) Create(ctx context.Context, profile *entities.UserProfile) error {
|
|
||||||
args := m.Called(ctx, profile)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserProfileRepository) Upsert(ctx context.Context, profile *entities.UserProfile) error {
|
|
||||||
args := m.Called(ctx, profile)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUserProfileRepository) Update(ctx context.Context, profile *entities.UserProfile) error {
|
|
||||||
args := m.Called(ctx, profile)
|
|
||||||
return args.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetActiveUsersForMention(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
search *string
|
|
||||||
limit int
|
|
||||||
mockUsers []*entities.User
|
|
||||||
mockRoles map[uuid.UUID][]entities.Role
|
|
||||||
expectedCount int
|
|
||||||
expectedError bool
|
|
||||||
setupMocks func(*MockUserRepository, *MockUserProfileRepository)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "success with search",
|
|
||||||
search: stringPtr("john"),
|
|
||||||
limit: 10,
|
|
||||||
mockUsers: []*entities.User{
|
|
||||||
{
|
|
||||||
ID: uuid.New(),
|
|
||||||
Name: "John Doe",
|
|
||||||
Email: "john@example.com",
|
|
||||||
IsActive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedCount: 1,
|
|
||||||
expectedError: false,
|
|
||||||
setupMocks: func(mockRepo *MockUserRepository, mockProfileRepo *MockUserProfileRepository) {
|
|
||||||
mockRepo.On("ListWithFilters", mock.Anything, stringPtr("john"), (*string)(nil), boolPtr(true), 10, 0).
|
|
||||||
Return([]*entities.User{
|
|
||||||
{
|
|
||||||
ID: uuid.New(),
|
|
||||||
Name: "John Doe",
|
|
||||||
Email: "john@example.com",
|
|
||||||
IsActive: true,
|
|
||||||
},
|
|
||||||
}, int64(1), nil)
|
|
||||||
|
|
||||||
mockRepo.On("GetRolesByUserIDs", mock.Anything, mock.AnythingOfType("[]uuid.UUID")).
|
|
||||||
Return(map[uuid.UUID][]entities.Role{}, nil)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "success without search",
|
|
||||||
search: nil,
|
|
||||||
limit: 50,
|
|
||||||
mockUsers: []*entities.User{
|
|
||||||
{
|
|
||||||
ID: uuid.New(),
|
|
||||||
Name: "Jane Doe",
|
|
||||||
Email: "jane@example.com",
|
|
||||||
IsActive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expectedCount: 1,
|
|
||||||
expectedError: false,
|
|
||||||
setupMocks: func(mockRepo *MockUserRepository, mockProfileRepo *MockUserProfileRepository) {
|
|
||||||
mockRepo.On("ListWithFilters", mock.Anything, (*string)(nil), (*string)(nil), boolPtr(true), 50, 0).
|
|
||||||
Return([]*entities.User{
|
|
||||||
{
|
|
||||||
ID: uuid.New(),
|
|
||||||
Name: "Jane Doe",
|
|
||||||
Email: "jane@example.com",
|
|
||||||
IsActive: true,
|
|
||||||
},
|
|
||||||
}, int64(1), nil)
|
|
||||||
|
|
||||||
mockRepo.On("GetRolesByUserIDs", mock.Anything, mock.AnythingOfType("[]uuid.UUID")).
|
|
||||||
Return(map[uuid.UUID][]entities.Role{}, nil)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "limit validation - too high",
|
|
||||||
search: nil,
|
|
||||||
limit: 150,
|
|
||||||
mockUsers: []*entities.User{},
|
|
||||||
expectedCount: 0,
|
|
||||||
expectedError: false,
|
|
||||||
setupMocks: func(mockRepo *MockUserRepository, mockProfileRepo *MockUserProfileRepository) {
|
|
||||||
mockRepo.On("ListWithFilters", mock.Anything, (*string)(nil), (*string)(nil), boolPtr(true), 100, 0).
|
|
||||||
Return([]*entities.User{}, int64(0), nil)
|
|
||||||
|
|
||||||
mockRepo.On("GetRolesByUserIDs", mock.Anything, mock.AnythingOfType("[]uuid.UUID")).
|
|
||||||
Return(map[uuid.UUID][]entities.Role{}, nil)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Create mocks
|
|
||||||
mockRepo := &MockUserRepository{}
|
|
||||||
mockProfileRepo := &MockUserProfileRepository{}
|
|
||||||
|
|
||||||
// Setup mocks
|
|
||||||
if tt.setupMocks != nil {
|
|
||||||
tt.setupMocks(mockRepo, mockProfileRepo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create processor
|
|
||||||
processor := NewUserProcessor(mockRepo, mockProfileRepo)
|
|
||||||
|
|
||||||
// Call method
|
|
||||||
result, err := processor.GetActiveUsersForMention(context.Background(), tt.search, tt.limit)
|
|
||||||
|
|
||||||
// Assertions
|
|
||||||
if tt.expectedError {
|
|
||||||
assert.Error(t, err)
|
|
||||||
} else {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Len(t, result, tt.expectedCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify mocks
|
|
||||||
mockRepo.AssertExpectations(t)
|
|
||||||
mockProfileRepo.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
func stringPtr(s string) *string {
|
|
||||||
return &s
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolPtr(b bool) *bool {
|
|
||||||
return &b
|
|
||||||
}
|
|
||||||
@ -28,4 +28,7 @@ type UserRepository interface {
|
|||||||
// New optimized helpers
|
// New optimized helpers
|
||||||
GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error)
|
GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error)
|
||||||
ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error)
|
ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error)
|
||||||
|
|
||||||
|
GetByIDWithDepartments(ctx context.Context, id uuid.UUID) (*entities.User, error)
|
||||||
|
UpdateDepartments(ctx context.Context, userID uuid.UUID, departments []entities.Department) error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,20 +23,20 @@ func (r *DispositionRouteRepository) Create(ctx context.Context, e *entities.Dis
|
|||||||
// Upsert creates or updates a disposition route based on from_department_id and to_department_id
|
// Upsert creates or updates a disposition route based on from_department_id and to_department_id
|
||||||
func (r *DispositionRouteRepository) Upsert(ctx context.Context, e *entities.DispositionRoute) error {
|
func (r *DispositionRouteRepository) Upsert(ctx context.Context, e *entities.DispositionRoute) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
|
|
||||||
// Check if route exists
|
// Check if route exists
|
||||||
var existing entities.DispositionRoute
|
var existing entities.DispositionRoute
|
||||||
err := db.WithContext(ctx).
|
err := db.WithContext(ctx).
|
||||||
Where("from_department_id = ? AND to_department_id = ?", e.FromDepartmentID, e.ToDepartmentID).
|
Where("from_department_id = ? AND to_department_id = ?", e.FromDepartmentID, e.ToDepartmentID).
|
||||||
First(&existing).Error
|
First(&existing).Error
|
||||||
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
// Create new route
|
// Create new route
|
||||||
return db.WithContext(ctx).Create(e).Error
|
return db.WithContext(ctx).Create(e).Error
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update existing route
|
// Update existing route
|
||||||
e.ID = existing.ID
|
e.ID = existing.ID
|
||||||
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).
|
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).
|
||||||
@ -47,7 +47,7 @@ func (r *DispositionRouteRepository) Upsert(ctx context.Context, e *entities.Dis
|
|||||||
// BulkUpsert performs bulk create or update for multiple routes
|
// BulkUpsert performs bulk create or update for multiple routes
|
||||||
func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID uuid.UUID, toDeptIDs []uuid.UUID, isActive bool, allowedActions entities.JSONB) (created int, updated int, err error) {
|
func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID uuid.UUID, toDeptIDs []uuid.UUID, isActive bool, allowedActions entities.JSONB) (created int, updated int, err error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
|
|
||||||
// Start transaction
|
// Start transaction
|
||||||
tx := db.WithContext(ctx).Begin()
|
tx := db.WithContext(ctx).Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -55,19 +55,19 @@ func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID
|
|||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Get existing routes for this from_department_id
|
// Get existing routes for this from_department_id
|
||||||
var existingRoutes []entities.DispositionRoute
|
var existingRoutes []entities.DispositionRoute
|
||||||
if err = tx.Where("from_department_id = ?", fromDeptID).Find(&existingRoutes).Error; err != nil {
|
if err = tx.Where("from_department_id = ?", fromDeptID).Find(&existingRoutes).Error; err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create map of existing routes
|
// Create map of existing routes
|
||||||
existingMap := make(map[uuid.UUID]entities.DispositionRoute)
|
existingMap := make(map[uuid.UUID]entities.DispositionRoute)
|
||||||
for _, route := range existingRoutes {
|
for _, route := range existingRoutes {
|
||||||
existingMap[route.ToDepartmentID] = route
|
existingMap[route.ToDepartmentID] = route
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each to_department_id
|
// Process each to_department_id
|
||||||
for _, toDeptID := range toDeptIDs {
|
for _, toDeptID := range toDeptIDs {
|
||||||
route := entities.DispositionRoute{
|
route := entities.DispositionRoute{
|
||||||
@ -76,7 +76,7 @@ func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID
|
|||||||
IsActive: isActive,
|
IsActive: isActive,
|
||||||
AllowedActions: allowedActions,
|
AllowedActions: allowedActions,
|
||||||
}
|
}
|
||||||
|
|
||||||
if existing, exists := existingMap[toDeptID]; exists {
|
if existing, exists := existingMap[toDeptID]; exists {
|
||||||
// Update existing route
|
// Update existing route
|
||||||
route.ID = existing.ID
|
route.ID = existing.ID
|
||||||
@ -96,7 +96,7 @@ func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID
|
|||||||
created++
|
created++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally deactivate routes that are no longer in the list
|
// Optionally deactivate routes that are no longer in the list
|
||||||
// (routes that exist in DB but not in the new list)
|
// (routes that exist in DB but not in the new list)
|
||||||
for _, oldRoute := range existingMap {
|
for _, oldRoute := range existingMap {
|
||||||
@ -106,12 +106,12 @@ func (r *DispositionRouteRepository) BulkUpsert(ctx context.Context, fromDeptID
|
|||||||
return created, updated, err
|
return created, updated, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commit transaction
|
// Commit transaction
|
||||||
if err = tx.Commit().Error; err != nil {
|
if err = tx.Commit().Error; err != nil {
|
||||||
return 0, 0, err
|
return 0, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return created, updated, nil
|
return created, updated, nil
|
||||||
}
|
}
|
||||||
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
|
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
|
||||||
@ -133,7 +133,7 @@ func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*en
|
|||||||
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
|
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var list []entities.DispositionRoute
|
var list []entities.DispositionRoute
|
||||||
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).
|
if err := db.WithContext(ctx).Where("from_department_id = ? and is_active=true", fromDept).
|
||||||
Preload("FromDepartment").
|
Preload("FromDepartment").
|
||||||
Preload("ToDepartment").
|
Preload("ToDepartment").
|
||||||
Order("to_department_id").Find(&list).Error; err != nil {
|
Order("to_department_id").Find(&list).Error; err != nil {
|
||||||
@ -160,20 +160,20 @@ func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID
|
|||||||
func (r *DispositionRouteRepository) ListAllGrouped(ctx context.Context) (map[uuid.UUID][]uuid.UUID, error) {
|
func (r *DispositionRouteRepository) ListAllGrouped(ctx context.Context) (map[uuid.UUID][]uuid.UUID, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var routes []entities.DispositionRoute
|
var routes []entities.DispositionRoute
|
||||||
|
|
||||||
if err := db.WithContext(ctx).
|
if err := db.WithContext(ctx).
|
||||||
Where("is_active = ?", true).
|
Where("is_active = ?", true).
|
||||||
Order("from_department_id, to_department_id").
|
Order("from_department_id, to_department_id").
|
||||||
Find(&routes).Error; err != nil {
|
Find(&routes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group by from_department_id
|
// Group by from_department_id
|
||||||
grouped := make(map[uuid.UUID][]uuid.UUID)
|
grouped := make(map[uuid.UUID][]uuid.UUID)
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
grouped[route.FromDepartmentID] = append(grouped[route.FromDepartmentID], route.ToDepartmentID)
|
grouped[route.FromDepartmentID] = append(grouped[route.FromDepartmentID], route.ToDepartmentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return grouped, nil
|
return grouped, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,7 +181,7 @@ func (r *DispositionRouteRepository) ListAllGrouped(ctx context.Context) (map[uu
|
|||||||
func (r *DispositionRouteRepository) ListAllGroupedWithDepartments(ctx context.Context) ([]entities.DispositionRoute, error) {
|
func (r *DispositionRouteRepository) ListAllGroupedWithDepartments(ctx context.Context) ([]entities.DispositionRoute, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var routes []entities.DispositionRoute
|
var routes []entities.DispositionRoute
|
||||||
|
|
||||||
if err := db.WithContext(ctx).
|
if err := db.WithContext(ctx).
|
||||||
Preload("FromDepartment").
|
Preload("FromDepartment").
|
||||||
Preload("ToDepartment").
|
Preload("ToDepartment").
|
||||||
@ -190,7 +190,7 @@ func (r *DispositionRouteRepository) ListAllGroupedWithDepartments(ctx context.C
|
|||||||
Find(&routes).Error; err != nil {
|
Find(&routes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes, nil
|
return routes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +198,7 @@ func (r *DispositionRouteRepository) ListAllGroupedWithDepartments(ctx context.C
|
|||||||
func (r *DispositionRouteRepository) ListAll(ctx context.Context) ([]entities.DispositionRoute, error) {
|
func (r *DispositionRouteRepository) ListAll(ctx context.Context) ([]entities.DispositionRoute, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var routes []entities.DispositionRoute
|
var routes []entities.DispositionRoute
|
||||||
|
|
||||||
if err := db.WithContext(ctx).
|
if err := db.WithContext(ctx).
|
||||||
Preload("FromDepartment").
|
Preload("FromDepartment").
|
||||||
Preload("ToDepartment").
|
Preload("ToDepartment").
|
||||||
@ -207,6 +207,6 @@ func (r *DispositionRouteRepository) ListAll(ctx context.Context) ([]entities.Di
|
|||||||
Find(&routes).Error; err != nil {
|
Find(&routes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes, nil
|
return routes, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -225,6 +225,45 @@ func (r *DepartmentRepository) List(ctx context.Context, search string, limit, o
|
|||||||
return list, total, nil
|
return list, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) ListWithParentFilter(ctx context.Context, search string, limit, offset int, parentPath string, excludedPaths []string) ([]entities.Department, int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
|
||||||
|
query := db.WithContext(ctx).Model(&entities.Department{})
|
||||||
|
|
||||||
|
// Filter by parent path if provided - include the parent itself and all descendants
|
||||||
|
if parentPath != "" {
|
||||||
|
query = query.Where("path = ? OR path <@ ?", parentPath, parentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude specific paths
|
||||||
|
for _, excludedPath := range excludedPaths {
|
||||||
|
query = query.Where("NOT (path ~ ?)", excludedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add search filter if provided
|
||||||
|
if search != "" {
|
||||||
|
query = query.Where("name ILIKE ? OR code ILIKE ?", "%"+search+"%", "%"+search+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paginated results
|
||||||
|
var list []entities.Department
|
||||||
|
if err := query.
|
||||||
|
Order("name ASC").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *DepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
|
func (r *DepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var e entities.Department
|
var e entities.Department
|
||||||
@ -233,3 +272,98 @@ func (r *DepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*enti
|
|||||||
}
|
}
|
||||||
return &e, nil
|
return &e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) Create(ctx context.Context, department *entities.Department) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(department).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) Update(ctx context.Context, department *entities.Department) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Save(department).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Delete(&entities.Department{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetByPath(ctx context.Context, path string) (*entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var department entities.Department
|
||||||
|
if err := db.WithContext(ctx).Where("path = ?", path).First(&department).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &department, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetAll(ctx context.Context) ([]entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var departments []entities.Department
|
||||||
|
if err := db.WithContext(ctx).Order("path ASC").Find(&departments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetAllWithParentFilter(ctx context.Context, parentPath string, excludedPaths []string) ([]entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var departments []entities.Department
|
||||||
|
|
||||||
|
query := db.WithContext(ctx)
|
||||||
|
|
||||||
|
// Filter by parent path if provided - include the parent itself and all descendants
|
||||||
|
if parentPath != "" {
|
||||||
|
query = query.Where("path = ? OR path <@ ?", parentPath, parentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclude specific paths
|
||||||
|
for _, excludedPath := range excludedPaths {
|
||||||
|
query = query.Where("NOT (path ~ ?)", excludedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := query.Order("path ASC").Find(&departments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetByPathPrefix(ctx context.Context, pathPrefix string) ([]entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var departments []entities.Department
|
||||||
|
// Using ltree operators for hierarchical queries
|
||||||
|
query := db.WithContext(ctx).Order("path ASC")
|
||||||
|
if pathPrefix != "" {
|
||||||
|
// Get all descendants of a path
|
||||||
|
query = query.Where("path <@ ?", pathPrefix)
|
||||||
|
}
|
||||||
|
if err := query.Find(&departments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetChildren(ctx context.Context, parentPath string) ([]entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var departments []entities.Department
|
||||||
|
// Get direct children and all descendants
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("path <@ ? AND path != ?", parentPath, parentPath).
|
||||||
|
Order("path ASC").
|
||||||
|
Find(&departments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return departments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) UpdateChildrenPaths(ctx context.Context, oldPath, newPath string) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
// Use raw SQL for ltree path update
|
||||||
|
// This will update all children paths by replacing the old prefix with the new one
|
||||||
|
query := `
|
||||||
|
UPDATE departments
|
||||||
|
SET path = ? || subpath(path, nlevel(?))
|
||||||
|
WHERE path <@ ? AND path != ?
|
||||||
|
`
|
||||||
|
return db.WithContext(ctx).Exec(query, newPath, oldPath, oldPath, oldPath).Error
|
||||||
|
}
|
||||||
|
|||||||
@ -194,26 +194,86 @@ func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string
|
|||||||
var users []*entities.User
|
var users []*entities.User
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
q := r.b.WithContext(ctx).Table("users").Model(&entities.User{})
|
// Build base query - use Model directly without Table for proper field mapping
|
||||||
|
baseQuery := r.b.WithContext(ctx).Model(&entities.User{})
|
||||||
|
|
||||||
if search != nil && *search != "" {
|
if search != nil && *search != "" {
|
||||||
like := "%" + *search + "%"
|
like := "%" + *search + "%"
|
||||||
q = q.Where("users.name ILIKE ?", like)
|
baseQuery = baseQuery.Where("name ILIKE ? OR email ILIKE ?", like, like)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isActive != nil {
|
if isActive != nil {
|
||||||
q = q.Where("users.is_active = ?", *isActive)
|
baseQuery = baseQuery.Where("is_active = ?", *isActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For counting with role filter, we need to use a subquery or join
|
||||||
|
countQuery := baseQuery
|
||||||
if roleCode != nil && *roleCode != "" {
|
if roleCode != nil && *roleCode != "" {
|
||||||
q = q.Joins("JOIN user_role ur ON ur.user_id = users.id AND ur.removed_at IS NULL").
|
countQuery = countQuery.
|
||||||
|
Joins("JOIN user_role ur ON ur.user_id = users.id AND ur.removed_at IS NULL").
|
||||||
Joins("JOIN roles r ON r.id = ur.role_id").
|
Joins("JOIN roles r ON r.id = ur.role_id").
|
||||||
Where("r.code = ?", *roleCode)
|
Where("r.code = ?", *roleCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := q.Distinct("users.id").Count(&total).Error; err != nil {
|
// Get total count
|
||||||
|
if err := countQuery.Count(&total).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := q.Select("users.*").Distinct("users.id").Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error; err != nil {
|
// Build query for fetching data
|
||||||
|
dataQuery := r.b.WithContext(ctx).Model(&entities.User{})
|
||||||
|
|
||||||
|
if search != nil && *search != "" {
|
||||||
|
like := "%" + *search + "%"
|
||||||
|
dataQuery = dataQuery.Where("name ILIKE ? OR email ILIKE ?", like, like)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isActive != nil {
|
||||||
|
dataQuery = dataQuery.Where("is_active = ?", *isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
if roleCode != nil && *roleCode != "" {
|
||||||
|
dataQuery = dataQuery.
|
||||||
|
Joins("JOIN user_role ur ON ur.user_id = users.id AND ur.removed_at IS NULL").
|
||||||
|
Joins("JOIN roles r ON r.id = ur.role_id").
|
||||||
|
Where("r.code = ?", *roleCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch users with preloads
|
||||||
|
if err := dataQuery.
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Preload("Profile").
|
||||||
|
Preload("Departments").
|
||||||
|
Find(&users).Error; err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, total, nil
|
return users, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) GetByIDWithDepartments(ctx context.Context, id uuid.UUID) (*entities.User, error) {
|
||||||
|
var user entities.User
|
||||||
|
err := r.b.WithContext(ctx).
|
||||||
|
Preload("Profile").
|
||||||
|
Preload("Departments").
|
||||||
|
First(&user, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) UpdateDepartments(ctx context.Context, userID uuid.UUID, departments []entities.Department) error {
|
||||||
|
// First, clear existing associations
|
||||||
|
if err := r.b.WithContext(ctx).Model(&entities.User{ID: userID}).Association("Departments").Clear(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add new associations
|
||||||
|
if len(departments) > 0 {
|
||||||
|
return r.b.WithContext(ctx).Model(&entities.User{ID: userID}).Association("Departments").Append(&departments)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -62,6 +62,12 @@ type MasterHandler interface {
|
|||||||
ListDispositionActions(c *gin.Context)
|
ListDispositionActions(c *gin.Context)
|
||||||
// departments
|
// departments
|
||||||
ListDepartments(c *gin.Context)
|
ListDepartments(c *gin.Context)
|
||||||
|
CreateDepartment(c *gin.Context)
|
||||||
|
GetDepartment(c *gin.Context)
|
||||||
|
UpdateDepartment(c *gin.Context)
|
||||||
|
DeleteDepartment(c *gin.Context)
|
||||||
|
GetOrganizationalChart(c *gin.Context)
|
||||||
|
GetOrganizationalChartByID(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LetterHandler interface {
|
type LetterHandler interface {
|
||||||
|
|||||||
@ -154,7 +154,13 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
master.PUT("/disposition-actions/:id", r.masterHandler.UpdateDispositionAction)
|
master.PUT("/disposition-actions/:id", r.masterHandler.UpdateDispositionAction)
|
||||||
master.DELETE("/disposition-actions/:id", r.masterHandler.DeleteDispositionAction)
|
master.DELETE("/disposition-actions/:id", r.masterHandler.DeleteDispositionAction)
|
||||||
|
|
||||||
|
master.POST("/departments", r.masterHandler.CreateDepartment)
|
||||||
master.GET("/departments", r.masterHandler.ListDepartments)
|
master.GET("/departments", r.masterHandler.ListDepartments)
|
||||||
|
master.GET("/departments/chart", r.masterHandler.GetOrganizationalChart)
|
||||||
|
master.GET("/departments/:id", r.masterHandler.GetDepartment)
|
||||||
|
master.GET("/departments/:id/chart", r.masterHandler.GetOrganizationalChartByID)
|
||||||
|
master.PUT("/departments/:id", r.masterHandler.UpdateDepartment)
|
||||||
|
master.DELETE("/departments/:id", r.masterHandler.DeleteDepartment)
|
||||||
}
|
}
|
||||||
|
|
||||||
lettersch := v1.Group("/letters")
|
lettersch := v1.Group("/letters")
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/logger"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -44,7 +43,7 @@ type LetterProcessor interface {
|
|||||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||||
|
|
||||||
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, string, error)
|
||||||
|
|
||||||
GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error)
|
GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error)
|
||||||
UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error)
|
UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error)
|
||||||
@ -60,6 +59,7 @@ type LetterServiceImpl struct {
|
|||||||
activityLogger ActivityLogger
|
activityLogger ActivityLogger
|
||||||
letterDispositionProcessor LetterDispositionProcessor
|
letterDispositionProcessor LetterDispositionProcessor
|
||||||
notificationProcessor processor.NotificationProcessor
|
notificationProcessor processor.NotificationProcessor
|
||||||
|
activityProcessor ActivityLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
type NumberGenerator interface {
|
type NumberGenerator interface {
|
||||||
@ -68,6 +68,7 @@ type NumberGenerator interface {
|
|||||||
|
|
||||||
type RecipientProcessor interface {
|
type RecipientProcessor interface {
|
||||||
CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
CreateDefaultRecipients(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
||||||
|
CreateRecipients(ctx context.Context, letterID uuid.UUID, departmentIDs []uuid.UUID) ([]entities.LetterIncomingRecipient, error)
|
||||||
CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error
|
CreateSingleRecipient(ctx context.Context, recipient *entities.LetterIncomingRecipient) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +90,7 @@ func NewLetterService(
|
|||||||
activityLogger ActivityLogger,
|
activityLogger ActivityLogger,
|
||||||
letterDispositionProcessor LetterDispositionProcessor,
|
letterDispositionProcessor LetterDispositionProcessor,
|
||||||
notificationProcessor processor.NotificationProcessor,
|
notificationProcessor processor.NotificationProcessor,
|
||||||
|
activityProc ActivityLogger,
|
||||||
) *LetterServiceImpl {
|
) *LetterServiceImpl {
|
||||||
return &LetterServiceImpl{
|
return &LetterServiceImpl{
|
||||||
processor: processor,
|
processor: processor,
|
||||||
@ -98,6 +100,7 @@ func NewLetterService(
|
|||||||
activityLogger: activityLogger,
|
activityLogger: activityLogger,
|
||||||
letterDispositionProcessor: letterDispositionProcessor,
|
letterDispositionProcessor: letterDispositionProcessor,
|
||||||
notificationProcessor: notificationProcessor,
|
notificationProcessor: notificationProcessor,
|
||||||
|
activityProcessor: activityProc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,7 +205,7 @@ func (s *LetterServiceImpl) logLetterCreation(ctx context.Context, letterID uuid
|
|||||||
userID := appcontext.FromGinContext(ctx).UserID
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
err := s.activityLogger.LogLetterCreated(ctx, letterID, userID, letterNumber)
|
err := s.activityLogger.LogLetterCreated(ctx, letterID, userID, letterNumber)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).Error("error when insert into log", err)
|
// Log error but don't fail the operation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,8 +232,7 @@ func (s *LetterServiceImpl) addCreatorAsRecipient(ctx context.Context, letterID
|
|||||||
|
|
||||||
// Save the recipient
|
// Save the recipient
|
||||||
if err := s.recipientProcessor.CreateSingleRecipient(ctx, &recipient); err != nil {
|
if err := s.recipientProcessor.CreateSingleRecipient(ctx, &recipient); err != nil {
|
||||||
// Log error but don't fail the whole operation
|
// Failed to add creator as recipient
|
||||||
logger.FromContext(ctx).Error("failed to add creator as recipient", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,7 +250,34 @@ func (s *LetterServiceImpl) sendLetterNotifications(ctx context.Context, letter
|
|||||||
fmt.Sprintf("%s: %s", letter.SenderInstitution.Name, letter.Subject))
|
fmt.Sprintf("%s: %s", letter.SenderInstitution.Name, letter.Subject))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.FromContext(ctx).Error("failed to send notification", err)
|
// Failed to send notification, continue anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) sendDispositionNotifications(ctx context.Context, letterID uuid.UUID, recipients []entities.LetterIncomingRecipient) {
|
||||||
|
// Get letter details for notification
|
||||||
|
appContext := appcontext.FromGinContext(ctx)
|
||||||
|
letter, err := s.processor.GetIncomingLetterByID(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, recipient := range recipients {
|
||||||
|
if recipient.RecipientUserID != nil && recipient.Status != entities.RecipientStatusCompleted {
|
||||||
|
subject := "Surat Masuk"
|
||||||
|
message := fmt.Sprintf("Disposisi surat dari %s: %s", appContext.UserName, letter.Subject)
|
||||||
|
|
||||||
|
err := s.notificationProcessor.SendIncomingLetterNotification(
|
||||||
|
ctx,
|
||||||
|
letterID,
|
||||||
|
*recipient.RecipientUserID,
|
||||||
|
subject,
|
||||||
|
message)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Failed to send notification, continue anyway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,7 +385,7 @@ func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contra
|
|||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
if err := <-errChan; err != nil {
|
if err := <-errChan; err != nil {
|
||||||
logger.FromContext(ctx).Error("batch load error", err)
|
// Batch load error, continue anyway
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,16 +456,40 @@ func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contrac
|
|||||||
}
|
}
|
||||||
|
|
||||||
var result *contract.ListDispositionsResponse
|
var result *contract.ListDispositionsResponse
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
|
||||||
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
var err error
|
var err error
|
||||||
result, err = s.processor.CreateDispositions(txCtx, req)
|
result, err = s.processor.CreateDispositions(txCtx, req)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.ToDepartmentIDs) > 0 && s.recipientProcessor != nil {
|
||||||
|
recipients, err = s.recipientProcessor.CreateRecipients(txCtx, req.LetterID, req.ToDepartmentIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.activityLogger != nil && result != nil && len(result.Dispositions) > 0 {
|
||||||
|
if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, req.LetterID, userID, "disposition_created"); err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send notifications to newly created recipients asynchronously
|
||||||
|
if s.notificationProcessor != nil && len(recipients) > 0 {
|
||||||
|
go s.sendDispositionNotifications(context.Background(), req.LetterID, recipients)
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,11 +498,64 @@ func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
return s.processor.CreateDiscussion(ctx, letterID, req)
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
|
|
||||||
|
var result *contract.LetterDiscussionResponse
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
var err error
|
||||||
|
result, err = s.processor.CreateDiscussion(txCtx, letterID, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log activity for discussion creation
|
||||||
|
if s.activityLogger != nil && result != nil {
|
||||||
|
// Create a simple activity log
|
||||||
|
if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, letterID, userID, "discussion_created"); err != nil {
|
||||||
|
// Don't fail the transaction for logging errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
return s.processor.UpdateDiscussion(ctx, letterID, discussionID, req)
|
userID := appcontext.FromGinContext(ctx).UserID
|
||||||
|
|
||||||
|
var result *contract.LetterDiscussionResponse
|
||||||
|
|
||||||
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
var err error
|
||||||
|
var oldMessage string
|
||||||
|
result, oldMessage, err = s.processor.UpdateDiscussion(txCtx, letterID, discussionID, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log activity for discussion update (could use oldMessage for more detailed logging)
|
||||||
|
if s.activityLogger != nil && result != nil {
|
||||||
|
// Create a simple activity log - oldMessage could be included in a more detailed log
|
||||||
|
_ = oldMessage // Mark as intentionally unused for now
|
||||||
|
if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, letterID, userID, "discussion_updated"); err != nil {
|
||||||
|
// Don't fail the transaction for logging errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
func (s *LetterServiceImpl) GetDepartmentDispositionStatus(ctx context.Context, req *contract.GetDepartmentDispositionStatusRequest) (*contract.ListDepartmentDispositionStatusResponse, error) {
|
||||||
|
|||||||
@ -2,7 +2,10 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"eslogad-be/config"
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
"eslogad-be/internal/entities"
|
"eslogad-be/internal/entities"
|
||||||
"eslogad-be/internal/repository"
|
"eslogad-be/internal/repository"
|
||||||
@ -17,10 +20,11 @@ type MasterServiceImpl struct {
|
|||||||
institutionRepo *repository.InstitutionRepository
|
institutionRepo *repository.InstitutionRepository
|
||||||
dispRepo *repository.DispositionActionRepository
|
dispRepo *repository.DispositionActionRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
|
config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository, department *repository.DepartmentRepository) *MasterServiceImpl {
|
func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository, department *repository.DepartmentRepository, cfg *config.Config) *MasterServiceImpl {
|
||||||
return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp, departmentRepo: department}
|
return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp, departmentRepo: department, config: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Labels
|
// Labels
|
||||||
@ -215,6 +219,385 @@ func (s *MasterServiceImpl) ListDispositionActions(ctx context.Context) (*contra
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Departments
|
// Departments
|
||||||
|
func (s *MasterServiceImpl) CreateDepartment(ctx context.Context, req *contract.CreateDepartmentRequest) (*contract.GetDepartmentResponse, error) {
|
||||||
|
// Build the path based on parent
|
||||||
|
var path string
|
||||||
|
if req.ParentID != nil {
|
||||||
|
// Get parent department to build the path
|
||||||
|
parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Build path as parent.path + code
|
||||||
|
path = parent.Path + "." + req.Code
|
||||||
|
} else {
|
||||||
|
// Root level department, just use the code as path
|
||||||
|
path = req.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
entity := &entities.Department{
|
||||||
|
Name: req.Name,
|
||||||
|
Code: req.Code,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
if err := s.departmentRepo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Get parent name if parent exists
|
||||||
|
var parentName *string
|
||||||
|
if req.ParentID != nil {
|
||||||
|
if parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID); err == nil {
|
||||||
|
parentName = &parent.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.GetDepartmentResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
Name: entity.Name,
|
||||||
|
Code: entity.Code,
|
||||||
|
Path: entity.Path,
|
||||||
|
ParentID: req.ParentID,
|
||||||
|
ParentName: parentName,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MasterServiceImpl) GetDepartment(ctx context.Context, id uuid.UUID) (*contract.GetDepartmentResponse, error) {
|
||||||
|
entity, err := s.departmentRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive parent_id and parent_name from path
|
||||||
|
var parentID *uuid.UUID
|
||||||
|
var parentName *string
|
||||||
|
parts := strings.Split(entity.Path, ".")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
// Has parent, try to find it
|
||||||
|
parentPath := strings.Join(parts[:len(parts)-1], ".")
|
||||||
|
if parent, err := s.departmentRepo.GetByPath(ctx, parentPath); err == nil {
|
||||||
|
parentID = &parent.ID
|
||||||
|
parentName = &parent.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.GetDepartmentResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
Name: entity.Name,
|
||||||
|
Code: entity.Code,
|
||||||
|
Path: entity.Path,
|
||||||
|
ParentID: parentID,
|
||||||
|
ParentName: parentName,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MasterServiceImpl) UpdateDepartment(ctx context.Context, id uuid.UUID, req *contract.UpdateDepartmentRequest) (*contract.GetDepartmentResponse, error) {
|
||||||
|
entity, err := s.departmentRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the old path before changes
|
||||||
|
oldPath := entity.Path
|
||||||
|
|
||||||
|
if req.Name != nil {
|
||||||
|
entity.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.Code != nil {
|
||||||
|
entity.Code = *req.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild path if parent is being changed or code is being changed
|
||||||
|
if req.ParentID != nil || req.Code != nil {
|
||||||
|
// Determine the code to use (new code if provided, otherwise existing)
|
||||||
|
code := entity.Code
|
||||||
|
if req.Code != nil {
|
||||||
|
code = *req.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the new path based on parent
|
||||||
|
var path string
|
||||||
|
if req.ParentID != nil {
|
||||||
|
if *req.ParentID == uuid.Nil {
|
||||||
|
// Moving to root level
|
||||||
|
path = code
|
||||||
|
} else {
|
||||||
|
// Get parent department to build the path
|
||||||
|
parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Build path as parent.path + code
|
||||||
|
path = parent.Path + "." + code
|
||||||
|
}
|
||||||
|
} else if req.Code != nil {
|
||||||
|
// Code changed but parent not specified, rebuild path with current parent
|
||||||
|
// Extract parent path from current path
|
||||||
|
parts := strings.Split(entity.Path, ".")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
// Has parent, rebuild with new code
|
||||||
|
parentPath := strings.Join(parts[:len(parts)-1], ".")
|
||||||
|
path = parentPath + "." + code
|
||||||
|
} else {
|
||||||
|
// Root level, just use new code
|
||||||
|
path = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path != "" {
|
||||||
|
entity.Path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the department
|
||||||
|
if err := s.departmentRepo.Update(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path changed, update all children paths
|
||||||
|
if oldPath != entity.Path {
|
||||||
|
if err := s.departmentRepo.UpdateChildrenPaths(ctx, oldPath, entity.Path); err != nil {
|
||||||
|
// Log the error but don't fail the operation
|
||||||
|
// You might want to handle this differently based on your requirements
|
||||||
|
// For now, we'll continue since the parent update succeeded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive parent_id and parent_name from path for response
|
||||||
|
var parentID *uuid.UUID
|
||||||
|
var parentName *string
|
||||||
|
if req.ParentID != nil {
|
||||||
|
parentID = req.ParentID
|
||||||
|
// Get parent name
|
||||||
|
if parent, err := s.departmentRepo.GetByID(ctx, *req.ParentID); err == nil {
|
||||||
|
parentName = &parent.Name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Derive from path if not provided in request
|
||||||
|
parts := strings.Split(entity.Path, ".")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
parentPath := strings.Join(parts[:len(parts)-1], ".")
|
||||||
|
if parent, err := s.departmentRepo.GetByPath(ctx, parentPath); err == nil {
|
||||||
|
parentID = &parent.ID
|
||||||
|
parentName = &parent.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.GetDepartmentResponse{
|
||||||
|
ID: entity.ID,
|
||||||
|
Name: entity.Name,
|
||||||
|
Code: entity.Code,
|
||||||
|
Path: entity.Path,
|
||||||
|
ParentID: parentID,
|
||||||
|
ParentName: parentName,
|
||||||
|
CreatedAt: entity.CreatedAt,
|
||||||
|
UpdatedAt: entity.UpdatedAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MasterServiceImpl) DeleteDepartment(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.departmentRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MasterServiceImpl) GetOrganizationalChartByID(ctx context.Context, departmentID uuid.UUID) (*contract.OrganizationalChartResponse, error) {
|
||||||
|
// First get the department to find its path
|
||||||
|
department, err := s.departmentRepo.Get(ctx, departmentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now get the organizational chart starting from this department's path
|
||||||
|
return s.GetOrganizationalChart(ctx, department.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MasterServiceImpl) GetOrganizationalChart(ctx context.Context, rootPath string) (*contract.OrganizationalChartResponse, error) {
|
||||||
|
var departments []entities.Department
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Get config values
|
||||||
|
parentPath := s.config.Department.ParentPath
|
||||||
|
excludedPaths := s.config.Department.ExcludedPaths
|
||||||
|
|
||||||
|
if rootPath == "" {
|
||||||
|
// Get all departments with parent filter
|
||||||
|
departments, err = s.departmentRepo.GetAllWithParentFilter(ctx, parentPath, excludedPaths)
|
||||||
|
} else {
|
||||||
|
// Get departments under specific path
|
||||||
|
departments, err = s.departmentRepo.GetByPathPrefix(ctx, rootPath)
|
||||||
|
// Filter out excluded paths manually for specific path queries
|
||||||
|
filteredDepts := make([]entities.Department, 0)
|
||||||
|
for _, dept := range departments {
|
||||||
|
excluded := false
|
||||||
|
for _, excludedPath := range excludedPaths {
|
||||||
|
if strings.Contains(dept.Path, excludedPath) {
|
||||||
|
excluded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !excluded {
|
||||||
|
filteredDepts = append(filteredDepts, dept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
departments = filteredDepts
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the tree structure
|
||||||
|
nodeMap := make(map[string]*contract.DepartmentNode)
|
||||||
|
roots := make([]*contract.DepartmentNode, 0)
|
||||||
|
|
||||||
|
// Calculate base level offset based on parent path
|
||||||
|
baseLevelOffset := 0
|
||||||
|
if parentPath != "" {
|
||||||
|
baseLevelOffset = len(strings.Split(parentPath, ".")) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// First pass: create all nodes including missing parents
|
||||||
|
for _, dept := range departments {
|
||||||
|
pathParts := strings.Split(dept.Path, ".")
|
||||||
|
|
||||||
|
// Create any missing parent nodes
|
||||||
|
for i := 1; i <= len(pathParts); i++ {
|
||||||
|
currentPath := strings.Join(pathParts[:i], ".")
|
||||||
|
if _, exists := nodeMap[currentPath]; !exists {
|
||||||
|
// Calculate level for this path
|
||||||
|
adjustedLevel := i - baseLevelOffset
|
||||||
|
if adjustedLevel < 1 {
|
||||||
|
adjustedLevel = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create node (placeholder for missing parents, real data for existing)
|
||||||
|
var node *contract.DepartmentNode
|
||||||
|
if currentPath == dept.Path {
|
||||||
|
// This is the actual department
|
||||||
|
node = &contract.DepartmentNode{
|
||||||
|
ID: dept.ID,
|
||||||
|
Name: dept.Name,
|
||||||
|
Code: dept.Code,
|
||||||
|
Path: dept.Path,
|
||||||
|
Level: adjustedLevel,
|
||||||
|
Children: make([]*contract.DepartmentNode, 0),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This is a missing parent - create placeholder
|
||||||
|
// Extract the last segment as the name
|
||||||
|
lastSegment := pathParts[i-1]
|
||||||
|
node = &contract.DepartmentNode{
|
||||||
|
ID: uuid.Nil, // Use nil UUID for placeholder
|
||||||
|
Name: strings.ToUpper(strings.ReplaceAll(lastSegment, "_", " ")),
|
||||||
|
Code: lastSegment,
|
||||||
|
Path: currentPath,
|
||||||
|
Level: adjustedLevel,
|
||||||
|
Children: make([]*contract.DepartmentNode, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodeMap[currentPath] = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: build the tree relationships
|
||||||
|
// Only process nodes that actually exist in the database (not placeholders)
|
||||||
|
processedPaths := make(map[string]bool)
|
||||||
|
for _, dept := range departments {
|
||||||
|
if processedPaths[dept.Path] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processedPaths[dept.Path] = true
|
||||||
|
|
||||||
|
node := nodeMap[dept.Path]
|
||||||
|
pathParts := strings.Split(dept.Path, ".")
|
||||||
|
|
||||||
|
// Check if this should be a root node
|
||||||
|
isRoot := false
|
||||||
|
if rootPath != "" && dept.Path == rootPath {
|
||||||
|
// Explicitly requested root
|
||||||
|
isRoot = true
|
||||||
|
} else if rootPath == "" && parentPath != "" && dept.Path == parentPath {
|
||||||
|
// The configured parent path is the root when showing all
|
||||||
|
isRoot = true
|
||||||
|
} else if len(pathParts) == 1 {
|
||||||
|
// Single segment path
|
||||||
|
isRoot = true
|
||||||
|
} else {
|
||||||
|
// Find parent path
|
||||||
|
parentPathStr := strings.Join(pathParts[:len(pathParts)-1], ".")
|
||||||
|
if parent, exists := nodeMap[parentPathStr]; exists {
|
||||||
|
// Check if this child is already added
|
||||||
|
alreadyAdded := false
|
||||||
|
for _, child := range parent.Children {
|
||||||
|
if child.Path == node.Path {
|
||||||
|
alreadyAdded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyAdded {
|
||||||
|
parent.Children = append(parent.Children, node)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parent doesn't exist - this is an orphaned node
|
||||||
|
// Only include it as a root if it's a direct child of the parent path
|
||||||
|
if parentPath != "" {
|
||||||
|
// Check if this is a direct child of the configured parent
|
||||||
|
expectedParent := parentPath
|
||||||
|
actualParent := strings.Join(pathParts[:len(pathParts)-1], ".")
|
||||||
|
if actualParent != expectedParent {
|
||||||
|
// This is an orphaned node - skip it
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRoot = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isRoot {
|
||||||
|
// Check for duplicates in roots
|
||||||
|
alreadyInRoots := false
|
||||||
|
for _, r := range roots {
|
||||||
|
if r.Path == node.Path {
|
||||||
|
alreadyInRoots = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyInRoots {
|
||||||
|
roots = append(roots, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort children at each level
|
||||||
|
var sortChildren func([]*contract.DepartmentNode)
|
||||||
|
sortChildren = func(nodes []*contract.DepartmentNode) {
|
||||||
|
for _, node := range nodes {
|
||||||
|
if len(node.Children) > 0 {
|
||||||
|
// Sort children by name
|
||||||
|
sort.Slice(node.Children, func(i, j int) bool {
|
||||||
|
return node.Children[i].Name < node.Children[j].Name
|
||||||
|
})
|
||||||
|
sortChildren(node.Children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort root nodes
|
||||||
|
sort.Slice(roots, func(i, j int) bool {
|
||||||
|
return roots[i].Name < roots[j].Name
|
||||||
|
})
|
||||||
|
sortChildren(roots)
|
||||||
|
|
||||||
|
return &contract.OrganizationalChartResponse{
|
||||||
|
Chart: roots,
|
||||||
|
TotalNodes: len(departments),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MasterServiceImpl) ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error) {
|
func (s *MasterServiceImpl) ListDepartments(ctx context.Context, req *contract.ListDepartmentsRequest) (*contract.ListDepartmentsResponse, error) {
|
||||||
// Set default values if not provided
|
// Set default values if not provided
|
||||||
page := req.Page
|
page := req.Page
|
||||||
@ -232,7 +615,11 @@ func (s *MasterServiceImpl) ListDepartments(ctx context.Context, req *contract.L
|
|||||||
|
|
||||||
offset := (page - 1) * limit
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
list, total, err := s.departmentRepo.List(ctx, req.Search, limit, offset)
|
// Use filtered list with parent path from config
|
||||||
|
parentPath := s.config.Department.ParentPath
|
||||||
|
excludedPaths := s.config.Department.ExcludedPaths
|
||||||
|
|
||||||
|
list, total, err := s.departmentRepo.ListWithParentFilter(ctx, req.Search, limit, offset, parentPath, excludedPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ type UserProcessor interface {
|
|||||||
UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
||||||
|
|
||||||
// New optimized listing
|
// New optimized listing
|
||||||
ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error)
|
ListUsersWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]contract.UserResponse, int, error)
|
||||||
|
|
||||||
// Get active users for mention purposes
|
// Get active users for mention purposes
|
||||||
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
||||||
|
|||||||
@ -47,16 +47,24 @@ func (s *UserServiceImpl) GetUserByEmail(ctx context.Context, email string) (*co
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) {
|
func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsersRequest) (*contract.ListUsersResponse, error) {
|
||||||
|
// Handle pagination parameters in service layer
|
||||||
page := req.Page
|
page := req.Page
|
||||||
if page <= 0 {
|
if page <= 0 {
|
||||||
page = 1
|
page = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := req.Limit
|
limit := req.Limit
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 10
|
limit = 10
|
||||||
}
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100 // Max limit to prevent performance issues
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req)
|
// Pass calculated offset and limit to processor
|
||||||
|
userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -99,5 +107,13 @@ func (s *UserServiceImpl) ListTitles(ctx context.Context) (*contract.ListTitlesR
|
|||||||
|
|
||||||
// GetActiveUsersForMention retrieves active users for mention purposes
|
// GetActiveUsersForMention retrieves active users for mention purposes
|
||||||
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
||||||
|
// Handle limit in service layer
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 50 // Default limit for mention suggestions
|
||||||
|
}
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100 // Max limit to prevent performance issues
|
||||||
|
}
|
||||||
|
|
||||||
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
|
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user