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/cast v1.5.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // 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/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // 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/aws/aws-sdk-go v1.55.7
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||||
github.com/sirupsen/logrus v1.9.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
|
go.uber.org/zap v1.21.0
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
gorm.io/driver/postgres v1.5.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/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.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.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.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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.2/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.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 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
|||||||
@ -117,38 +117,40 @@ type repositories struct {
|
|||||||
activityLogRepo *repository.LetterIncomingActivityLogRepository
|
activityLogRepo *repository.LetterIncomingActivityLogRepository
|
||||||
dispositionRouteRepo *repository.DispositionRouteRepository
|
dispositionRouteRepo *repository.DispositionRouteRepository
|
||||||
// new repos
|
// new repos
|
||||||
letterDispositionRepo *repository.LetterDispositionRepository
|
letterDispositionRepo *repository.LetterIncomingDispositionRepository
|
||||||
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
letterDispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||||
letterDiscussionRepo *repository.LetterDiscussionRepository
|
dispositionNoteRepo *repository.DispositionNoteRepository
|
||||||
settingRepo *repository.AppSettingRepository
|
letterDiscussionRepo *repository.LetterDiscussionRepository
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
settingRepo *repository.AppSettingRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
return &repositories{
|
return &repositories{
|
||||||
userRepo: repository.NewUserRepository(a.db),
|
userRepo: repository.NewUserRepository(a.db),
|
||||||
userProfileRepo: repository.NewUserProfileRepository(a.db),
|
userProfileRepo: repository.NewUserProfileRepository(a.db),
|
||||||
titleRepo: repository.NewTitleRepository(a.db),
|
titleRepo: repository.NewTitleRepository(a.db),
|
||||||
rbacRepo: repository.NewRBACRepository(a.db),
|
rbacRepo: repository.NewRBACRepository(a.db),
|
||||||
labelRepo: repository.NewLabelRepository(a.db),
|
labelRepo: repository.NewLabelRepository(a.db),
|
||||||
priorityRepo: repository.NewPriorityRepository(a.db),
|
priorityRepo: repository.NewPriorityRepository(a.db),
|
||||||
institutionRepo: repository.NewInstitutionRepository(a.db),
|
institutionRepo: repository.NewInstitutionRepository(a.db),
|
||||||
dispRepo: repository.NewDispositionActionRepository(a.db),
|
dispRepo: repository.NewDispositionActionRepository(a.db),
|
||||||
letterRepo: repository.NewLetterIncomingRepository(a.db),
|
letterRepo: repository.NewLetterIncomingRepository(a.db),
|
||||||
letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db),
|
letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db),
|
||||||
activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db),
|
activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db),
|
||||||
dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db),
|
dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db),
|
||||||
letterDispositionRepo: repository.NewLetterDispositionRepository(a.db),
|
letterDispositionRepo: repository.NewLetterIncomingDispositionRepository(a.db),
|
||||||
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db),
|
||||||
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
||||||
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
||||||
settingRepo: repository.NewAppSettingRepository(a.db),
|
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
||||||
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
settingRepo: repository.NewAppSettingRepository(a.db),
|
||||||
departmentRepo: repository.NewDepartmentRepository(a.db),
|
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
||||||
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
departmentRepo: repository.NewDepartmentRepository(a.db),
|
||||||
|
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +165,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
|
|||||||
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
|
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
|
||||||
return &processors{
|
return &processors{
|
||||||
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
|
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
|
||||||
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, 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,
|
activityLogger: activity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const (
|
|||||||
CorrelationIDKey = key("CorrelationID")
|
CorrelationIDKey = key("CorrelationID")
|
||||||
OrganizationIDKey = key("OrganizationIDKey")
|
OrganizationIDKey = key("OrganizationIDKey")
|
||||||
UserIDKey = key("UserID")
|
UserIDKey = key("UserID")
|
||||||
OutletIDKey = key("OutletID")
|
DepartmentIDKey = key("DepartmentID")
|
||||||
RoleIDKey = key("RoleID")
|
RoleIDKey = key("RoleID")
|
||||||
AppVersionKey = key("AppVersion")
|
AppVersionKey = key("AppVersion")
|
||||||
AppIDKey = key("AppID")
|
AppIDKey = key("AppID")
|
||||||
@ -27,7 +27,7 @@ func LogFields(ctx interface{}) map[string]interface{} {
|
|||||||
fields := make(map[string]interface{})
|
fields := make(map[string]interface{})
|
||||||
fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey)
|
fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey)
|
||||||
fields[string(OrganizationIDKey)] = value(ctx, OrganizationIDKey)
|
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(AppVersionKey)] = value(ctx, AppVersionKey)
|
||||||
fields[string(AppIDKey)] = value(ctx, AppIDKey)
|
fields[string(AppIDKey)] = value(ctx, AppIDKey)
|
||||||
fields[string(AppTypeKey)] = value(ctx, AppTypeKey)
|
fields[string(AppTypeKey)] = value(ctx, AppTypeKey)
|
||||||
|
|||||||
@ -17,17 +17,16 @@ type Logger struct {
|
|||||||
var log *Logger
|
var log *Logger
|
||||||
|
|
||||||
type ContextInfo struct {
|
type ContextInfo struct {
|
||||||
CorrelationID string
|
CorrelationID string
|
||||||
UserID uuid.UUID
|
UserID uuid.UUID
|
||||||
OrganizationID uuid.UUID
|
DepartmentID uuid.UUID
|
||||||
OutletID uuid.UUID
|
AppVersion string
|
||||||
AppVersion string
|
AppID string
|
||||||
AppID string
|
AppType string
|
||||||
AppType string
|
Platform string
|
||||||
Platform string
|
DeviceOS string
|
||||||
DeviceOS string
|
UserLocale string
|
||||||
UserLocale string
|
UserRole string
|
||||||
UserRole string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ctxKeyType struct{}
|
type ctxKeyType struct{}
|
||||||
@ -59,17 +58,16 @@ func NewContext(ctx context.Context, baseFields map[string]interface{}) context.
|
|||||||
|
|
||||||
func FromGinContext(ctx context.Context) *ContextInfo {
|
func FromGinContext(ctx context.Context) *ContextInfo {
|
||||||
return &ContextInfo{
|
return &ContextInfo{
|
||||||
CorrelationID: value(ctx, CorrelationIDKey),
|
CorrelationID: value(ctx, CorrelationIDKey),
|
||||||
UserID: uuidValue(ctx, UserIDKey),
|
UserID: uuidValue(ctx, UserIDKey),
|
||||||
OutletID: uuidValue(ctx, OutletIDKey),
|
DepartmentID: uuidValue(ctx, DepartmentIDKey),
|
||||||
OrganizationID: uuidValue(ctx, OrganizationIDKey),
|
AppVersion: value(ctx, AppVersionKey),
|
||||||
AppVersion: value(ctx, AppVersionKey),
|
AppID: value(ctx, AppIDKey),
|
||||||
AppID: value(ctx, AppIDKey),
|
AppType: value(ctx, AppTypeKey),
|
||||||
AppType: value(ctx, AppTypeKey),
|
Platform: value(ctx, PlatformKey),
|
||||||
Platform: value(ctx, PlatformKey),
|
DeviceOS: value(ctx, DeviceOSKey),
|
||||||
DeviceOS: value(ctx, DeviceOSKey),
|
UserLocale: value(ctx, UserLocaleKey),
|
||||||
UserLocale: value(ctx, UserLocaleKey),
|
UserRole: value(ctx, UserRoleKey),
|
||||||
UserRole: value(ctx, UserRoleKey),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const (
|
|||||||
XAppIDHeader = "x-appid"
|
XAppIDHeader = "x-appid"
|
||||||
XPhoneModelHeader = "X-PhoneModel"
|
XPhoneModelHeader = "X-PhoneModel"
|
||||||
OrganizationID = "x_organization_id"
|
OrganizationID = "x_organization_id"
|
||||||
OutletID = "x_owner_id"
|
DepartmentID = "x_department_id"
|
||||||
CountryCodeHeader = "country-code"
|
CountryCodeHeader = "country-code"
|
||||||
AcceptedLanguageHeader = "accept-language"
|
AcceptedLanguageHeader = "accept-language"
|
||||||
XUserLocaleHeader = "x-user-locale"
|
XUserLocaleHeader = "x-user-locale"
|
||||||
|
|||||||
@ -132,6 +132,10 @@ type ListInstitutionsResponse struct {
|
|||||||
Institutions []InstitutionResponse `json:"institutions"`
|
Institutions []InstitutionResponse `json:"institutions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListInstitutionsRequest struct {
|
||||||
|
Search *string `json:"search,omitempty" form:"search"`
|
||||||
|
}
|
||||||
|
|
||||||
type DispositionActionResponse struct {
|
type DispositionActionResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
|
|||||||
@ -6,6 +6,12 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type DepartmentInfo struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type DispositionRouteResponse struct {
|
type DispositionRouteResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
||||||
@ -14,6 +20,10 @@ type DispositionRouteResponse struct {
|
|||||||
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
|
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Department information
|
||||||
|
FromDepartment DepartmentInfo `json:"from_department"`
|
||||||
|
ToDepartment DepartmentInfo `json:"to_department"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateDispositionRouteRequest struct {
|
type CreateDispositionRouteRequest struct {
|
||||||
|
|||||||
@ -32,20 +32,20 @@ type IncomingLetterAttachmentResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IncomingLetterResponse struct {
|
type IncomingLetterResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
LetterNumber string `json:"letter_number"`
|
LetterNumber string `json:"letter_number"`
|
||||||
ReferenceNumber *string `json:"reference_number,omitempty"`
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
Priority *PriorityResponse `json:"priority,omitempty"`
|
||||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
SenderInstitution *InstitutionResponse `json:"sender_institution,omitempty"`
|
||||||
ReceivedDate time.Time `json:"received_date"`
|
ReceivedDate time.Time `json:"received_date"`
|
||||||
DueDate *time.Time `json:"due_date,omitempty"`
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
|
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateIncomingLetterRequest struct {
|
type UpdateIncomingLetterRequest struct {
|
||||||
@ -77,6 +77,7 @@ type CreateDispositionActionSelection struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateLetterDispositionRequest struct {
|
type CreateLetterDispositionRequest struct {
|
||||||
|
FromDepartment uuid.UUID `json:"from_department"`
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
LetterID uuid.UUID `json:"letter_id"`
|
||||||
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
@ -84,20 +85,64 @@ type CreateLetterDispositionRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DispositionResponse struct {
|
type DispositionResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
LetterID uuid.UUID `json:"letter_id"`
|
LetterID uuid.UUID `json:"letter_id"`
|
||||||
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
|
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||||
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
|
Notes *string `json:"notes,omitempty"`
|
||||||
Notes *string `json:"notes,omitempty"`
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
Status string `json:"status"`
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
CreatedBy uuid.UUID `json:"created_by"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListDispositionsResponse struct {
|
type ListDispositionsResponse struct {
|
||||||
Dispositions []DispositionResponse `json:"dispositions"`
|
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 {
|
type CreateLetterDiscussionRequest struct {
|
||||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@ -119,4 +164,10 @@ type LetterDiscussionResponse struct {
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,14 +48,15 @@ type LoginResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
Roles []RoleResponse `json:"roles,omitempty"`
|
Roles []RoleResponse `json:"roles,omitempty"`
|
||||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
DepartmentResponse []DepartmentResponse `json:"department_response"`
|
||||||
|
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListUsersRequest struct {
|
type ListUsersRequest struct {
|
||||||
@ -128,3 +129,15 @@ type TitleResponse struct {
|
|||||||
type ListTitlesResponse struct {
|
type ListTitlesResponse struct {
|
||||||
Titles []TitleResponse `json:"titles"`
|
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"`
|
AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_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" }
|
func (DispositionRoute) TableName() string { return "disposition_routes" }
|
||||||
|
|||||||
@ -16,6 +16,9 @@ type LetterDiscussion struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
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" }
|
func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" }
|
||||||
|
|||||||
@ -6,32 +6,36 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LetterDispositionStatus string
|
type LetterIncomingDisposition struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
const (
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
DispositionPending LetterDispositionStatus = "pending"
|
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||||
DispositionRead LetterDispositionStatus = "read"
|
Notes *string `json:"notes,omitempty"`
|
||||||
DispositionRejected LetterDispositionStatus = "rejected"
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
DispositionCompleted LetterDispositionStatus = "completed"
|
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"`
|
||||||
type LetterDisposition struct {
|
Department Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
Departments []LetterIncomingDispositionDepartment `gorm:"foreignKey:LetterIncomingDispositionID;references:ID" json:"departments,omitempty"`
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
ActionSelections []LetterDispositionActionSelection `gorm:"foreignKey:DispositionID;references:ID" json:"action_selections,omitempty"`
|
||||||
FromUserID *uuid.UUID `json:"from_user_id,omitempty"`
|
DispositionNotes []DispositionNote `gorm:"foreignKey:DispositionID;references:ID" json:"disposition_notes,omitempty"`
|
||||||
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
|
|
||||||
ToUserID *uuid.UUID `json:"to_user_id,omitempty"`
|
|
||||||
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
|
||||||
Status LetterDispositionStatus `gorm:"not null;default:'pending'" json:"status"`
|
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
|
||||||
ReadAt *time.Time `json:"read_at,omitempty"`
|
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (LetterDisposition) TableName() string { return "letter_dispositions" }
|
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 {
|
type DispositionNote struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
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"`
|
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||||
Note string `gorm:"not null" json:"note"`
|
Note string `gorm:"not null" json:"note"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
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" }
|
func (DispositionNote) TableName() string { return "disposition_notes" }
|
||||||
@ -50,6 +57,9 @@ type LetterDispositionActionSelection struct {
|
|||||||
Note *string `json:"note,omitempty"`
|
Note *string `json:"note,omitempty"`
|
||||||
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
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" }
|
func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" }
|
||||||
|
|||||||
@ -48,6 +48,7 @@ type User struct {
|
|||||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
|
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 {
|
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
@ -71,12 +72,9 @@ func (h *DispositionRouteHandler) Get(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
|
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
|
||||||
fromID, err := uuid.Parse(c.Param("from_department_id"))
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
if err != nil {
|
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid from_department_id", Code: 400})
|
resp, err := h.svc.ListByFromDept(c.Request.Context(), appCtx.DepartmentID)
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, err := h.svc.ListByFromDept(c.Request.Context(), fromID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
return
|
return
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ type LetterService interface {
|
|||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, 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)
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
@ -112,11 +113,13 @@ func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
||||||
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
var req contract.CreateLetterDispositionRequest
|
var req contract.CreateLetterDispositionRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.FromDepartment = appCtx.DepartmentID
|
||||||
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
|
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
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))
|
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"))
|
letterID, err := uuid.Parse(c.Param("letter_id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, err := h.svc.ListDispositionsByLetter(c.Request.Context(), letterID)
|
resp, err := h.svc.GetEnhancedDispositionsByLetter(c.Request.Context(), letterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
return
|
return
|
||||||
|
|||||||
@ -24,7 +24,7 @@ type MasterService interface {
|
|||||||
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
|
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
|
||||||
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
|
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
|
||||||
DeleteInstitution(ctx context.Context, id uuid.UUID) 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)
|
CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
||||||
UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*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) {
|
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 {
|
if err != nil {
|
||||||
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
return
|
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) {
|
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 {
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::ListTitles -> Failed to get titles from service")
|
||||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
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) {
|
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)
|
UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
||||||
|
|
||||||
ListTitles(ctx context.Context) (*contract.ListTitlesResponse, 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())
|
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 {
|
if roles, perms, err := m.authService.ExtractAccess(token); err == nil {
|
||||||
c.Set("user_roles", roles)
|
c.Set("user_roles", roles)
|
||||||
|
|||||||
@ -13,7 +13,7 @@ func PopulateContext() gin.HandlerFunc {
|
|||||||
setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c))
|
setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c))
|
||||||
setKeyInContext(c, appcontext.AppTypeKey, getAppType(c))
|
setKeyInContext(c, appcontext.AppTypeKey, getAppType(c))
|
||||||
setKeyInContext(c, appcontext.OrganizationIDKey, getOrganizationID(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.DeviceOSKey, getDeviceOS(c))
|
||||||
setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c))
|
setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c))
|
||||||
setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c))
|
setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c))
|
||||||
@ -37,8 +37,8 @@ func getOrganizationID(c *gin.Context) string {
|
|||||||
return c.GetHeader(constants.OrganizationID)
|
return c.GetHeader(constants.OrganizationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOutletID(c *gin.Context) string {
|
func getDepartmentID(c *gin.Context) string {
|
||||||
return c.GetHeader(constants.OutletID)
|
return c.GetHeader(constants.DepartmentID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDeviceOS(c *gin.Context) string {
|
func getDeviceOS(c *gin.Context) string {
|
||||||
|
|||||||
@ -15,25 +15,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type LetterProcessorImpl struct {
|
type LetterProcessorImpl struct {
|
||||||
letterRepo *repository.LetterIncomingRepository
|
letterRepo *repository.LetterIncomingRepository
|
||||||
attachRepo *repository.LetterIncomingAttachmentRepository
|
attachRepo *repository.LetterIncomingAttachmentRepository
|
||||||
txManager *repository.TxManager
|
txManager *repository.TxManager
|
||||||
activity *ActivityLogProcessorImpl
|
activity *ActivityLogProcessorImpl
|
||||||
// new repos for dispositions
|
dispositionRepo *repository.LetterIncomingDispositionRepository
|
||||||
dispositionRepo *repository.LetterDispositionRepository
|
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||||
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
dispositionNoteRepo *repository.DispositionNoteRepository
|
||||||
// discussion repo
|
discussionRepo *repository.LetterDiscussionRepository
|
||||||
discussionRepo *repository.LetterDiscussionRepository
|
settingRepo *repository.AppSettingRepository
|
||||||
// settings and recipients
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
settingRepo *repository.AppSettingRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
priorityRepo *repository.PriorityRepository
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
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 {
|
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, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo}
|
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) {
|
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))
|
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
|
||||||
for _, code := range defaultDeptCodes {
|
for _, code := range defaultDeptCodes {
|
||||||
dep, err := p.departmentRepo.GetByCode(txCtx, code)
|
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)
|
depIDs = append(depIDs, dep.ID)
|
||||||
}
|
}
|
||||||
// query user memberships for all departments at once
|
|
||||||
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
|
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
|
||||||
// build recipients: one department recipient per department + one user recipient per membership
|
var recipients []entities.LetterIncomingRecipient
|
||||||
recipients := make([]entities.LetterIncomingRecipient, 0, len(depIDs)+len(userMemberships))
|
|
||||||
// department recipients
|
mapsUsers := map[string]bool{}
|
||||||
for _, depID := range depIDs {
|
|
||||||
id := depID
|
|
||||||
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientDepartmentID: &id, Status: entities.RecipientStatusNew})
|
|
||||||
}
|
|
||||||
// user recipients
|
|
||||||
for _, row := range userMemberships {
|
for _, row := range userMemberships {
|
||||||
uid := row.UserID
|
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 len(recipients) > 0 {
|
||||||
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -141,9 +140,26 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
|
|||||||
}
|
}
|
||||||
|
|
||||||
savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID)
|
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
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -156,7 +172,19 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
atts, _ := p.attachRepo.ListByLetter(ctx, id)
|
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) {
|
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))
|
respList := make([]contract.IncomingLetterResponse, 0, len(list))
|
||||||
for _, e := range list {
|
for _, e := range list {
|
||||||
atts, _ := p.attachRepo.ListByLetter(ctx, e.ID)
|
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)
|
respList = append(respList, *resp)
|
||||||
}
|
}
|
||||||
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
|
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)
|
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
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -254,48 +306,53 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
|
|||||||
var out *contract.ListDispositionsResponse
|
var out *contract.ListDispositionsResponse
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
userID := appcontext.FromGinContext(txCtx).UserID
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
created := make([]entities.LetterDisposition, 0, len(req.ToDepartmentIDs))
|
|
||||||
|
disp := entities.LetterIncomingDisposition{
|
||||||
|
LetterID: req.LetterID,
|
||||||
|
DepartmentID: &req.FromDepartment,
|
||||||
|
Notes: req.Notes,
|
||||||
|
CreatedBy: userID,
|
||||||
|
}
|
||||||
|
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dispDepartments []entities.LetterIncomingDispositionDepartment
|
||||||
for _, toDept := range req.ToDepartmentIDs {
|
for _, toDept := range req.ToDepartmentIDs {
|
||||||
disp := entities.LetterDisposition{
|
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
||||||
LetterID: req.LetterID,
|
LetterIncomingDispositionID: disp.ID,
|
||||||
FromDepartmentID: nil,
|
DepartmentID: toDept,
|
||||||
ToDepartmentID: &toDept,
|
})
|
||||||
Notes: req.Notes,
|
}
|
||||||
Status: entities.DispositionPending,
|
|
||||||
CreatedBy: userID,
|
if err := p.dispositionDeptRepo.CreateBulk(txCtx, dispDepartments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.SelectedActions) > 0 {
|
||||||
|
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
||||||
|
for _, sel := range req.SelectedActions {
|
||||||
|
selections = append(selections, entities.LetterDispositionActionSelection{
|
||||||
|
DispositionID: disp.ID,
|
||||||
|
ActionID: sel.ActionID,
|
||||||
|
Note: sel.Note,
|
||||||
|
CreatedBy: userID,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
created = append(created, disp)
|
|
||||||
|
|
||||||
if len(req.SelectedActions) > 0 {
|
|
||||||
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
|
||||||
for _, sel := range req.SelectedActions {
|
|
||||||
selections = append(selections, entities.LetterDispositionActionSelection{
|
|
||||||
DispositionID: disp.ID,
|
|
||||||
ActionID: sel.ActionID,
|
|
||||||
Note: sel.Note,
|
|
||||||
CreatedBy: userID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.activity != nil {
|
|
||||||
action := "disposition.created"
|
|
||||||
for _, d := range created {
|
|
||||||
ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID}
|
|
||||||
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
out = &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(created)}
|
if p.activity != nil {
|
||||||
|
action := "disposition.created"
|
||||||
|
ctxMap := map[string]interface{}{"to_department_id": dispDepartments}
|
||||||
|
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &disp.ID, nil, nil, ctxMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = &contract.ListDispositionsResponse{Dispositions: []contract.DispositionResponse{transformer.DispoToContract(disp)}}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -312,6 +369,64 @@ func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, lett
|
|||||||
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
|
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) {
|
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
var out *contract.LetterDiscussionResponse
|
var out *contract.LetterDiscussionResponse
|
||||||
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
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 {
|
if req.Mentions != nil {
|
||||||
mentions = entities.JSONB(req.Mentions)
|
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 {
|
if err := p.discussionRepo.Create(txCtx, disc); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,9 +112,11 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
|
|||||||
}
|
}
|
||||||
resp := transformer.EntityToContract(user)
|
resp := transformer.EntityToContract(user)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
|
// Roles are loaded separately since they're not preloaded
|
||||||
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
|
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
|
||||||
resp.Roles = transformer.RolesToContract(roles)
|
resp.Roles = transformer.RolesToContract(roles)
|
||||||
}
|
}
|
||||||
|
// Departments are now preloaded, so they're already in the response
|
||||||
}
|
}
|
||||||
return resp, nil
|
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)
|
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
|
return transformer.EntityToContract(user), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +152,7 @@ func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contr
|
|||||||
for i := range responses {
|
for i := range responses {
|
||||||
userIDs = append(userIDs, responses[i].ID)
|
userIDs = append(userIDs, responses[i].ID)
|
||||||
}
|
}
|
||||||
|
// Roles are loaded separately since they're not preloaded
|
||||||
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
|
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for i := range responses {
|
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
|
return responses, int(totalCount), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,3 +277,38 @@ func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.U
|
|||||||
}
|
}
|
||||||
return transformer.ProfileEntityToContract(entity), nil
|
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) {
|
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var e entities.DispositionRoute
|
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 nil, err
|
||||||
}
|
}
|
||||||
return &e, nil
|
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) {
|
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
var list []entities.DispositionRoute
|
var list []entities.DispositionRoute
|
||||||
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).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 nil, err
|
||||||
}
|
}
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
|
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||||
db := DBFromContext(ctx, r.db)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
|
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
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type LetterDispositionRepository struct{ db *gorm.DB }
|
type LetterIncomingDispositionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterDispositionRepository(db *gorm.DB) *LetterDispositionRepository {
|
func NewLetterIncomingDispositionRepository(db *gorm.DB) *LetterIncomingDispositionRepository {
|
||||||
return &LetterDispositionRepository{db: db}
|
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)
|
db := DBFromContext(ctx, r.db)
|
||||||
return db.WithContext(ctx).Create(e).Error
|
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)
|
db := DBFromContext(ctx, r.db)
|
||||||
var list []entities.LetterDisposition
|
var list []entities.LetterIncomingDisposition
|
||||||
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("created_at ASC").Find(&list).Error; err != nil {
|
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 nil, err
|
||||||
}
|
}
|
||||||
return list, nil
|
return list, nil
|
||||||
@ -132,6 +169,27 @@ func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.Disp
|
|||||||
return db.WithContext(ctx).Create(e).Error
|
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 }
|
type LetterDispositionActionSelectionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
|
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
|
||||||
@ -150,6 +208,18 @@ func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx conte
|
|||||||
return list, nil
|
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 }
|
type LetterDiscussionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository {
|
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
|
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 }
|
type AppSettingRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository { return &AppSettingRepository{db: 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
|
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
|
||||||
return list, err
|
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) {
|
func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) {
|
||||||
var e entities.Institution
|
var e entities.Institution
|
||||||
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
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
|
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 }
|
type DepartmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
func NewDepartmentRepository(db *gorm.DB) *DepartmentRepository { return &DepartmentRepository{db: 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
|
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) {
|
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
|
||||||
var user entities.User
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||||
var user entities.User
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
|
||||||
var users []*entities.User
|
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
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +58,7 @@ func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID
|
|||||||
err := r.b.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
Where(" is_active = ?", organizationID, true).
|
Where(" is_active = ?", organizationID, true).
|
||||||
Preload("Profile").
|
Preload("Profile").
|
||||||
|
Preload("Departments").
|
||||||
Find(&users).Error
|
Find(&users).Error
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
@ -90,7 +97,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
|
|||||||
return nil, 0, err
|
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
|
return users, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,19 +148,19 @@ func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID
|
|||||||
return departments, err
|
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) {
|
func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
|
||||||
result := make(map[uuid.UUID][]entities.Role)
|
result := make(map[uuid.UUID][]entities.Role)
|
||||||
if len(userIDs) == 0 {
|
if len(userIDs) == 0 {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
// fetch pairs user_id, role
|
|
||||||
type row struct {
|
type row struct {
|
||||||
UserID uuid.UUID
|
UserID uuid.UUID
|
||||||
RoleID uuid.UUID
|
RoleID uuid.UUID
|
||||||
Name string
|
Name string
|
||||||
Code string
|
Code string
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows []row
|
var rows []row
|
||||||
err := r.b.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
Table("user_role as ur").
|
Table("user_role as ur").
|
||||||
@ -171,7 +178,6 @@ func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uu
|
|||||||
return result, nil
|
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) {
|
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 users []*entities.User
|
||||||
var total int64
|
var total int64
|
||||||
@ -194,7 +200,7 @@ func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string
|
|||||||
return nil, 0, err
|
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 nil, 0, err
|
||||||
}
|
}
|
||||||
return users, total, nil
|
return users, total, nil
|
||||||
|
|||||||
@ -12,6 +12,7 @@ type UserHandler interface {
|
|||||||
UpdateProfile(c *gin.Context)
|
UpdateProfile(c *gin.Context)
|
||||||
ChangePassword(c *gin.Context)
|
ChangePassword(c *gin.Context)
|
||||||
ListTitles(c *gin.Context)
|
ListTitles(c *gin.Context)
|
||||||
|
GetActiveUsersForMention(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileHandler interface {
|
type FileHandler interface {
|
||||||
@ -62,7 +63,8 @@ type LetterHandler interface {
|
|||||||
DeleteIncomingLetter(c *gin.Context)
|
DeleteIncomingLetter(c *gin.Context)
|
||||||
|
|
||||||
CreateDispositions(c *gin.Context)
|
CreateDispositions(c *gin.Context)
|
||||||
ListDispositionsByLetter(c *gin.Context)
|
//ListDispositionsByLetter(c *gin.Context)
|
||||||
|
GetEnhancedDispositionsByLetter(c *gin.Context)
|
||||||
|
|
||||||
CreateDiscussion(c *gin.Context)
|
CreateDiscussion(c *gin.Context)
|
||||||
UpdateDiscussion(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("/profile", r.userHandler.UpdateProfile)
|
||||||
users.PUT(":id/password", r.userHandler.ChangePassword)
|
users.PUT(":id/password", r.userHandler.ChangePassword)
|
||||||
users.GET("/titles", r.userHandler.ListTitles)
|
users.GET("/titles", r.userHandler.ListTitles)
|
||||||
|
users.GET("/mention", r.userHandler.GetActiveUsersForMention)
|
||||||
users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar)
|
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.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
||||||
|
|
||||||
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
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.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
|
||||||
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
|
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.POST("", r.dispRouteHandler.Create)
|
||||||
droutes.GET(":id", r.dispRouteHandler.Get)
|
droutes.GET(":id", r.dispRouteHandler.Get)
|
||||||
droutes.PUT(":id", r.dispRouteHandler.Update)
|
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)
|
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)
|
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
|
||||||
permCodes, _ := s.userProcessor.GetUserPermissionCodes(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)
|
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -71,7 +71,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
|||||||
User: *userResponse,
|
User: *userResponse,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
Permissions: permCodes,
|
Permissions: permCodes,
|
||||||
Departments: departments,
|
Departments: userResponse.DepartmentResponse,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +90,7 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
|
|||||||
return nil, fmt.Errorf("user account is deactivated")
|
return nil, fmt.Errorf("user account is deactivated")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Departments are now preloaded, so they're already in the response
|
||||||
return userResponse, nil
|
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)
|
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{
|
return &contract.LoginResponse{
|
||||||
Token: newToken,
|
Token: newToken,
|
||||||
ExpiresAt: expiresAt,
|
ExpiresAt: expiresAt,
|
||||||
User: *userResponse,
|
User: *userResponse,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
Permissions: permCodes,
|
Permissions: permCodes,
|
||||||
Departments: departments,
|
Departments: userResponse.DepartmentResponse,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ type LetterProcessor interface {
|
|||||||
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, 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)
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
@ -50,8 +50,8 @@ func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contrac
|
|||||||
return s.processor.CreateDispositions(ctx, req)
|
return s.processor.CreateDispositions(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
|
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||||
return s.processor.ListDispositionsByLetter(ctx, letterID)
|
return s.processor.GetEnhancedDispositionsByLetter(ctx, letterID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
|
|||||||
@ -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 {
|
func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
|
||||||
return s.institutionRepo.Delete(ctx, id)
|
return s.institutionRepo.Delete(ctx, id)
|
||||||
}
|
}
|
||||||
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error) {
|
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
|
||||||
list, err := s.institutionRepo.List(ctx)
|
list, err := s.institutionRepo.ListWithSearch(ctx, req.Search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,4 +26,7 @@ type UserProcessor interface {
|
|||||||
|
|
||||||
// New optimized listing
|
// New optimized listing
|
||||||
ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error)
|
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
|
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
|
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 {
|
func ProfileEntityToContract(p *entities.UserProfile) *contract.UserProfileResponse {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
@ -241,7 +245,8 @@ func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.Di
|
|||||||
if e.AllowedActions != nil {
|
if e.AllowedActions != nil {
|
||||||
allowed = map[string]interface{}(e.AllowedActions)
|
allowed = map[string]interface{}(e.AllowedActions)
|
||||||
}
|
}
|
||||||
out = append(out, contract.DispositionRouteResponse{
|
|
||||||
|
resp := contract.DispositionRouteResponse{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
FromDepartmentID: e.FromDepartmentID,
|
FromDepartmentID: e.FromDepartmentID,
|
||||||
ToDepartmentID: e.ToDepartmentID,
|
ToDepartmentID: e.ToDepartmentID,
|
||||||
@ -249,7 +254,26 @@ func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.Di
|
|||||||
AllowedActions: allowed,
|
AllowedActions: allowed,
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
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
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,24 +3,55 @@ package transformer
|
|||||||
import (
|
import (
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
"eslogad-be/internal/entities"
|
"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{
|
resp := &contract.IncomingLetterResponse{
|
||||||
ID: e.ID,
|
ID: e.ID,
|
||||||
LetterNumber: e.LetterNumber,
|
LetterNumber: e.LetterNumber,
|
||||||
ReferenceNumber: e.ReferenceNumber,
|
ReferenceNumber: e.ReferenceNumber,
|
||||||
Subject: e.Subject,
|
Subject: e.Subject,
|
||||||
Description: e.Description,
|
Description: e.Description,
|
||||||
PriorityID: e.PriorityID,
|
ReceivedDate: e.ReceivedDate,
|
||||||
SenderInstitutionID: e.SenderInstitutionID,
|
DueDate: e.DueDate,
|
||||||
ReceivedDate: e.ReceivedDate,
|
Status: string(e.Status),
|
||||||
DueDate: e.DueDate,
|
CreatedBy: e.CreatedBy,
|
||||||
Status: string(e.Status),
|
CreatedAt: e.CreatedAt,
|
||||||
CreatedBy: e.CreatedBy,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedAt: e.CreatedAt,
|
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)),
|
||||||
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 {
|
for _, a := range attachments {
|
||||||
resp.Attachments = append(resp.Attachments, contract.IncomingLetterAttachmentResponse{
|
resp.Attachments = append(resp.Attachments, contract.IncomingLetterAttachmentResponse{
|
||||||
@ -34,19 +65,171 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
|
|||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func DispositionsToContract(list []entities.LetterDisposition) []contract.DispositionResponse {
|
func DispositionsToContract(list []entities.LetterIncomingDisposition) []contract.DispositionResponse {
|
||||||
out := make([]contract.DispositionResponse, 0, len(list))
|
out := make([]contract.DispositionResponse, 0, len(list))
|
||||||
for _, d := range 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,
|
||||||
|
DepartmentID: d.DepartmentID,
|
||||||
|
Notes: d.Notes,
|
||||||
|
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,
|
ID: d.ID,
|
||||||
LetterID: d.LetterID,
|
LetterID: d.LetterID,
|
||||||
FromDepartmentID: d.FromDepartmentID,
|
DepartmentID: d.DepartmentID,
|
||||||
ToDepartmentID: d.ToDepartmentID,
|
|
||||||
Notes: d.Notes,
|
Notes: d.Notes,
|
||||||
Status: string(d.Status),
|
ReadAt: d.ReadAt,
|
||||||
CreatedBy: d.CreatedBy,
|
CreatedBy: d.CreatedBy,
|
||||||
CreatedAt: d.CreatedAt,
|
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
|
return out
|
||||||
}
|
}
|
||||||
@ -68,3 +251,138 @@ func DiscussionEntityToContract(e *entities.LetterDiscussion) *contract.LetterDi
|
|||||||
EditedAt: e.EditedAt,
|
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 {
|
if user == nil {
|
||||||
return 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{
|
resp := &contract.UserResponse{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Name: user.Profile.FullName,
|
Name: displayName,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
IsActive: user.IsActive,
|
IsActive: user.IsActive,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
@ -48,6 +55,9 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
|
|||||||
if user.Profile != nil {
|
if user.Profile != nil {
|
||||||
resp.Profile = ProfileEntityToContract(user.Profile)
|
resp.Profile = ProfileEntityToContract(user.Profile)
|
||||||
}
|
}
|
||||||
|
if user.Departments != nil && len(user.Departments) > 0 {
|
||||||
|
resp.DepartmentResponse = DepartmentsToContract(user.Departments)
|
||||||
|
}
|
||||||
return resp
|
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_incoming_discussions;
|
||||||
DROP TABLE IF EXISTS letter_disposition_actions;
|
DROP TABLE IF EXISTS letter_disposition_actions;
|
||||||
DROP TABLE IF EXISTS disposition_notes;
|
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_attachments;
|
||||||
DROP TABLE IF EXISTS letter_incoming_labels;
|
DROP TABLE IF EXISTS letter_incoming_labels;
|
||||||
DROP TABLE IF EXISTS letter_incoming_recipients;
|
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 (
|
CREATE TABLE IF NOT EXISTS disposition_notes (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
note TEXT NOT NULL,
|
note TEXT NOT NULL,
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
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 (
|
CREATE TABLE IF NOT EXISTS letter_disposition_actions (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
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,
|
action_id UUID NOT NULL REFERENCES disposition_actions(id) ON DELETE RESTRICT,
|
||||||
note TEXT,
|
note TEXT,
|
||||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
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