Add init
This commit is contained in:
parent
de60983e4e
commit
826c5d26ad
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
|
||||||
8
Makefile
8
Makefile
@ -1,9 +1,9 @@
|
|||||||
#PROJECT_NAME = "enaklo-pos-backend"
|
#PROJECT_NAME = "enaklo-pos-backend"
|
||||||
DB_USERNAME :=eslogad_user
|
DB_USERNAME :=metidb
|
||||||
DB_PASSWORD :=M9u%24e%23jT2%40qR4pX%21zL
|
DB_PASSWORD :=metipassword%23123
|
||||||
DB_HOST :=103.191.71.2
|
DB_HOST :=103.191.71.2
|
||||||
DB_PORT :=5432
|
DB_PORT :=5433
|
||||||
DB_NAME :=eslogad_db
|
DB_NAME :=mydb
|
||||||
|
|
||||||
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
APP_NAME="eslogad"
|
APP_NAME="meti-backend"
|
||||||
PORT="4000"
|
PORT="4001"
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
echo "🔄 Pulling latest code..."
|
||||||
git pull
|
git pull
|
||||||
|
|||||||
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=
|
||||||
|
|||||||
@ -10,11 +10,11 @@ jwt:
|
|||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
host: 103.191.71.2
|
host: 103.191.71.2
|
||||||
port: 5432
|
port: 5433
|
||||||
driver: postgres
|
driver: postgres
|
||||||
db: eslogad_db
|
db: mydb
|
||||||
username: eslogad_user
|
username: metidb
|
||||||
password: 'M9u$e#jT2@qR4pX!zL'
|
password: 'metipassword#123'
|
||||||
ssl-mode: disable
|
ssl-mode: disable
|
||||||
max-idle-connections-in-second: 600
|
max-idle-connections-in-second: 600
|
||||||
max-open-connections-in-second: 600
|
max-open-connections-in-second: 600
|
||||||
|
|||||||
@ -47,6 +47,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
masterHandler := handler.NewMasterHandler(services.masterService)
|
masterHandler := handler.NewMasterHandler(services.masterService)
|
||||||
letterHandler := handler.NewLetterHandler(services.letterService)
|
letterHandler := handler.NewLetterHandler(services.letterService)
|
||||||
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
||||||
|
voteEventHandler := handler.NewVoteEventHandler(services.voteEventService)
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
cfg,
|
cfg,
|
||||||
@ -59,6 +60,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
masterHandler,
|
masterHandler,
|
||||||
letterHandler,
|
letterHandler,
|
||||||
dispositionRouteHandler,
|
dispositionRouteHandler,
|
||||||
|
voteEventHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -117,7 +119,8 @@ 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
|
||||||
|
letterDispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||||
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||||
dispositionNoteRepo *repository.DispositionNoteRepository
|
dispositionNoteRepo *repository.DispositionNoteRepository
|
||||||
letterDiscussionRepo *repository.LetterDiscussionRepository
|
letterDiscussionRepo *repository.LetterDiscussionRepository
|
||||||
@ -125,6 +128,7 @@ type repositories struct {
|
|||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
|
voteEventRepo *repository.VoteEventRepositoryImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@ -141,7 +145,8 @@ func (a *App) initRepositories() *repositories {
|
|||||||
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),
|
||||||
|
letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db),
|
||||||
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
||||||
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
||||||
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
||||||
@ -149,6 +154,7 @@ func (a *App) initRepositories() *repositories {
|
|||||||
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
||||||
departmentRepo: repository.NewDepartmentRepository(a.db),
|
departmentRepo: repository.NewDepartmentRepository(a.db),
|
||||||
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
||||||
|
voteEventRepo: repository.NewVoteEventRepository(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +169,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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,6 +182,7 @@ type services struct {
|
|||||||
masterService *service.MasterServiceImpl
|
masterService *service.MasterServiceImpl
|
||||||
letterService *service.LetterServiceImpl
|
letterService *service.LetterServiceImpl
|
||||||
dispositionRouteService *service.DispositionRouteServiceImpl
|
dispositionRouteService *service.DispositionRouteServiceImpl
|
||||||
|
voteEventService *service.VoteEventServiceImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
|
||||||
@ -195,6 +202,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
|
|
||||||
letterSvc := service.NewLetterService(processors.letterProcessor)
|
letterSvc := service.NewLetterService(processors.letterProcessor)
|
||||||
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
||||||
|
voteEventSvc := service.NewVoteEventService(repos.voteEventRepo)
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
userService: userSvc,
|
userService: userSvc,
|
||||||
@ -204,6 +212,7 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
masterService: masterSvc,
|
masterService: masterSvc,
|
||||||
letterService: letterSvc,
|
letterService: letterSvc,
|
||||||
dispositionRouteService: dispRouteSvc,
|
dispositionRouteService: dispRouteSvc,
|
||||||
|
voteEventService: voteEventSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -19,8 +19,7 @@ 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
|
||||||
@ -61,8 +60,7 @@ 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),
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -37,8 +37,8 @@ type IncomingLetterResponse struct {
|
|||||||
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"`
|
||||||
@ -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"`
|
||||||
@ -86,18 +87,62 @@ 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"`
|
||||||
Status string `json:"status"`
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,7 @@ type UserResponse 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"`
|
||||||
Roles []RoleResponse `json:"roles,omitempty"`
|
Roles []RoleResponse `json:"roles,omitempty"`
|
||||||
|
DepartmentResponse []DepartmentResponse `json:"department_response"`
|
||||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,3 +129,48 @@ 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreateUsersRequest represents the request for creating multiple users
|
||||||
|
type BulkCreateUsersRequest struct {
|
||||||
|
Users []BulkUserRequest `json:"users" validate:"required,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkUserRequest represents a single user in bulk creation request
|
||||||
|
type BulkUserRequest struct {
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
Email string `json:"email" validate:"required,email"`
|
||||||
|
Password string `json:"password" validate:"required,min=6"`
|
||||||
|
Role string `json:"role" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreateUsersResponse represents the response for bulk user creation
|
||||||
|
type BulkCreateUsersResponse struct {
|
||||||
|
Created []UserResponse `json:"created"`
|
||||||
|
Failed []BulkUserErrorResult `json:"failed"`
|
||||||
|
Summary BulkCreationSummary `json:"summary"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkUserErrorResult represents a failed user creation with error details
|
||||||
|
type BulkUserErrorResult struct {
|
||||||
|
User BulkUserRequest `json:"user"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreationSummary provides summary of bulk creation results
|
||||||
|
type BulkCreationSummary struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
Succeeded int `json:"succeeded"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
}
|
||||||
|
|||||||
96
internal/contract/vote_event_contract.go
Normal file
96
internal/contract/vote_event_contract.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateVoteEventRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StartDate time.Time `json:"start_date" validate:"required"`
|
||||||
|
EndDate time.Time `json:"end_date" validate:"required"`
|
||||||
|
ResultsOpen *bool `json:"results_open,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateVoteEventRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StartDate time.Time `json:"start_date" validate:"required"`
|
||||||
|
EndDate time.Time `json:"end_date" validate:"required"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
ResultsOpen *bool `json:"results_open,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteEventResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
StartDate time.Time `json:"start_date"`
|
||||||
|
EndDate time.Time `json:"end_date"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
ResultsOpen bool `json:"results_open"`
|
||||||
|
IsVotingOpen bool `json:"is_voting_open"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Candidates []CandidateResponse `json:"candidates,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVoteEventsRequest struct {
|
||||||
|
Page int `json:"page" validate:"min=1"`
|
||||||
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListVoteEventsResponse struct {
|
||||||
|
VoteEvents []VoteEventResponse `json:"vote_events"`
|
||||||
|
Pagination PaginationResponse `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateCandidateRequest struct {
|
||||||
|
VoteEventID uuid.UUID `json:"vote_event_id" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CandidateResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
VoteEventID uuid.UUID `json:"vote_event_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
VoteCount int64 `json:"vote_count,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmitVoteRequest struct {
|
||||||
|
VoteEventID uuid.UUID `json:"vote_event_id" validate:"required"`
|
||||||
|
CandidateID uuid.UUID `json:"candidate_id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
VoteEventID uuid.UUID `json:"vote_event_id"`
|
||||||
|
CandidateID uuid.UUID `json:"candidate_id"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteResultsResponse struct {
|
||||||
|
VoteEventID uuid.UUID `json:"vote_event_id"`
|
||||||
|
Candidates []CandidateWithVotesResponse `json:"candidates"`
|
||||||
|
TotalVotes int64 `json:"total_votes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CandidateWithVotesResponse struct {
|
||||||
|
CandidateResponse
|
||||||
|
VoteCount int64 `json:"vote_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckVoteStatusResponse struct {
|
||||||
|
HasVoted bool `json:"has_voted"`
|
||||||
|
VotedAt *time.Time `json:"voted_at,omitempty"`
|
||||||
|
CandidateID *uuid.UUID `json:"candidate_id,omitempty"`
|
||||||
|
}
|
||||||
31
internal/entities/candidate.go
Normal file
31
internal/entities/candidate.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Candidate struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
VoteEventID uuid.UUID `gorm:"type:uuid;not null" json:"vote_event_id"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||||
|
ImageURL string `gorm:"size:500" json:"image_url"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
VoteEvent VoteEvent `gorm:"foreignKey:VoteEventID;references:ID" json:"vote_event,omitempty"`
|
||||||
|
Votes []Vote `gorm:"foreignKey:CandidateID;references:ID" json:"votes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Candidate) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if c.ID == uuid.Nil {
|
||||||
|
c.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Candidate) TableName() string {
|
||||||
|
return "candidates"
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|
||||||
const (
|
|
||||||
DispositionPending LetterDispositionStatus = "pending"
|
|
||||||
DispositionRead LetterDispositionStatus = "read"
|
|
||||||
DispositionRejected LetterDispositionStatus = "rejected"
|
|
||||||
DispositionCompleted LetterDispositionStatus = "completed"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LetterDisposition struct {
|
|
||||||
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
FromUserID *uuid.UUID `json:"from_user_id,omitempty"`
|
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||||
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
|
|
||||||
ToUserID *uuid.UUID `json:"to_user_id,omitempty"`
|
|
||||||
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
|
|
||||||
Notes *string `json:"notes,omitempty"`
|
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"`
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
Department Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||||
|
Departments []LetterIncomingDispositionDepartment `gorm:"foreignKey:LetterIncomingDispositionID;references:ID" json:"departments,omitempty"`
|
||||||
|
ActionSelections []LetterDispositionActionSelection `gorm:"foreignKey:DispositionID;references:ID" json:"action_selections,omitempty"`
|
||||||
|
DispositionNotes []DispositionNote `gorm:"foreignKey:DispositionID;references:ID" json:"disposition_notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (LetterDisposition) TableName() string { return "letter_dispositions" }
|
func (LetterIncomingDisposition) TableName() string { return "letter_incoming_dispositions" }
|
||||||
|
|
||||||
|
type LetterIncomingDispositionDepartment struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterIncomingDispositionID uuid.UUID `gorm:"type:uuid;not null" json:"letter_incoming_disposition_id"`
|
||||||
|
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
Department *Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncomingDispositionDepartment) TableName() string {
|
||||||
|
return "letter_incoming_dispositions_department"
|
||||||
|
}
|
||||||
|
|
||||||
type DispositionNote struct {
|
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 {
|
||||||
|
|||||||
30
internal/entities/vote.go
Normal file
30
internal/entities/vote.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Vote struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
VoteEventID uuid.UUID `gorm:"type:uuid;not null" json:"vote_event_id"`
|
||||||
|
CandidateID uuid.UUID `gorm:"type:uuid;not null" json:"candidate_id"`
|
||||||
|
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
VoteEvent VoteEvent `gorm:"foreignKey:VoteEventID;references:ID" json:"vote_event,omitempty"`
|
||||||
|
Candidate Candidate `gorm:"foreignKey:CandidateID;references:ID" json:"candidate,omitempty"`
|
||||||
|
User User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vote) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if v.ID == uuid.Nil {
|
||||||
|
v.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Vote) TableName() string {
|
||||||
|
return "votes"
|
||||||
|
}
|
||||||
38
internal/entities/vote_event.go
Normal file
38
internal/entities/vote_event.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoteEvent struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Title string `gorm:"not null;size:255" json:"title" validate:"required,min=1,max=255"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
StartDate time.Time `gorm:"not null" json:"start_date" validate:"required"`
|
||||||
|
EndDate time.Time `gorm:"not null" json:"end_date" validate:"required"`
|
||||||
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
|
ResultsOpen bool `gorm:"default:false" json:"results_open"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
Candidates []Candidate `gorm:"foreignKey:VoteEventID;references:ID" json:"candidates,omitempty"`
|
||||||
|
Votes []Vote `gorm:"foreignKey:VoteEventID;references:ID" json:"votes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ve *VoteEvent) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if ve.ID == uuid.Nil {
|
||||||
|
ve.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (VoteEvent) TableName() string {
|
||||||
|
return "vote_events"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ve *VoteEvent) IsVotingOpen() bool {
|
||||||
|
now := time.Now()
|
||||||
|
return ve.IsActive && now.After(ve.StartDate) && now.Before(ve.EndDate)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -51,6 +51,43 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, userResponse)
|
c.JSON(http.StatusCreated, userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) BulkCreateUsers(c *gin.Context) {
|
||||||
|
var req contract.BulkCreateUsersRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::BulkCreateUsers -> request binding failed")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Users) == 0 {
|
||||||
|
h.sendValidationErrorResponse(c, "Users list cannot be empty", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.Users) > 100 {
|
||||||
|
h.sendValidationErrorResponse(c, "Cannot create more than 100 users at once", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := h.userService.BulkCreateUsers(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::BulkCreateUsers -> Failed to bulk create users")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
statusCode := http.StatusCreated
|
||||||
|
if response.Summary.Failed > 0 && response.Summary.Succeeded == 0 {
|
||||||
|
statusCode = http.StatusBadRequest
|
||||||
|
} else if response.Summary.Failed > 0 {
|
||||||
|
statusCode = http.StatusMultiStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("UserHandler::BulkCreateUsers -> Successfully processed bulk creation: %d succeeded, %d failed",
|
||||||
|
response.Summary.Succeeded, response.Summary.Failed)
|
||||||
|
c.JSON(statusCode, contract.BuildSuccessResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||||
userIDStr := c.Param("id")
|
userIDStr := c.Param("id")
|
||||||
userID, err := uuid.Parse(userIDStr)
|
userID, err := uuid.Parse(userIDStr)
|
||||||
@ -285,12 +322,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) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
type UserService interface {
|
type UserService interface {
|
||||||
CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error)
|
CreateUser(ctx context.Context, req *contract.CreateUserRequest) (*contract.UserResponse, error)
|
||||||
|
BulkCreateUsers(ctx context.Context, req *contract.BulkCreateUsersRequest) (*contract.BulkCreateUsersResponse, error)
|
||||||
UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error)
|
UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error)
|
||||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||||
@ -20,4 +21,6 @@ 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)
|
||||||
|
|
||||||
|
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
||||||
}
|
}
|
||||||
|
|||||||
323
internal/handler/vote_event_handler.go
Normal file
323
internal/handler/vote_event_handler.go
Normal file
@ -0,0 +1,323 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
"eslogad-be/internal/constants"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/logger"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoteEventService interface {
|
||||||
|
CreateVoteEvent(ctx context.Context, req *contract.CreateVoteEventRequest) (*contract.VoteEventResponse, error)
|
||||||
|
GetVoteEventByID(ctx context.Context, id uuid.UUID) (*contract.VoteEventResponse, error)
|
||||||
|
GetActiveEvents(ctx context.Context) ([]contract.VoteEventResponse, error)
|
||||||
|
ListVoteEvents(ctx context.Context, req *contract.ListVoteEventsRequest) (*contract.ListVoteEventsResponse, error)
|
||||||
|
UpdateVoteEvent(ctx context.Context, id uuid.UUID, req *contract.UpdateVoteEventRequest) (*contract.VoteEventResponse, error)
|
||||||
|
DeleteVoteEvent(ctx context.Context, id uuid.UUID) error
|
||||||
|
CreateCandidate(ctx context.Context, req *contract.CreateCandidateRequest) (*contract.CandidateResponse, error)
|
||||||
|
GetCandidates(ctx context.Context, eventID uuid.UUID) ([]contract.CandidateResponse, error)
|
||||||
|
SubmitVote(ctx context.Context, userID uuid.UUID, req *contract.SubmitVoteRequest) (*contract.VoteResponse, error)
|
||||||
|
GetVoteResults(ctx context.Context, eventID uuid.UUID) (*contract.VoteResultsResponse, error)
|
||||||
|
CheckVoteStatus(ctx context.Context, userID, eventID uuid.UUID) (*contract.CheckVoteStatusResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteEventHandler struct {
|
||||||
|
voteEventService VoteEventService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoteEventHandler(voteEventService VoteEventService) *VoteEventHandler {
|
||||||
|
return &VoteEventHandler{
|
||||||
|
voteEventService: voteEventService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) CreateVoteEvent(c *gin.Context) {
|
||||||
|
var req contract.CreateVoteEventRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::CreateVoteEvent -> request binding failed")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEventResponse, err := h.voteEventService.CreateVoteEvent(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::CreateVoteEvent -> Failed to create vote event")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::CreateVoteEvent -> Successfully created vote event = %+v", voteEventResponse)
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(voteEventResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) GetVoteEvent(c *gin.Context) {
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteEvent -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEventResponse, err := h.voteEventService.GetVoteEventByID(c.Request.Context(), eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteEvent -> Failed to get vote event")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::GetVoteEvent -> Successfully retrieved vote event = %+v", voteEventResponse)
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(voteEventResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) GetActiveEvents(c *gin.Context) {
|
||||||
|
events, err := h.voteEventService.GetActiveEvents(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetActiveEvents -> Failed to get active events")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::GetActiveEvents -> Successfully retrieved %d active events", len(events))
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
||||||
|
"events": events,
|
||||||
|
"count": len(events),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) ListVoteEvents(c *gin.Context) {
|
||||||
|
req := &contract.ListVoteEventsRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if page := c.Query("page"); page != "" {
|
||||||
|
if p, err := strconv.Atoi(page); err == nil && p > 0 {
|
||||||
|
req.Page = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit := c.Query("limit"); limit != "" {
|
||||||
|
if l, err := strconv.Atoi(limit); err == nil && l > 0 && l <= 100 {
|
||||||
|
req.Limit = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEventsResponse, err := h.voteEventService.ListVoteEvents(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::ListVoteEvents -> Failed to list vote events")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::ListVoteEvents -> Successfully listed vote events")
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(voteEventsResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) UpdateVoteEvent(c *gin.Context) {
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::UpdateVoteEvent -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.UpdateVoteEventRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::UpdateVoteEvent -> request binding failed")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEventResponse, err := h.voteEventService.UpdateVoteEvent(c.Request.Context(), eventID, &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::UpdateVoteEvent -> Failed to update vote event")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::UpdateVoteEvent -> Successfully updated vote event = %+v", voteEventResponse)
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(voteEventResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) DeleteVoteEvent(c *gin.Context) {
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::DeleteVoteEvent -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.voteEventService.DeleteVoteEvent(c.Request.Context(), eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::DeleteVoteEvent -> Failed to delete vote event")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Info("VoteEventHandler::DeleteVoteEvent -> Successfully deleted vote event")
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]string{
|
||||||
|
"message": "Vote event deleted successfully",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) CreateCandidate(c *gin.Context) {
|
||||||
|
var req contract.CreateCandidateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::CreateCandidate -> request binding failed")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
candidateResponse, err := h.voteEventService.CreateCandidate(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::CreateCandidate -> Failed to create candidate")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::CreateCandidate -> Successfully created candidate = %+v", candidateResponse)
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(candidateResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) GetCandidates(c *gin.Context) {
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetCandidates -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates, err := h.voteEventService.GetCandidates(c.Request.Context(), eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetCandidates -> Failed to get candidates")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::GetCandidates -> Successfully retrieved %d candidates", len(candidates))
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(map[string]interface{}{
|
||||||
|
"candidates": candidates,
|
||||||
|
"count": len(candidates),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) SubmitVote(c *gin.Context) {
|
||||||
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
|
if appCtx.UserID == uuid.Nil {
|
||||||
|
h.sendErrorResponse(c, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req contract.SubmitVoteRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::SubmitVote -> request binding failed")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid request body", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voteResponse, err := h.voteEventService.SubmitVote(c.Request.Context(), appCtx.UserID, &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::SubmitVote -> Failed to submit vote")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::SubmitVote -> Successfully submitted vote = %+v", voteResponse)
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(voteResponse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) GetVoteResults(c *gin.Context) {
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteResults -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEvent, err := h.voteEventService.GetVoteEventByID(c.Request.Context(), eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteResults -> Failed to get vote event")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !voteEvent.ResultsOpen {
|
||||||
|
logger.FromContext(c).Info("VoteEventHandler::GetVoteResults -> Results not open for viewing")
|
||||||
|
h.sendErrorResponse(c, "Results are not open for viewing", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := h.voteEventService.GetVoteResults(c.Request.Context(), eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteResults -> Failed to get vote results")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::GetVoteResults -> Successfully retrieved vote results")
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) CheckVoteStatus(c *gin.Context) {
|
||||||
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
|
if appCtx.UserID == uuid.Nil {
|
||||||
|
h.sendErrorResponse(c, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::CheckVoteStatus -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := h.voteEventService.CheckVoteStatus(c.Request.Context(), appCtx.UserID, eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::CheckVoteStatus -> Failed to check vote status")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::CheckVoteStatus -> Successfully checked vote status")
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
||||||
|
errorResponse := &contract.ErrorResponse{
|
||||||
|
Error: message,
|
||||||
|
Code: statusCode,
|
||||||
|
Details: map[string]interface{}{},
|
||||||
|
}
|
||||||
|
c.JSON(statusCode, errorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *VoteEventHandler) sendValidationErrorResponse(c *gin.Context, message string, errorCode string) {
|
||||||
|
statusCode := constants.HttpErrorMap[errorCode]
|
||||||
|
if statusCode == 0 {
|
||||||
|
statusCode = http.StatusBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
errorResponse := &contract.ErrorResponse{
|
||||||
|
Error: message,
|
||||||
|
Code: statusCode,
|
||||||
|
Details: map[string]interface{}{
|
||||||
|
"error_code": errorCode,
|
||||||
|
"entity": "vote_event",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.JSON(statusCode, errorResponse)
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func CORS() gin.HandlerFunc {
|
func CORS() gin.HandlerFunc {
|
||||||
return gin.HandlerFunc(func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
c.Header("Access-Control-Allow-Origin", "*")
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
c.Header("Access-Control-Allow-Credentials", "true")
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-Correlation-ID")
|
||||||
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
c.Header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||||
|
c.Header("Access-Control-Expose-Headers", "X-Correlation-ID")
|
||||||
|
c.Header("Access-Control-Max-Age", "86400")
|
||||||
|
|
||||||
if c.Request.Method == "OPTIONS" {
|
if c.Request.Method == "OPTIONS" {
|
||||||
c.AbortWithStatus(204)
|
c.AbortWithStatus(204)
|
||||||
@ -17,5 +19,5 @@ func CORS() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,21 +19,22 @@ type LetterProcessorImpl struct {
|
|||||||
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
|
||||||
// settings and recipients
|
|
||||||
settingRepo *repository.AppSettingRepository
|
settingRepo *repository.AppSettingRepository
|
||||||
recipientRepo *repository.LetterIncomingRecipientRepository
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
departmentRepo *repository.DepartmentRepository
|
departmentRepo *repository.DepartmentRepository
|
||||||
userDeptRepo *repository.UserDepartmentRepository
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
|
priorityRepo *repository.PriorityRepository
|
||||||
|
institutionRepo *repository.InstitutionRepository
|
||||||
|
dispActionRepo *repository.DispositionActionRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository) *LetterProcessorImpl {
|
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,20 +306,28 @@ 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))
|
|
||||||
for _, toDept := range req.ToDepartmentIDs {
|
disp := entities.LetterIncomingDisposition{
|
||||||
disp := entities.LetterDisposition{
|
|
||||||
LetterID: req.LetterID,
|
LetterID: req.LetterID,
|
||||||
FromDepartmentID: nil,
|
DepartmentID: &req.FromDepartment,
|
||||||
ToDepartmentID: &toDept,
|
|
||||||
Notes: req.Notes,
|
Notes: req.Notes,
|
||||||
Status: entities.DispositionPending,
|
|
||||||
CreatedBy: userID,
|
CreatedBy: userID,
|
||||||
}
|
}
|
||||||
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
created = append(created, disp)
|
|
||||||
|
var dispDepartments []entities.LetterIncomingDispositionDepartment
|
||||||
|
for _, toDept := range req.ToDepartmentIDs {
|
||||||
|
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
||||||
|
LetterIncomingDispositionID: disp.ID,
|
||||||
|
DepartmentID: toDept,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.dispositionDeptRepo.CreateBulk(txCtx, dispDepartments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if len(req.SelectedActions) > 0 {
|
if len(req.SelectedActions) > 0 {
|
||||||
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
||||||
@ -286,16 +346,13 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
|
|||||||
|
|
||||||
if p.activity != nil {
|
if p.activity != nil {
|
||||||
action := "disposition.created"
|
action := "disposition.created"
|
||||||
for _, d := range created {
|
ctxMap := map[string]interface{}{"to_department_id": dispDepartments}
|
||||||
ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID}
|
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &disp.ID, nil, nil, ctxMap); err != nil {
|
||||||
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out = &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(created)}
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,7 +53,6 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
|
|||||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// create default user profile
|
|
||||||
defaultFullName := userEntity.Name
|
defaultFullName := userEntity.Name
|
||||||
profile := &entities.UserProfile{
|
profile := &entities.UserProfile{
|
||||||
UserID: userEntity.ID,
|
UserID: userEntity.ID,
|
||||||
@ -63,6 +62,7 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
|
|||||||
Preferences: entities.JSONB{},
|
Preferences: entities.JSONB{},
|
||||||
NotificationPrefs: entities.JSONB{},
|
NotificationPrefs: entities.JSONB{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = p.profileRepo.Create(ctx, profile)
|
_ = p.profileRepo.Create(ctx, profile)
|
||||||
|
|
||||||
return transformer.EntityToContract(userEntity), nil
|
return transformer.EntityToContract(userEntity), nil
|
||||||
@ -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} }
|
||||||
|
|||||||
@ -113,6 +113,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 +136,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
|
||||||
|
|||||||
122
internal/repository/vote_event_repository.go
Normal file
122
internal/repository/vote_event_repository.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoteEventRepositoryImpl struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoteEventRepository(db *gorm.DB) *VoteEventRepositoryImpl {
|
||||||
|
return &VoteEventRepositoryImpl{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) Create(ctx context.Context, voteEvent *entities.VoteEvent) error {
|
||||||
|
return r.db.WithContext(ctx).Create(voteEvent).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.VoteEvent, error) {
|
||||||
|
var voteEvent entities.VoteEvent
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Candidates").
|
||||||
|
First(&voteEvent, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &voteEvent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) GetActiveEvents(ctx context.Context) ([]*entities.VoteEvent, error) {
|
||||||
|
var events []*entities.VoteEvent
|
||||||
|
now := time.Now()
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Candidates").
|
||||||
|
Where("is_active = ? AND start_date <= ? AND end_date >= ?", true, now, now).
|
||||||
|
Find(&events).Error
|
||||||
|
return events, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) List(ctx context.Context, limit, offset int) ([]*entities.VoteEvent, int64, error) {
|
||||||
|
var events []*entities.VoteEvent
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
if err := r.db.WithContext(ctx).Model(&entities.VoteEvent{}).Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Preload("Candidates").
|
||||||
|
Limit(limit).
|
||||||
|
Offset(offset).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&events).Error
|
||||||
|
return events, total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) Update(ctx context.Context, voteEvent *entities.VoteEvent) error {
|
||||||
|
return r.db.WithContext(ctx).Save(voteEvent).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.VoteEvent{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) CreateCandidate(ctx context.Context, candidate *entities.Candidate) error {
|
||||||
|
return r.db.WithContext(ctx).Create(candidate).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) GetCandidatesByEventID(ctx context.Context, eventID uuid.UUID) ([]*entities.Candidate, error) {
|
||||||
|
var candidates []*entities.Candidate
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("vote_event_id = ?", eventID).
|
||||||
|
Find(&candidates).Error
|
||||||
|
return candidates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) SubmitVote(ctx context.Context, vote *entities.Vote) error {
|
||||||
|
return r.db.WithContext(ctx).Create(vote).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) HasUserVoted(ctx context.Context, userID, eventID uuid.UUID) (bool, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.Vote{}).
|
||||||
|
Where("user_id = ? AND vote_event_id = ?", userID, eventID).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) GetVoteResults(ctx context.Context, eventID uuid.UUID) (map[uuid.UUID]int64, error) {
|
||||||
|
type result struct {
|
||||||
|
CandidateID uuid.UUID
|
||||||
|
VoteCount int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []result
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.Vote{}).
|
||||||
|
Select("candidate_id, COUNT(*) as vote_count").
|
||||||
|
Where("vote_event_id = ?", eventID).
|
||||||
|
Group("candidate_id").
|
||||||
|
Scan(&results).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resultMap := make(map[uuid.UUID]int64)
|
||||||
|
for _, r := range results {
|
||||||
|
resultMap[r.CandidateID] = r.VoteCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultMap, nil
|
||||||
|
}
|
||||||
@ -12,6 +12,8 @@ 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)
|
||||||
|
BulkCreateUsers(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileHandler interface {
|
type FileHandler interface {
|
||||||
@ -62,7 +64,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)
|
||||||
@ -75,3 +78,17 @@ type DispositionRouteHandler interface {
|
|||||||
ListByFromDept(c *gin.Context)
|
ListByFromDept(c *gin.Context)
|
||||||
SetActive(c *gin.Context)
|
SetActive(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VoteEventHandler interface {
|
||||||
|
CreateVoteEvent(c *gin.Context)
|
||||||
|
GetVoteEvent(c *gin.Context)
|
||||||
|
GetActiveEvents(c *gin.Context)
|
||||||
|
ListVoteEvents(c *gin.Context)
|
||||||
|
UpdateVoteEvent(c *gin.Context)
|
||||||
|
DeleteVoteEvent(c *gin.Context)
|
||||||
|
CreateCandidate(c *gin.Context)
|
||||||
|
SubmitVote(c *gin.Context)
|
||||||
|
GetVoteResults(c *gin.Context)
|
||||||
|
CheckVoteStatus(c *gin.Context)
|
||||||
|
GetCandidates(c *gin.Context)
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ type Router struct {
|
|||||||
masterHandler MasterHandler
|
masterHandler MasterHandler
|
||||||
letterHandler LetterHandler
|
letterHandler LetterHandler
|
||||||
dispRouteHandler DispositionRouteHandler
|
dispRouteHandler DispositionRouteHandler
|
||||||
|
voteEventHandler VoteEventHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(
|
func NewRouter(
|
||||||
@ -31,6 +32,7 @@ func NewRouter(
|
|||||||
masterHandler MasterHandler,
|
masterHandler MasterHandler,
|
||||||
letterHandler LetterHandler,
|
letterHandler LetterHandler,
|
||||||
dispRouteHandler DispositionRouteHandler,
|
dispRouteHandler DispositionRouteHandler,
|
||||||
|
voteEventHandler VoteEventHandler,
|
||||||
) *Router {
|
) *Router {
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -43,6 +45,7 @@ func NewRouter(
|
|||||||
masterHandler: masterHandler,
|
masterHandler: masterHandler,
|
||||||
letterHandler: letterHandler,
|
letterHandler: letterHandler,
|
||||||
dispRouteHandler: dispRouteHandler,
|
dispRouteHandler: dispRouteHandler,
|
||||||
|
voteEventHandler: voteEventHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,12 +53,12 @@ func (r *Router) Init() *gin.Engine {
|
|||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
engine := gin.New()
|
engine := gin.New()
|
||||||
engine.Use(
|
engine.Use(
|
||||||
|
middleware.CORS(),
|
||||||
middleware.JsonAPI(),
|
middleware.JsonAPI(),
|
||||||
middleware.CorrelationID(),
|
middleware.CorrelationID(),
|
||||||
middleware.Recover(),
|
middleware.Recover(),
|
||||||
middleware.HTTPStatLogger(),
|
middleware.HTTPStatLogger(),
|
||||||
middleware.PopulateContext(),
|
middleware.PopulateContext(),
|
||||||
middleware.CORS(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
r.addAppRoutes(engine)
|
r.addAppRoutes(engine)
|
||||||
@ -78,10 +81,12 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
users.Use(r.authMiddleware.RequireAuth())
|
users.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
users.GET("", r.authMiddleware.RequirePermissions("user.read"), r.userHandler.ListUsers)
|
users.GET("", r.authMiddleware.RequirePermissions("user.read"), r.userHandler.ListUsers)
|
||||||
|
users.POST("/bulk", r.userHandler.BulkCreateUsers)
|
||||||
users.GET("/profile", r.userHandler.GetProfile)
|
users.GET("/profile", r.userHandler.GetProfile)
|
||||||
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 +144,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,8 +156,34 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
voteEvents := v1.Group("/vote-events")
|
||||||
|
voteEvents.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
voteEvents.POST("", r.voteEventHandler.CreateVoteEvent)
|
||||||
|
voteEvents.GET("", r.voteEventHandler.ListVoteEvents)
|
||||||
|
voteEvents.GET("/active", r.voteEventHandler.GetActiveEvents)
|
||||||
|
voteEvents.GET("/:id", r.voteEventHandler.GetVoteEvent)
|
||||||
|
voteEvents.PUT("/:id", r.voteEventHandler.UpdateVoteEvent)
|
||||||
|
voteEvents.DELETE("/:id", r.voteEventHandler.DeleteVoteEvent)
|
||||||
|
voteEvents.GET("/:id/candidates", r.voteEventHandler.GetCandidates)
|
||||||
|
voteEvents.GET("/:id/results", r.voteEventHandler.GetVoteResults)
|
||||||
|
voteEvents.GET("/:id/vote-status", r.voteEventHandler.CheckVoteStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates := v1.Group("/candidates")
|
||||||
|
candidates.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
candidates.POST("", r.voteEventHandler.CreateCandidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
votes := v1.Group("/votes")
|
||||||
|
votes.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
votes.POST("", r.voteEventHandler.SubmitVote)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,40 @@ func (s *UserServiceImpl) CreateUser(ctx context.Context, req *contract.CreateUs
|
|||||||
return s.userProcessor.CreateUser(ctx, req)
|
return s.userProcessor.CreateUser(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *UserServiceImpl) BulkCreateUsers(ctx context.Context, req *contract.BulkCreateUsersRequest) (*contract.BulkCreateUsersResponse, error) {
|
||||||
|
response := &contract.BulkCreateUsersResponse{
|
||||||
|
Created: []contract.UserResponse{},
|
||||||
|
Failed: []contract.BulkUserErrorResult{},
|
||||||
|
Summary: contract.BulkCreationSummary{
|
||||||
|
Total: len(req.Users),
|
||||||
|
Succeeded: 0,
|
||||||
|
Failed: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, userReq := range req.Users {
|
||||||
|
createReq := &contract.CreateUserRequest{
|
||||||
|
Name: userReq.Name,
|
||||||
|
Email: userReq.Email,
|
||||||
|
Password: userReq.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
userResponse, err := s.userProcessor.CreateUser(ctx, createReq)
|
||||||
|
if err != nil {
|
||||||
|
response.Failed = append(response.Failed, contract.BulkUserErrorResult{
|
||||||
|
User: userReq,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
response.Summary.Failed++
|
||||||
|
} else {
|
||||||
|
response.Created = append(response.Created, *userResponse)
|
||||||
|
response.Summary.Succeeded++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *UserServiceImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) {
|
func (s *UserServiceImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *contract.UpdateUserRequest) (*contract.UserResponse, error) {
|
||||||
return s.userProcessor.UpdateUser(ctx, id, req)
|
return s.userProcessor.UpdateUser(ctx, id, req)
|
||||||
}
|
}
|
||||||
@ -96,3 +130,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)
|
||||||
|
}
|
||||||
|
|||||||
258
internal/service/vote_event_service.go
Normal file
258
internal/service/vote_event_service.go
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VoteEventRepository interface {
|
||||||
|
Create(ctx context.Context, voteEvent *entities.VoteEvent) error
|
||||||
|
GetByID(ctx context.Context, id uuid.UUID) (*entities.VoteEvent, error)
|
||||||
|
GetActiveEvents(ctx context.Context) ([]*entities.VoteEvent, error)
|
||||||
|
List(ctx context.Context, limit, offset int) ([]*entities.VoteEvent, int64, error)
|
||||||
|
Update(ctx context.Context, voteEvent *entities.VoteEvent) error
|
||||||
|
Delete(ctx context.Context, id uuid.UUID) error
|
||||||
|
CreateCandidate(ctx context.Context, candidate *entities.Candidate) error
|
||||||
|
GetCandidatesByEventID(ctx context.Context, eventID uuid.UUID) ([]*entities.Candidate, error)
|
||||||
|
SubmitVote(ctx context.Context, vote *entities.Vote) error
|
||||||
|
HasUserVoted(ctx context.Context, userID, eventID uuid.UUID) (bool, error)
|
||||||
|
GetVoteResults(ctx context.Context, eventID uuid.UUID) (map[uuid.UUID]int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteEventServiceImpl struct {
|
||||||
|
voteEventRepo VoteEventRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVoteEventService(voteEventRepo VoteEventRepository) *VoteEventServiceImpl {
|
||||||
|
return &VoteEventServiceImpl{
|
||||||
|
voteEventRepo: voteEventRepo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) CreateVoteEvent(ctx context.Context, req *contract.CreateVoteEventRequest) (*contract.VoteEventResponse, error) {
|
||||||
|
if req.EndDate.Before(req.StartDate) {
|
||||||
|
return nil, errors.New("end date must be after start date")
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEvent := &entities.VoteEvent{
|
||||||
|
Title: req.Title,
|
||||||
|
Description: req.Description,
|
||||||
|
StartDate: req.StartDate,
|
||||||
|
EndDate: req.EndDate,
|
||||||
|
IsActive: true,
|
||||||
|
ResultsOpen: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.ResultsOpen != nil {
|
||||||
|
voteEvent.ResultsOpen = *req.ResultsOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.voteEventRepo.Create(ctx, voteEvent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.VoteEventToContract(voteEvent), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) GetVoteEventByID(ctx context.Context, id uuid.UUID) (*contract.VoteEventResponse, error) {
|
||||||
|
voteEvent, err := s.voteEventRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.VoteEventToContract(voteEvent), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) GetActiveEvents(ctx context.Context) ([]contract.VoteEventResponse, error) {
|
||||||
|
events, err := s.voteEventRepo.GetActiveEvents(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses []contract.VoteEventResponse
|
||||||
|
for _, event := range events {
|
||||||
|
responses = append(responses, *transformer.VoteEventToContract(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) ListVoteEvents(ctx context.Context, req *contract.ListVoteEventsRequest) (*contract.ListVoteEventsResponse, error) {
|
||||||
|
page := req.Page
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
limit := req.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
|
events, total, err := s.voteEventRepo.List(ctx, limit, offset)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses []contract.VoteEventResponse
|
||||||
|
for _, event := range events {
|
||||||
|
responses = append(responses, *transformer.VoteEventToContract(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.ListVoteEventsResponse{
|
||||||
|
VoteEvents: responses,
|
||||||
|
Pagination: transformer.CreatePaginationResponse(int(total), page, limit),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) UpdateVoteEvent(ctx context.Context, id uuid.UUID, req *contract.UpdateVoteEventRequest) (*contract.VoteEventResponse, error) {
|
||||||
|
if req.EndDate.Before(req.StartDate) {
|
||||||
|
return nil, errors.New("end date must be after start date")
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEvent, err := s.voteEventRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
voteEvent.Title = req.Title
|
||||||
|
voteEvent.Description = req.Description
|
||||||
|
voteEvent.StartDate = req.StartDate
|
||||||
|
voteEvent.EndDate = req.EndDate
|
||||||
|
voteEvent.IsActive = req.IsActive
|
||||||
|
|
||||||
|
if req.ResultsOpen != nil {
|
||||||
|
voteEvent.ResultsOpen = *req.ResultsOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.voteEventRepo.Update(ctx, voteEvent); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.VoteEventToContract(voteEvent), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) DeleteVoteEvent(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.voteEventRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) CreateCandidate(ctx context.Context, req *contract.CreateCandidateRequest) (*contract.CandidateResponse, error) {
|
||||||
|
voteEvent, err := s.voteEventRepo.GetByID(ctx, req.VoteEventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !voteEvent.IsActive {
|
||||||
|
return nil, errors.New("cannot add candidates to inactive vote event")
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate := &entities.Candidate{
|
||||||
|
VoteEventID: req.VoteEventID,
|
||||||
|
Name: req.Name,
|
||||||
|
ImageURL: req.ImageURL,
|
||||||
|
Description: req.Description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.voteEventRepo.CreateCandidate(ctx, candidate); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.CandidateToContract(candidate), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) GetCandidates(ctx context.Context, eventID uuid.UUID) ([]contract.CandidateResponse, error) {
|
||||||
|
candidates, err := s.voteEventRepo.GetCandidatesByEventID(ctx, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var responses []contract.CandidateResponse
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
responses = append(responses, *transformer.CandidateToContract(candidate))
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) SubmitVote(ctx context.Context, userID uuid.UUID, req *contract.SubmitVoteRequest) (*contract.VoteResponse, error) {
|
||||||
|
voteEvent, err := s.voteEventRepo.GetByID(ctx, req.VoteEventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !voteEvent.IsVotingOpen() {
|
||||||
|
return nil, errors.New("voting is not open for this event")
|
||||||
|
}
|
||||||
|
|
||||||
|
hasVoted, err := s.voteEventRepo.HasUserVoted(ctx, userID, req.VoteEventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasVoted {
|
||||||
|
return nil, errors.New("user has already voted for this event")
|
||||||
|
}
|
||||||
|
|
||||||
|
vote := &entities.Vote{
|
||||||
|
VoteEventID: req.VoteEventID,
|
||||||
|
CandidateID: req.CandidateID,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.voteEventRepo.SubmitVote(ctx, vote); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformer.VoteToContract(vote), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) GetVoteResults(ctx context.Context, eventID uuid.UUID) (*contract.VoteResultsResponse, error) {
|
||||||
|
|
||||||
|
candidates, err := s.voteEventRepo.GetCandidatesByEventID(ctx, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
voteResults, err := s.voteEventRepo.GetVoteResults(ctx, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidatesWithVotes []contract.CandidateWithVotesResponse
|
||||||
|
var totalVotes int64
|
||||||
|
|
||||||
|
for _, candidate := range candidates {
|
||||||
|
voteCount := voteResults[candidate.ID]
|
||||||
|
totalVotes += voteCount
|
||||||
|
|
||||||
|
candidatesWithVotes = append(candidatesWithVotes, contract.CandidateWithVotesResponse{
|
||||||
|
CandidateResponse: *transformer.CandidateToContract(candidate),
|
||||||
|
VoteCount: voteCount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.VoteResultsResponse{
|
||||||
|
VoteEventID: eventID,
|
||||||
|
Candidates: candidatesWithVotes,
|
||||||
|
TotalVotes: totalVotes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) CheckVoteStatus(ctx context.Context, userID, eventID uuid.UUID) (*contract.CheckVoteStatusResponse, error) {
|
||||||
|
hasVoted, err := s.voteEventRepo.HasUserVoted(ctx, userID, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &contract.CheckVoteStatusResponse{
|
||||||
|
HasVoted: hasVoted,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
@ -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,17 +3,17 @@ 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,
|
|
||||||
SenderInstitutionID: e.SenderInstitutionID,
|
|
||||||
ReceivedDate: e.ReceivedDate,
|
ReceivedDate: e.ReceivedDate,
|
||||||
DueDate: e.DueDate,
|
DueDate: e.DueDate,
|
||||||
Status: string(e.Status),
|
Status: string(e.Status),
|
||||||
@ -22,6 +22,37 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
|
|||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)),
|
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{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
@ -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,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnhancedDispositionsToContract(list []entities.LetterIncomingDisposition) []contract.EnhancedDispositionResponse {
|
||||||
|
out := make([]contract.EnhancedDispositionResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.EnhancedDispositionResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
LetterID: d.LetterID,
|
||||||
|
DepartmentID: d.DepartmentID,
|
||||||
|
Notes: d.Notes,
|
||||||
|
ReadAt: d.ReadAt,
|
||||||
|
CreatedBy: d.CreatedBy,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
UpdatedAt: d.UpdatedAt,
|
||||||
|
Departments: []contract.DispositionDepartmentResponse{},
|
||||||
|
Actions: []contract.DispositionActionSelectionResponse{},
|
||||||
|
DispositionNotes: []contract.DispositionNoteResponse{},
|
||||||
|
}
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionDepartmentsToContract(list []entities.LetterIncomingDispositionDepartment) []contract.DispositionDepartmentResponse {
|
||||||
|
out := make([]contract.DispositionDepartmentResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.DispositionDepartmentResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
DepartmentID: d.DepartmentID,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
}
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionDepartmentsWithDetailsToContract(list []entities.LetterIncomingDispositionDepartment) []contract.DispositionDepartmentResponse {
|
||||||
|
out := make([]contract.DispositionDepartmentResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.DispositionDepartmentResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
DepartmentID: d.DepartmentID,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include department details if preloaded
|
||||||
|
if d.Department != nil {
|
||||||
|
resp.Department = &contract.DepartmentResponse{
|
||||||
|
ID: d.Department.ID,
|
||||||
|
Name: d.Department.Name,
|
||||||
|
Code: d.Department.Code,
|
||||||
|
Path: d.Department.Path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionActionSelectionsToContract(list []entities.LetterDispositionActionSelection) []contract.DispositionActionSelectionResponse {
|
||||||
|
out := make([]contract.DispositionActionSelectionResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.DispositionActionSelectionResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
ActionID: d.ActionID,
|
||||||
|
Action: nil, // Will be populated by processor
|
||||||
|
Note: d.Note,
|
||||||
|
CreatedBy: d.CreatedBy,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
}
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionActionSelectionsWithDetailsToContract(list []entities.LetterDispositionActionSelection) []contract.DispositionActionSelectionResponse {
|
||||||
|
out := make([]contract.DispositionActionSelectionResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.DispositionActionSelectionResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
ActionID: d.ActionID,
|
||||||
|
Action: nil, // Will be populated by processor
|
||||||
|
Note: d.Note,
|
||||||
|
CreatedBy: d.CreatedBy,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include action details if preloaded
|
||||||
|
if d.Action != nil {
|
||||||
|
resp.Action = &contract.DispositionActionResponse{
|
||||||
|
ID: d.Action.ID.String(),
|
||||||
|
Code: d.Action.Code,
|
||||||
|
Label: d.Action.Label,
|
||||||
|
Description: d.Action.Description,
|
||||||
|
RequiresNote: d.Action.RequiresNote,
|
||||||
|
GroupName: d.Action.GroupName,
|
||||||
|
SortOrder: d.Action.SortOrder,
|
||||||
|
IsActive: d.Action.IsActive,
|
||||||
|
CreatedAt: d.Action.CreatedAt,
|
||||||
|
UpdatedAt: d.Action.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionNotesToContract(list []entities.DispositionNote) []contract.DispositionNoteResponse {
|
||||||
|
out := make([]contract.DispositionNoteResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.DispositionNoteResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
UserID: d.UserID,
|
||||||
|
Note: d.Note,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
}
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionNotesWithDetailsToContract(list []entities.DispositionNote) []contract.DispositionNoteResponse {
|
||||||
|
out := make([]contract.DispositionNoteResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.DispositionNoteResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
UserID: d.UserID,
|
||||||
|
Note: d.Note,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include user details if preloaded
|
||||||
|
if d.User != nil {
|
||||||
|
resp.User = &contract.UserResponse{
|
||||||
|
ID: d.User.ID,
|
||||||
|
Name: d.User.Name,
|
||||||
|
Email: d.User.Email,
|
||||||
|
IsActive: d.User.IsActive,
|
||||||
|
CreatedAt: d.User.CreatedAt,
|
||||||
|
UpdatedAt: d.User.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, resp)
|
||||||
}
|
}
|
||||||
return out
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
76
internal/transformer/vote_event_transformer.go
Normal file
76
internal/transformer/vote_event_transformer.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package transformer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
)
|
||||||
|
|
||||||
|
func VoteEventToContract(voteEvent *entities.VoteEvent) *contract.VoteEventResponse {
|
||||||
|
if voteEvent == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &contract.VoteEventResponse{
|
||||||
|
ID: voteEvent.ID,
|
||||||
|
Title: voteEvent.Title,
|
||||||
|
Description: voteEvent.Description,
|
||||||
|
StartDate: voteEvent.StartDate,
|
||||||
|
EndDate: voteEvent.EndDate,
|
||||||
|
IsActive: voteEvent.IsActive,
|
||||||
|
ResultsOpen: voteEvent.ResultsOpen,
|
||||||
|
IsVotingOpen: voteEvent.IsVotingOpen(),
|
||||||
|
CreatedAt: voteEvent.CreatedAt,
|
||||||
|
UpdatedAt: voteEvent.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
if voteEvent.Candidates != nil && len(voteEvent.Candidates) > 0 {
|
||||||
|
response.Candidates = CandidatesToContract(voteEvent.Candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func CandidateToContract(candidate *entities.Candidate) *contract.CandidateResponse {
|
||||||
|
if candidate == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.CandidateResponse{
|
||||||
|
ID: candidate.ID,
|
||||||
|
VoteEventID: candidate.VoteEventID,
|
||||||
|
Name: candidate.Name,
|
||||||
|
ImageURL: candidate.ImageURL,
|
||||||
|
Description: candidate.Description,
|
||||||
|
CreatedAt: candidate.CreatedAt,
|
||||||
|
UpdatedAt: candidate.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CandidatesToContract(candidates []entities.Candidate) []contract.CandidateResponse {
|
||||||
|
if candidates == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
responses := make([]contract.CandidateResponse, len(candidates))
|
||||||
|
for i, c := range candidates {
|
||||||
|
resp := CandidateToContract(&c)
|
||||||
|
if resp != nil {
|
||||||
|
responses[i] = *resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
func VoteToContract(vote *entities.Vote) *contract.VoteResponse {
|
||||||
|
if vote == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &contract.VoteResponse{
|
||||||
|
ID: vote.ID,
|
||||||
|
VoteEventID: vote.VoteEventID,
|
||||||
|
CandidateID: vote.CandidateID,
|
||||||
|
UserID: vote.UserID,
|
||||||
|
CreatedAt: vote.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,60 +0,0 @@
|
|||||||
-- =======================
|
|
||||||
-- TITLES
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS titles (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL, -- e.g., "Senior Software Engineer"
|
|
||||||
code TEXT UNIQUE, -- e.g., "senior-software-engineer"
|
|
||||||
description TEXT, -- optional: extra details
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Trigger for updated_at
|
|
||||||
CREATE TRIGGER trg_titles_updated_at
|
|
||||||
BEFORE UPDATE ON titles
|
|
||||||
FOR EACH ROW
|
|
||||||
EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
-- Perwira Tinggi (High-ranking Officers)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Jenderal', 'jenderal', 'Pangkat tertinggi di TNI AD'),
|
|
||||||
('Letnan Jenderal', 'letnan-jenderal', 'Pangkat tinggi di bawah Jenderal'),
|
|
||||||
('Mayor Jenderal', 'mayor-jenderal', 'Pangkat tinggi di bawah Letnan Jenderal'),
|
|
||||||
('Brigadir Jenderal', 'brigadir-jenderal', 'Pangkat tinggi di bawah Mayor Jenderal');
|
|
||||||
|
|
||||||
-- Perwira Menengah (Middle-ranking Officers)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Kolonel', 'kolonel', 'Pangkat perwira menengah tertinggi'),
|
|
||||||
('Letnan Kolonel', 'letnan-kolonel', 'Pangkat perwira menengah di bawah Kolonel'),
|
|
||||||
('Mayor', 'mayor', 'Pangkat perwira menengah di bawah Letnan Kolonel');
|
|
||||||
|
|
||||||
-- Perwira Pertama (Junior Officers)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Kapten', 'kapten', 'Pangkat perwira pertama tertinggi'),
|
|
||||||
('Letnan Satu', 'letnan-satu', 'Pangkat perwira pertama di bawah Kapten'),
|
|
||||||
('Letnan Dua', 'letnan-dua', 'Pangkat perwira pertama di bawah Letnan Satu');
|
|
||||||
|
|
||||||
-- Bintara Tinggi (Senior NCOs)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Pembantu Letnan Satu', 'pembantu-letnan-satu', 'Pangkat bintara tinggi tertinggi'),
|
|
||||||
('Pembantu Letnan Dua', 'pembantu-letnan-dua', 'Pangkat bintara tinggi di bawah Pelda');
|
|
||||||
|
|
||||||
-- Bintara (NCOs)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Sersan Mayor', 'sersan-mayor', 'Pangkat bintara di bawah Pelda'),
|
|
||||||
('Sersan Kepala', 'sersan-kepala', 'Pangkat bintara di bawah Serma'),
|
|
||||||
('Sersan Satu', 'sersan-satu', 'Pangkat bintara di bawah Serka'),
|
|
||||||
('Sersan Dua', 'sersan-dua', 'Pangkat bintara di bawah Sertu');
|
|
||||||
|
|
||||||
-- Tamtama Tinggi (Senior Enlisted)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Kopral Kepala', 'kopral-kepala', 'Pangkat tamtama tinggi tertinggi'),
|
|
||||||
('Kopral Satu', 'kopral-satu', 'Pangkat tamtama tinggi di bawah Kopka'),
|
|
||||||
('Kopral Dua', 'kopral-dua', 'Pangkat tamtama tinggi di bawah Koptu');
|
|
||||||
|
|
||||||
-- Tamtama (Enlisted)
|
|
||||||
INSERT INTO titles (name, code, description) VALUES
|
|
||||||
('Prajurit Kepala', 'prajurit-kepala', 'Pangkat tamtama di bawah Kopda'),
|
|
||||||
('Prajurit Satu', 'prajurit-satu', 'Pangkat tamtama di bawah Prada'),
|
|
||||||
('Prajurit Dua', 'prajurit-dua', 'Pangkat tamtama terendah');
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS institutions;
|
|
||||||
DROP TABLE IF EXISTS priorities;
|
|
||||||
DROP TABLE IF EXISTS labels;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LABELS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS labels (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
color VARCHAR(16), -- HEX color code (e.g., #FF0000)
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_labels_updated_at
|
|
||||||
BEFORE UPDATE ON labels
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- PRIORITIES
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS priorities (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
level INT NOT NULL,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_priorities_updated_at
|
|
||||||
BEFORE UPDATE ON priorities
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- INSTITUTIONS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS institutions (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
type TEXT NOT NULL CHECK (type IN ('government','private','ngo','individual')),
|
|
||||||
address TEXT,
|
|
||||||
contact_person VARCHAR(255),
|
|
||||||
phone VARCHAR(50),
|
|
||||||
email VARCHAR(255),
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_institutions_updated_at
|
|
||||||
BEFORE UPDATE ON institutions
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS disposition_actions;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- DISPOSITION ACTIONS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS disposition_actions (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
code TEXT UNIQUE NOT NULL,
|
|
||||||
label TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
requires_note BOOLEAN NOT NULL DEFAULT FALSE,
|
|
||||||
group_name TEXT,
|
|
||||||
sort_order INT,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_disposition_actions_updated_at
|
|
||||||
BEFORE UPDATE ON disposition_actions
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS letter_incoming_activity_logs;
|
|
||||||
DROP TABLE IF EXISTS letter_incoming_discussion_attachments;
|
|
||||||
DROP TABLE IF EXISTS letter_incoming_discussions;
|
|
||||||
DROP TABLE IF EXISTS letter_disposition_actions;
|
|
||||||
DROP TABLE IF EXISTS disposition_notes;
|
|
||||||
DROP TABLE IF EXISTS letter_dispositions;
|
|
||||||
DROP TABLE IF EXISTS letter_incoming_attachments;
|
|
||||||
DROP TABLE IF EXISTS letter_incoming_labels;
|
|
||||||
DROP TABLE IF EXISTS letter_incoming_recipients;
|
|
||||||
DROP TABLE IF EXISTS letters_incoming;
|
|
||||||
|
|
||||||
DROP SEQUENCE IF EXISTS letters_incoming_seq;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,189 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- SEQUENCE FOR LETTER NUMBER
|
|
||||||
-- =======================
|
|
||||||
CREATE SEQUENCE IF NOT EXISTS letters_incoming_seq;
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTERS INCOMING
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letters_incoming (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_number TEXT NOT NULL UNIQUE DEFAULT ('IN-' || lpad(nextval('letters_incoming_seq')::text, 8, '0')),
|
|
||||||
reference_number TEXT,
|
|
||||||
subject TEXT NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
priority_id UUID REFERENCES priorities(id) ON DELETE SET NULL,
|
|
||||||
sender_institution_id UUID REFERENCES institutions(id) ON DELETE SET NULL,
|
|
||||||
received_date DATE NOT NULL,
|
|
||||||
due_date DATE,
|
|
||||||
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','in_progress','completed')),
|
|
||||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted_at TIMESTAMP WITHOUT TIME ZONE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status ON letters_incoming(status);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_received_date ON letters_incoming(received_date);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_letters_incoming_updated_at
|
|
||||||
BEFORE UPDATE ON letters_incoming
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER INCOMING RECIPIENTS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_incoming_recipients (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
|
|
||||||
recipient_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
recipient_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
|
||||||
status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','read','completed')),
|
|
||||||
read_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_letter ON letter_incoming_recipients(letter_id);
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER INCOMING LABELS (M:N)
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_incoming_labels (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
|
|
||||||
label_id UUID NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE (letter_id, label_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_labels_letter ON letter_incoming_labels(letter_id);
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER INCOMING ATTACHMENTS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_incoming_attachments (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
|
|
||||||
file_url TEXT NOT NULL,
|
|
||||||
file_name TEXT NOT NULL,
|
|
||||||
file_type TEXT NOT NULL,
|
|
||||||
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_attachments_letter ON letter_incoming_attachments(letter_id);
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER DISPOSITIONS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_dispositions (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
|
|
||||||
from_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
from_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
|
||||||
to_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
to_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
|
||||||
notes TEXT,
|
|
||||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','read','rejected','completed')),
|
|
||||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
read_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
completed_at TIMESTAMP WITHOUT TIME ZONE,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_dispositions_letter ON letter_dispositions(letter_id);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_letter_dispositions_updated_at
|
|
||||||
BEFORE UPDATE ON letter_dispositions
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- DISPOSITION NOTES
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS disposition_notes (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE,
|
|
||||||
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
note TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_disposition_notes_disposition ON disposition_notes(disposition_id);
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER DISPOSITION ACTIONS (Selections)
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_disposition_actions (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
disposition_id UUID NOT NULL REFERENCES letter_dispositions(id) ON DELETE CASCADE,
|
|
||||||
action_id UUID NOT NULL REFERENCES disposition_actions(id) ON DELETE RESTRICT,
|
|
||||||
note TEXT,
|
|
||||||
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE (disposition_id, action_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_disposition_actions_disposition ON letter_disposition_actions(disposition_id);
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER INCOMING DISCUSSIONS (Threaded)
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_incoming_discussions (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
|
|
||||||
parent_id UUID REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE,
|
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
mentions JSONB,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
edited_at TIMESTAMP WITHOUT TIME ZONE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_letter ON letter_incoming_discussions(letter_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussions_parent ON letter_incoming_discussions(parent_id);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_letter_incoming_discussions_updated_at
|
|
||||||
BEFORE UPDATE ON letter_incoming_discussions
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER INCOMING DISCUSSION ATTACHMENTS
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_incoming_discussion_attachments (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
discussion_id UUID NOT NULL REFERENCES letter_incoming_discussions(id) ON DELETE CASCADE,
|
|
||||||
file_url TEXT NOT NULL,
|
|
||||||
file_name TEXT NOT NULL,
|
|
||||||
file_type TEXT NOT NULL,
|
|
||||||
uploaded_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
uploaded_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_discussion_attachments_discussion ON letter_incoming_discussion_attachments(discussion_id);
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- LETTER INCOMING ACTIVITY LOGS (Immutable)
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS letter_incoming_activity_logs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
letter_id UUID NOT NULL REFERENCES letters_incoming(id) ON DELETE CASCADE,
|
|
||||||
action_type TEXT NOT NULL,
|
|
||||||
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
||||||
actor_department_id UUID REFERENCES departments(id) ON DELETE SET NULL,
|
|
||||||
target_type TEXT,
|
|
||||||
target_id UUID,
|
|
||||||
from_status TEXT,
|
|
||||||
to_status TEXT,
|
|
||||||
context JSONB,
|
|
||||||
occurred_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_letter ON letter_incoming_activity_logs(letter_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_activity_logs_action ON letter_incoming_activity_logs(action_type);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS disposition_routes;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
-- =======================
|
|
||||||
-- DISPOSITION ROUTES
|
|
||||||
-- =======================
|
|
||||||
CREATE TABLE IF NOT EXISTS disposition_routes (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
from_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
|
|
||||||
to_department_id UUID NOT NULL REFERENCES departments(id) ON DELETE CASCADE,
|
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
||||||
allowed_actions JSONB,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_disposition_routes_from_dept ON disposition_routes(from_department_id);
|
|
||||||
|
|
||||||
-- Prevent duplicate active routes from -> to
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_disposition_routes_active
|
|
||||||
ON disposition_routes(from_department_id, to_department_id)
|
|
||||||
WHERE is_active = TRUE;
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_disposition_routes_updated_at
|
|
||||||
BEFORE UPDATE ON disposition_routes
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
DROP TRIGGER IF EXISTS trg_app_settings_updated_at ON app_settings;
|
|
||||||
DROP TABLE IF EXISTS app_settings;
|
|
||||||
COMMIT;
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
BEGIN;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS app_settings (
|
|
||||||
key TEXT PRIMARY KEY,
|
|
||||||
value JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
||||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_app_settings_updated_at
|
|
||||||
BEFORE UPDATE ON app_settings
|
|
||||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
|
||||||
|
|
||||||
INSERT INTO app_settings(key, value)
|
|
||||||
VALUES
|
|
||||||
('INCOMING_LETTER_PREFIX', '{"value": "ESLI"}'::jsonb),
|
|
||||||
('INCOMING_LETTER_SEQUENCE', '{"value": 0}'::jsonb),
|
|
||||||
('INCOMING_LETTER_RECIPIENTS', '{"department_codes": ["aslog"]}'::jsonb)
|
|
||||||
ON CONFLICT (key) DO NOTHING;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
5
migrations/000012_voting_system.down.sql
Normal file
5
migrations/000012_voting_system.down.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- Drop Voting System Tables
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS votes;
|
||||||
|
DROP TABLE IF EXISTS candidates;
|
||||||
|
DROP TABLE IF EXISTS vote_events;
|
||||||
51
migrations/000012_voting_system.up.sql
Normal file
51
migrations/000012_voting_system.up.sql
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-- Voting System Tables
|
||||||
|
|
||||||
|
-- Vote Events Table
|
||||||
|
CREATE TABLE IF NOT EXISTS vote_events (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
start_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||||
|
end_date TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_vote_events_updated_at
|
||||||
|
BEFORE UPDATE ON vote_events
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- Candidates Table
|
||||||
|
CREATE TABLE IF NOT EXISTS candidates (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
vote_event_id UUID NOT NULL REFERENCES vote_events(id) ON DELETE CASCADE,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
image_url VARCHAR(500),
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_candidates_updated_at
|
||||||
|
BEFORE UPDATE ON candidates
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_candidates_vote_event_id ON candidates(vote_event_id);
|
||||||
|
|
||||||
|
-- Votes Table
|
||||||
|
CREATE TABLE IF NOT EXISTS votes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
vote_event_id UUID NOT NULL REFERENCES vote_events(id) ON DELETE CASCADE,
|
||||||
|
candidate_id UUID NOT NULL REFERENCES candidates(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Ensure one vote per user per event
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_votes_user_event
|
||||||
|
ON votes(user_id, vote_event_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_votes_vote_event_id ON votes(vote_event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_votes_candidate_id ON votes(candidate_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_votes_user_id ON votes(user_id);
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
-- Remove results_open column from vote_events table
|
||||||
|
ALTER TABLE vote_events
|
||||||
|
DROP COLUMN IF EXISTS results_open;
|
||||||
3
migrations/000013_add_results_open_to_vote_events.up.sql
Normal file
3
migrations/000013_add_results_open_to_vote_events.up.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-- Add results_open column to vote_events table
|
||||||
|
ALTER TABLE vote_events
|
||||||
|
ADD COLUMN results_open BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
Loading…
x
Reference in New Issue
Block a user