Update
This commit is contained in:
parent
de60983e4e
commit
1964fe50de
130
MIGRATION_SUMMARY.md
Normal file
130
MIGRATION_SUMMARY.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Table Restructuring Summary
|
||||
|
||||
## Overview
|
||||
This document summarizes the changes made to restructure the letter dispositions system from a single table to a more normalized structure with an association table.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Database Schema Changes
|
||||
|
||||
#### New Migration Files Created:
|
||||
- `migrations/000012_rename_dispositions_table.up.sql` - Main migration to restructure tables
|
||||
- `migrations/000012_rename_dispositions_table.down.sql` - Rollback migration
|
||||
|
||||
#### Table Changes:
|
||||
- **`letter_dispositions`** → **`letter_incoming_dispositions`**
|
||||
- Renamed table
|
||||
- Removed columns: `from_user_id`, `to_user_id`, `to_department_id`, `status`, `completed_at`
|
||||
- Renamed `from_department_id` → `department_id`
|
||||
- Added `read_at` column
|
||||
- Kept columns: `id`, `letter_id`, `department_id`, `notes`, `read_at`, `created_at`, `created_by`, `updated_at`
|
||||
|
||||
#### New Table Created:
|
||||
- **`letter_incoming_dispositions_department`**
|
||||
- Purpose: Associates dispositions with target departments
|
||||
- Columns: `id`, `letter_incoming_disposition_id`, `department_id`, `created_at`
|
||||
- Unique constraint on `(letter_incoming_disposition_id, department_id)`
|
||||
|
||||
### 2. Entity Changes
|
||||
|
||||
#### Updated Entities:
|
||||
- **`LetterDisposition`** → **`LetterIncomingDisposition`**
|
||||
- Simplified structure with only required fields
|
||||
- New table name mapping
|
||||
|
||||
#### New Entity:
|
||||
- **`LetterIncomingDispositionDepartment`**
|
||||
- Represents the many-to-many relationship between dispositions and departments
|
||||
|
||||
### 3. Repository Changes
|
||||
|
||||
#### Updated Repositories:
|
||||
- **`LetterDispositionRepository`** → **`LetterIncomingDispositionRepository`**
|
||||
- Updated to work with new entity
|
||||
|
||||
#### New Repository:
|
||||
- **`LetterIncomingDispositionDepartmentRepository`**
|
||||
- Handles CRUD operations for the association table
|
||||
- Methods: `CreateBulk`, `ListByDisposition`
|
||||
|
||||
### 4. Processor Changes
|
||||
|
||||
#### Updated Processor:
|
||||
- **`LetterProcessorImpl`**
|
||||
- Added new repository dependency
|
||||
- Updated `CreateDispositions` method to:
|
||||
- Create main disposition record
|
||||
- Create department association records
|
||||
- Maintain existing action selection functionality
|
||||
|
||||
### 5. Transformer Changes
|
||||
|
||||
#### Updated Transformer:
|
||||
- **`DispositionsToContract`** function
|
||||
- Updated to work with new entity structure
|
||||
- Maps new fields: `DepartmentID`, `ReadAt`, `UpdatedAt`
|
||||
- Removed old fields: `FromDepartmentID`, `ToDepartmentID`, `Status`
|
||||
|
||||
### 6. Contract Changes
|
||||
|
||||
#### Updated Contract:
|
||||
- **`DispositionResponse`** struct
|
||||
- Updated fields to match new entity structure
|
||||
- Added `ReadAt` and `UpdatedAt` fields
|
||||
- Replaced `FromDepartmentID` and `ToDepartmentID` with `DepartmentID`
|
||||
|
||||
### 7. Application Configuration Changes
|
||||
|
||||
#### Updated App Configuration:
|
||||
- **`internal/app/app.go`**
|
||||
- Updated repository initialization
|
||||
- Added new repository dependency
|
||||
- Updated processor initialization with new repository
|
||||
|
||||
## Migration Process
|
||||
|
||||
### Up Migration (000012_rename_dispositions_table.up.sql):
|
||||
1. Rename `letter_dispositions` to `letter_incoming_dispositions`
|
||||
2. Drop unnecessary columns
|
||||
3. Rename `from_department_id` to `department_id`
|
||||
4. Add missing columns (`read_at`, `updated_at`)
|
||||
5. Create new association table
|
||||
6. Update triggers and indexes
|
||||
|
||||
### Down Migration (000012_rename_dispositions_table.down.sql):
|
||||
1. Drop association table
|
||||
2. Restore removed columns
|
||||
3. Rename `department_id` back to `from_department_id`
|
||||
4. Restore old triggers and indexes
|
||||
5. Rename table back to `letter_dispositions`
|
||||
|
||||
## Benefits of New Structure
|
||||
|
||||
1. **Normalization**: Separates disposition metadata from department associations
|
||||
2. **Flexibility**: Allows multiple departments per disposition
|
||||
3. **Cleaner Data Model**: Removes redundant fields and simplifies the main table
|
||||
4. **Better Performance**: Smaller main table with focused indexes
|
||||
5. **Easier Maintenance**: Clear separation of concerns
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- Table name change from `letter_dispositions` to `letter_incoming_dispositions`
|
||||
- Entity structure changes (removed fields, renamed fields)
|
||||
- Repository interface changes
|
||||
- API response structure changes
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. Run migration on test database
|
||||
2. Test disposition creation with new structure
|
||||
3. Verify department associations are created correctly
|
||||
4. Test existing functionality (action selections, notes)
|
||||
5. Verify rollback migration works correctly
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise, the down migration will:
|
||||
1. Restore the original table structure
|
||||
2. Preserve all existing data
|
||||
3. Remove the new association table
|
||||
4. Restore original triggers and indexes
|
||||
4
go.mod
4
go.mod
@ -45,7 +45,7 @@ require (
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
@ -64,7 +64,7 @@ require (
|
||||
github.com/aws/aws-sdk-go v1.55.7
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
gorm.io/driver/postgres v1.5.0
|
||||
|
||||
6
go.sum
6
go.sum
@ -236,8 +236,9 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
||||
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@ -247,8 +248,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
|
||||
@ -117,7 +117,8 @@ type repositories struct {
|
||||
activityLogRepo *repository.LetterIncomingActivityLogRepository
|
||||
dispositionRouteRepo *repository.DispositionRouteRepository
|
||||
// new repos
|
||||
letterDispositionRepo *repository.LetterDispositionRepository
|
||||
letterDispositionRepo *repository.LetterIncomingDispositionRepository
|
||||
letterDispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
||||
letterDiscussionRepo *repository.LetterDiscussionRepository
|
||||
@ -141,7 +142,8 @@ func (a *App) initRepositories() *repositories {
|
||||
letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db),
|
||||
activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db),
|
||||
dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db),
|
||||
letterDispositionRepo: repository.NewLetterDispositionRepository(a.db),
|
||||
letterDispositionRepo: repository.NewLetterIncomingDispositionRepository(a.db),
|
||||
letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db),
|
||||
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
||||
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
||||
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
||||
@ -163,7 +165,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
||||
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
|
||||
return &processors{
|
||||
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
|
||||
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo),
|
||||
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispositionDeptRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo),
|
||||
activityLogger: activity,
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ const (
|
||||
CorrelationIDKey = key("CorrelationID")
|
||||
OrganizationIDKey = key("OrganizationIDKey")
|
||||
UserIDKey = key("UserID")
|
||||
OutletIDKey = key("OutletID")
|
||||
DepartmentIDKey = key("DepartmentID")
|
||||
RoleIDKey = key("RoleID")
|
||||
AppVersionKey = key("AppVersion")
|
||||
AppIDKey = key("AppID")
|
||||
@ -27,7 +27,7 @@ func LogFields(ctx interface{}) map[string]interface{} {
|
||||
fields := make(map[string]interface{})
|
||||
fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey)
|
||||
fields[string(OrganizationIDKey)] = value(ctx, OrganizationIDKey)
|
||||
fields[string(OutletIDKey)] = value(ctx, OutletIDKey)
|
||||
fields[string(DepartmentIDKey)] = value(ctx, DepartmentIDKey)
|
||||
fields[string(AppVersionKey)] = value(ctx, AppVersionKey)
|
||||
fields[string(AppIDKey)] = value(ctx, AppIDKey)
|
||||
fields[string(AppTypeKey)] = value(ctx, AppTypeKey)
|
||||
|
||||
@ -19,8 +19,7 @@ var log *Logger
|
||||
type ContextInfo struct {
|
||||
CorrelationID string
|
||||
UserID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID uuid.UUID
|
||||
DepartmentID uuid.UUID
|
||||
AppVersion string
|
||||
AppID string
|
||||
AppType string
|
||||
@ -61,8 +60,7 @@ func FromGinContext(ctx context.Context) *ContextInfo {
|
||||
return &ContextInfo{
|
||||
CorrelationID: value(ctx, CorrelationIDKey),
|
||||
UserID: uuidValue(ctx, UserIDKey),
|
||||
OutletID: uuidValue(ctx, OutletIDKey),
|
||||
OrganizationID: uuidValue(ctx, OrganizationIDKey),
|
||||
DepartmentID: uuidValue(ctx, DepartmentIDKey),
|
||||
AppVersion: value(ctx, AppVersionKey),
|
||||
AppID: value(ctx, AppIDKey),
|
||||
AppType: value(ctx, AppTypeKey),
|
||||
|
||||
@ -9,7 +9,7 @@ const (
|
||||
XAppIDHeader = "x-appid"
|
||||
XPhoneModelHeader = "X-PhoneModel"
|
||||
OrganizationID = "x_organization_id"
|
||||
OutletID = "x_owner_id"
|
||||
DepartmentID = "x_department_id"
|
||||
CountryCodeHeader = "country-code"
|
||||
AcceptedLanguageHeader = "accept-language"
|
||||
XUserLocaleHeader = "x-user-locale"
|
||||
|
||||
@ -132,6 +132,10 @@ type ListInstitutionsResponse struct {
|
||||
Institutions []InstitutionResponse `json:"institutions"`
|
||||
}
|
||||
|
||||
type ListInstitutionsRequest struct {
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
}
|
||||
|
||||
type DispositionActionResponse struct {
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
|
||||
@ -6,6 +6,12 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type DepartmentInfo struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
type DispositionRouteResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
||||
@ -14,6 +20,10 @@ type DispositionRouteResponse struct {
|
||||
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Department information
|
||||
FromDepartment DepartmentInfo `json:"from_department"`
|
||||
ToDepartment DepartmentInfo `json:"to_department"`
|
||||
}
|
||||
|
||||
type CreateDispositionRouteRequest struct {
|
||||
|
||||
@ -37,8 +37,8 @@ type IncomingLetterResponse struct {
|
||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||
Priority *PriorityResponse `json:"priority,omitempty"`
|
||||
SenderInstitution *InstitutionResponse `json:"sender_institution,omitempty"`
|
||||
ReceivedDate time.Time `json:"received_date"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Status string `json:"status"`
|
||||
@ -77,6 +77,7 @@ type CreateDispositionActionSelection struct {
|
||||
}
|
||||
|
||||
type CreateLetterDispositionRequest struct {
|
||||
FromDepartment uuid.UUID `json:"from_department"`
|
||||
LetterID uuid.UUID `json:"letter_id"`
|
||||
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
@ -86,18 +87,62 @@ type CreateLetterDispositionRequest struct {
|
||||
type DispositionResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
LetterID uuid.UUID `json:"letter_id"`
|
||||
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
|
||||
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
|
||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListDispositionsResponse struct {
|
||||
Dispositions []DispositionResponse `json:"dispositions"`
|
||||
}
|
||||
|
||||
type EnhancedDispositionResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
LetterID uuid.UUID `json:"letter_id"`
|
||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Department DepartmentResponse `json:"department"`
|
||||
Departments []DispositionDepartmentResponse `json:"departments"`
|
||||
Actions []DispositionActionSelectionResponse `json:"actions"`
|
||||
DispositionNotes []DispositionNoteResponse `json:"disposition_notes"`
|
||||
}
|
||||
|
||||
type DispositionDepartmentResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
DepartmentID uuid.UUID `json:"department_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Department *DepartmentResponse `json:"department,omitempty"`
|
||||
}
|
||||
|
||||
type DispositionActionSelectionResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ActionID uuid.UUID `json:"action_id"`
|
||||
Action *DispositionActionResponse `json:"action,omitempty"`
|
||||
Note *string `json:"note,omitempty"`
|
||||
CreatedBy uuid.UUID `json:"created_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type DispositionNoteResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||
Note string `json:"note"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type ListEnhancedDispositionsResponse struct {
|
||||
Dispositions []EnhancedDispositionResponse `json:"dispositions"`
|
||||
Discussions []LetterDiscussionResponse `json:"discussions"`
|
||||
}
|
||||
|
||||
type CreateLetterDiscussionRequest struct {
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Message string `json:"message"`
|
||||
@ -119,4 +164,10 @@ type LetterDiscussionResponse struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||
|
||||
// Preloaded user profile who created the discussion
|
||||
User *UserResponse `json:"user,omitempty"`
|
||||
|
||||
// Preloaded user profiles for mentions
|
||||
MentionedUsers []UserResponse `json:"mentioned_users,omitempty"`
|
||||
}
|
||||
|
||||
@ -55,6 +55,7 @@ type UserResponse struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Roles []RoleResponse `json:"roles,omitempty"`
|
||||
DepartmentResponse []DepartmentResponse `json:"department_response"`
|
||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
@ -128,3 +129,15 @@ type TitleResponse struct {
|
||||
type ListTitlesResponse struct {
|
||||
Titles []TitleResponse `json:"titles"`
|
||||
}
|
||||
|
||||
// MentionUsersRequest represents the request for getting users for mention purposes
|
||||
type MentionUsersRequest struct {
|
||||
Search *string `json:"search,omitempty" form:"search"` // Optional search term for username
|
||||
Limit *int `json:"limit,omitempty" form:"limit"` // Optional limit, defaults to 50, max 100
|
||||
}
|
||||
|
||||
// MentionUsersResponse represents the response for getting users for mention purposes
|
||||
type MentionUsersResponse struct {
|
||||
Users []UserResponse `json:"users"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
@ -14,6 +14,10 @@ type DispositionRoute struct {
|
||||
AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Relationships
|
||||
FromDepartment Department `gorm:"foreignKey:FromDepartmentID;references:ID" json:"from_department,omitempty"`
|
||||
ToDepartment Department `gorm:"foreignKey:ToDepartmentID;references:ID" json:"to_department,omitempty"`
|
||||
}
|
||||
|
||||
func (DispositionRoute) TableName() string { return "disposition_routes" }
|
||||
|
||||
@ -16,6 +16,9 @@ type LetterDiscussion struct {
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||
|
||||
// Relationships
|
||||
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" }
|
||||
|
||||
@ -6,32 +6,36 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LetterDispositionStatus string
|
||||
|
||||
const (
|
||||
DispositionPending LetterDispositionStatus = "pending"
|
||||
DispositionRead LetterDispositionStatus = "read"
|
||||
DispositionRejected LetterDispositionStatus = "rejected"
|
||||
DispositionCompleted LetterDispositionStatus = "completed"
|
||||
)
|
||||
|
||||
type LetterDisposition struct {
|
||||
type LetterIncomingDisposition struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||
FromUserID *uuid.UUID `json:"from_user_id,omitempty"`
|
||||
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
|
||||
ToUserID *uuid.UUID `json:"to_user_id,omitempty"`
|
||||
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
|
||||
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Status LetterDispositionStatus `gorm:"not null;default:'pending'" json:"status"`
|
||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
Department Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||
Departments []LetterIncomingDispositionDepartment `gorm:"foreignKey:LetterIncomingDispositionID;references:ID" json:"departments,omitempty"`
|
||||
ActionSelections []LetterDispositionActionSelection `gorm:"foreignKey:DispositionID;references:ID" json:"action_selections,omitempty"`
|
||||
DispositionNotes []DispositionNote `gorm:"foreignKey:DispositionID;references:ID" json:"disposition_notes,omitempty"`
|
||||
}
|
||||
|
||||
func (LetterDisposition) TableName() string { return "letter_dispositions" }
|
||||
func (LetterIncomingDisposition) TableName() string { return "letter_incoming_dispositions" }
|
||||
|
||||
type LetterIncomingDispositionDepartment struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
LetterIncomingDispositionID uuid.UUID `gorm:"type:uuid;not null" json:"letter_incoming_disposition_id"`
|
||||
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
|
||||
// Relationships
|
||||
Department *Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||
}
|
||||
|
||||
func (LetterIncomingDispositionDepartment) TableName() string {
|
||||
return "letter_incoming_dispositions_department"
|
||||
}
|
||||
|
||||
type DispositionNote struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||
@ -39,6 +43,9 @@ type DispositionNote struct {
|
||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||
Note string `gorm:"not null" json:"note"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
|
||||
// Relationships
|
||||
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
func (DispositionNote) TableName() string { return "disposition_notes" }
|
||||
@ -50,6 +57,9 @@ type LetterDispositionActionSelection struct {
|
||||
Note *string `json:"note,omitempty"`
|
||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
|
||||
// Relationships
|
||||
Action *DispositionAction `gorm:"foreignKey:ActionID;references:ID" json:"action,omitempty"`
|
||||
}
|
||||
|
||||
func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" }
|
||||
|
||||
@ -48,6 +48,7 @@ type User struct {
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
|
||||
Departments []Department `gorm:"many2many:user_department;foreignKey:ID;joinForeignKey:user_id;References:ID;joinReferences:department_id" json:"departments,omitempty"`
|
||||
}
|
||||
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/appcontext"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
@ -71,12 +72,9 @@ func (h *DispositionRouteHandler) Get(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
|
||||
fromID, err := uuid.Parse(c.Param("from_department_id"))
|
||||
if err != nil {
|
||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid from_department_id", Code: 400})
|
||||
return
|
||||
}
|
||||
resp, err := h.svc.ListByFromDept(c.Request.Context(), fromID)
|
||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||
|
||||
resp, err := h.svc.ListByFromDept(c.Request.Context(), appCtx.DepartmentID)
|
||||
if err != nil {
|
||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||
return
|
||||
|
||||
@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/appcontext"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@ -19,7 +20,7 @@ type LetterService interface {
|
||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error)
|
||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, 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)
|
||||
@ -112,11 +113,13 @@ func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
||||
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||
var req contract.CreateLetterDispositionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||
return
|
||||
}
|
||||
req.FromDepartment = appCtx.DepartmentID
|
||||
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||
@ -125,13 +128,13 @@ func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
||||
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||
}
|
||||
|
||||
func (h *LetterHandler) ListDispositionsByLetter(c *gin.Context) {
|
||||
func (h *LetterHandler) GetEnhancedDispositionsByLetter(c *gin.Context) {
|
||||
letterID, err := uuid.Parse(c.Param("letter_id"))
|
||||
if err != nil {
|
||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
|
||||
return
|
||||
}
|
||||
resp, err := h.svc.ListDispositionsByLetter(c.Request.Context(), letterID)
|
||||
resp, err := h.svc.GetEnhancedDispositionsByLetter(c.Request.Context(), letterID)
|
||||
if err != nil {
|
||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||
return
|
||||
|
||||
@ -24,7 +24,7 @@ type MasterService interface {
|
||||
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
|
||||
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
|
||||
DeleteInstitution(ctx context.Context, id uuid.UUID) error
|
||||
ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error)
|
||||
ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error)
|
||||
|
||||
CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
||||
UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
||||
@ -190,7 +190,13 @@ func (h *MasterHandler) DeleteInstitution(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *MasterHandler) ListInstitutions(c *gin.Context) {
|
||||
resp, err := h.svc.ListInstitutions(c.Request.Context())
|
||||
var req contract.ListInstitutionsRequest
|
||||
|
||||
if search := c.Query("search"); search != "" {
|
||||
req.Search = &search
|
||||
}
|
||||
|
||||
resp, err := h.svc.ListInstitutions(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||
return
|
||||
|
||||
190
internal/handler/master_handler_test.go
Normal file
190
internal/handler/master_handler_test.go
Normal file
@ -0,0 +1,190 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockMasterService is a mock implementation of MasterService
|
||||
type MockMasterService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockMasterService) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*contract.LabelResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) {
|
||||
args := m.Called(ctx, id, req)
|
||||
return args.Get(0).(*contract.LabelResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) DeleteLabel(ctx context.Context, id uuid.UUID) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(*contract.ListLabelsResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*contract.PriorityResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) {
|
||||
args := m.Called(ctx, id, req)
|
||||
return args.Get(0).(*contract.PriorityResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) DeletePriority(ctx context.Context, id uuid.UUID) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(*contract.ListPrioritiesResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*contract.InstitutionResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
||||
args := m.Called(ctx, id, req)
|
||||
return args.Get(0).(*contract.InstitutionResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*contract.ListInstitutionsResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
||||
args := m.Called(ctx, req)
|
||||
return args.Get(0).(*contract.DispositionActionResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
||||
args := m.Called(ctx, id, req)
|
||||
return args.Get(0).(*contract.DispositionActionResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockMasterService) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(*contract.ListDispositionActionsResponse), args.Error(1)
|
||||
}
|
||||
|
||||
func TestMasterHandler_ListInstitutions_WithSearch(t *testing.T) {
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
mockService := new(MockMasterService)
|
||||
handler := NewMasterHandler(mockService)
|
||||
|
||||
// Test data
|
||||
searchTerm := "university"
|
||||
expectedResponse := &contract.ListInstitutionsResponse{
|
||||
Institutions: []contract.InstitutionResponse{
|
||||
{
|
||||
ID: "123",
|
||||
Name: "Test University",
|
||||
Type: "university",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Setup mock expectations
|
||||
mockService.On("ListInstitutions", mock.Anything, &contract.ListInstitutionsRequest{
|
||||
Search: &searchTerm,
|
||||
}).Return(expectedResponse, nil)
|
||||
|
||||
// Create request
|
||||
req, _ := http.NewRequest("GET", "/institutions?search="+searchTerm, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create gin context
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Execute
|
||||
handler.ListInstitutions(c)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify mock was called correctly
|
||||
mockService.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestMasterHandler_ListInstitutions_WithoutSearch(t *testing.T) {
|
||||
// Setup
|
||||
gin.SetMode(gin.TestMode)
|
||||
mockService := new(MockMasterService)
|
||||
handler := NewMasterHandler(mockService)
|
||||
|
||||
// Test data
|
||||
expectedResponse := &contract.ListInstitutionsResponse{
|
||||
Institutions: []contract.InstitutionResponse{
|
||||
{
|
||||
ID: "123",
|
||||
Name: "Test Institution",
|
||||
Type: "company",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Setup mock expectations
|
||||
mockService.On("ListInstitutions", mock.Anything, &contract.ListInstitutionsRequest{
|
||||
Search: nil,
|
||||
}).Return(expectedResponse, nil)
|
||||
|
||||
// Create request
|
||||
req, _ := http.NewRequest("GET", "/institutions", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Create gin context
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Execute
|
||||
handler.ListInstitutions(c)
|
||||
|
||||
// Assertions
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify mock was called correctly
|
||||
mockService.AssertExpectations(t)
|
||||
}
|
||||
@ -285,12 +285,48 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *UserHandler) ListTitles(c *gin.Context) {
|
||||
resp, err := h.userService.ListTitles(c.Request.Context())
|
||||
titles, err := h.userService.ListTitles(c.Request.Context())
|
||||
if err != nil {
|
||||
logger.FromContext(c).WithError(err).Error("UserHandler::ListTitles -> Failed to get titles from service")
|
||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||
|
||||
logger.FromContext(c).Infof("UserHandler::ListTitles -> Successfully retrieved titles = %+v", titles)
|
||||
c.JSON(http.StatusOK, titles)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
|
||||
search := c.Query("search")
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit <= 0 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
var searchPtr *string
|
||||
if search != "" {
|
||||
searchPtr = &search
|
||||
}
|
||||
|
||||
users, err := h.userService.GetActiveUsersForMention(c.Request.Context(), searchPtr, limit)
|
||||
if err != nil {
|
||||
logger.FromContext(c).WithError(err).Error("UserHandler::GetActiveUsersForMention -> Failed to get active users from service")
|
||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := contract.MentionUsersResponse{
|
||||
Users: users,
|
||||
Count: len(users),
|
||||
}
|
||||
|
||||
logger.FromContext(c).Infof("UserHandler::GetActiveUsersForMention -> Successfully retrieved %d active users", len(users))
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
||||
|
||||
@ -20,4 +20,7 @@ type UserService interface {
|
||||
UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
||||
|
||||
ListTitles(ctx context.Context) (*contract.ListTitlesResponse, error)
|
||||
|
||||
// Get active users for mention purposes
|
||||
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
||||
}
|
||||
|
||||
@ -41,6 +41,12 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||
if len(userResponse.DepartmentResponse) > 0 {
|
||||
departmentID := userResponse.DepartmentResponse[0].ID.String()
|
||||
setKeyInContext(c, appcontext.DepartmentIDKey, departmentID)
|
||||
} else {
|
||||
setKeyInContext(c, appcontext.DepartmentIDKey, "")
|
||||
}
|
||||
|
||||
if roles, perms, err := m.authService.ExtractAccess(token); err == nil {
|
||||
c.Set("user_roles", roles)
|
||||
|
||||
@ -13,7 +13,7 @@ func PopulateContext() gin.HandlerFunc {
|
||||
setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c))
|
||||
setKeyInContext(c, appcontext.AppTypeKey, getAppType(c))
|
||||
setKeyInContext(c, appcontext.OrganizationIDKey, getOrganizationID(c))
|
||||
setKeyInContext(c, appcontext.OutletIDKey, getOutletID(c))
|
||||
setKeyInContext(c, appcontext.DepartmentIDKey, getDepartmentID(c))
|
||||
setKeyInContext(c, appcontext.DeviceOSKey, getDeviceOS(c))
|
||||
setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c))
|
||||
setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c))
|
||||
@ -37,8 +37,8 @@ func getOrganizationID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.OrganizationID)
|
||||
}
|
||||
|
||||
func getOutletID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.OutletID)
|
||||
func getDepartmentID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.DepartmentID)
|
||||
}
|
||||
|
||||
func getDeviceOS(c *gin.Context) string {
|
||||
|
||||
@ -19,21 +19,22 @@ type LetterProcessorImpl struct {
|
||||
attachRepo *repository.LetterIncomingAttachmentRepository
|
||||
txManager *repository.TxManager
|
||||
activity *ActivityLogProcessorImpl
|
||||
// new repos for dispositions
|
||||
dispositionRepo *repository.LetterDispositionRepository
|
||||
dispositionRepo *repository.LetterIncomingDispositionRepository
|
||||
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
||||
// discussion repo
|
||||
discussionRepo *repository.LetterDiscussionRepository
|
||||
// settings and recipients
|
||||
settingRepo *repository.AppSettingRepository
|
||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||
departmentRepo *repository.DepartmentRepository
|
||||
userDeptRepo *repository.UserDepartmentRepository
|
||||
priorityRepo *repository.PriorityRepository
|
||||
institutionRepo *repository.InstitutionRepository
|
||||
dispActionRepo *repository.DispositionActionRepository
|
||||
}
|
||||
|
||||
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository) *LetterProcessorImpl {
|
||||
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo}
|
||||
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository, priorityRepo *repository.PriorityRepository, institutionRepo *repository.InstitutionRepository, dispActionRepo *repository.DispositionActionRepository) *LetterProcessorImpl {
|
||||
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo, institutionRepo: institutionRepo, dispActionRepo: dispActionRepo}
|
||||
}
|
||||
|
||||
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||
@ -85,7 +86,6 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
|
||||
}
|
||||
}
|
||||
|
||||
// resolve department codes to ids using repository
|
||||
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
|
||||
for _, code := range defaultDeptCodes {
|
||||
dep, err := p.departmentRepo.GetByCode(txCtx, code)
|
||||
@ -94,20 +94,19 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
|
||||
}
|
||||
depIDs = append(depIDs, dep.ID)
|
||||
}
|
||||
// query user memberships for all departments at once
|
||||
|
||||
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
|
||||
// build recipients: one department recipient per department + one user recipient per membership
|
||||
recipients := make([]entities.LetterIncomingRecipient, 0, len(depIDs)+len(userMemberships))
|
||||
// department recipients
|
||||
for _, depID := range depIDs {
|
||||
id := depID
|
||||
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientDepartmentID: &id, Status: entities.RecipientStatusNew})
|
||||
}
|
||||
// user recipients
|
||||
var recipients []entities.LetterIncomingRecipient
|
||||
|
||||
mapsUsers := map[string]bool{}
|
||||
for _, row := range userMemberships {
|
||||
uid := row.UserID
|
||||
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, Status: entities.RecipientStatusNew})
|
||||
if _, ok := mapsUsers[uid.String()]; !ok {
|
||||
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, RecipientDepartmentID: &row.DepartmentID, Status: entities.RecipientStatusNew})
|
||||
}
|
||||
mapsUsers[uid.String()] = true
|
||||
}
|
||||
|
||||
if len(recipients) > 0 {
|
||||
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
||||
return err
|
||||
@ -141,9 +140,26 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
|
||||
}
|
||||
|
||||
savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID)
|
||||
result = transformer.LetterEntityToContract(entity, savedAttachments)
|
||||
var pr *entities.Priority
|
||||
if entity.PriorityID != nil {
|
||||
if p.priorityRepo != nil {
|
||||
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
|
||||
pr = got
|
||||
}
|
||||
}
|
||||
}
|
||||
var inst *entities.Institution
|
||||
if entity.SenderInstitutionID != nil {
|
||||
if p.institutionRepo != nil {
|
||||
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
|
||||
inst = got
|
||||
}
|
||||
}
|
||||
}
|
||||
result = transformer.LetterEntityToContract(entity, savedAttachments, pr, inst)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -156,7 +172,19 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid
|
||||
return nil, err
|
||||
}
|
||||
atts, _ := p.attachRepo.ListByLetter(ctx, id)
|
||||
return transformer.LetterEntityToContract(entity, atts), nil
|
||||
var pr *entities.Priority
|
||||
if entity.PriorityID != nil && p.priorityRepo != nil {
|
||||
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
|
||||
pr = got
|
||||
}
|
||||
}
|
||||
var inst *entities.Institution
|
||||
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
|
||||
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
|
||||
inst = got
|
||||
}
|
||||
}
|
||||
return transformer.LetterEntityToContract(entity, atts, pr, inst), nil
|
||||
}
|
||||
|
||||
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
|
||||
@ -175,7 +203,19 @@ func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *cont
|
||||
respList := make([]contract.IncomingLetterResponse, 0, len(list))
|
||||
for _, e := range list {
|
||||
atts, _ := p.attachRepo.ListByLetter(ctx, e.ID)
|
||||
resp := transformer.LetterEntityToContract(&e, atts)
|
||||
var pr *entities.Priority
|
||||
if e.PriorityID != nil && p.priorityRepo != nil {
|
||||
if got, err := p.priorityRepo.Get(ctx, *e.PriorityID); err == nil {
|
||||
pr = got
|
||||
}
|
||||
}
|
||||
var inst *entities.Institution
|
||||
if e.SenderInstitutionID != nil && p.institutionRepo != nil {
|
||||
if got, err := p.institutionRepo.Get(ctx, *e.SenderInstitutionID); err == nil {
|
||||
inst = got
|
||||
}
|
||||
}
|
||||
resp := transformer.LetterEntityToContract(&e, atts, pr, inst)
|
||||
respList = append(respList, *resp)
|
||||
}
|
||||
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
|
||||
@ -225,7 +265,19 @@ func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.
|
||||
}
|
||||
}
|
||||
atts, _ := p.attachRepo.ListByLetter(txCtx, id)
|
||||
out = transformer.LetterEntityToContract(entity, atts)
|
||||
var pr *entities.Priority
|
||||
if entity.PriorityID != nil && p.priorityRepo != nil {
|
||||
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
|
||||
pr = got
|
||||
}
|
||||
}
|
||||
var inst *entities.Institution
|
||||
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
|
||||
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
|
||||
inst = got
|
||||
}
|
||||
}
|
||||
out = transformer.LetterEntityToContract(entity, atts, pr, inst)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@ -254,20 +306,28 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
|
||||
var out *contract.ListDispositionsResponse
|
||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
userID := appcontext.FromGinContext(txCtx).UserID
|
||||
created := make([]entities.LetterDisposition, 0, len(req.ToDepartmentIDs))
|
||||
for _, toDept := range req.ToDepartmentIDs {
|
||||
disp := entities.LetterDisposition{
|
||||
|
||||
disp := entities.LetterIncomingDisposition{
|
||||
LetterID: req.LetterID,
|
||||
FromDepartmentID: nil,
|
||||
ToDepartmentID: &toDept,
|
||||
DepartmentID: &req.FromDepartment,
|
||||
Notes: req.Notes,
|
||||
Status: entities.DispositionPending,
|
||||
CreatedBy: userID,
|
||||
}
|
||||
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
||||
return err
|
||||
}
|
||||
created = append(created, disp)
|
||||
|
||||
var dispDepartments []entities.LetterIncomingDispositionDepartment
|
||||
for _, toDept := range req.ToDepartmentIDs {
|
||||
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
||||
LetterIncomingDispositionID: disp.ID,
|
||||
DepartmentID: toDept,
|
||||
})
|
||||
}
|
||||
|
||||
if err := p.dispositionDeptRepo.CreateBulk(txCtx, dispDepartments); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(req.SelectedActions) > 0 {
|
||||
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
||||
@ -286,16 +346,13 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
|
||||
|
||||
if p.activity != nil {
|
||||
action := "disposition.created"
|
||||
for _, d := range created {
|
||||
ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID}
|
||||
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil {
|
||||
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: transformer.DispositionsToContract(created)}
|
||||
out = &contract.ListDispositionsResponse{Dispositions: []contract.DispositionResponse{transformer.DispoToContract(disp)}}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@ -312,6 +369,64 @@ func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, lett
|
||||
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
|
||||
}
|
||||
|
||||
func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||
// Get dispositions with all related data preloaded in a single query
|
||||
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get discussions with preloaded user profiles
|
||||
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract all mentioned user IDs from discussions for efficient batch fetching
|
||||
var mentionedUserIDs []uuid.UUID
|
||||
mentionedUserIDsMap := make(map[uuid.UUID]bool)
|
||||
|
||||
for _, discussion := range discussions {
|
||||
if discussion.Mentions != nil {
|
||||
mentions := map[string]interface{}(discussion.Mentions)
|
||||
if userIDs, ok := mentions["user_ids"]; ok {
|
||||
if userIDList, ok := userIDs.([]interface{}); ok {
|
||||
for _, userID := range userIDList {
|
||||
if userIDStr, ok := userID.(string); ok {
|
||||
if userUUID, err := uuid.Parse(userIDStr); err == nil {
|
||||
if !mentionedUserIDsMap[userUUID] {
|
||||
mentionedUserIDsMap[userUUID] = true
|
||||
mentionedUserIDs = append(mentionedUserIDs, userUUID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all mentioned users in a single batch query
|
||||
var mentionedUsers []entities.User
|
||||
if len(mentionedUserIDs) > 0 {
|
||||
mentionedUsers, err = p.discussionRepo.GetUsersByIDs(ctx, mentionedUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Transform dispositions
|
||||
enhancedDispositions := transformer.EnhancedDispositionsWithPreloadedDataToContract(dispositions)
|
||||
|
||||
// Transform discussions with mentioned users
|
||||
enhancedDiscussions := transformer.DiscussionsWithPreloadedDataToContract(discussions, mentionedUsers)
|
||||
|
||||
return &contract.ListEnhancedDispositionsResponse{
|
||||
Dispositions: enhancedDispositions,
|
||||
Discussions: enhancedDiscussions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||
var out *contract.LetterDiscussionResponse
|
||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||
@ -320,7 +435,7 @@ func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uui
|
||||
if req.Mentions != nil {
|
||||
mentions = entities.JSONB(req.Mentions)
|
||||
}
|
||||
disc := &entities.LetterDiscussion{LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: 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
|
||||
}
|
||||
|
||||
@ -112,9 +112,11 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
|
||||
}
|
||||
resp := transformer.EntityToContract(user)
|
||||
if resp != nil {
|
||||
// Roles are loaded separately since they're not preloaded
|
||||
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
|
||||
resp.Roles = transformer.RolesToContract(roles)
|
||||
}
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@ -125,6 +127,7 @@ func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
return transformer.EntityToContract(user), nil
|
||||
}
|
||||
|
||||
@ -149,6 +152,7 @@ func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contr
|
||||
for i := range responses {
|
||||
userIDs = append(userIDs, responses[i].ID)
|
||||
}
|
||||
// Roles are loaded separately since they're not preloaded
|
||||
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
|
||||
if err == nil {
|
||||
for i := range responses {
|
||||
@ -157,6 +161,7 @@ func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contr
|
||||
}
|
||||
}
|
||||
}
|
||||
// Departments are now preloaded, so they're already in the responses
|
||||
return responses, int(totalCount), nil
|
||||
}
|
||||
|
||||
@ -272,3 +277,38 @@ func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.U
|
||||
}
|
||||
return transformer.ProfileEntityToContract(entity), nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if limit <= 0 {
|
||||
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
|
||||
isActive := true
|
||||
users, _, err := p.userRepo.ListWithFilters(ctx, search, nil, &isActive, limit, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active users: %w", err)
|
||||
}
|
||||
|
||||
responses := transformer.EntitiesToContracts(users)
|
||||
userIDs := make([]uuid.UUID, 0, len(responses))
|
||||
for i := range responses {
|
||||
userIDs = append(userIDs, responses[i].ID)
|
||||
}
|
||||
|
||||
// Load roles for the users
|
||||
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
|
||||
if err == nil {
|
||||
for i := range responses {
|
||||
if roles, ok := rolesMap[responses[i].ID]; ok {
|
||||
responses[i].Roles = transformer.RolesToContract(roles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
250
internal/processor/user_processor_test.go
Normal file
250
internal/processor/user_processor_test.go
Normal file
@ -0,0 +1,250 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"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
|
||||
}
|
||||
@ -26,7 +26,10 @@ func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.Dis
|
||||
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var e entities.DispositionRoute
|
||||
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||
if err := db.WithContext(ctx).
|
||||
Preload("FromDepartment").
|
||||
Preload("ToDepartment").
|
||||
First(&e, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
@ -34,11 +37,15 @@ func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*en
|
||||
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.DispositionRoute
|
||||
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).Order("to_department_id").Find(&list).Error; err != nil {
|
||||
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).
|
||||
Preload("FromDepartment").
|
||||
Preload("ToDepartment").
|
||||
Order("to_department_id").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
|
||||
|
||||
@ -104,19 +104,56 @@ func (r *LetterIncomingActivityLogRepository) ListByLetter(ctx context.Context,
|
||||
return list, nil
|
||||
}
|
||||
|
||||
type LetterDispositionRepository struct{ db *gorm.DB }
|
||||
type LetterIncomingDispositionRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewLetterDispositionRepository(db *gorm.DB) *LetterDispositionRepository {
|
||||
return &LetterDispositionRepository{db: db}
|
||||
func NewLetterIncomingDispositionRepository(db *gorm.DB) *LetterIncomingDispositionRepository {
|
||||
return &LetterIncomingDispositionRepository{db: db}
|
||||
}
|
||||
func (r *LetterDispositionRepository) Create(ctx context.Context, e *entities.LetterDisposition) error {
|
||||
func (r *LetterIncomingDispositionRepository) Create(ctx context.Context, e *entities.LetterIncomingDisposition) error {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
return db.WithContext(ctx).Create(e).Error
|
||||
}
|
||||
func (r *LetterDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDisposition, error) {
|
||||
func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.LetterDisposition
|
||||
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("created_at ASC").Find(&list).Error; err != nil {
|
||||
var list []entities.LetterIncomingDisposition
|
||||
if err := db.WithContext(ctx).
|
||||
Where("letter_id = ?", letterID).
|
||||
Preload("Department").
|
||||
Preload("Departments.Department").
|
||||
Preload("ActionSelections.Action").
|
||||
Preload("DispositionNotes.User").
|
||||
Order("created_at ASC").
|
||||
Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
type LetterIncomingDispositionDepartmentRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewLetterIncomingDispositionDepartmentRepository(db *gorm.DB) *LetterIncomingDispositionDepartmentRepository {
|
||||
return &LetterIncomingDispositionDepartmentRepository{db: db}
|
||||
}
|
||||
func (r *LetterIncomingDispositionDepartmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingDispositionDepartment) error {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
return db.WithContext(ctx).Create(&list).Error
|
||||
}
|
||||
func (r *LetterIncomingDispositionDepartmentRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.LetterIncomingDispositionDepartment
|
||||
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *LetterIncomingDispositionDepartmentRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.LetterIncomingDispositionDepartment
|
||||
if len(dispositionIDs) == 0 {
|
||||
return list, nil
|
||||
}
|
||||
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id IN ?", dispositionIDs).Order("letter_incoming_disposition_id, created_at ASC").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
@ -132,6 +169,27 @@ func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.Disp
|
||||
return db.WithContext(ctx).Create(e).Error
|
||||
}
|
||||
|
||||
func (r *DispositionNoteRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.DispositionNote, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.DispositionNote
|
||||
if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *DispositionNoteRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.DispositionNote, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.DispositionNote
|
||||
if len(dispositionIDs) == 0 {
|
||||
return list, nil
|
||||
}
|
||||
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
type LetterDispositionActionSelectionRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
|
||||
@ -150,6 +208,18 @@ func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx conte
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *LetterDispositionActionSelectionRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.LetterDispositionActionSelection
|
||||
if len(dispositionIDs) == 0 {
|
||||
return list, nil
|
||||
}
|
||||
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
type LetterDiscussionRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository {
|
||||
@ -179,6 +249,35 @@ func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.Let
|
||||
Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error
|
||||
}
|
||||
|
||||
func (r *LetterDiscussionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDiscussion, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var list []entities.LetterDiscussion
|
||||
if err := db.WithContext(ctx).
|
||||
Where("letter_id = ?", letterID).
|
||||
Preload("User.Profile").
|
||||
Order("created_at ASC").
|
||||
Find(&list).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (r *LetterDiscussionRepository) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) {
|
||||
if len(userIDs) == 0 {
|
||||
return []entities.User{}, nil
|
||||
}
|
||||
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var users []entities.User
|
||||
if err := db.WithContext(ctx).
|
||||
Where("id IN ?", userIDs).
|
||||
Preload("Profile").
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
type AppSettingRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository { return &AppSettingRepository{db: db} }
|
||||
|
||||
@ -78,6 +78,20 @@ func (r *InstitutionRepository) List(ctx context.Context) ([]entities.Institutio
|
||||
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *InstitutionRepository) ListWithSearch(ctx context.Context, search *string) ([]entities.Institution, error) {
|
||||
var list []entities.Institution
|
||||
q := r.db.WithContext(ctx).Model(&entities.Institution{})
|
||||
|
||||
if search != nil && *search != "" {
|
||||
like := "%" + *search + "%"
|
||||
q = q.Where("name ILIKE ? OR type ILIKE ? OR address ILIKE ? OR contact_person ILIKE ?", like, like, like, like)
|
||||
}
|
||||
|
||||
err := q.Order("name ASC").Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) {
|
||||
var e entities.Institution
|
||||
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||
@ -113,6 +127,17 @@ func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*e
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func (r *DispositionActionRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]entities.DispositionAction, error) {
|
||||
var actions []entities.DispositionAction
|
||||
if len(ids) == 0 {
|
||||
return actions, nil
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&actions).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
type DepartmentRepository struct{ db *gorm.DB }
|
||||
|
||||
func NewDepartmentRepository(db *gorm.DB) *DepartmentRepository { return &DepartmentRepository{db: db} }
|
||||
@ -125,3 +150,12 @@ func (r *DepartmentRepository) GetByCode(ctx context.Context, code string) (*ent
|
||||
}
|
||||
return &dep, nil
|
||||
}
|
||||
|
||||
func (r *DepartmentRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
var dep entities.Department
|
||||
if err := db.WithContext(ctx).First(&dep, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dep, nil
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@ func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) er
|
||||
|
||||
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
|
||||
var user entities.User
|
||||
err := r.b.WithContext(ctx).Preload("Profile").First(&user, "id = ?", id).Error
|
||||
err := r.b.WithContext(ctx).
|
||||
Preload("Profile").
|
||||
Preload("Departments").
|
||||
First(&user, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -34,7 +37,10 @@ func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entiti
|
||||
|
||||
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
var user entities.User
|
||||
err := r.b.WithContext(ctx).Preload("Profile").Where("email = ?", email).First(&user).Error
|
||||
err := r.b.WithContext(ctx).
|
||||
Preload("Profile").
|
||||
Preload("Departments").
|
||||
Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -43,7 +49,7 @@ func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*ent
|
||||
|
||||
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
err := r.b.WithContext(ctx).Preload("Profile").Where("role = ?", role).Find(&users).Error
|
||||
err := r.b.WithContext(ctx).Preload("Profile").Preload("Departments").Where("role = ?", role).Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
@ -52,6 +58,7 @@ func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID
|
||||
err := r.b.WithContext(ctx).
|
||||
Where(" is_active = ?", organizationID, true).
|
||||
Preload("Profile").
|
||||
Preload("Departments").
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
@ -90,7 +97,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := query.Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error
|
||||
err := query.Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
@ -141,19 +148,19 @@ func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID
|
||||
return departments, err
|
||||
}
|
||||
|
||||
// GetRolesByUserIDs returns roles per user for a batch of user IDs
|
||||
func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
|
||||
result := make(map[uuid.UUID][]entities.Role)
|
||||
if len(userIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
// fetch pairs user_id, role
|
||||
|
||||
type row struct {
|
||||
UserID uuid.UUID
|
||||
RoleID uuid.UUID
|
||||
Name string
|
||||
Code string
|
||||
}
|
||||
|
||||
var rows []row
|
||||
err := r.b.WithContext(ctx).
|
||||
Table("user_role as ur").
|
||||
@ -171,7 +178,6 @@ func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uu
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListWithFilters supports name search and filtering by role code
|
||||
func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
|
||||
var users []*entities.User
|
||||
var total int64
|
||||
@ -194,7 +200,7 @@ func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := q.Select("users.*").Distinct("users.id").Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error; err != nil {
|
||||
if err := q.Select("users.*").Distinct("users.id").Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return users, total, nil
|
||||
|
||||
@ -12,6 +12,7 @@ type UserHandler interface {
|
||||
UpdateProfile(c *gin.Context)
|
||||
ChangePassword(c *gin.Context)
|
||||
ListTitles(c *gin.Context)
|
||||
GetActiveUsersForMention(c *gin.Context)
|
||||
}
|
||||
|
||||
type FileHandler interface {
|
||||
@ -62,7 +63,8 @@ type LetterHandler interface {
|
||||
DeleteIncomingLetter(c *gin.Context)
|
||||
|
||||
CreateDispositions(c *gin.Context)
|
||||
ListDispositionsByLetter(c *gin.Context)
|
||||
//ListDispositionsByLetter(c *gin.Context)
|
||||
GetEnhancedDispositionsByLetter(c *gin.Context)
|
||||
|
||||
CreateDiscussion(c *gin.Context)
|
||||
UpdateDiscussion(c *gin.Context)
|
||||
|
||||
@ -82,6 +82,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
users.PUT("/profile", r.userHandler.UpdateProfile)
|
||||
users.PUT(":id/password", r.userHandler.ChangePassword)
|
||||
users.GET("/titles", r.userHandler.ListTitles)
|
||||
users.GET("/mention", r.userHandler.GetActiveUsersForMention)
|
||||
users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar)
|
||||
}
|
||||
|
||||
@ -139,7 +140,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
||||
|
||||
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
||||
lettersch.GET("/dispositions/:letter_id", r.letterHandler.ListDispositionsByLetter)
|
||||
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
|
||||
|
||||
lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
|
||||
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
|
||||
@ -151,7 +152,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
droutes.POST("", r.dispRouteHandler.Create)
|
||||
droutes.GET(":id", r.dispRouteHandler.Get)
|
||||
droutes.PUT(":id", r.dispRouteHandler.Update)
|
||||
droutes.GET("from/:from_department_id", r.dispRouteHandler.ListByFromDept)
|
||||
droutes.GET("department", r.dispRouteHandler.ListByFromDept)
|
||||
droutes.PUT(":id/active", r.dispRouteHandler.SetActive)
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
||||
|
||||
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
|
||||
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
|
||||
departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
|
||||
// Departments are now preloaded, so they're already in userResponse
|
||||
|
||||
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
|
||||
if err != nil {
|
||||
@ -71,7 +71,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
||||
User: *userResponse,
|
||||
Roles: roles,
|
||||
Permissions: permCodes,
|
||||
Departments: departments,
|
||||
Departments: userResponse.DepartmentResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -90,6 +90,7 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
return userResponse, nil
|
||||
}
|
||||
|
||||
@ -115,14 +116,14 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
|
||||
// Departments are now preloaded, so they're already in userResponse
|
||||
return &contract.LoginResponse{
|
||||
Token: newToken,
|
||||
ExpiresAt: expiresAt,
|
||||
User: *userResponse,
|
||||
Roles: roles,
|
||||
Permissions: permCodes,
|
||||
Departments: departments,
|
||||
Departments: userResponse.DepartmentResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ type LetterProcessor interface {
|
||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error)
|
||||
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, 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)
|
||||
@ -50,8 +50,8 @@ func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contrac
|
||||
return s.processor.CreateDispositions(ctx, req)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
|
||||
return s.processor.ListDispositionsByLetter(ctx, letterID)
|
||||
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||
return s.processor.GetEnhancedDispositionsByLetter(ctx, letterID)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||
|
||||
@ -140,8 +140,8 @@ func (s *MasterServiceImpl) UpdateInstitution(ctx context.Context, id uuid.UUID,
|
||||
func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
|
||||
return s.institutionRepo.Delete(ctx, id)
|
||||
}
|
||||
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error) {
|
||||
list, err := s.institutionRepo.List(ctx)
|
||||
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
|
||||
list, err := s.institutionRepo.ListWithSearch(ctx, req.Search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -26,4 +26,7 @@ type UserProcessor interface {
|
||||
|
||||
// New optimized listing
|
||||
ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error)
|
||||
|
||||
// Get active users for mention purposes
|
||||
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
||||
}
|
||||
|
||||
@ -96,3 +96,8 @@ func (s *UserServiceImpl) ListTitles(ctx context.Context) (*contract.ListTitlesR
|
||||
}
|
||||
return &contract.ListTitlesResponse{Titles: transformer.TitlesToContract(titles)}, nil
|
||||
}
|
||||
|
||||
// GetActiveUsersForMention retrieves active users for mention purposes
|
||||
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
||||
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
|
||||
}
|
||||
|
||||
@ -89,6 +89,10 @@ func DepartmentsToContract(positions []entities.Department) []contract.Departmen
|
||||
return res
|
||||
}
|
||||
|
||||
func DepartmentToContract(p entities.Department) contract.DepartmentResponse {
|
||||
return contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path}
|
||||
}
|
||||
|
||||
func ProfileEntityToContract(p *entities.UserProfile) *contract.UserProfileResponse {
|
||||
if p == nil {
|
||||
return nil
|
||||
@ -241,7 +245,8 @@ func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.Di
|
||||
if e.AllowedActions != nil {
|
||||
allowed = map[string]interface{}(e.AllowedActions)
|
||||
}
|
||||
out = append(out, contract.DispositionRouteResponse{
|
||||
|
||||
resp := contract.DispositionRouteResponse{
|
||||
ID: e.ID,
|
||||
FromDepartmentID: e.FromDepartmentID,
|
||||
ToDepartmentID: e.ToDepartmentID,
|
||||
@ -249,7 +254,26 @@ func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.Di
|
||||
AllowedActions: allowed,
|
||||
CreatedAt: e.CreatedAt,
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// Add department information if available
|
||||
if e.FromDepartment.ID != uuid.Nil {
|
||||
resp.FromDepartment = contract.DepartmentInfo{
|
||||
ID: e.FromDepartment.ID,
|
||||
Name: e.FromDepartment.Name,
|
||||
Code: e.FromDepartment.Code,
|
||||
}
|
||||
}
|
||||
|
||||
if e.ToDepartment.ID != uuid.Nil {
|
||||
resp.ToDepartment = contract.DepartmentInfo{
|
||||
ID: e.ToDepartment.ID,
|
||||
Name: e.ToDepartment.Name,
|
||||
Code: e.ToDepartment.Code,
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@ -3,17 +3,17 @@ package transformer
|
||||
import (
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment) *contract.IncomingLetterResponse {
|
||||
func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment, refs ...interface{}) *contract.IncomingLetterResponse {
|
||||
resp := &contract.IncomingLetterResponse{
|
||||
ID: e.ID,
|
||||
LetterNumber: e.LetterNumber,
|
||||
ReferenceNumber: e.ReferenceNumber,
|
||||
Subject: e.Subject,
|
||||
Description: e.Description,
|
||||
PriorityID: e.PriorityID,
|
||||
SenderInstitutionID: e.SenderInstitutionID,
|
||||
ReceivedDate: e.ReceivedDate,
|
||||
DueDate: e.DueDate,
|
||||
Status: string(e.Status),
|
||||
@ -22,6 +22,37 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)),
|
||||
}
|
||||
|
||||
// optional refs: allow passing already-fetched related objects
|
||||
// expected ordering (if provided): *entities.Priority, *entities.Institution
|
||||
for _, r := range refs {
|
||||
switch v := r.(type) {
|
||||
case *entities.Priority:
|
||||
if v != nil {
|
||||
resp.Priority = &contract.PriorityResponse{
|
||||
ID: v.ID.String(),
|
||||
Name: v.Name,
|
||||
Level: v.Level,
|
||||
CreatedAt: v.CreatedAt,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
}
|
||||
}
|
||||
case *entities.Institution:
|
||||
if v != nil {
|
||||
resp.SenderInstitution = &contract.InstitutionResponse{
|
||||
ID: v.ID.String(),
|
||||
Name: v.Name,
|
||||
Type: string(v.Type),
|
||||
Address: v.Address,
|
||||
ContactPerson: v.ContactPerson,
|
||||
Phone: v.Phone,
|
||||
Email: v.Email,
|
||||
CreatedAt: v.CreatedAt,
|
||||
UpdatedAt: v.UpdatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, a := range attachments {
|
||||
resp.Attachments = append(resp.Attachments, contract.IncomingLetterAttachmentResponse{
|
||||
ID: a.ID,
|
||||
@ -34,19 +65,171 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
|
||||
return resp
|
||||
}
|
||||
|
||||
func DispositionsToContract(list []entities.LetterDisposition) []contract.DispositionResponse {
|
||||
func DispositionsToContract(list []entities.LetterIncomingDisposition) []contract.DispositionResponse {
|
||||
out := make([]contract.DispositionResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
out = append(out, contract.DispositionResponse{
|
||||
out = append(out, DispoToContract(d))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispoToContract(d entities.LetterIncomingDisposition) contract.DispositionResponse {
|
||||
return contract.DispositionResponse{
|
||||
ID: d.ID,
|
||||
LetterID: d.LetterID,
|
||||
FromDepartmentID: d.FromDepartmentID,
|
||||
ToDepartmentID: d.ToDepartmentID,
|
||||
DepartmentID: d.DepartmentID,
|
||||
Notes: d.Notes,
|
||||
Status: string(d.Status),
|
||||
ReadAt: d.ReadAt,
|
||||
CreatedBy: d.CreatedBy,
|
||||
CreatedAt: d.CreatedAt,
|
||||
})
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func EnhancedDispositionsToContract(list []entities.LetterIncomingDisposition) []contract.EnhancedDispositionResponse {
|
||||
out := make([]contract.EnhancedDispositionResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.EnhancedDispositionResponse{
|
||||
ID: d.ID,
|
||||
LetterID: d.LetterID,
|
||||
DepartmentID: d.DepartmentID,
|
||||
Notes: d.Notes,
|
||||
ReadAt: d.ReadAt,
|
||||
CreatedBy: d.CreatedBy,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
Departments: []contract.DispositionDepartmentResponse{},
|
||||
Actions: []contract.DispositionActionSelectionResponse{},
|
||||
DispositionNotes: []contract.DispositionNoteResponse{},
|
||||
}
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispositionDepartmentsToContract(list []entities.LetterIncomingDispositionDepartment) []contract.DispositionDepartmentResponse {
|
||||
out := make([]contract.DispositionDepartmentResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.DispositionDepartmentResponse{
|
||||
ID: d.ID,
|
||||
DepartmentID: d.DepartmentID,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispositionDepartmentsWithDetailsToContract(list []entities.LetterIncomingDispositionDepartment) []contract.DispositionDepartmentResponse {
|
||||
out := make([]contract.DispositionDepartmentResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.DispositionDepartmentResponse{
|
||||
ID: d.ID,
|
||||
DepartmentID: d.DepartmentID,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
|
||||
// Include department details if preloaded
|
||||
if d.Department != nil {
|
||||
resp.Department = &contract.DepartmentResponse{
|
||||
ID: d.Department.ID,
|
||||
Name: d.Department.Name,
|
||||
Code: d.Department.Code,
|
||||
Path: d.Department.Path,
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispositionActionSelectionsToContract(list []entities.LetterDispositionActionSelection) []contract.DispositionActionSelectionResponse {
|
||||
out := make([]contract.DispositionActionSelectionResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.DispositionActionSelectionResponse{
|
||||
ID: d.ID,
|
||||
ActionID: d.ActionID,
|
||||
Action: nil, // Will be populated by processor
|
||||
Note: d.Note,
|
||||
CreatedBy: d.CreatedBy,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispositionActionSelectionsWithDetailsToContract(list []entities.LetterDispositionActionSelection) []contract.DispositionActionSelectionResponse {
|
||||
out := make([]contract.DispositionActionSelectionResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.DispositionActionSelectionResponse{
|
||||
ID: d.ID,
|
||||
ActionID: d.ActionID,
|
||||
Action: nil, // Will be populated by processor
|
||||
Note: d.Note,
|
||||
CreatedBy: d.CreatedBy,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
|
||||
// Include action details if preloaded
|
||||
if d.Action != nil {
|
||||
resp.Action = &contract.DispositionActionResponse{
|
||||
ID: d.Action.ID.String(),
|
||||
Code: d.Action.Code,
|
||||
Label: d.Action.Label,
|
||||
Description: d.Action.Description,
|
||||
RequiresNote: d.Action.RequiresNote,
|
||||
GroupName: d.Action.GroupName,
|
||||
SortOrder: d.Action.SortOrder,
|
||||
IsActive: d.Action.IsActive,
|
||||
CreatedAt: d.Action.CreatedAt,
|
||||
UpdatedAt: d.Action.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispositionNotesToContract(list []entities.DispositionNote) []contract.DispositionNoteResponse {
|
||||
out := make([]contract.DispositionNoteResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.DispositionNoteResponse{
|
||||
ID: d.ID,
|
||||
UserID: d.UserID,
|
||||
Note: d.Note,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func DispositionNotesWithDetailsToContract(list []entities.DispositionNote) []contract.DispositionNoteResponse {
|
||||
out := make([]contract.DispositionNoteResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.DispositionNoteResponse{
|
||||
ID: d.ID,
|
||||
UserID: d.UserID,
|
||||
Note: d.Note,
|
||||
CreatedAt: d.CreatedAt,
|
||||
}
|
||||
|
||||
// Include user details if preloaded
|
||||
if d.User != nil {
|
||||
resp.User = &contract.UserResponse{
|
||||
ID: d.User.ID,
|
||||
Name: d.User.Name,
|
||||
Email: d.User.Email,
|
||||
IsActive: d.User.IsActive,
|
||||
CreatedAt: d.User.CreatedAt,
|
||||
UpdatedAt: d.User.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@ -68,3 +251,138 @@ func DiscussionEntityToContract(e *entities.LetterDiscussion) *contract.LetterDi
|
||||
EditedAt: e.EditedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func DiscussionsWithPreloadedDataToContract(list []entities.LetterDiscussion, mentionedUsers []entities.User) []contract.LetterDiscussionResponse {
|
||||
// Create a map for efficient user lookup
|
||||
userMap := make(map[uuid.UUID]entities.User)
|
||||
for _, user := range mentionedUsers {
|
||||
userMap[user.ID] = user
|
||||
}
|
||||
|
||||
out := make([]contract.LetterDiscussionResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.LetterDiscussionResponse{
|
||||
ID: d.ID,
|
||||
LetterID: d.LetterID,
|
||||
ParentID: d.ParentID,
|
||||
UserID: d.UserID,
|
||||
Message: d.Message,
|
||||
Mentions: map[string]interface{}(d.Mentions),
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
EditedAt: d.EditedAt,
|
||||
}
|
||||
|
||||
// Include user profile if preloaded
|
||||
if d.User != nil {
|
||||
resp.User = &contract.UserResponse{
|
||||
ID: d.User.ID,
|
||||
Name: d.User.Name,
|
||||
Email: d.User.Email,
|
||||
IsActive: d.User.IsActive,
|
||||
CreatedAt: d.User.CreatedAt,
|
||||
UpdatedAt: d.User.UpdatedAt,
|
||||
}
|
||||
|
||||
// Include user profile if available
|
||||
if d.User.Profile != nil {
|
||||
resp.User.Profile = &contract.UserProfileResponse{
|
||||
UserID: d.User.Profile.UserID,
|
||||
FullName: d.User.Profile.FullName,
|
||||
DisplayName: d.User.Profile.DisplayName,
|
||||
Phone: d.User.Profile.Phone,
|
||||
AvatarURL: d.User.Profile.AvatarURL,
|
||||
JobTitle: d.User.Profile.JobTitle,
|
||||
EmployeeNo: d.User.Profile.EmployeeNo,
|
||||
Bio: d.User.Profile.Bio,
|
||||
Timezone: d.User.Profile.Timezone,
|
||||
Locale: d.User.Profile.Locale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process mentions to get mentioned users with profiles
|
||||
if d.Mentions != nil {
|
||||
mentions := map[string]interface{}(d.Mentions)
|
||||
if userIDs, ok := mentions["user_ids"]; ok {
|
||||
if userIDList, ok := userIDs.([]interface{}); ok {
|
||||
mentionedUsersList := make([]contract.UserResponse, 0)
|
||||
for _, userID := range userIDList {
|
||||
if userIDStr, ok := userID.(string); ok {
|
||||
if userUUID, err := uuid.Parse(userIDStr); err == nil {
|
||||
if user, exists := userMap[userUUID]; exists {
|
||||
userResp := contract.UserResponse{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
// Include user profile if available
|
||||
if user.Profile != nil {
|
||||
userResp.Profile = &contract.UserProfileResponse{
|
||||
UserID: user.Profile.UserID,
|
||||
FullName: user.Profile.FullName,
|
||||
DisplayName: user.Profile.DisplayName,
|
||||
Phone: user.Profile.Phone,
|
||||
AvatarURL: user.Profile.AvatarURL,
|
||||
JobTitle: user.Profile.JobTitle,
|
||||
EmployeeNo: user.Profile.EmployeeNo,
|
||||
Bio: user.Profile.Bio,
|
||||
Timezone: user.Profile.Timezone,
|
||||
Locale: user.Profile.Locale,
|
||||
}
|
||||
}
|
||||
mentionedUsersList = append(mentionedUsersList, userResp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.MentionedUsers = mentionedUsersList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func EnhancedDispositionsWithPreloadedDataToContract(list []entities.LetterIncomingDisposition) []contract.EnhancedDispositionResponse {
|
||||
out := make([]contract.EnhancedDispositionResponse, 0, len(list))
|
||||
for _, d := range list {
|
||||
resp := contract.EnhancedDispositionResponse{
|
||||
ID: d.ID,
|
||||
LetterID: d.LetterID,
|
||||
DepartmentID: d.DepartmentID,
|
||||
Notes: d.Notes,
|
||||
ReadAt: d.ReadAt,
|
||||
CreatedBy: d.CreatedBy,
|
||||
CreatedAt: d.CreatedAt,
|
||||
UpdatedAt: d.UpdatedAt,
|
||||
Departments: []contract.DispositionDepartmentResponse{},
|
||||
Actions: []contract.DispositionActionSelectionResponse{},
|
||||
DispositionNotes: []contract.DispositionNoteResponse{},
|
||||
Department: DepartmentToContract(d.Department),
|
||||
}
|
||||
|
||||
if len(d.Departments) > 0 {
|
||||
resp.Departments = DispositionDepartmentsWithDetailsToContract(d.Departments)
|
||||
}
|
||||
|
||||
// Include preloaded action selections with details
|
||||
if len(d.ActionSelections) > 0 {
|
||||
resp.Actions = DispositionActionSelectionsWithDetailsToContract(d.ActionSelections)
|
||||
}
|
||||
|
||||
// Include preloaded notes with user details
|
||||
if len(d.DispositionNotes) > 0 {
|
||||
resp.DispositionNotes = DispositionNotesWithDetailsToContract(d.DispositionNotes)
|
||||
}
|
||||
|
||||
out = append(out, resp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@ -37,9 +37,16 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use Profile.FullName if available, otherwise fall back to user.Name
|
||||
displayName := user.Name
|
||||
if user.Profile != nil && user.Profile.FullName != "" {
|
||||
displayName = user.Profile.FullName
|
||||
}
|
||||
|
||||
resp := &contract.UserResponse{
|
||||
ID: user.ID,
|
||||
Name: user.Profile.FullName,
|
||||
Name: displayName,
|
||||
Email: user.Email,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
@ -48,6 +55,9 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
|
||||
if user.Profile != nil {
|
||||
resp.Profile = ProfileEntityToContract(user.Profile)
|
||||
}
|
||||
if user.Departments != nil && len(user.Departments) > 0 {
|
||||
resp.DepartmentResponse = DepartmentsToContract(user.Departments)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,8 @@ DROP TABLE IF EXISTS letter_incoming_discussion_attachments;
|
||||
DROP TABLE IF EXISTS letter_incoming_discussions;
|
||||
DROP TABLE IF EXISTS letter_disposition_actions;
|
||||
DROP TABLE IF EXISTS disposition_notes;
|
||||
DROP TABLE IF EXISTS letter_dispositions;
|
||||
DROP TABLE IF EXISTS letter_incoming_dispositions_department;
|
||||
DROP TABLE IF EXISTS letter_incoming_dispositions;
|
||||
DROP TABLE IF EXISTS letter_incoming_attachments;
|
||||
DROP TABLE IF EXISTS letter_incoming_labels;
|
||||
DROP TABLE IF EXISTS letter_incoming_recipients;
|
||||
|
||||
@ -106,7 +106,7 @@ CREATE TRIGGER trg_letter_dispositions_updated_at
|
||||
-- =======================
|
||||
CREATE TABLE IF NOT EXISTS disposition_notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE,
|
||||
disposition_id UUID NOT NULL REFERENCES letter_incoming_dispositions(id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
note TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
@ -119,7 +119,7 @@ CREATE INDEX IF NOT EXISTS idx_disposition_notes_disposition ON disposition_note
|
||||
-- =======================
|
||||
CREATE TABLE IF NOT EXISTS letter_disposition_actions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE,
|
||||
disposition_id UUID NOT NULL REFERENCES letter_incoming_dispositions(id) ON DELETE CASCADE,
|
||||
action_id UUID NOT NULL REFERENCES disposition_actions(id) ON DELETE RESTRICT,
|
||||
note TEXT,
|
||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
|
||||
41
migrations/000012_rename_dispositions_table.down.sql
Normal file
41
migrations/000012_rename_dispositions_table.down.sql
Normal file
@ -0,0 +1,41 @@
|
||||
BEGIN;
|
||||
|
||||
-- =======================
|
||||
-- DROP NEW ASSOCIATION TABLE
|
||||
-- =======================
|
||||
DROP TABLE IF EXISTS letter_incoming_dispositions_department;
|
||||
|
||||
-- =======================
|
||||
-- RESTORE LETTER DISPOSITIONS TABLE STRUCTURE
|
||||
-- =======================
|
||||
-- Add back the columns that were removed
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS from_user_id UUID REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS to_user_id UUID REFERENCES users(id) ON DELETE SET NULL;
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS to_department_id UUID REFERENCES departments(id) ON DELETE SET NULL;
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','read','rejected','completed'));
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS completed_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
|
||||
-- Rename department_id back to from_department_id
|
||||
ALTER TABLE letter_incoming_dispositions RENAME COLUMN department_id TO from_department_id;
|
||||
|
||||
-- =======================
|
||||
-- RESTORE TRIGGERS AND INDEXES
|
||||
-- =======================
|
||||
-- Drop new trigger
|
||||
DROP TRIGGER IF EXISTS trg_letter_incoming_dispositions_updated_at ON letter_incoming_dispositions;
|
||||
|
||||
-- Restore old trigger
|
||||
CREATE TRIGGER trg_letter_dispositions_updated_at
|
||||
BEFORE UPDATE ON letter_incoming_dispositions
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- Restore index names
|
||||
DROP INDEX IF EXISTS idx_letter_incoming_dispositions_letter;
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_dispositions_letter ON letter_incoming_dispositions(letter_id);
|
||||
|
||||
-- =======================
|
||||
-- RENAME TABLE BACK
|
||||
-- =======================
|
||||
ALTER TABLE letter_incoming_dispositions RENAME TO letter_dispositions;
|
||||
|
||||
COMMIT;
|
||||
54
migrations/000012_rename_dispositions_table.up.sql
Normal file
54
migrations/000012_rename_dispositions_table.up.sql
Normal file
@ -0,0 +1,54 @@
|
||||
BEGIN;
|
||||
|
||||
-- =======================
|
||||
-- RENAME LETTER DISPOSITIONS TABLE
|
||||
-- =======================
|
||||
ALTER TABLE letter_dispositions RENAME TO letter_incoming_dispositions;
|
||||
|
||||
-- =======================
|
||||
-- MODIFY LETTER INCOMING DISPOSITIONS TABLE STRUCTURE
|
||||
-- =======================
|
||||
-- Drop existing columns that are not needed
|
||||
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS from_user_id;
|
||||
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS to_user_id;
|
||||
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS to_department_id;
|
||||
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS status;
|
||||
ALTER TABLE letter_incoming_dispositions DROP COLUMN IF EXISTS completed_at;
|
||||
|
||||
-- Rename from_department_id to department_id
|
||||
ALTER TABLE letter_incoming_dispositions RENAME COLUMN from_department_id TO department_id;
|
||||
|
||||
-- Add missing columns if they don't exist
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS read_at TIMESTAMP WITHOUT TIME ZONE;
|
||||
ALTER TABLE letter_incoming_dispositions ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- =======================
|
||||
-- CREATE LETTER INCOMING DISPOSITIONS DEPARTMENT ASSOCIATION TABLE
|
||||
-- =======================
|
||||
CREATE TABLE IF NOT EXISTS letter_incoming_dispositions_department (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
letter_incoming_disposition_id UUID NOT NULL REFERENCES letter_incoming_dispositions(id) ON DELETE CASCADE,
|
||||
department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (letter_incoming_disposition_id, department_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_disposition ON letter_incoming_dispositions_department(letter_incoming_disposition_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_department_dept ON letter_incoming_dispositions_department(department_id);
|
||||
|
||||
-- =======================
|
||||
-- UPDATE TRIGGERS AND INDEXES
|
||||
-- =======================
|
||||
-- Drop old trigger
|
||||
DROP TRIGGER IF EXISTS trg_letter_dispositions_updated_at ON letter_incoming_dispositions;
|
||||
|
||||
-- Create new trigger
|
||||
CREATE TRIGGER trg_letter_incoming_dispositions_updated_at
|
||||
BEFORE UPDATE ON letter_incoming_dispositions
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- Update index names
|
||||
DROP INDEX IF EXISTS idx_letter_dispositions_letter;
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_dispositions_letter ON letter_incoming_dispositions(letter_id);
|
||||
|
||||
COMMIT;
|
||||
Loading…
x
Reference in New Issue
Block a user