Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
104cd987e9 | ||
|
|
228f6a78c9 | ||
|
|
8322bbef18 | ||
|
|
5966301165 | ||
|
|
e1a5e9efd3 | ||
|
|
826c5d26ad | ||
|
|
de60983e4e | ||
|
|
61d6eed373 | ||
|
|
001d02c587 |
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
|
||||||
|
|
||||||
|
|||||||
41
config/db_optimized.yaml
Normal file
41
config/db_optimized.yaml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Optimized Database Configuration for handling 1000+ users
|
||||||
|
database:
|
||||||
|
host: localhost
|
||||||
|
port: 5432
|
||||||
|
db: meti_vote
|
||||||
|
driver: postgres
|
||||||
|
username: ${DB_USERNAME}
|
||||||
|
password: ${DB_PASSWORD}
|
||||||
|
ssl-mode: disable
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
# Connection Pool Settings - Optimized for high load
|
||||||
|
# For 1000+ concurrent users, these settings help manage database connections efficiently
|
||||||
|
|
||||||
|
# Maximum number of idle connections in the pool
|
||||||
|
# Keeping more idle connections reduces connection setup overhead
|
||||||
|
max-idle-connections-in-second: 25
|
||||||
|
|
||||||
|
# Maximum number of open connections to the database
|
||||||
|
# This prevents overwhelming the database with too many connections
|
||||||
|
max-open-connections-in-second: 100
|
||||||
|
|
||||||
|
# Maximum lifetime of a connection in seconds (30 minutes)
|
||||||
|
# This helps prevent stale connections and memory leaks
|
||||||
|
connection-max-life-time-in-second: 1800
|
||||||
|
|
||||||
|
# Additional PostgreSQL tuning recommendations:
|
||||||
|
#
|
||||||
|
# In postgresql.conf:
|
||||||
|
# - max_connections = 200
|
||||||
|
# - shared_buffers = 256MB
|
||||||
|
# - effective_cache_size = 1GB
|
||||||
|
# - work_mem = 4MB
|
||||||
|
# - maintenance_work_mem = 64MB
|
||||||
|
# - checkpoint_completion_target = 0.9
|
||||||
|
# - wal_buffers = 16MB
|
||||||
|
# - default_statistics_target = 100
|
||||||
|
# - random_page_cost = 1.1
|
||||||
|
# - effective_io_concurrency = 200
|
||||||
|
# - min_wal_size = 1GB
|
||||||
|
# - max_wal_size = 4GB
|
||||||
@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
APP_NAME="eslogad"
|
APP_NAME="meti-backend"
|
||||||
PORT="4000"
|
PORT="4000"
|
||||||
|
|
||||||
echo "🔄 Pulling latest code..."
|
echo "🔄 Pulling latest code..."
|
||||||
@ -15,7 +15,7 @@ docker rm $APP_NAME 2>/dev/null
|
|||||||
|
|
||||||
echo "🚀 Running new container..."
|
echo "🚀 Running new container..."
|
||||||
docker run -d --name $APP_NAME \
|
docker run -d --name $APP_NAME \
|
||||||
-p $PORT:$PORT \
|
-p 4001:$PORT \
|
||||||
-v "$(pwd)/infra":/infra:ro \
|
-v "$(pwd)/infra":/infra:ro \
|
||||||
-v "$(pwd)/templates":/templates:ro \
|
-v "$(pwd)/templates":/templates:ro \
|
||||||
$APP_NAME:latest
|
$APP_NAME:latest
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@ -43,14 +43,24 @@ func (a *App) Initialize(cfg *config.Config) error {
|
|||||||
middlewares := a.initMiddleware(services)
|
middlewares := a.initMiddleware(services)
|
||||||
healthHandler := handler.NewHealthHandler()
|
healthHandler := handler.NewHealthHandler()
|
||||||
fileHandler := handler.NewFileHandler(services.fileService)
|
fileHandler := handler.NewFileHandler(services.fileService)
|
||||||
|
rbacHandler := handler.NewRBACHandler(services.rbacService)
|
||||||
|
masterHandler := handler.NewMasterHandler(services.masterService)
|
||||||
|
letterHandler := handler.NewLetterHandler(services.letterService)
|
||||||
|
dispositionRouteHandler := handler.NewDispositionRouteHandler(services.dispositionRouteService)
|
||||||
|
voteEventHandler := handler.NewVoteEventHandler(services.voteEventService)
|
||||||
|
|
||||||
a.router = router.NewRouter(
|
a.router = router.NewRouter(
|
||||||
cfg,
|
cfg,
|
||||||
handler.NewAuthHandler(services.authService),
|
handler.NewAuthHandler(services.authService),
|
||||||
middlewares.authMiddleware,
|
middlewares.authMiddleware,
|
||||||
healthHandler,
|
healthHandler,
|
||||||
handler.NewUserHandler(services.userService, &validator.UserValidatorImpl{}),
|
handler.NewUserHandler(services.userService, validator.NewUserValidator()),
|
||||||
fileHandler,
|
fileHandler,
|
||||||
|
rbacHandler,
|
||||||
|
masterHandler,
|
||||||
|
letterHandler,
|
||||||
|
dispositionRouteHandler,
|
||||||
|
voteEventHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -99,6 +109,26 @@ type repositories struct {
|
|||||||
userRepo *repository.UserRepositoryImpl
|
userRepo *repository.UserRepositoryImpl
|
||||||
userProfileRepo *repository.UserProfileRepository
|
userProfileRepo *repository.UserProfileRepository
|
||||||
titleRepo *repository.TitleRepository
|
titleRepo *repository.TitleRepository
|
||||||
|
rbacRepo *repository.RBACRepository
|
||||||
|
labelRepo *repository.LabelRepository
|
||||||
|
priorityRepo *repository.PriorityRepository
|
||||||
|
institutionRepo *repository.InstitutionRepository
|
||||||
|
dispRepo *repository.DispositionActionRepository
|
||||||
|
letterRepo *repository.LetterIncomingRepository
|
||||||
|
letterAttachRepo *repository.LetterIncomingAttachmentRepository
|
||||||
|
activityLogRepo *repository.LetterIncomingActivityLogRepository
|
||||||
|
dispositionRouteRepo *repository.DispositionRouteRepository
|
||||||
|
// new repos
|
||||||
|
letterDispositionRepo *repository.LetterIncomingDispositionRepository
|
||||||
|
letterDispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||||
|
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||||
|
dispositionNoteRepo *repository.DispositionNoteRepository
|
||||||
|
letterDiscussionRepo *repository.LetterDiscussionRepository
|
||||||
|
settingRepo *repository.AppSettingRepository
|
||||||
|
recipientRepo *repository.LetterIncomingRecipientRepository
|
||||||
|
departmentRepo *repository.DepartmentRepository
|
||||||
|
userDeptRepo *repository.UserDepartmentRepository
|
||||||
|
voteEventRepo *repository.VoteEventRepositoryImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initRepositories() *repositories {
|
func (a *App) initRepositories() *repositories {
|
||||||
@ -106,16 +136,41 @@ func (a *App) initRepositories() *repositories {
|
|||||||
userRepo: repository.NewUserRepository(a.db),
|
userRepo: repository.NewUserRepository(a.db),
|
||||||
userProfileRepo: repository.NewUserProfileRepository(a.db),
|
userProfileRepo: repository.NewUserProfileRepository(a.db),
|
||||||
titleRepo: repository.NewTitleRepository(a.db),
|
titleRepo: repository.NewTitleRepository(a.db),
|
||||||
|
rbacRepo: repository.NewRBACRepository(a.db),
|
||||||
|
labelRepo: repository.NewLabelRepository(a.db),
|
||||||
|
priorityRepo: repository.NewPriorityRepository(a.db),
|
||||||
|
institutionRepo: repository.NewInstitutionRepository(a.db),
|
||||||
|
dispRepo: repository.NewDispositionActionRepository(a.db),
|
||||||
|
letterRepo: repository.NewLetterIncomingRepository(a.db),
|
||||||
|
letterAttachRepo: repository.NewLetterIncomingAttachmentRepository(a.db),
|
||||||
|
activityLogRepo: repository.NewLetterIncomingActivityLogRepository(a.db),
|
||||||
|
dispositionRouteRepo: repository.NewDispositionRouteRepository(a.db),
|
||||||
|
letterDispositionRepo: repository.NewLetterIncomingDispositionRepository(a.db),
|
||||||
|
letterDispositionDeptRepo: repository.NewLetterIncomingDispositionDepartmentRepository(a.db),
|
||||||
|
letterDispActionSelRepo: repository.NewLetterDispositionActionSelectionRepository(a.db),
|
||||||
|
dispositionNoteRepo: repository.NewDispositionNoteRepository(a.db),
|
||||||
|
letterDiscussionRepo: repository.NewLetterDiscussionRepository(a.db),
|
||||||
|
settingRepo: repository.NewAppSettingRepository(a.db),
|
||||||
|
recipientRepo: repository.NewLetterIncomingRecipientRepository(a.db),
|
||||||
|
departmentRepo: repository.NewDepartmentRepository(a.db),
|
||||||
|
userDeptRepo: repository.NewUserDepartmentRepository(a.db),
|
||||||
|
voteEventRepo: repository.NewVoteEventRepository(a.db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type processors struct {
|
type processors struct {
|
||||||
userProcessor *processor.UserProcessorImpl
|
userProcessor *processor.UserProcessorImpl
|
||||||
|
letterProcessor *processor.LetterProcessorImpl
|
||||||
|
activityLogger *processor.ActivityLogProcessorImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||||
|
txMgr := repository.NewTxManager(a.db)
|
||||||
|
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.letterDispositionDeptRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo),
|
||||||
|
activityLogger: activity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +178,11 @@ type services struct {
|
|||||||
userService *service.UserServiceImpl
|
userService *service.UserServiceImpl
|
||||||
authService *service.AuthServiceImpl
|
authService *service.AuthServiceImpl
|
||||||
fileService *service.FileServiceImpl
|
fileService *service.FileServiceImpl
|
||||||
|
rbacService *service.RBACServiceImpl
|
||||||
|
masterService *service.MasterServiceImpl
|
||||||
|
letterService *service.LetterServiceImpl
|
||||||
|
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 {
|
||||||
@ -132,15 +192,27 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
|||||||
|
|
||||||
userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo)
|
userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo)
|
||||||
|
|
||||||
// File storage client and service
|
|
||||||
fileCfg := cfg.S3Config
|
fileCfg := cfg.S3Config
|
||||||
s3Client := client.NewFileClient(fileCfg)
|
s3Client := client.NewFileClient(fileCfg)
|
||||||
fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents")
|
fileSvc := service.NewFileService(s3Client, processors.userProcessor, "profile", "documents")
|
||||||
|
|
||||||
|
rbacSvc := service.NewRBACService(repos.rbacRepo)
|
||||||
|
|
||||||
|
masterSvc := service.NewMasterService(repos.labelRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo)
|
||||||
|
|
||||||
|
letterSvc := service.NewLetterService(processors.letterProcessor)
|
||||||
|
dispRouteSvc := service.NewDispositionRouteService(repos.dispositionRouteRepo)
|
||||||
|
voteEventSvc := service.NewVoteEventService(repos.voteEventRepo)
|
||||||
|
|
||||||
return &services{
|
return &services{
|
||||||
userService: userSvc,
|
userService: userSvc,
|
||||||
authService: authService,
|
authService: authService,
|
||||||
fileService: fileSvc,
|
fileService: fileSvc,
|
||||||
|
rbacService: rbacSvc,
|
||||||
|
masterService: masterSvc,
|
||||||
|
letterService: letterSvc,
|
||||||
|
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"
|
||||||
|
|||||||
@ -2,6 +2,12 @@ package contract
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX"
|
||||||
|
SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE"
|
||||||
|
SettingIncomingLetterRecipients = "INCOMING_LETTER_RECIPIENTS"
|
||||||
|
)
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@ -47,3 +53,118 @@ type HealthResponse struct {
|
|||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LabelResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Color *string `json:"color,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLabelRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Color *string `json:"color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLabelRequest struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Color *string `json:"color,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListLabelsResponse struct {
|
||||||
|
Labels []LabelResponse `json:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriorityResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePriorityRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePriorityRequest struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Level *int `json:"level,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPrioritiesResponse struct {
|
||||||
|
Priorities []PriorityResponse `json:"priorities"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstitutionResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Address *string `json:"address,omitempty"`
|
||||||
|
ContactPerson *string `json:"contact_person,omitempty"`
|
||||||
|
Phone *string `json:"phone,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateInstitutionRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Address *string `json:"address,omitempty"`
|
||||||
|
ContactPerson *string `json:"contact_person,omitempty"`
|
||||||
|
Phone *string `json:"phone,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateInstitutionRequest struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Type *string `json:"type,omitempty"`
|
||||||
|
Address *string `json:"address,omitempty"`
|
||||||
|
ContactPerson *string `json:"contact_person,omitempty"`
|
||||||
|
Phone *string `json:"phone,omitempty"`
|
||||||
|
Email *string `json:"email,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListInstitutionsResponse struct {
|
||||||
|
Institutions []InstitutionResponse `json:"institutions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionActionResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
RequiresNote bool `json:"requires_note"`
|
||||||
|
GroupName *string `json:"group_name,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDispositionActionRequest struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
RequiresNote *bool `json:"requires_note,omitempty"`
|
||||||
|
GroupName *string `json:"group_name,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDispositionActionRequest struct {
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Label *string `json:"label,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
RequiresNote *bool `json:"requires_note,omitempty"`
|
||||||
|
GroupName *string `json:"group_name,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDispositionActionsResponse struct {
|
||||||
|
Actions []DispositionActionResponse `json:"actions"`
|
||||||
|
}
|
||||||
|
|||||||
43
internal/contract/disposition_route_contract.go
Normal file
43
internal/contract/disposition_route_contract.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DepartmentInfo struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionRouteResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
||||||
|
ToDepartmentID uuid.UUID `json:"to_department_id"`
|
||||||
|
IsActive bool `json:"is_active"`
|
||||||
|
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Department information
|
||||||
|
FromDepartment DepartmentInfo `json:"from_department"`
|
||||||
|
ToDepartment DepartmentInfo `json:"to_department"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDispositionRouteRequest struct {
|
||||||
|
FromDepartmentID uuid.UUID `json:"from_department_id"`
|
||||||
|
ToDepartmentID uuid.UUID `json:"to_department_id"`
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDispositionRouteRequest struct {
|
||||||
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
AllowedActions *map[string]interface{} `json:"allowed_actions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDispositionRoutesResponse struct {
|
||||||
|
Routes []DispositionRouteResponse `json:"routes"`
|
||||||
|
}
|
||||||
173
internal/contract/letter_contract.go
Normal file
173
internal/contract/letter_contract.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateIncomingLetterAttachment struct {
|
||||||
|
FileURL string `json:"file_url"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
FileType string `json:"file_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateIncomingLetterRequest struct {
|
||||||
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||||
|
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||||
|
ReceivedDate time.Time `json:"received_date"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
|
Attachments []CreateIncomingLetterAttachment `json:"attachments,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncomingLetterAttachmentResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
FileURL string `json:"file_url"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
FileType string `json:"file_type"`
|
||||||
|
UploadedAt time.Time `json:"uploaded_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncomingLetterResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
LetterNumber string `json:"letter_number"`
|
||||||
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Priority *PriorityResponse `json:"priority,omitempty"`
|
||||||
|
SenderInstitution *InstitutionResponse `json:"sender_institution,omitempty"`
|
||||||
|
ReceivedDate time.Time `json:"received_date"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Attachments []IncomingLetterAttachmentResponse `json:"attachments"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateIncomingLetterRequest struct {
|
||||||
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
|
Subject *string `json:"subject,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||||
|
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||||
|
ReceivedDate *time.Time `json:"received_date,omitempty"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListIncomingLettersRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
|
Query *string `json:"query,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListIncomingLettersResponse struct {
|
||||||
|
Letters []IncomingLetterResponse `json:"letters"`
|
||||||
|
Pagination PaginationResponse `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDispositionActionSelection struct {
|
||||||
|
ActionID uuid.UUID `json:"action_id"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLetterDispositionRequest struct {
|
||||||
|
FromDepartment uuid.UUID `json:"from_department"`
|
||||||
|
LetterID uuid.UUID `json:"letter_id"`
|
||||||
|
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
SelectedActions []CreateDispositionActionSelection `json:"selected_actions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionResponse 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDispositionsResponse struct {
|
||||||
|
Dispositions []DispositionResponse `json:"dispositions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnhancedDispositionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
LetterID uuid.UUID `json:"letter_id"`
|
||||||
|
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Department DepartmentResponse `json:"department"`
|
||||||
|
Departments []DispositionDepartmentResponse `json:"departments"`
|
||||||
|
Actions []DispositionActionSelectionResponse `json:"actions"`
|
||||||
|
DispositionNotes []DispositionNoteResponse `json:"disposition_notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionDepartmentResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
DepartmentID uuid.UUID `json:"department_id"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Department *DepartmentResponse `json:"department,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionActionSelectionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
ActionID uuid.UUID `json:"action_id"`
|
||||||
|
Action *DispositionActionResponse `json:"action,omitempty"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
CreatedBy uuid.UUID `json:"created_by"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionNoteResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||||
|
Note string `json:"note"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
User *UserResponse `json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListEnhancedDispositionsResponse struct {
|
||||||
|
Dispositions []EnhancedDispositionResponse `json:"dispositions"`
|
||||||
|
Discussions []LetterDiscussionResponse `json:"discussions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateLetterDiscussionRequest struct {
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateLetterDiscussionRequest struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterDiscussionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
LetterID uuid.UUID `json:"letter_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Mentions map[string]interface{} `json:"mentions,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||||
|
|
||||||
|
// Preloaded user profile who created the discussion
|
||||||
|
User *UserResponse `json:"user,omitempty"`
|
||||||
|
|
||||||
|
// Preloaded user profiles for mentions
|
||||||
|
MentionedUsers []UserResponse `json:"mentioned_users,omitempty"`
|
||||||
|
}
|
||||||
57
internal/contract/rbac_contract.go
Normal file
57
internal/contract/rbac_contract.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PermissionResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreatePermissionRequest struct {
|
||||||
|
Code string `json:"code"` // unique
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdatePermissionRequest struct {
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListPermissionsResponse struct {
|
||||||
|
Permissions []PermissionResponse `json:"permissions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RoleWithPermissionsResponse struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Permissions []PermissionResponse `json:"permissions"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateRoleRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PermissionCodes []string `json:"permission_codes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateRoleRequest struct {
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
Code *string `json:"code,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PermissionCodes *[]string `json:"permission_codes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListRolesResponse struct {
|
||||||
|
Roles []RoleWithPermissionsResponse `json:"roles"`
|
||||||
|
}
|
||||||
@ -44,16 +44,20 @@ type LoginResponse struct {
|
|||||||
User UserResponse `json:"user"`
|
User UserResponse `json:"user"`
|
||||||
Roles []RoleResponse `json:"roles"`
|
Roles []RoleResponse `json:"roles"`
|
||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
Positions []PositionResponse `json:"positions"`
|
Departments []DepartmentResponse `json:"departments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserResponse struct {
|
type UserResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
Roles []RoleResponse `json:"roles,omitempty"`
|
||||||
|
DepartmentResponse []DepartmentResponse `json:"department_response"`
|
||||||
|
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListUsersRequest struct {
|
type ListUsersRequest struct {
|
||||||
@ -61,6 +65,8 @@ type ListUsersRequest struct {
|
|||||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
Role *string `json:"role,omitempty"`
|
Role *string `json:"role,omitempty"`
|
||||||
IsActive *bool `json:"is_active,omitempty"`
|
IsActive *bool `json:"is_active,omitempty"`
|
||||||
|
Search *string `json:"search,omitempty"`
|
||||||
|
RoleCode *string `json:"role_code,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ListUsersResponse struct {
|
type ListUsersResponse struct {
|
||||||
@ -74,7 +80,7 @@ type RoleResponse struct {
|
|||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PositionResponse struct {
|
type DepartmentResponse struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
@ -97,6 +103,7 @@ type UserProfileResponse struct {
|
|||||||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
LastSeenAt *time.Time `json:"last_seen_at,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"`
|
||||||
|
Roles []RoleResponse `json:"roles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserProfileRequest struct {
|
type UpdateUserProfileRequest struct {
|
||||||
@ -123,3 +130,55 @@ 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=5000"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreateAsyncResponse represents the immediate response for async bulk creation
|
||||||
|
type BulkCreateAsyncResponse struct {
|
||||||
|
JobID uuid.UUID `json:"job_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|||||||
118
internal/contract/vote_event_contract.go
Normal file
118
internal/contract/vote_event_contract.go
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoteEventDetailsResponse struct {
|
||||||
|
VoteEvent VoteEventResponse `json:"vote_event"`
|
||||||
|
TotalParticipants int64 `json:"total_participants"`
|
||||||
|
TotalVoted int64 `json:"total_voted"`
|
||||||
|
TotalNotVoted int64 `json:"total_not_voted"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserVoteInfo struct {
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
VotedAt time.Time `json:"voted_at"`
|
||||||
|
CandidateID uuid.UUID `json:"candidate_id"`
|
||||||
|
CandidateName string `json:"candidate_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserBasicInfo struct {
|
||||||
|
UserID uuid.UUID `json:"user_id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
}
|
||||||
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"
|
||||||
|
}
|
||||||
18
internal/entities/department.go
Normal file
18
internal/entities/department.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Department struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
|
Path string `gorm:"not null" json:"path"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Department) TableName() string { return "departments" }
|
||||||
22
internal/entities/disposition_action.go
Normal file
22
internal/entities/disposition_action.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DispositionAction struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Code string `gorm:"uniqueIndex;not null" json:"code"`
|
||||||
|
Label string `gorm:"not null" json:"label"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
RequiresNote bool `gorm:"not null;default:false" json:"requires_note"`
|
||||||
|
GroupName *string `json:"group_name,omitempty"`
|
||||||
|
SortOrder *int `json:"sort_order,omitempty"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DispositionAction) TableName() string { return "disposition_actions" }
|
||||||
23
internal/entities/disposition_route.go
Normal file
23
internal/entities/disposition_route.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DispositionRoute struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
FromDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"from_department_id"`
|
||||||
|
ToDepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"to_department_id"`
|
||||||
|
IsActive bool `gorm:"not null;default:true" json:"is_active"`
|
||||||
|
AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
FromDepartment Department `gorm:"foreignKey:FromDepartmentID;references:ID" json:"from_department,omitempty"`
|
||||||
|
ToDepartment Department `gorm:"foreignKey:ToDepartmentID;references:ID" json:"to_department,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DispositionRoute) TableName() string { return "disposition_routes" }
|
||||||
30
internal/entities/institution.go
Normal file
30
internal/entities/institution.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstitutionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
InstGovernment InstitutionType = "government"
|
||||||
|
InstPrivate InstitutionType = "private"
|
||||||
|
InstNGO InstitutionType = "ngo"
|
||||||
|
InstIndividual InstitutionType = "individual"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Institution struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Type InstitutionType `gorm:"not null;size:32" json:"type"`
|
||||||
|
Address *string `json:"address,omitempty"`
|
||||||
|
ContactPerson *string `gorm:"size:255" json:"contact_person,omitempty"`
|
||||||
|
Phone *string `gorm:"size:50" json:"phone,omitempty"`
|
||||||
|
Email *string `gorm:"size:255" json:"email,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Institution) TableName() string { return "institutions" }
|
||||||
17
internal/entities/label.go
Normal file
17
internal/entities/label.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Label struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Color *string `gorm:"size:16" json:"color,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Label) TableName() string { return "labels" }
|
||||||
24
internal/entities/letter_discussion.go
Normal file
24
internal/entities/letter_discussion.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterDiscussion struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
|
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||||
|
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
|
||||||
|
Message string `gorm:"not null" json:"message"`
|
||||||
|
Mentions JSONB `gorm:"type:jsonb" json:"mentions,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" }
|
||||||
65
internal/entities/letter_disposition.go
Normal file
65
internal/entities/letter_disposition.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncomingDisposition struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
|
DepartmentID *uuid.UUID `json:"department_id,omitempty"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
Department Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||||
|
Departments []LetterIncomingDispositionDepartment `gorm:"foreignKey:LetterIncomingDispositionID;references:ID" json:"departments,omitempty"`
|
||||||
|
ActionSelections []LetterDispositionActionSelection `gorm:"foreignKey:DispositionID;references:ID" json:"action_selections,omitempty"`
|
||||||
|
DispositionNotes []DispositionNote `gorm:"foreignKey:DispositionID;references:ID" json:"disposition_notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncomingDisposition) TableName() string { return "letter_incoming_dispositions" }
|
||||||
|
|
||||||
|
type LetterIncomingDispositionDepartment struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterIncomingDispositionID uuid.UUID `gorm:"type:uuid;not null" json:"letter_incoming_disposition_id"`
|
||||||
|
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
Department *Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncomingDispositionDepartment) TableName() string {
|
||||||
|
return "letter_incoming_dispositions_department"
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionNote struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"`
|
||||||
|
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||||
|
Note string `gorm:"not null" json:"note"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DispositionNote) TableName() string { return "disposition_notes" }
|
||||||
|
|
||||||
|
type LetterDispositionActionSelection struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
DispositionID uuid.UUID `gorm:"type:uuid;not null" json:"disposition_id"`
|
||||||
|
ActionID uuid.UUID `gorm:"type:uuid;not null" json:"action_id"`
|
||||||
|
Note *string `json:"note,omitempty"`
|
||||||
|
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
Action *DispositionAction `gorm:"foreignKey:ActionID;references:ID" json:"action,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" }
|
||||||
45
internal/entities/letter_incoming.go
Normal file
45
internal/entities/letter_incoming.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncomingStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LetterIncomingStatusNew LetterIncomingStatus = "new"
|
||||||
|
LetterIncomingStatusInProgress LetterIncomingStatus = "in_progress"
|
||||||
|
LetterIncomingStatusCompleted LetterIncomingStatus = "completed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncoming struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterNumber string `gorm:"uniqueIndex;not null" json:"letter_number"`
|
||||||
|
ReferenceNumber *string `json:"reference_number,omitempty"`
|
||||||
|
Subject string `gorm:"not null" json:"subject"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||||
|
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||||
|
ReceivedDate time.Time `json:"received_date"`
|
||||||
|
DueDate *time.Time `json:"due_date,omitempty"`
|
||||||
|
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"`
|
||||||
|
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncoming) TableName() string { return "letters_incoming" }
|
||||||
|
|
||||||
|
type LetterIncomingAttachment struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
|
FileURL string `gorm:"not null" json:"file_url"`
|
||||||
|
FileName string `gorm:"not null" json:"file_name"`
|
||||||
|
FileType string `gorm:"not null" json:"file_type"`
|
||||||
|
UploadedBy *uuid.UUID `json:"uploaded_by,omitempty"`
|
||||||
|
UploadedAt time.Time `gorm:"autoCreateTime" json:"uploaded_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncomingAttachment) TableName() string { return "letter_incoming_attachments" }
|
||||||
23
internal/entities/letter_incoming_activity_log.go
Normal file
23
internal/entities/letter_incoming_activity_log.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncomingActivityLog struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
|
ActionType string `gorm:"not null" json:"action_type"`
|
||||||
|
ActorUserID *uuid.UUID `json:"actor_user_id,omitempty"`
|
||||||
|
ActorDepartmentID *uuid.UUID `json:"actor_department_id,omitempty"`
|
||||||
|
TargetType *string `json:"target_type,omitempty"`
|
||||||
|
TargetID *uuid.UUID `json:"target_id,omitempty"`
|
||||||
|
FromStatus *string `json:"from_status,omitempty"`
|
||||||
|
ToStatus *string `json:"to_status,omitempty"`
|
||||||
|
Context JSONB `gorm:"type:jsonb" json:"context,omitempty"`
|
||||||
|
OccurredAt time.Time `gorm:"autoCreateTime" json:"occurred_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncomingActivityLog) TableName() string { return "letter_incoming_activity_logs" }
|
||||||
28
internal/entities/letter_incoming_recipient.go
Normal file
28
internal/entities/letter_incoming_recipient.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncomingRecipientStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RecipientStatusNew LetterIncomingRecipientStatus = "new"
|
||||||
|
RecipientStatusRead LetterIncomingRecipientStatus = "read"
|
||||||
|
RecipientStatusCompleted LetterIncomingRecipientStatus = "completed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncomingRecipient struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
|
||||||
|
RecipientUserID *uuid.UUID `json:"recipient_user_id,omitempty"`
|
||||||
|
RecipientDepartmentID *uuid.UUID `json:"recipient_department_id,omitempty"`
|
||||||
|
Status LetterIncomingRecipientStatus `gorm:"not null;default:'new'" json:"status"`
|
||||||
|
ReadAt *time.Time `json:"read_at,omitempty"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LetterIncomingRecipient) TableName() string { return "letter_incoming_recipients" }
|
||||||
17
internal/entities/priority.go
Normal file
17
internal/entities/priority.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Priority struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
Name string `gorm:"not null;size:255" json:"name"`
|
||||||
|
Level int `gorm:"not null" json:"level"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Priority) TableName() string { return "priorities" }
|
||||||
10
internal/entities/role_permission.go
Normal file
10
internal/entities/role_permission.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "github.com/google/uuid"
|
||||||
|
|
||||||
|
type RolePermission struct {
|
||||||
|
RoleID uuid.UUID `gorm:"type:uuid;primaryKey" json:"role_id"`
|
||||||
|
PermissionID uuid.UUID `gorm:"type:uuid;primaryKey" json:"permission_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RolePermission) TableName() string { return "role_permissions" }
|
||||||
14
internal/entities/setting.go
Normal file
14
internal/entities/setting.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppSetting struct {
|
||||||
|
Key string `gorm:"primaryKey;size:100" json:"key"`
|
||||||
|
Value JSONB `gorm:"type:jsonb;default:'{}'" json:"value"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (AppSetting) TableName() string { return "app_settings" }
|
||||||
@ -41,12 +41,15 @@ func (p *Permissions) Scan(value interface{}) error {
|
|||||||
|
|
||||||
type User struct {
|
type User 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"`
|
||||||
|
Username string `gorm:"uniqueIndex;not null;size:100" json:"username" validate:"required,min=1,max=100"`
|
||||||
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
|
||||||
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
|
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
|
||||||
PasswordHash string `gorm:"not null;size:255" json:"-"`
|
PasswordHash string `gorm:"not null;size:255" json:"-"`
|
||||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||||
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"`
|
||||||
|
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)
|
||||||
|
}
|
||||||
98
internal/handler/disposition_route_handler.go
Normal file
98
internal/handler/disposition_route_handler.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DispositionRouteService interface {
|
||||||
|
Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
|
||||||
|
Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error)
|
||||||
|
Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error)
|
||||||
|
ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error)
|
||||||
|
SetActive(ctx context.Context, id uuid.UUID, active bool) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionRouteHandler struct{ svc DispositionRouteService }
|
||||||
|
|
||||||
|
func NewDispositionRouteHandler(svc DispositionRouteService) *DispositionRouteHandler {
|
||||||
|
return &DispositionRouteHandler{svc: svc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DispositionRouteHandler) Create(c *gin.Context) {
|
||||||
|
var req contract.CreateDispositionRouteRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.Create(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DispositionRouteHandler) Update(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateDispositionRouteRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.Update(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DispositionRouteHandler) Get(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.Get(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
|
||||||
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
|
|
||||||
|
resp, err := h.svc.ListByFromDept(c.Request.Context(), appCtx.DepartmentID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DispositionRouteHandler) SetActive(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
toggle := c.Query("active")
|
||||||
|
active := toggle != "false"
|
||||||
|
if err := h.svc.SetActive(c.Request.Context(), id, active); err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &contract.SuccessResponse{Message: "updated"})
|
||||||
|
}
|
||||||
186
internal/handler/letter_handler.go
Normal file
186
internal/handler/letter_handler.go
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterService interface {
|
||||||
|
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
|
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||||
|
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
||||||
|
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||||
|
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||||
|
|
||||||
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
|
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterHandler struct{ svc LetterService }
|
||||||
|
|
||||||
|
func NewLetterHandler(svc LetterService) *LetterHandler { return &LetterHandler{svc: svc} }
|
||||||
|
|
||||||
|
func (h *LetterHandler) CreateIncomingLetter(c *gin.Context) {
|
||||||
|
var req contract.CreateIncomingLetterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateIncomingLetter(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) GetIncomingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.GetIncomingLetterByID(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) ListIncomingLetters(c *gin.Context) {
|
||||||
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||||
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||||
|
status := c.Query("status")
|
||||||
|
query := c.Query("q")
|
||||||
|
var statusPtr *string
|
||||||
|
var queryPtr *string
|
||||||
|
if status != "" {
|
||||||
|
statusPtr = &status
|
||||||
|
}
|
||||||
|
if query != "" {
|
||||||
|
queryPtr = &query
|
||||||
|
}
|
||||||
|
req := &contract.ListIncomingLettersRequest{Page: page, Limit: limit, Status: statusPtr, Query: queryPtr}
|
||||||
|
resp, err := h.svc.ListIncomingLetters(c.Request.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) UpdateIncomingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateIncomingLetterRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateIncomingLetter(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.SoftDeleteIncomingLetter(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
|
||||||
|
appCtx := appcontext.FromGinContext(c.Request.Context())
|
||||||
|
var req contract.CreateLetterDispositionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.FromDepartment = appCtx.DepartmentID
|
||||||
|
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) GetEnhancedDispositionsByLetter(c *gin.Context) {
|
||||||
|
letterID, err := uuid.Parse(c.Param("letter_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.GetEnhancedDispositionsByLetter(c.Request.Context(), letterID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) CreateDiscussion(c *gin.Context) {
|
||||||
|
letterID, err := uuid.Parse(c.Param("letter_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.CreateLetterDiscussionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateDiscussion(c.Request.Context(), letterID, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *LetterHandler) UpdateDiscussion(c *gin.Context) {
|
||||||
|
letterID, err := uuid.Parse(c.Param("letter_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
discussionID, err := uuid.Parse(c.Param("discussion_id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid discussion_id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateLetterDiscussionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateDiscussion(c.Request.Context(), letterID, discussionID, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
252
internal/handler/master_handler.go
Normal file
252
internal/handler/master_handler.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterService interface {
|
||||||
|
CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error)
|
||||||
|
UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error)
|
||||||
|
DeleteLabel(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error)
|
||||||
|
|
||||||
|
CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error)
|
||||||
|
UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error)
|
||||||
|
DeletePriority(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error)
|
||||||
|
|
||||||
|
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
|
||||||
|
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
|
||||||
|
DeleteInstitution(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error)
|
||||||
|
|
||||||
|
CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
||||||
|
UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error)
|
||||||
|
DeleteDispositionAction(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MasterHandler struct{ svc MasterService }
|
||||||
|
|
||||||
|
func NewMasterHandler(svc MasterService) *MasterHandler { return &MasterHandler{svc: svc} }
|
||||||
|
|
||||||
|
func (h *MasterHandler) CreateLabel(c *gin.Context) {
|
||||||
|
var req contract.CreateLabelRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateLabel(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) UpdateLabel(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateLabelRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateLabel(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) DeleteLabel(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeleteLabel(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) ListLabels(c *gin.Context) {
|
||||||
|
resp, err := h.svc.ListLabels(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorities
|
||||||
|
func (h *MasterHandler) CreatePriority(c *gin.Context) {
|
||||||
|
var req contract.CreatePriorityRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreatePriority(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) UpdatePriority(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdatePriorityRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdatePriority(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) DeletePriority(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeletePriority(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) ListPriorities(c *gin.Context) {
|
||||||
|
resp, err := h.svc.ListPriorities(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Institutions
|
||||||
|
func (h *MasterHandler) CreateInstitution(c *gin.Context) {
|
||||||
|
var req contract.CreateInstitutionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateInstitution(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) UpdateInstitution(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateInstitutionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateInstitution(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) DeleteInstitution(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeleteInstitution(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MasterHandler) ListInstitutions(c *gin.Context) {
|
||||||
|
resp, err := h.svc.ListInstitutions(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disposition Actions
|
||||||
|
func (h *MasterHandler) CreateDispositionAction(c *gin.Context) {
|
||||||
|
var req contract.CreateDispositionActionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateDispositionAction(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(201, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) UpdateDispositionAction(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateDispositionActionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateDispositionAction(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) DeleteDispositionAction(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(400, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeleteDispositionAction(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
func (h *MasterHandler) ListDispositionActions(c *gin.Context) {
|
||||||
|
resp, err := h.svc.ListDispositionActions(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
137
internal/handler/rbac_handler.go
Normal file
137
internal/handler/rbac_handler.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RBACService interface {
|
||||||
|
CreatePermission(ctx context.Context, req *contract.CreatePermissionRequest) (*contract.PermissionResponse, error)
|
||||||
|
UpdatePermission(ctx context.Context, id uuid.UUID, req *contract.UpdatePermissionRequest) (*contract.PermissionResponse, error)
|
||||||
|
DeletePermission(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListPermissions(ctx context.Context) (*contract.ListPermissionsResponse, error)
|
||||||
|
|
||||||
|
CreateRole(ctx context.Context, req *contract.CreateRoleRequest) (*contract.RoleWithPermissionsResponse, error)
|
||||||
|
UpdateRole(ctx context.Context, id uuid.UUID, req *contract.UpdateRoleRequest) (*contract.RoleWithPermissionsResponse, error)
|
||||||
|
DeleteRole(ctx context.Context, id uuid.UUID) error
|
||||||
|
ListRoles(ctx context.Context) (*contract.ListRolesResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type RBACHandler struct{ svc RBACService }
|
||||||
|
|
||||||
|
func NewRBACHandler(svc RBACService) *RBACHandler { return &RBACHandler{svc: svc} }
|
||||||
|
|
||||||
|
func (h *RBACHandler) CreatePermission(c *gin.Context) {
|
||||||
|
var req contract.CreatePermissionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: http.StatusBadRequest})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreatePermission(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) UpdatePermission(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdatePermissionRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdatePermission(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) DeletePermission(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeletePermission(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) ListPermissions(c *gin.Context) {
|
||||||
|
resp, err := h.svc.ListPermissions(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) CreateRole(c *gin.Context) {
|
||||||
|
var req contract.CreateRoleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.CreateRole(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusCreated, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) UpdateRole(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req contract.UpdateRoleRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid body", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.svc.UpdateRole(c.Request.Context(), id, &req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) DeleteRole(c *gin.Context) {
|
||||||
|
id, err := uuid.Parse(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid id", Code: 400})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.svc.DeleteRole(c.Request.Context(), id); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "deleted"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RBACHandler) ListRoles(c *gin.Context) {
|
||||||
|
resp, err := h.svc.ListRoles(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: 500})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||||
|
}
|
||||||
@ -1,8 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"eslogad-be/internal/appcontext"
|
"eslogad-be/internal/appcontext"
|
||||||
"eslogad-be/internal/constants"
|
"eslogad-be/internal/constants"
|
||||||
@ -51,6 +53,102 @@ 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) > 5000 {
|
||||||
|
h.sendValidationErrorResponse(c, "Cannot create more than 5000 users at once", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
if len(req.Users) > 500 {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, 10*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("UserHandler::BulkCreateUsers -> Starting bulk creation of %d users", len(req.Users))
|
||||||
|
|
||||||
|
response, err := h.userService.BulkCreateUsers(ctx, &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) BulkCreateUsersAsync(c *gin.Context) {
|
||||||
|
var req contract.BulkCreateUsersRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::BulkCreateUsersAsync -> 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) > 5000 {
|
||||||
|
h.sendValidationErrorResponse(c, "Cannot create more than 5000 users at once", constants.MissingFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("UserHandler::BulkCreateUsersAsync -> Starting async bulk creation of %d users", len(req.Users))
|
||||||
|
|
||||||
|
response, err := h.userService.BulkCreateUsersAsync(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::BulkCreateUsersAsync -> Failed to start async bulk creation")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("UserHandler::BulkCreateUsersAsync -> Job created with ID: %s", response.JobID)
|
||||||
|
c.JSON(http.StatusAccepted, contract.BuildSuccessResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *UserHandler) GetBulkJobStatus(c *gin.Context) {
|
||||||
|
jobIDStr := c.Param("jobId")
|
||||||
|
jobID, err := uuid.Parse(jobIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::GetBulkJobStatus -> invalid job ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid job ID", constants.ValidationErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := h.userService.GetBulkJobStatus(c.Request.Context(), jobID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("UserHandler::GetBulkJobStatus -> Failed to get job status")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(job))
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
@ -166,10 +264,24 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var roleParam *string
|
||||||
if role := c.Query("role"); role != "" {
|
if role := c.Query("role"); role != "" {
|
||||||
|
roleParam = &role
|
||||||
req.Role = &role
|
req.Role = &role
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if roleCode := c.Query("role_code"); roleCode != "" {
|
||||||
|
req.RoleCode = &roleCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.RoleCode == nil && roleParam != nil {
|
||||||
|
req.RoleCode = roleParam
|
||||||
|
}
|
||||||
|
|
||||||
|
if search := c.Query("search"); search != "" {
|
||||||
|
req.Search = &search
|
||||||
|
}
|
||||||
|
|
||||||
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
if isActiveStr := c.Query("is_active"); isActiveStr != "" {
|
||||||
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
if isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||||
req.IsActive = &isActive
|
req.IsActive = &isActive
|
||||||
@ -271,12 +383,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) {
|
||||||
|
|||||||
@ -3,12 +3,16 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/manager"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
BulkCreateUsersAsync(ctx context.Context, req *contract.BulkCreateUsersRequest) (*contract.BulkCreateAsyncResponse, error)
|
||||||
|
GetBulkJobStatus(ctx context.Context, jobID uuid.UUID) (*manager.BulkJobResult, 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 +24,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)
|
||||||
}
|
}
|
||||||
|
|||||||
344
internal/handler/vote_event_handler.go
Normal file
344
internal/handler/vote_event_handler.go
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
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)
|
||||||
|
GetVoteEventDetails(ctx context.Context, eventID uuid.UUID) (*contract.VoteEventDetailsResponse, 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) GetVoteEventDetails(c *gin.Context) {
|
||||||
|
eventIDStr := c.Param("id")
|
||||||
|
eventID, err := uuid.Parse(eventIDStr)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteEventDetails -> Invalid event ID")
|
||||||
|
h.sendValidationErrorResponse(c, "Invalid event ID", constants.MalformedFieldErrorCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
details, err := h.voteEventService.GetVoteEventDetails(c.Request.Context(), eventID)
|
||||||
|
if err != nil {
|
||||||
|
logger.FromContext(c).WithError(err).Error("VoteEventHandler::GetVoteEventDetails -> Failed to get vote event details")
|
||||||
|
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.FromContext(c).Infof("VoteEventHandler::GetVoteEventDetails -> Successfully retrieved vote event details")
|
||||||
|
c.JSON(http.StatusOK, contract.BuildSuccessResponse(details))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
117
internal/manager/job_manager.go
Normal file
117
internal/manager/job_manager.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JobStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
JobStatusPending JobStatus = "pending"
|
||||||
|
JobStatusProcessing JobStatus = "processing"
|
||||||
|
JobStatusCompleted JobStatus = "completed"
|
||||||
|
JobStatusFailed JobStatus = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BulkJobResult struct {
|
||||||
|
JobID uuid.UUID `json:"job_id"`
|
||||||
|
Status JobStatus `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
StartedAt time.Time `json:"started_at"`
|
||||||
|
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||||
|
Summary contract.BulkCreationSummary `json:"summary"`
|
||||||
|
Created []contract.UserResponse `json:"created"`
|
||||||
|
Failed []contract.BulkUserErrorResult `json:"failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type JobManager struct {
|
||||||
|
jobs sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobManagerInstance *JobManager
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
func GetJobManager() *JobManager {
|
||||||
|
once.Do(func() {
|
||||||
|
jobManagerInstance = &JobManager{}
|
||||||
|
})
|
||||||
|
return jobManagerInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm *JobManager) CreateJob() uuid.UUID {
|
||||||
|
jobID := uuid.New()
|
||||||
|
job := &BulkJobResult{
|
||||||
|
JobID: jobID,
|
||||||
|
Status: JobStatusPending,
|
||||||
|
Message: "Job created, waiting to start",
|
||||||
|
StartedAt: time.Now(),
|
||||||
|
Summary: contract.BulkCreationSummary{
|
||||||
|
Total: 0,
|
||||||
|
Succeeded: 0,
|
||||||
|
Failed: 0,
|
||||||
|
},
|
||||||
|
Created: []contract.UserResponse{},
|
||||||
|
Failed: []contract.BulkUserErrorResult{},
|
||||||
|
}
|
||||||
|
jm.jobs.Store(jobID, job)
|
||||||
|
return jobID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm *JobManager) UpdateJob(jobID uuid.UUID, status JobStatus, message string) {
|
||||||
|
if val, ok := jm.jobs.Load(jobID); ok {
|
||||||
|
job := val.(*BulkJobResult)
|
||||||
|
job.Status = status
|
||||||
|
job.Message = message
|
||||||
|
if status == JobStatusCompleted || status == JobStatusFailed {
|
||||||
|
now := time.Now()
|
||||||
|
job.FinishedAt = &now
|
||||||
|
}
|
||||||
|
jm.jobs.Store(jobID, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm *JobManager) UpdateJobResults(jobID uuid.UUID, created []contract.UserResponse, failed []contract.BulkUserErrorResult, summary contract.BulkCreationSummary) {
|
||||||
|
if val, ok := jm.jobs.Load(jobID); ok {
|
||||||
|
job := val.(*BulkJobResult)
|
||||||
|
job.Created = append(job.Created, created...)
|
||||||
|
job.Failed = append(job.Failed, failed...)
|
||||||
|
job.Summary.Total = summary.Total
|
||||||
|
job.Summary.Succeeded += summary.Succeeded
|
||||||
|
job.Summary.Failed += summary.Failed
|
||||||
|
jm.jobs.Store(jobID, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm *JobManager) GetJob(jobID uuid.UUID) (*BulkJobResult, bool) {
|
||||||
|
if val, ok := jm.jobs.Load(jobID); ok {
|
||||||
|
return val.(*BulkJobResult), true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jm *JobManager) CleanupOldJobs(ctx context.Context, maxAge time.Duration) {
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
cutoff := time.Now().Add(-maxAge)
|
||||||
|
jm.jobs.Range(func(key, value interface{}) bool {
|
||||||
|
job := value.(*BulkJobResult)
|
||||||
|
if job.FinishedAt != nil && job.FinishedAt.Before(cutoff) {
|
||||||
|
jm.jobs.Delete(key)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
37
internal/processor/activity_log_processor.go
Normal file
37
internal/processor/activity_log_processor.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityLogProcessorImpl struct {
|
||||||
|
repo *repository.LetterIncomingActivityLogRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActivityLogProcessor(repo *repository.LetterIncomingActivityLogRepository) *ActivityLogProcessorImpl {
|
||||||
|
return &ActivityLogProcessorImpl{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ActivityLogProcessorImpl) Log(ctx context.Context, letterID uuid.UUID, actionType string, actorUserID *uuid.UUID, actorDepartmentID *uuid.UUID, targetType *string, targetID *uuid.UUID, fromStatus *string, toStatus *string, contextData map[string]interface{}) error {
|
||||||
|
ctxJSON := entities.JSONB{}
|
||||||
|
for k, v := range contextData {
|
||||||
|
ctxJSON[k] = v
|
||||||
|
}
|
||||||
|
entry := &entities.LetterIncomingActivityLog{
|
||||||
|
LetterID: letterID,
|
||||||
|
ActionType: actionType,
|
||||||
|
ActorUserID: actorUserID,
|
||||||
|
ActorDepartmentID: actorDepartmentID,
|
||||||
|
TargetType: targetType,
|
||||||
|
TargetID: targetID,
|
||||||
|
FromStatus: fromStatus,
|
||||||
|
ToStatus: toStatus,
|
||||||
|
Context: ctxJSON,
|
||||||
|
}
|
||||||
|
return p.repo.Create(ctx, entry)
|
||||||
|
}
|
||||||
492
internal/processor/letter_processor.go
Normal file
492
internal/processor/letter_processor.go
Normal file
@ -0,0 +1,492 @@
|
|||||||
|
package processor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/appcontext"
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterProcessorImpl struct {
|
||||||
|
letterRepo *repository.LetterIncomingRepository
|
||||||
|
attachRepo *repository.LetterIncomingAttachmentRepository
|
||||||
|
txManager *repository.TxManager
|
||||||
|
activity *ActivityLogProcessorImpl
|
||||||
|
dispositionRepo *repository.LetterIncomingDispositionRepository
|
||||||
|
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
|
||||||
|
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
|
||||||
|
dispositionNoteRepo *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
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository, priorityRepo *repository.PriorityRepository, institutionRepo *repository.InstitutionRepository, dispActionRepo *repository.DispositionActionRepository) *LetterProcessorImpl {
|
||||||
|
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo, institutionRepo: institutionRepo, dispActionRepo: dispActionRepo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
|
var result *contract.IncomingLetterResponse
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
|
||||||
|
prefix := "ESLI"
|
||||||
|
seq := 0
|
||||||
|
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterPrefix); err == nil {
|
||||||
|
if v, ok := s.Value["value"].(string); ok && v != "" {
|
||||||
|
prefix = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterSequence); err == nil {
|
||||||
|
if v, ok := s.Value["value"].(float64); ok {
|
||||||
|
seq = int(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seq = seq + 1
|
||||||
|
letterNumber := fmt.Sprintf("%s%04d", prefix, seq)
|
||||||
|
|
||||||
|
entity := &entities.LetterIncoming{
|
||||||
|
ReferenceNumber: req.ReferenceNumber,
|
||||||
|
Subject: req.Subject,
|
||||||
|
Description: req.Description,
|
||||||
|
PriorityID: req.PriorityID,
|
||||||
|
SenderInstitutionID: req.SenderInstitutionID,
|
||||||
|
ReceivedDate: req.ReceivedDate,
|
||||||
|
DueDate: req.DueDate,
|
||||||
|
Status: entities.LetterIncomingStatusNew,
|
||||||
|
CreatedBy: userID,
|
||||||
|
}
|
||||||
|
entity.LetterNumber = letterNumber
|
||||||
|
if err := p.letterRepo.Create(txCtx, entity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = p.settingRepo.Upsert(txCtx, contract.SettingIncomingLetterSequence, entities.JSONB{"value": seq})
|
||||||
|
|
||||||
|
defaultDeptCodes := []string{}
|
||||||
|
if s, err := p.settingRepo.Get(txCtx, contract.SettingIncomingLetterRecipients); err == nil {
|
||||||
|
if arr, ok := s.Value["department_codes"].([]interface{}); ok {
|
||||||
|
for _, it := range arr {
|
||||||
|
if str, ok := it.(string); ok {
|
||||||
|
defaultDeptCodes = append(defaultDeptCodes, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
|
||||||
|
for _, code := range defaultDeptCodes {
|
||||||
|
dep, err := p.departmentRepo.GetByCode(txCtx, code)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
depIDs = append(depIDs, dep.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
|
||||||
|
var recipients []entities.LetterIncomingRecipient
|
||||||
|
|
||||||
|
mapsUsers := map[string]bool{}
|
||||||
|
for _, row := range userMemberships {
|
||||||
|
uid := row.UserID
|
||||||
|
if _, ok := mapsUsers[uid.String()]; !ok {
|
||||||
|
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, RecipientDepartmentID: &row.DepartmentID, Status: entities.RecipientStatusNew})
|
||||||
|
}
|
||||||
|
mapsUsers[uid.String()] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recipients) > 0 {
|
||||||
|
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.activity != nil {
|
||||||
|
action := "letter.created"
|
||||||
|
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{"letter_number": letterNumber}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments := make([]entities.LetterIncomingAttachment, 0, len(req.Attachments))
|
||||||
|
for _, a := range req.Attachments {
|
||||||
|
attachments = append(attachments, entities.LetterIncomingAttachment{LetterID: entity.ID, FileURL: a.FileURL, FileName: a.FileName, FileType: a.FileType, UploadedBy: &userID})
|
||||||
|
}
|
||||||
|
if len(attachments) > 0 {
|
||||||
|
if err := p.attachRepo.CreateBulk(txCtx, attachments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.activity != nil {
|
||||||
|
action := "attachment.uploaded"
|
||||||
|
for _, a := range attachments {
|
||||||
|
ctxMap := map[string]interface{}{"file_name": a.FileName, "file_type": a.FileType}
|
||||||
|
if err := p.activity.Log(txCtx, entity.ID, action, &userID, nil, nil, nil, nil, nil, ctxMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID)
|
||||||
|
var pr *entities.Priority
|
||||||
|
if entity.PriorityID != nil {
|
||||||
|
if p.priorityRepo != nil {
|
||||||
|
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
|
||||||
|
pr = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var inst *entities.Institution
|
||||||
|
if entity.SenderInstitutionID != nil {
|
||||||
|
if p.institutionRepo != nil {
|
||||||
|
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
|
||||||
|
inst = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = transformer.LetterEntityToContract(entity, savedAttachments, pr, inst)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
|
||||||
|
entity, err := p.letterRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
atts, _ := p.attachRepo.ListByLetter(ctx, id)
|
||||||
|
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) {
|
||||||
|
page, limit := req.Page, req.Limit
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
filter := repository.ListIncomingLettersFilter{Status: req.Status, Query: req.Query}
|
||||||
|
list, total, err := p.letterRepo.List(ctx, filter, limit, (page-1)*limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
respList := make([]contract.IncomingLetterResponse, 0, len(list))
|
||||||
|
for _, e := range list {
|
||||||
|
atts, _ := p.attachRepo.ListByLetter(ctx, e.ID)
|
||||||
|
var pr *entities.Priority
|
||||||
|
if e.PriorityID != nil && p.priorityRepo != nil {
|
||||||
|
if got, err := p.priorityRepo.Get(ctx, *e.PriorityID); err == nil {
|
||||||
|
pr = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var inst *entities.Institution
|
||||||
|
if e.SenderInstitutionID != nil && p.institutionRepo != nil {
|
||||||
|
if got, err := p.institutionRepo.Get(ctx, *e.SenderInstitutionID); err == nil {
|
||||||
|
inst = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp := transformer.LetterEntityToContract(&e, atts, pr, inst)
|
||||||
|
respList = append(respList, *resp)
|
||||||
|
}
|
||||||
|
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
|
var out *contract.IncomingLetterResponse
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
entity, err := p.letterRepo.Get(txCtx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fromStatus := string(entity.Status)
|
||||||
|
if req.ReferenceNumber != nil {
|
||||||
|
entity.ReferenceNumber = req.ReferenceNumber
|
||||||
|
}
|
||||||
|
if req.Subject != nil {
|
||||||
|
entity.Subject = *req.Subject
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
entity.Description = req.Description
|
||||||
|
}
|
||||||
|
if req.PriorityID != nil {
|
||||||
|
entity.PriorityID = req.PriorityID
|
||||||
|
}
|
||||||
|
if req.SenderInstitutionID != nil {
|
||||||
|
entity.SenderInstitutionID = req.SenderInstitutionID
|
||||||
|
}
|
||||||
|
if req.ReceivedDate != nil {
|
||||||
|
entity.ReceivedDate = *req.ReceivedDate
|
||||||
|
}
|
||||||
|
if req.DueDate != nil {
|
||||||
|
entity.DueDate = req.DueDate
|
||||||
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
entity.Status = entities.LetterIncomingStatus(*req.Status)
|
||||||
|
}
|
||||||
|
if err := p.letterRepo.Update(txCtx, entity); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toStatus := string(entity.Status)
|
||||||
|
if p.activity != nil && fromStatus != toStatus {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
action := "status.changed"
|
||||||
|
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, &fromStatus, &toStatus, map[string]interface{}{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
atts, _ := p.attachRepo.ListByLetter(txCtx, id)
|
||||||
|
var pr *entities.Priority
|
||||||
|
if entity.PriorityID != nil && p.priorityRepo != nil {
|
||||||
|
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
|
||||||
|
pr = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var inst *entities.Institution
|
||||||
|
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
|
||||||
|
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
|
||||||
|
inst = got
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = transformer.LetterEntityToContract(entity, atts, pr, inst)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
if err := p.letterRepo.SoftDelete(txCtx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.activity != nil {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
action := "letter.deleted"
|
||||||
|
if err := p.activity.Log(txCtx, id, action, &userID, nil, nil, nil, nil, nil, map[string]interface{}{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
||||||
|
var out *contract.ListDispositionsResponse
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
|
||||||
|
disp := entities.LetterIncomingDisposition{
|
||||||
|
LetterID: req.LetterID,
|
||||||
|
DepartmentID: &req.FromDepartment,
|
||||||
|
Notes: req.Notes,
|
||||||
|
CreatedBy: userID,
|
||||||
|
}
|
||||||
|
if err := p.dispositionRepo.Create(txCtx, &disp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dispDepartments []entities.LetterIncomingDispositionDepartment
|
||||||
|
for _, toDept := range req.ToDepartmentIDs {
|
||||||
|
dispDepartments = append(dispDepartments, entities.LetterIncomingDispositionDepartment{
|
||||||
|
LetterIncomingDispositionID: disp.ID,
|
||||||
|
DepartmentID: toDept,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.dispositionDeptRepo.CreateBulk(txCtx, dispDepartments); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.SelectedActions) > 0 {
|
||||||
|
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
|
||||||
|
for _, sel := range req.SelectedActions {
|
||||||
|
selections = append(selections, entities.LetterDispositionActionSelection{
|
||||||
|
DispositionID: disp.ID,
|
||||||
|
ActionID: sel.ActionID,
|
||||||
|
Note: sel.Note,
|
||||||
|
CreatedBy: userID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.activity != nil {
|
||||||
|
action := "disposition.created"
|
||||||
|
ctxMap := map[string]interface{}{"to_department_id": dispDepartments}
|
||||||
|
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &disp.ID, nil, nil, ctxMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = &contract.ListDispositionsResponse{Dispositions: []contract.DispositionResponse{transformer.DispoToContract(disp)}}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
|
||||||
|
list, err := p.dispositionRepo.ListByLetter(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||||
|
// Get dispositions with all related data preloaded in a single query
|
||||||
|
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get discussions with preloaded user profiles
|
||||||
|
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all mentioned user IDs from discussions for efficient batch fetching
|
||||||
|
var mentionedUserIDs []uuid.UUID
|
||||||
|
mentionedUserIDsMap := make(map[uuid.UUID]bool)
|
||||||
|
|
||||||
|
for _, discussion := range discussions {
|
||||||
|
if discussion.Mentions != nil {
|
||||||
|
mentions := map[string]interface{}(discussion.Mentions)
|
||||||
|
if userIDs, ok := mentions["user_ids"]; ok {
|
||||||
|
if userIDList, ok := userIDs.([]interface{}); ok {
|
||||||
|
for _, userID := range userIDList {
|
||||||
|
if userIDStr, ok := userID.(string); ok {
|
||||||
|
if userUUID, err := uuid.Parse(userIDStr); err == nil {
|
||||||
|
if !mentionedUserIDsMap[userUUID] {
|
||||||
|
mentionedUserIDsMap[userUUID] = true
|
||||||
|
mentionedUserIDs = append(mentionedUserIDs, userUUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all mentioned users in a single batch query
|
||||||
|
var mentionedUsers []entities.User
|
||||||
|
if len(mentionedUserIDs) > 0 {
|
||||||
|
mentionedUsers, err = p.discussionRepo.GetUsersByIDs(ctx, mentionedUserIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform dispositions
|
||||||
|
enhancedDispositions := transformer.EnhancedDispositionsWithPreloadedDataToContract(dispositions)
|
||||||
|
|
||||||
|
// Transform discussions with mentioned users
|
||||||
|
enhancedDiscussions := transformer.DiscussionsWithPreloadedDataToContract(discussions, mentionedUsers)
|
||||||
|
|
||||||
|
return &contract.ListEnhancedDispositionsResponse{
|
||||||
|
Dispositions: enhancedDispositions,
|
||||||
|
Discussions: enhancedDiscussions,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
|
var out *contract.LetterDiscussionResponse
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
mentions := entities.JSONB(nil)
|
||||||
|
if req.Mentions != nil {
|
||||||
|
mentions = entities.JSONB(req.Mentions)
|
||||||
|
}
|
||||||
|
disc := &entities.LetterDiscussion{ID: uuid.New(), LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions}
|
||||||
|
if err := p.discussionRepo.Create(txCtx, disc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.activity != nil {
|
||||||
|
action := "reference_numberdiscussion.created"
|
||||||
|
tgt := "discussion"
|
||||||
|
ctxMap := map[string]interface{}{"message": req.Message, "parent_id": req.ParentID}
|
||||||
|
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = transformer.DiscussionEntityToContract(disc)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *LetterProcessorImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
|
var out *contract.LetterDiscussionResponse
|
||||||
|
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
||||||
|
disc, err := p.discussionRepo.Get(txCtx, discussionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oldMessage := disc.Message
|
||||||
|
disc.Message = req.Message
|
||||||
|
if req.Mentions != nil {
|
||||||
|
disc.Mentions = entities.JSONB(req.Mentions)
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
disc.EditedAt = &now
|
||||||
|
if err := p.discussionRepo.Update(txCtx, disc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.activity != nil {
|
||||||
|
userID := appcontext.FromGinContext(txCtx).UserID
|
||||||
|
action := "discussion.updated"
|
||||||
|
tgt := "discussion"
|
||||||
|
ctxMap := map[string]interface{}{"old_message": oldMessage, "new_message": req.Message}
|
||||||
|
if err := p.activity.Log(txCtx, letterID, action, &userID, nil, &tgt, &disc.ID, nil, nil, ctxMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = transformer.DiscussionEntityToContract(disc)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@ -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
|
||||||
@ -110,8 +110,15 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("user not found: %w", err)
|
return nil, fmt.Errorf("user not found: %w", err)
|
||||||
}
|
}
|
||||||
|
resp := transformer.EntityToContract(user)
|
||||||
return transformer.EntityToContract(user), nil
|
if resp != nil {
|
||||||
|
// Roles are loaded separately since they're not preloaded
|
||||||
|
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
|
||||||
|
resp.Roles = transformer.RolesToContract(roles)
|
||||||
|
}
|
||||||
|
// Departments are now preloaded, so they're already in the response
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
|
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
|
||||||
@ -120,20 +127,41 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) ListUsers(ctx context.Context, page, limit int) ([]contract.UserResponse, int, error) {
|
func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error) {
|
||||||
|
page := req.Page
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
limit := req.Limit
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
offset := (page - 1) * limit
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
filters := map[string]interface{}{}
|
users, totalCount, err := p.userRepo.ListWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
|
||||||
|
|
||||||
users, totalCount, err := p.userRepo.List(ctx, filters, limit, offset)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
responses := transformer.EntitiesToContracts(users)
|
responses := transformer.EntitiesToContracts(users)
|
||||||
|
userIDs := make([]uuid.UUID, 0, len(responses))
|
||||||
|
for i := range responses {
|
||||||
|
userIDs = append(userIDs, responses[i].ID)
|
||||||
|
}
|
||||||
|
// Roles are loaded separately since they're not preloaded
|
||||||
|
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
|
||||||
|
if err == nil {
|
||||||
|
for i := range responses {
|
||||||
|
if roles, ok := rolesMap[responses[i].ID]; ok {
|
||||||
|
responses[i].Roles = transformer.RolesToContract(roles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Departments are now preloaded, so they're already in the responses
|
||||||
return responses, int(totalCount), nil
|
return responses, int(totalCount), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -219,12 +247,12 @@ func (p *UserProcessorImpl) GetUserPermissionCodes(ctx context.Context, userID u
|
|||||||
return codes, nil
|
return codes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error) {
|
func (p *UserProcessorImpl) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error) {
|
||||||
positions, err := p.userRepo.GetPositionsByUserID(ctx, userID)
|
departments, err := p.userRepo.GetDepartmentsByUserID(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return transformer.PositionsToContract(positions), nil
|
return transformer.DepartmentsToContract(departments), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
||||||
@ -249,3 +277,126 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreateUsersWithTransaction creates multiple users in a transaction with proper error handling
|
||||||
|
func (p *UserProcessorImpl) BulkCreateUsersWithTransaction(ctx context.Context, userRequests []contract.BulkUserRequest) ([]contract.UserResponse, []contract.BulkUserErrorResult, error) {
|
||||||
|
created := []contract.UserResponse{}
|
||||||
|
failed := []contract.BulkUserErrorResult{}
|
||||||
|
|
||||||
|
usersToCreate := []*entities.User{}
|
||||||
|
emailMap := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, req := range userRequests {
|
||||||
|
if emailMap[req.Email] {
|
||||||
|
failed = append(failed, contract.BulkUserErrorResult{
|
||||||
|
User: req,
|
||||||
|
Error: "Duplicate email in batch",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
emailMap[req.Email] = true
|
||||||
|
|
||||||
|
existing, _ := p.userRepo.GetByEmail(ctx, req.Email)
|
||||||
|
if existing != nil {
|
||||||
|
failed = append(failed, contract.BulkUserErrorResult{
|
||||||
|
User: req,
|
||||||
|
Error: "Email already exists",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
failed = append(failed, contract.BulkUserErrorResult{
|
||||||
|
User: req,
|
||||||
|
Error: "Failed to hash password",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &entities.User{
|
||||||
|
ID: uuid.New(),
|
||||||
|
Username: req.Email,
|
||||||
|
Name: req.Name,
|
||||||
|
Email: req.Email,
|
||||||
|
PasswordHash: string(hashedPassword),
|
||||||
|
IsActive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
usersToCreate = append(usersToCreate, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(usersToCreate) > 0 {
|
||||||
|
// Use CreateInBatches for large datasets
|
||||||
|
err := p.userRepo.CreateInBatches(ctx, usersToCreate, 50)
|
||||||
|
if err != nil {
|
||||||
|
// If bulk creation fails, try individual creation
|
||||||
|
for i, user := range usersToCreate {
|
||||||
|
err := p.userRepo.Create(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
failed = append(failed, contract.BulkUserErrorResult{
|
||||||
|
User: userRequests[i],
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Create default profile for the user
|
||||||
|
profile := &entities.UserProfile{
|
||||||
|
UserID: user.ID,
|
||||||
|
FullName: user.Name,
|
||||||
|
}
|
||||||
|
_ = p.profileRepo.Create(ctx, profile)
|
||||||
|
|
||||||
|
created = append(created, *transformer.EntityToContract(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create profiles for all successfully created users
|
||||||
|
for _, user := range usersToCreate {
|
||||||
|
profile := &entities.UserProfile{
|
||||||
|
UserID: user.ID,
|
||||||
|
FullName: user.Name,
|
||||||
|
}
|
||||||
|
_ = p.profileRepo.Create(ctx, profile)
|
||||||
|
|
||||||
|
created = append(created, *transformer.EntityToContract(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return created, failed, 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
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package processor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"eslogad-be/internal/entities"
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,5 +22,13 @@ type UserRepository interface {
|
|||||||
|
|
||||||
GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error)
|
GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error)
|
||||||
GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error)
|
GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error)
|
||||||
GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error)
|
GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error)
|
||||||
|
|
||||||
|
// New optimized helpers
|
||||||
|
GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error)
|
||||||
|
ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error)
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
BulkCreate(ctx context.Context, users []*entities.User) error
|
||||||
|
CreateInBatches(ctx context.Context, users []*entities.User, batchSize int) error
|
||||||
}
|
}
|
||||||
|
|||||||
52
internal/repository/disposition_route_repository.go
Normal file
52
internal/repository/disposition_route_repository.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DispositionRouteRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewDispositionRouteRepository(db *gorm.DB) *DispositionRouteRepository {
|
||||||
|
return &DispositionRouteRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DispositionRouteRepository) Create(ctx context.Context, e *entities.DispositionRoute) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.DispositionRoute) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.DispositionRoute
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Preload("FromDepartment").
|
||||||
|
Preload("ToDepartment").
|
||||||
|
First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.DispositionRoute
|
||||||
|
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).
|
||||||
|
Preload("FromDepartment").
|
||||||
|
Preload("ToDepartment").
|
||||||
|
Order("to_department_id").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
|
||||||
|
}
|
||||||
307
internal/repository/letter_repository.go
Normal file
307
internal/repository/letter_repository.go
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterIncomingRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterIncomingRepository(db *gorm.DB) *LetterIncomingRepository {
|
||||||
|
return &LetterIncomingRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) Create(ctx context.Context, e *entities.LetterIncoming) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterIncoming
|
||||||
|
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) Update(ctx context.Context, e *entities.LetterIncoming) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("id = ? AND deleted_at IS NULL", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) SoftDelete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Exec("UPDATE letters_incoming SET deleted_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL", id).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListIncomingLettersFilter struct {
|
||||||
|
Status *string
|
||||||
|
Query *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncomingLettersFilter, limit, offset int) ([]entities.LetterIncoming, int64, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL")
|
||||||
|
if filter.Status != nil {
|
||||||
|
query = query.Where("status = ?", *filter.Status)
|
||||||
|
}
|
||||||
|
if filter.Query != nil {
|
||||||
|
q := "%" + *filter.Query + "%"
|
||||||
|
query = query.Where("subject ILIKE ? OR reference_number ILIKE ?", q, q)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
var list []entities.LetterIncoming
|
||||||
|
if err := query.Order("created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return list, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterIncomingAttachmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterIncomingAttachmentRepository(db *gorm.DB) *LetterIncomingAttachmentRepository {
|
||||||
|
return &LetterIncomingAttachmentRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingAttachmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingAttachment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingAttachmentRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingAttachment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingAttachment
|
||||||
|
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("uploaded_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterIncomingActivityLogRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterIncomingActivityLogRepository(db *gorm.DB) *LetterIncomingActivityLogRepository {
|
||||||
|
return &LetterIncomingActivityLogRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingActivityLogRepository) Create(ctx context.Context, e *entities.LetterIncomingActivityLog) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingActivityLogRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingActivityLog, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingActivityLog
|
||||||
|
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("occurred_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterIncomingDispositionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterIncomingDispositionRepository(db *gorm.DB) *LetterIncomingDispositionRepository {
|
||||||
|
return &LetterIncomingDispositionRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingDispositionRepository) Create(ctx context.Context, e *entities.LetterIncomingDisposition) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingDisposition
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_id = ?", letterID).
|
||||||
|
Preload("Department").
|
||||||
|
Preload("Departments.Department").
|
||||||
|
Preload("ActionSelections.Action").
|
||||||
|
Preload("DispositionNotes.User").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterIncomingDispositionDepartmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterIncomingDispositionDepartmentRepository(db *gorm.DB) *LetterIncomingDispositionDepartmentRepository {
|
||||||
|
return &LetterIncomingDispositionDepartmentRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingDispositionDepartment) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingDispositionDepartment
|
||||||
|
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterIncomingDispositionDepartmentRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterIncomingDispositionDepartment
|
||||||
|
if len(dispositionIDs) == 0 {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id IN ?", dispositionIDs).Order("letter_incoming_disposition_id, created_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionNoteRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewDispositionNoteRepository(db *gorm.DB) *DispositionNoteRepository {
|
||||||
|
return &DispositionNoteRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.DispositionNote) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DispositionNoteRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.DispositionNote, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.DispositionNote
|
||||||
|
if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DispositionNoteRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.DispositionNote, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.DispositionNote
|
||||||
|
if len(dispositionIDs) == 0 {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterDispositionActionSelectionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
|
||||||
|
return &LetterDispositionActionSelectionRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *LetterDispositionActionSelectionRepository) CreateBulk(ctx context.Context, list []entities.LetterDispositionActionSelection) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(&list).Error
|
||||||
|
}
|
||||||
|
func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterDispositionActionSelection
|
||||||
|
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 *LetterDispositionActionSelectionRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterDispositionActionSelection
|
||||||
|
if len(dispositionIDs) == 0 {
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterDiscussionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository {
|
||||||
|
return &LetterDiscussionRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *LetterDiscussionRepository) Create(ctx context.Context, e *entities.LetterDiscussion) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *LetterDiscussionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.LetterDiscussion, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.LetterDiscussion
|
||||||
|
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.LetterDiscussion) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
// ensure edited_at is set when updating
|
||||||
|
if e.EditedAt == nil {
|
||||||
|
now := time.Now()
|
||||||
|
e.EditedAt = &now
|
||||||
|
}
|
||||||
|
return db.WithContext(ctx).Model(&entities.LetterDiscussion{}).
|
||||||
|
Where("id = ?", e.ID).
|
||||||
|
Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterDiscussionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDiscussion, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var list []entities.LetterDiscussion
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("letter_id = ?", letterID).
|
||||||
|
Preload("User.Profile").
|
||||||
|
Order("created_at ASC").
|
||||||
|
Find(&list).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LetterDiscussionRepository) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return []entities.User{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var users []entities.User
|
||||||
|
if err := db.WithContext(ctx).
|
||||||
|
Where("id IN ?", userIDs).
|
||||||
|
Preload("Profile").
|
||||||
|
Find(&users).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppSettingRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository { return &AppSettingRepository{db: db} }
|
||||||
|
func (r *AppSettingRepository) Get(ctx context.Context, key string) (*entities.AppSetting, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var e entities.AppSetting
|
||||||
|
if err := db.WithContext(ctx).First(&e, "key = ?", key).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
func (r *AppSettingRepository) Upsert(ctx context.Context, key string, value entities.JSONB) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Exec("INSERT INTO app_settings(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP", key, value).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// recipients
|
||||||
|
|
||||||
|
type LetterIncomingRecipientRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLetterIncomingRecipientRepository(db *gorm.DB) *LetterIncomingRecipientRepository {
|
||||||
|
return &LetterIncomingRecipientRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *LetterIncomingRecipientRepository) CreateBulk(ctx context.Context, recs []entities.LetterIncomingRecipient) error {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
return db.WithContext(ctx).Create(&recs).Error
|
||||||
|
}
|
||||||
147
internal/repository/master_repository.go
Normal file
147
internal/repository/master_repository.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LabelRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewLabelRepository(db *gorm.DB) *LabelRepository { return &LabelRepository{db: db} }
|
||||||
|
func (r *LabelRepository) Create(ctx context.Context, e *entities.Label) error {
|
||||||
|
return r.db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *LabelRepository) Update(ctx context.Context, e *entities.Label) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.Label{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
func (r *LabelRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Label{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
func (r *LabelRepository) List(ctx context.Context) ([]entities.Label, error) {
|
||||||
|
var list []entities.Label
|
||||||
|
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
func (r *LabelRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Label, error) {
|
||||||
|
var e entities.Label
|
||||||
|
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PriorityRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewPriorityRepository(db *gorm.DB) *PriorityRepository { return &PriorityRepository{db: db} }
|
||||||
|
func (r *PriorityRepository) Create(ctx context.Context, e *entities.Priority) error {
|
||||||
|
return r.db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *PriorityRepository) Update(ctx context.Context, e *entities.Priority) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.Priority{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
func (r *PriorityRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Priority{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
func (r *PriorityRepository) List(ctx context.Context) ([]entities.Priority, error) {
|
||||||
|
var list []entities.Priority
|
||||||
|
err := r.db.WithContext(ctx).Order("level ASC").Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
func (r *PriorityRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Priority, error) {
|
||||||
|
var e entities.Priority
|
||||||
|
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type InstitutionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewInstitutionRepository(db *gorm.DB) *InstitutionRepository {
|
||||||
|
return &InstitutionRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *InstitutionRepository) Create(ctx context.Context, e *entities.Institution) error {
|
||||||
|
return r.db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *InstitutionRepository) Update(ctx context.Context, e *entities.Institution) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.Institution{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
func (r *InstitutionRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Institution{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
func (r *InstitutionRepository) List(ctx context.Context) ([]entities.Institution, error) {
|
||||||
|
var list []entities.Institution
|
||||||
|
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) {
|
||||||
|
var e entities.Institution
|
||||||
|
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionActionRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewDispositionActionRepository(db *gorm.DB) *DispositionActionRepository {
|
||||||
|
return &DispositionActionRepository{db: db}
|
||||||
|
}
|
||||||
|
func (r *DispositionActionRepository) Create(ctx context.Context, e *entities.DispositionAction) error {
|
||||||
|
return r.db.WithContext(ctx).Create(e).Error
|
||||||
|
}
|
||||||
|
func (r *DispositionActionRepository) Update(ctx context.Context, e *entities.DispositionAction) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.DispositionAction{}).Where("id = ?", e.ID).Updates(e).Error
|
||||||
|
}
|
||||||
|
func (r *DispositionActionRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.DispositionAction{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
func (r *DispositionActionRepository) List(ctx context.Context) ([]entities.DispositionAction, error) {
|
||||||
|
var list []entities.DispositionAction
|
||||||
|
err := r.db.WithContext(ctx).Order("sort_order NULLS LAST, label ASC").Find(&list).Error
|
||||||
|
return list, err
|
||||||
|
}
|
||||||
|
func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionAction, error) {
|
||||||
|
var e entities.DispositionAction
|
||||||
|
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DispositionActionRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]entities.DispositionAction, error) {
|
||||||
|
var actions []entities.DispositionAction
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&actions).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DepartmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewDepartmentRepository(db *gorm.DB) *DepartmentRepository { return &DepartmentRepository{db: db} }
|
||||||
|
|
||||||
|
func (r *DepartmentRepository) GetByCode(ctx context.Context, code string) (*entities.Department, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
var dep entities.Department
|
||||||
|
if err := db.WithContext(ctx).Where("code = ?", code).First(&dep).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
97
internal/repository/rbac_repository.go
Normal file
97
internal/repository/rbac_repository.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RBACRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRBACRepository(db *gorm.DB) *RBACRepository { return &RBACRepository{db: db} }
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
func (r *RBACRepository) CreatePermission(ctx context.Context, p *entities.Permission) error {
|
||||||
|
return r.db.WithContext(ctx).Create(p).Error
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) UpdatePermission(ctx context.Context, p *entities.Permission) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.Permission{}).Where("id = ?", p.ID).Updates(p).Error
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) DeletePermission(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Permission{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) ListPermissions(ctx context.Context) ([]entities.Permission, error) {
|
||||||
|
var perms []entities.Permission
|
||||||
|
if err := r.db.WithContext(ctx).Order("code ASC").Find(&perms).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return perms, nil
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) GetPermissionByCode(ctx context.Context, code string) (*entities.Permission, error) {
|
||||||
|
var p entities.Permission
|
||||||
|
if err := r.db.WithContext(ctx).First(&p, "code = ?", code).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roles
|
||||||
|
func (r *RBACRepository) CreateRole(ctx context.Context, role *entities.Role) error {
|
||||||
|
return r.db.WithContext(ctx).Create(role).Error
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) UpdateRole(ctx context.Context, role *entities.Role) error {
|
||||||
|
return r.db.WithContext(ctx).Model(&entities.Role{}).Where("id = ?", role.ID).Updates(role).Error
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) DeleteRole(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return r.db.WithContext(ctx).Delete(&entities.Role{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) ListRoles(ctx context.Context) ([]entities.Role, error) {
|
||||||
|
var roles []entities.Role
|
||||||
|
if err := r.db.WithContext(ctx).Order("name ASC").Find(&roles).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return roles, nil
|
||||||
|
}
|
||||||
|
func (r *RBACRepository) GetRoleByCode(ctx context.Context, code string) (*entities.Role, error) {
|
||||||
|
var role entities.Role
|
||||||
|
if err := r.db.WithContext(ctx).First(&role, "code = ?", code).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &role, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RBACRepository) SetRolePermissionsByCodes(ctx context.Context, roleID uuid.UUID, permCodes []string) error {
|
||||||
|
if err := r.db.WithContext(ctx).Where("role_id = ?", roleID).Delete(&entities.RolePermission{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(permCodes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var perms []entities.Permission
|
||||||
|
if err := r.db.WithContext(ctx).Where("code IN ?", permCodes).Find(&perms).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pairs := make([]entities.RolePermission, 0, len(perms))
|
||||||
|
for _, p := range perms {
|
||||||
|
pairs = append(pairs, entities.RolePermission{RoleID: roleID, PermissionID: p.ID})
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Create(&pairs).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RBACRepository) GetPermissionsByRoleID(ctx context.Context, roleID uuid.UUID) ([]entities.Permission, error) {
|
||||||
|
var perms []entities.Permission
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Table("permissions p").
|
||||||
|
Select("p.*").
|
||||||
|
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
|
||||||
|
Where("rp.role_id = ?", roleID).
|
||||||
|
Find(&perms).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return perms, nil
|
||||||
|
}
|
||||||
35
internal/repository/tx_manager.go
Normal file
35
internal/repository/tx_manager.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type txKeyType struct{}
|
||||||
|
|
||||||
|
var txKey = txKeyType{}
|
||||||
|
|
||||||
|
// DBFromContext returns the transactional *gorm.DB from context if present; otherwise returns base.
|
||||||
|
func DBFromContext(ctx context.Context, base *gorm.DB) *gorm.DB {
|
||||||
|
if v := ctx.Value(txKey); v != nil {
|
||||||
|
if tx, ok := v.(*gorm.DB); ok && tx != nil {
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
type TxManager struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTxManager(db *gorm.DB) *TxManager { return &TxManager{db: db} }
|
||||||
|
|
||||||
|
// WithTransaction runs fn inside a DB transaction, injecting the *gorm.DB tx into ctx.
|
||||||
|
func (m *TxManager) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||||
|
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
ctxTx := context.WithValue(ctx, txKey, tx)
|
||||||
|
return fn(ctxTx)
|
||||||
|
})
|
||||||
|
}
|
||||||
34
internal/repository/user_department_repository.go
Normal file
34
internal/repository/user_department_repository.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserDepartmentRepository struct{ db *gorm.DB }
|
||||||
|
|
||||||
|
func NewUserDepartmentRepository(db *gorm.DB) *UserDepartmentRepository {
|
||||||
|
return &UserDepartmentRepository{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type userDepartmentRow struct {
|
||||||
|
UserID uuid.UUID `gorm:"column:user_id"`
|
||||||
|
DepartmentID uuid.UUID `gorm:"column:department_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListActiveByDepartmentIDs returns active user-department memberships for given department IDs.
|
||||||
|
func (r *UserDepartmentRepository) ListActiveByDepartmentIDs(ctx context.Context, departmentIDs []uuid.UUID) ([]userDepartmentRow, error) {
|
||||||
|
db := DBFromContext(ctx, r.db)
|
||||||
|
rows := make([]userDepartmentRow, 0)
|
||||||
|
if len(departmentIDs) == 0 {
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
err := db.WithContext(ctx).
|
||||||
|
Table("user_department").
|
||||||
|
Select("user_id, department_id").
|
||||||
|
Where("department_id IN ? AND removed_at IS NULL", departmentIDs).
|
||||||
|
Find(&rows).Error
|
||||||
|
return rows, err
|
||||||
|
}
|
||||||
@ -10,22 +10,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type UserRepositoryImpl struct {
|
type UserRepositoryImpl struct {
|
||||||
db *gorm.DB
|
b *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserRepository(db *gorm.DB) *UserRepositoryImpl {
|
func NewUserRepository(db *gorm.DB) *UserRepositoryImpl {
|
||||||
return &UserRepositoryImpl{
|
return &UserRepositoryImpl{
|
||||||
db: db,
|
b: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) error {
|
func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) error {
|
||||||
return r.db.WithContext(ctx).Create(user).Error
|
return r.b.WithContext(ctx).Create(user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
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.db.WithContext(ctx).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.db.WithContext(ctx).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,34 +49,36 @@ 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.db.WithContext(ctx).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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
|
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
|
||||||
var users []*entities.User
|
var users []*entities.User
|
||||||
err := r.db.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
Where(" is_active = ?", organizationID, true).
|
Where(" is_active = ?", organizationID, true).
|
||||||
|
Preload("Profile").
|
||||||
|
Preload("Departments").
|
||||||
Find(&users).Error
|
Find(&users).Error
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error {
|
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error {
|
||||||
return r.db.WithContext(ctx).Save(user).Error
|
return r.b.WithContext(ctx).Save(user).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
return r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error
|
return r.b.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
|
func (r *UserRepositoryImpl) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
|
||||||
return r.db.WithContext(ctx).Model(&entities.User{}).
|
return r.b.WithContext(ctx).Model(&entities.User{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Update("password_hash", passwordHash).Error
|
Update("password_hash", passwordHash).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
|
func (r *UserRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||||
return r.db.WithContext(ctx).Model(&entities.User{}).
|
return r.b.WithContext(ctx).Model(&entities.User{}).
|
||||||
Where("id = ?", id).
|
Where("id = ?", id).
|
||||||
Update("is_active", isActive).Error
|
Update("is_active", isActive).Error
|
||||||
}
|
}
|
||||||
@ -79,7 +87,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
|
|||||||
var users []*entities.User
|
var users []*entities.User
|
||||||
var total int64
|
var total int64
|
||||||
|
|
||||||
query := r.db.WithContext(ctx).Model(&entities.User{})
|
query := r.b.WithContext(ctx).Model(&entities.User{})
|
||||||
|
|
||||||
for key, value := range filters {
|
for key, value := range filters {
|
||||||
query = query.Where(key+" = ?", value)
|
query = query.Where(key+" = ?", value)
|
||||||
@ -89,13 +97,13 @@ 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).Find(&users).Error
|
err := query.Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error
|
||||||
return users, total, err
|
return users, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
query := r.db.WithContext(ctx).Model(&entities.User{})
|
query := r.b.WithContext(ctx).Model(&entities.User{})
|
||||||
|
|
||||||
for key, value := range filters {
|
for key, value := range filters {
|
||||||
query = query.Where(key+" = ?", value)
|
query = query.Where(key+" = ?", value)
|
||||||
@ -108,7 +116,7 @@ func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]inter
|
|||||||
// RBAC helpers
|
// RBAC helpers
|
||||||
func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
|
func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
|
||||||
var roles []entities.Role
|
var roles []entities.Role
|
||||||
err := r.db.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
Table("roles as r").
|
Table("roles as r").
|
||||||
Select("r.*").
|
Select("r.*").
|
||||||
Joins("JOIN user_role ur ON ur.role_id = r.id AND ur.removed_at IS NULL").
|
Joins("JOIN user_role ur ON ur.role_id = r.id AND ur.removed_at IS NULL").
|
||||||
@ -119,7 +127,7 @@ func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.U
|
|||||||
|
|
||||||
func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
|
func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
|
||||||
var perms []entities.Permission
|
var perms []entities.Permission
|
||||||
err := r.db.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
Table("permissions as p").
|
Table("permissions as p").
|
||||||
Select("DISTINCT p.*").
|
Select("DISTINCT p.*").
|
||||||
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
|
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
|
||||||
@ -129,13 +137,99 @@ func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID
|
|||||||
return perms, err
|
return perms, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *UserRepositoryImpl) GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error) {
|
func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error) {
|
||||||
var positions []entities.Position
|
var departments []entities.Department
|
||||||
err := r.db.WithContext(ctx).
|
err := r.b.WithContext(ctx).
|
||||||
Table("positions as p").
|
Table("departments as d").
|
||||||
Select("p.*").
|
Select("d.*").
|
||||||
Joins("JOIN user_position up ON up.position_id = p.id AND up.removed_at IS NULL").
|
Joins("JOIN user_department ud ON ud.department_id = d.id AND ud.removed_at IS NULL").
|
||||||
Where("up.user_id = ?", userID).
|
Where("ud.user_id = ?", userID).
|
||||||
Find(&positions).Error
|
Find(&departments).Error
|
||||||
return positions, err
|
return departments, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
|
||||||
|
result := make(map[uuid.UUID][]entities.Role)
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type row struct {
|
||||||
|
UserID uuid.UUID
|
||||||
|
RoleID uuid.UUID
|
||||||
|
Name string
|
||||||
|
Code string
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows []row
|
||||||
|
err := r.b.WithContext(ctx).
|
||||||
|
Table("user_role as ur").
|
||||||
|
Select("ur.user_id, r.id as role_id, r.name, r.code").
|
||||||
|
Joins("JOIN roles r ON r.id = ur.role_id").
|
||||||
|
Where("ur.removed_at IS NULL AND ur.user_id IN ?", userIDs).
|
||||||
|
Scan(&rows).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, rw := range rows {
|
||||||
|
role := entities.Role{ID: rw.RoleID, Name: rw.Name, Code: rw.Code}
|
||||||
|
result[rw.UserID] = append(result[rw.UserID], role)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
|
||||||
|
var users []*entities.User
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
q := r.b.WithContext(ctx).Model(&entities.User{})
|
||||||
|
if search != nil && *search != "" {
|
||||||
|
like := "%" + *search + "%"
|
||||||
|
q = q.Where("name ILIKE ? OR email ILIKE ?", like, like)
|
||||||
|
}
|
||||||
|
if isActive != nil {
|
||||||
|
q = q.Where("is_active = ?", *isActive)
|
||||||
|
}
|
||||||
|
if roleCode != nil && *roleCode != "" {
|
||||||
|
q = q.Joins("JOIN user_role ur ON ur.user_id = users.id AND ur.removed_at IS NULL").
|
||||||
|
Joins("JOIN roles r ON r.id = ur.role_id").
|
||||||
|
Where("r.code = ?", *roleCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return users, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BulkCreate creates multiple users in a single database transaction
|
||||||
|
func (r *UserRepositoryImpl) BulkCreate(ctx context.Context, users []*entities.User) error {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.b.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Create all users in a single batch
|
||||||
|
if err := tx.Create(&users).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInBatches creates users in smaller batches to avoid memory issues
|
||||||
|
func (r *UserRepositoryImpl) CreateInBatches(ctx context.Context, users []*entities.User, batchSize int) error {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if batchSize <= 0 {
|
||||||
|
batchSize = 100 // Default batch size
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.b.WithContext(ctx).CreateInBatches(users, batchSize).Error
|
||||||
}
|
}
|
||||||
|
|||||||
140
internal/repository/vote_event_repository.go
Normal file
140
internal/repository/vote_event_repository.go
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) GetVotedCount(ctx context.Context, eventID uuid.UUID) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.Vote{}).
|
||||||
|
Where("vote_event_id = ?", eventID).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *VoteEventRepositoryImpl) GetTotalActiveUsersCount(ctx context.Context) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Model(&entities.User{}).
|
||||||
|
Where("is_active = ?", true).
|
||||||
|
Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
@ -12,9 +12,86 @@ 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)
|
||||||
|
BulkCreateUsersAsync(c *gin.Context)
|
||||||
|
GetBulkJobStatus(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileHandler interface {
|
type FileHandler interface {
|
||||||
UploadProfileAvatar(c *gin.Context)
|
UploadProfileAvatar(c *gin.Context)
|
||||||
UploadDocument(c *gin.Context)
|
UploadDocument(c *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RBACHandler interface {
|
||||||
|
CreatePermission(c *gin.Context)
|
||||||
|
UpdatePermission(c *gin.Context)
|
||||||
|
DeletePermission(c *gin.Context)
|
||||||
|
ListPermissions(c *gin.Context)
|
||||||
|
|
||||||
|
CreateRole(c *gin.Context)
|
||||||
|
UpdateRole(c *gin.Context)
|
||||||
|
DeleteRole(c *gin.Context)
|
||||||
|
ListRoles(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MasterHandler interface {
|
||||||
|
// labels
|
||||||
|
CreateLabel(c *gin.Context)
|
||||||
|
UpdateLabel(c *gin.Context)
|
||||||
|
DeleteLabel(c *gin.Context)
|
||||||
|
ListLabels(c *gin.Context)
|
||||||
|
// priorities
|
||||||
|
CreatePriority(c *gin.Context)
|
||||||
|
UpdatePriority(c *gin.Context)
|
||||||
|
DeletePriority(c *gin.Context)
|
||||||
|
ListPriorities(c *gin.Context)
|
||||||
|
// institutions
|
||||||
|
CreateInstitution(c *gin.Context)
|
||||||
|
UpdateInstitution(c *gin.Context)
|
||||||
|
DeleteInstitution(c *gin.Context)
|
||||||
|
ListInstitutions(c *gin.Context)
|
||||||
|
// disposition actions
|
||||||
|
CreateDispositionAction(c *gin.Context)
|
||||||
|
UpdateDispositionAction(c *gin.Context)
|
||||||
|
DeleteDispositionAction(c *gin.Context)
|
||||||
|
ListDispositionActions(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterHandler interface {
|
||||||
|
CreateIncomingLetter(c *gin.Context)
|
||||||
|
GetIncomingLetter(c *gin.Context)
|
||||||
|
ListIncomingLetters(c *gin.Context)
|
||||||
|
UpdateIncomingLetter(c *gin.Context)
|
||||||
|
DeleteIncomingLetter(c *gin.Context)
|
||||||
|
|
||||||
|
CreateDispositions(c *gin.Context)
|
||||||
|
//ListDispositionsByLetter(c *gin.Context)
|
||||||
|
GetEnhancedDispositionsByLetter(c *gin.Context)
|
||||||
|
|
||||||
|
CreateDiscussion(c *gin.Context)
|
||||||
|
UpdateDiscussion(c *gin.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DispositionRouteHandler interface {
|
||||||
|
Create(c *gin.Context)
|
||||||
|
Update(c *gin.Context)
|
||||||
|
Get(c *gin.Context)
|
||||||
|
ListByFromDept(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)
|
||||||
|
GetVoteEventDetails(c *gin.Context)
|
||||||
|
}
|
||||||
|
|||||||
@ -14,6 +14,11 @@ type Router struct {
|
|||||||
authMiddleware AuthMiddleware
|
authMiddleware AuthMiddleware
|
||||||
userHandler UserHandler
|
userHandler UserHandler
|
||||||
fileHandler FileHandler
|
fileHandler FileHandler
|
||||||
|
rbacHandler RBACHandler
|
||||||
|
masterHandler MasterHandler
|
||||||
|
letterHandler LetterHandler
|
||||||
|
dispRouteHandler DispositionRouteHandler
|
||||||
|
voteEventHandler VoteEventHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(
|
func NewRouter(
|
||||||
@ -23,6 +28,11 @@ func NewRouter(
|
|||||||
healthHandler HealthHandler,
|
healthHandler HealthHandler,
|
||||||
userHandler UserHandler,
|
userHandler UserHandler,
|
||||||
fileHandler FileHandler,
|
fileHandler FileHandler,
|
||||||
|
rbacHandler RBACHandler,
|
||||||
|
masterHandler MasterHandler,
|
||||||
|
letterHandler LetterHandler,
|
||||||
|
dispRouteHandler DispositionRouteHandler,
|
||||||
|
voteEventHandler VoteEventHandler,
|
||||||
) *Router {
|
) *Router {
|
||||||
return &Router{
|
return &Router{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@ -31,6 +41,11 @@ func NewRouter(
|
|||||||
healthHandler: healthHandler,
|
healthHandler: healthHandler,
|
||||||
userHandler: userHandler,
|
userHandler: userHandler,
|
||||||
fileHandler: fileHandler,
|
fileHandler: fileHandler,
|
||||||
|
rbacHandler: rbacHandler,
|
||||||
|
masterHandler: masterHandler,
|
||||||
|
letterHandler: letterHandler,
|
||||||
|
dispRouteHandler: dispRouteHandler,
|
||||||
|
voteEventHandler: voteEventHandler,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +53,7 @@ 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(),
|
||||||
@ -64,11 +80,15 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
users := v1.Group("/users")
|
users := v1.Group("/users")
|
||||||
users.Use(r.authMiddleware.RequireAuth())
|
users.Use(r.authMiddleware.RequireAuth())
|
||||||
{
|
{
|
||||||
users.GET("", r.authMiddleware.RequirePermissions("user.view"), r.userHandler.ListUsers)
|
users.GET("", r.userHandler.ListUsers)
|
||||||
|
users.POST("/bulk", r.userHandler.BulkCreateUsersAsync)
|
||||||
|
users.POST("/bulk/async", r.userHandler.BulkCreateUsersAsync)
|
||||||
|
users.GET("/bulk/job/:jobId", r.userHandler.GetBulkJobStatus)
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,5 +97,96 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
|||||||
{
|
{
|
||||||
files.POST("/documents", r.fileHandler.UploadDocument)
|
files.POST("/documents", r.fileHandler.UploadDocument)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rbac := v1.Group("/rbac")
|
||||||
|
rbac.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
rbac.GET("/permissions", r.rbacHandler.ListPermissions)
|
||||||
|
rbac.POST("/permissions", r.rbacHandler.CreatePermission)
|
||||||
|
rbac.PUT("/permissions/:id", r.rbacHandler.UpdatePermission)
|
||||||
|
rbac.DELETE("/permissions/:id", r.rbacHandler.DeletePermission)
|
||||||
|
|
||||||
|
rbac.GET("/roles", r.rbacHandler.ListRoles)
|
||||||
|
rbac.POST("/roles", r.rbacHandler.CreateRole)
|
||||||
|
rbac.PUT("/roles/:id", r.rbacHandler.UpdateRole)
|
||||||
|
rbac.DELETE("/roles/:id", r.rbacHandler.DeleteRole)
|
||||||
|
}
|
||||||
|
|
||||||
|
master := v1.Group("/master")
|
||||||
|
master.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
master.GET("/labels", r.masterHandler.ListLabels)
|
||||||
|
master.POST("/labels", r.masterHandler.CreateLabel)
|
||||||
|
master.PUT("/labels/:id", r.masterHandler.UpdateLabel)
|
||||||
|
master.DELETE("/labels/:id", r.masterHandler.DeleteLabel)
|
||||||
|
|
||||||
|
master.GET("/priorities", r.masterHandler.ListPriorities)
|
||||||
|
master.POST("/priorities", r.masterHandler.CreatePriority)
|
||||||
|
master.PUT("/priorities/:id", r.masterHandler.UpdatePriority)
|
||||||
|
master.DELETE("/priorities/:id", r.masterHandler.DeletePriority)
|
||||||
|
|
||||||
|
master.GET("/institutions", r.masterHandler.ListInstitutions)
|
||||||
|
master.POST("/institutions", r.masterHandler.CreateInstitution)
|
||||||
|
master.PUT("/institutions/:id", r.masterHandler.UpdateInstitution)
|
||||||
|
master.DELETE("/institutions/:id", r.masterHandler.DeleteInstitution)
|
||||||
|
|
||||||
|
master.GET("/disposition-actions", r.masterHandler.ListDispositionActions)
|
||||||
|
master.POST("/disposition-actions", r.masterHandler.CreateDispositionAction)
|
||||||
|
master.PUT("/disposition-actions/:id", r.masterHandler.UpdateDispositionAction)
|
||||||
|
master.DELETE("/disposition-actions/:id", r.masterHandler.DeleteDispositionAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
lettersch := v1.Group("/letters")
|
||||||
|
lettersch.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
lettersch.POST("/incoming", r.letterHandler.CreateIncomingLetter)
|
||||||
|
lettersch.GET("/incoming/:id", r.letterHandler.GetIncomingLetter)
|
||||||
|
lettersch.GET("/incoming", r.letterHandler.ListIncomingLetters)
|
||||||
|
lettersch.PUT("/incoming/:id", r.letterHandler.UpdateIncomingLetter)
|
||||||
|
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
|
||||||
|
|
||||||
|
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
|
||||||
|
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
|
||||||
|
|
||||||
|
lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
|
||||||
|
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
|
||||||
|
}
|
||||||
|
|
||||||
|
droutes := v1.Group("/disposition-routes")
|
||||||
|
droutes.Use(r.authMiddleware.RequireAuth())
|
||||||
|
{
|
||||||
|
droutes.POST("", r.dispRouteHandler.Create)
|
||||||
|
droutes.GET(":id", r.dispRouteHandler.Get)
|
||||||
|
droutes.PUT(":id", r.dispRouteHandler.Update)
|
||||||
|
droutes.GET("department", r.dispRouteHandler.ListByFromDept)
|
||||||
|
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)
|
||||||
|
voteEvents.GET("/:id/details", r.voteEventHandler.GetVoteEventDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,10 +56,9 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
|||||||
return nil, fmt.Errorf("invalid credentials")
|
return nil, fmt.Errorf("invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch roles, permissions, positions for response and token
|
|
||||||
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)
|
||||||
positions, _ := s.userProcessor.GetUserPositions(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 {
|
||||||
@ -72,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,
|
||||||
Positions: positions,
|
Departments: userResponse.DepartmentResponse,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
positions, _ := s.userProcessor.GetUserPositions(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,
|
||||||
Positions: positions,
|
Departments: userResponse.DepartmentResponse,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
70
internal/service/disposition_route_service.go
Normal file
70
internal/service/disposition_route_service.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DispositionRouteServiceImpl struct {
|
||||||
|
repo *repository.DispositionRouteRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDispositionRouteService(repo *repository.DispositionRouteRepository) *DispositionRouteServiceImpl {
|
||||||
|
return &DispositionRouteServiceImpl{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DispositionRouteServiceImpl) Create(ctx context.Context, req *contract.CreateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||||
|
entity := &entities.DispositionRoute{FromDepartmentID: req.FromDepartmentID, ToDepartmentID: req.ToDepartmentID}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
entity.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
if req.AllowedActions != nil {
|
||||||
|
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||||
|
}
|
||||||
|
if err := s.repo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *DispositionRouteServiceImpl) Update(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionRouteRequest) (*contract.DispositionRouteResponse, error) {
|
||||||
|
entity, err := s.repo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
entity.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
if req.AllowedActions != nil {
|
||||||
|
entity.AllowedActions = entities.JSONB(*req.AllowedActions)
|
||||||
|
}
|
||||||
|
if err := s.repo.Update(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *DispositionRouteServiceImpl) Get(ctx context.Context, id uuid.UUID) (*contract.DispositionRouteResponse, error) {
|
||||||
|
entity, err := s.repo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.DispositionRoutesToContract([]entities.DispositionRoute{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *DispositionRouteServiceImpl) ListByFromDept(ctx context.Context, from uuid.UUID) (*contract.ListDispositionRoutesResponse, error) {
|
||||||
|
list, err := s.repo.ListByFromDept(ctx, from)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListDispositionRoutesResponse{Routes: transformer.DispositionRoutesToContract(list)}, nil
|
||||||
|
}
|
||||||
|
func (s *DispositionRouteServiceImpl) SetActive(ctx context.Context, id uuid.UUID, active bool) error {
|
||||||
|
return s.repo.SetActive(ctx, id, active)
|
||||||
|
}
|
||||||
63
internal/service/letter_service.go
Normal file
63
internal/service/letter_service.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LetterProcessor interface {
|
||||||
|
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
|
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||||
|
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
||||||
|
UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||||
|
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
|
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
|
||||||
|
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
|
||||||
|
|
||||||
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
|
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LetterServiceImpl struct {
|
||||||
|
processor LetterProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLetterService(processor LetterProcessor) *LetterServiceImpl {
|
||||||
|
return &LetterServiceImpl{processor: processor}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
|
return s.processor.CreateIncomingLetter(ctx, req)
|
||||||
|
}
|
||||||
|
func (s *LetterServiceImpl) GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error) {
|
||||||
|
return s.processor.GetIncomingLetterByID(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
|
||||||
|
return s.processor.ListIncomingLetters(ctx, req)
|
||||||
|
}
|
||||||
|
func (s *LetterServiceImpl) UpdateIncomingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
|
||||||
|
return s.processor.UpdateIncomingLetter(ctx, id, req)
|
||||||
|
}
|
||||||
|
func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.processor.SoftDeleteIncomingLetter(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
||||||
|
return s.processor.CreateDispositions(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
|
||||||
|
return s.processor.GetEnhancedDispositionsByLetter(ctx, letterID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
|
return s.processor.CreateDiscussion(ctx, letterID, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
|
||||||
|
return s.processor.UpdateDiscussion(ctx, letterID, discussionID, req)
|
||||||
|
}
|
||||||
214
internal/service/master_service.go
Normal file
214
internal/service/master_service.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterServiceImpl struct {
|
||||||
|
labelRepo *repository.LabelRepository
|
||||||
|
priorityRepo *repository.PriorityRepository
|
||||||
|
institutionRepo *repository.InstitutionRepository
|
||||||
|
dispRepo *repository.DispositionActionRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMasterService(label *repository.LabelRepository, priority *repository.PriorityRepository, institution *repository.InstitutionRepository, disp *repository.DispositionActionRepository) *MasterServiceImpl {
|
||||||
|
return &MasterServiceImpl{labelRepo: label, priorityRepo: priority, institutionRepo: institution, dispRepo: disp}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels
|
||||||
|
func (s *MasterServiceImpl) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) {
|
||||||
|
entity := &entities.Label{Name: req.Name, Color: req.Color}
|
||||||
|
if err := s.labelRepo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.LabelsToContract([]entities.Label{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) {
|
||||||
|
entity := &entities.Label{ID: id}
|
||||||
|
if req.Name != nil {
|
||||||
|
entity.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.Color != nil {
|
||||||
|
entity.Color = req.Color
|
||||||
|
}
|
||||||
|
if err := s.labelRepo.Update(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, err := s.labelRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.LabelsToContract([]entities.Label{*e})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) DeleteLabel(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.labelRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) {
|
||||||
|
list, err := s.labelRepo.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListLabelsResponse{Labels: transformer.LabelsToContract(list)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorities
|
||||||
|
func (s *MasterServiceImpl) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) {
|
||||||
|
entity := &entities.Priority{Name: req.Name, Level: req.Level}
|
||||||
|
if err := s.priorityRepo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.PrioritiesToContract([]entities.Priority{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) {
|
||||||
|
entity := &entities.Priority{ID: id}
|
||||||
|
if req.Name != nil {
|
||||||
|
entity.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.Level != nil {
|
||||||
|
entity.Level = *req.Level
|
||||||
|
}
|
||||||
|
if err := s.priorityRepo.Update(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, err := s.priorityRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.PrioritiesToContract([]entities.Priority{*e})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) DeletePriority(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.priorityRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) {
|
||||||
|
list, err := s.priorityRepo.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListPrioritiesResponse{Priorities: transformer.PrioritiesToContract(list)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Institutions
|
||||||
|
func (s *MasterServiceImpl) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
||||||
|
entity := &entities.Institution{Name: req.Name, Type: entities.InstitutionType(req.Type), Address: req.Address, ContactPerson: req.ContactPerson, Phone: req.Phone, Email: req.Email}
|
||||||
|
if err := s.institutionRepo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.InstitutionsToContract([]entities.Institution{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) {
|
||||||
|
entity := &entities.Institution{ID: id}
|
||||||
|
if req.Name != nil {
|
||||||
|
entity.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.Type != nil {
|
||||||
|
entity.Type = entities.InstitutionType(*req.Type)
|
||||||
|
}
|
||||||
|
if req.Address != nil {
|
||||||
|
entity.Address = req.Address
|
||||||
|
}
|
||||||
|
if req.ContactPerson != nil {
|
||||||
|
entity.ContactPerson = req.ContactPerson
|
||||||
|
}
|
||||||
|
if req.Phone != nil {
|
||||||
|
entity.Phone = req.Phone
|
||||||
|
}
|
||||||
|
if req.Email != nil {
|
||||||
|
entity.Email = req.Email
|
||||||
|
}
|
||||||
|
if err := s.institutionRepo.Update(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, err := s.institutionRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.InstitutionsToContract([]entities.Institution{*e})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.institutionRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error) {
|
||||||
|
list, err := s.institutionRepo.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListInstitutionsResponse{Institutions: transformer.InstitutionsToContract(list)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disposition Actions
|
||||||
|
func (s *MasterServiceImpl) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
||||||
|
entity := &entities.DispositionAction{Code: req.Code, Label: req.Label, Description: req.Description}
|
||||||
|
if req.RequiresNote != nil {
|
||||||
|
entity.RequiresNote = *req.RequiresNote
|
||||||
|
}
|
||||||
|
if req.GroupName != nil {
|
||||||
|
entity.GroupName = req.GroupName
|
||||||
|
}
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
entity.SortOrder = req.SortOrder
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
entity.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
if err := s.dispRepo.Create(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*entity})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
|
||||||
|
entity := &entities.DispositionAction{ID: id}
|
||||||
|
if req.Code != nil {
|
||||||
|
entity.Code = *req.Code
|
||||||
|
}
|
||||||
|
if req.Label != nil {
|
||||||
|
entity.Label = *req.Label
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
entity.Description = req.Description
|
||||||
|
}
|
||||||
|
if req.RequiresNote != nil {
|
||||||
|
entity.RequiresNote = *req.RequiresNote
|
||||||
|
}
|
||||||
|
if req.GroupName != nil {
|
||||||
|
entity.GroupName = req.GroupName
|
||||||
|
}
|
||||||
|
if req.SortOrder != nil {
|
||||||
|
entity.SortOrder = req.SortOrder
|
||||||
|
}
|
||||||
|
if req.IsActive != nil {
|
||||||
|
entity.IsActive = *req.IsActive
|
||||||
|
}
|
||||||
|
if err := s.dispRepo.Update(ctx, entity); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e, err := s.dispRepo.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resp := transformer.DispositionActionsToContract([]entities.DispositionAction{*e})[0]
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.dispRepo.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *MasterServiceImpl) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) {
|
||||||
|
list, err := s.dispRepo.List(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListDispositionActionsResponse{Actions: transformer.DispositionActionsToContract(list)}, nil
|
||||||
|
}
|
||||||
128
internal/service/rbac_service.go
Normal file
128
internal/service/rbac_service.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/repository"
|
||||||
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RBACServiceImpl struct {
|
||||||
|
repo *repository.RBACRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRBACService(repo *repository.RBACRepository) *RBACServiceImpl {
|
||||||
|
return &RBACServiceImpl{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Permissions
|
||||||
|
func (s *RBACServiceImpl) CreatePermission(ctx context.Context, req *contract.CreatePermissionRequest) (*contract.PermissionResponse, error) {
|
||||||
|
p := &entities.Permission{Code: req.Code}
|
||||||
|
if req.Description != nil {
|
||||||
|
p.Description = *req.Description
|
||||||
|
}
|
||||||
|
if err := s.repo.CreatePermission(ctx, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.PermissionResponse{ID: p.ID, Code: p.Code, Description: &p.Description, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt}, nil
|
||||||
|
}
|
||||||
|
func (s *RBACServiceImpl) UpdatePermission(ctx context.Context, id uuid.UUID, req *contract.UpdatePermissionRequest) (*contract.PermissionResponse, error) {
|
||||||
|
p := &entities.Permission{ID: id}
|
||||||
|
if req.Code != nil {
|
||||||
|
p.Code = *req.Code
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
p.Description = *req.Description
|
||||||
|
}
|
||||||
|
if err := s.repo.UpdatePermission(ctx, p); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// fetch full row
|
||||||
|
perms, err := s.repo.ListPermissions(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, x := range perms {
|
||||||
|
if x.ID == id {
|
||||||
|
return &contract.PermissionResponse{ID: x.ID, Code: x.Code, Description: &x.Description, CreatedAt: x.CreatedAt, UpdatedAt: x.UpdatedAt}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (s *RBACServiceImpl) DeletePermission(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.repo.DeletePermission(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *RBACServiceImpl) ListPermissions(ctx context.Context) (*contract.ListPermissionsResponse, error) {
|
||||||
|
perms, err := s.repo.ListPermissions(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &contract.ListPermissionsResponse{Permissions: transformer.PermissionsToContract(perms)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roles
|
||||||
|
func (s *RBACServiceImpl) CreateRole(ctx context.Context, req *contract.CreateRoleRequest) (*contract.RoleWithPermissionsResponse, error) {
|
||||||
|
role := &entities.Role{Name: req.Name, Code: req.Code}
|
||||||
|
if req.Description != nil {
|
||||||
|
role.Description = *req.Description
|
||||||
|
}
|
||||||
|
if err := s.repo.CreateRole(ctx, role); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(req.PermissionCodes) > 0 {
|
||||||
|
_ = s.repo.SetRolePermissionsByCodes(ctx, role.ID, req.PermissionCodes)
|
||||||
|
}
|
||||||
|
perms, _ := s.repo.GetPermissionsByRoleID(ctx, role.ID)
|
||||||
|
resp := transformer.RoleWithPermissionsToContract(*role, perms)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
func (s *RBACServiceImpl) UpdateRole(ctx context.Context, id uuid.UUID, req *contract.UpdateRoleRequest) (*contract.RoleWithPermissionsResponse, error) {
|
||||||
|
role := &entities.Role{ID: id}
|
||||||
|
if req.Name != nil {
|
||||||
|
role.Name = *req.Name
|
||||||
|
}
|
||||||
|
if req.Code != nil {
|
||||||
|
role.Code = *req.Code
|
||||||
|
}
|
||||||
|
if req.Description != nil {
|
||||||
|
role.Description = *req.Description
|
||||||
|
}
|
||||||
|
if err := s.repo.UpdateRole(ctx, role); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if req.PermissionCodes != nil {
|
||||||
|
_ = s.repo.SetRolePermissionsByCodes(ctx, id, *req.PermissionCodes)
|
||||||
|
}
|
||||||
|
perms, _ := s.repo.GetPermissionsByRoleID(ctx, id)
|
||||||
|
// fetch updated role
|
||||||
|
roles, err := s.repo.ListRoles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, r := range roles {
|
||||||
|
if r.ID == id {
|
||||||
|
resp := transformer.RoleWithPermissionsToContract(r, perms)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (s *RBACServiceImpl) DeleteRole(ctx context.Context, id uuid.UUID) error {
|
||||||
|
return s.repo.DeleteRole(ctx, id)
|
||||||
|
}
|
||||||
|
func (s *RBACServiceImpl) ListRoles(ctx context.Context) (*contract.ListRolesResponse, error) {
|
||||||
|
roles, err := s.repo.ListRoles(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]contract.RoleWithPermissionsResponse, 0, len(roles))
|
||||||
|
for _, r := range roles {
|
||||||
|
perms, _ := s.repo.GetPermissionsByRoleID(ctx, r.ID)
|
||||||
|
out = append(out, transformer.RoleWithPermissionsToContract(r, perms))
|
||||||
|
}
|
||||||
|
return &contract.ListRolesResponse{Roles: out}, nil
|
||||||
|
}
|
||||||
@ -14,14 +14,22 @@ type UserProcessor interface {
|
|||||||
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)
|
||||||
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error)
|
||||||
ListUsers(ctx context.Context, page, limit int) ([]contract.UserResponse, int, error)
|
|
||||||
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
|
GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error)
|
||||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
||||||
|
|
||||||
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error)
|
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error)
|
||||||
GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error)
|
GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error)
|
||||||
GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error)
|
GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error)
|
||||||
|
|
||||||
GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error)
|
GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error)
|
||||||
UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
UpdateUserProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
||||||
|
|
||||||
|
// New optimized listing
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Bulk create users with transaction
|
||||||
|
BulkCreateUsersWithTransaction(ctx context.Context, users []contract.BulkUserRequest) ([]contract.UserResponse, []contract.BulkUserErrorResult, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,12 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"eslogad-be/internal/contract"
|
"eslogad-be/internal/contract"
|
||||||
"eslogad-be/internal/entities"
|
"eslogad-be/internal/entities"
|
||||||
|
"eslogad-be/internal/manager"
|
||||||
"eslogad-be/internal/transformer"
|
"eslogad-be/internal/transformer"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -30,6 +33,186 @@ 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
batchSize := 50
|
||||||
|
for i := 0; i < len(req.Users); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(req.Users) {
|
||||||
|
end = len(req.Users)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := req.Users[i:end]
|
||||||
|
batchResults, err := s.processBulkUserBatch(ctx, batch)
|
||||||
|
if err != nil {
|
||||||
|
for _, userReq := range batch {
|
||||||
|
response.Failed = append(response.Failed, contract.BulkUserErrorResult{
|
||||||
|
User: userReq,
|
||||||
|
Error: "Batch processing error: " + err.Error(),
|
||||||
|
})
|
||||||
|
response.Summary.Failed++
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Created = append(response.Created, batchResults.Created...)
|
||||||
|
response.Failed = append(response.Failed, batchResults.Failed...)
|
||||||
|
response.Summary.Succeeded += batchResults.Summary.Succeeded
|
||||||
|
response.Summary.Failed += batchResults.Summary.Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserServiceImpl) BulkCreateUsersAsync(ctx context.Context, req *contract.BulkCreateUsersRequest) (*contract.BulkCreateAsyncResponse, error) {
|
||||||
|
jobManager := manager.GetJobManager()
|
||||||
|
jobID := jobManager.CreateJob()
|
||||||
|
|
||||||
|
// Start async processing
|
||||||
|
go s.processBulkUsersAsync(context.Background(), jobID, req)
|
||||||
|
|
||||||
|
return &contract.BulkCreateAsyncResponse{
|
||||||
|
JobID: jobID,
|
||||||
|
Message: fmt.Sprintf("Job started for %d users", len(req.Users)),
|
||||||
|
Status: "processing",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserServiceImpl) processBulkUsersAsync(ctx context.Context, jobID uuid.UUID, req *contract.BulkCreateUsersRequest) {
|
||||||
|
jobManager := manager.GetJobManager()
|
||||||
|
jobManager.UpdateJob(jobID, manager.JobStatusProcessing, fmt.Sprintf("Processing %d users", len(req.Users)))
|
||||||
|
|
||||||
|
batchSize := 50
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
resultChan := make(chan *contract.BulkCreateUsersResponse, (len(req.Users)/batchSize)+1)
|
||||||
|
|
||||||
|
// Process each batch independently in its own goroutine
|
||||||
|
for i := 0; i < len(req.Users); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(req.Users) {
|
||||||
|
end = len(req.Users)
|
||||||
|
}
|
||||||
|
|
||||||
|
batch := req.Users[i:end]
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
// Launch goroutine for each batch
|
||||||
|
go func(batchNum int, users []contract.BulkUserRequest) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
batchResult := &contract.BulkCreateUsersResponse{
|
||||||
|
Created: []contract.UserResponse{},
|
||||||
|
Failed: []contract.BulkUserErrorResult{},
|
||||||
|
Summary: contract.BulkCreationSummary{
|
||||||
|
Total: len(users),
|
||||||
|
Succeeded: 0,
|
||||||
|
Failed: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process batch
|
||||||
|
created, failed, err := s.userProcessor.BulkCreateUsersWithTransaction(ctx, users)
|
||||||
|
if err != nil {
|
||||||
|
// If entire batch fails, mark all users as failed
|
||||||
|
for _, userReq := range users {
|
||||||
|
batchResult.Failed = append(batchResult.Failed, contract.BulkUserErrorResult{
|
||||||
|
User: userReq,
|
||||||
|
Error: fmt.Sprintf("Batch %d error: %v", batchNum, err),
|
||||||
|
})
|
||||||
|
batchResult.Summary.Failed++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
batchResult.Created = created
|
||||||
|
batchResult.Failed = failed
|
||||||
|
batchResult.Summary.Succeeded = len(created)
|
||||||
|
batchResult.Summary.Failed = len(failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultChan <- batchResult
|
||||||
|
}(i/batchSize, batch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all batches to complete
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(resultChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Aggregate results
|
||||||
|
totalSummary := contract.BulkCreationSummary{
|
||||||
|
Total: len(req.Users),
|
||||||
|
Succeeded: 0,
|
||||||
|
Failed: 0,
|
||||||
|
}
|
||||||
|
allCreated := []contract.UserResponse{}
|
||||||
|
allFailed := []contract.BulkUserErrorResult{}
|
||||||
|
|
||||||
|
for result := range resultChan {
|
||||||
|
allCreated = append(allCreated, result.Created...)
|
||||||
|
allFailed = append(allFailed, result.Failed...)
|
||||||
|
totalSummary.Succeeded += result.Summary.Succeeded
|
||||||
|
totalSummary.Failed += result.Summary.Failed
|
||||||
|
|
||||||
|
// Update job progress
|
||||||
|
jobManager.UpdateJobResults(jobID, result.Created, result.Failed, result.Summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark job as completed
|
||||||
|
status := manager.JobStatusCompleted
|
||||||
|
message := fmt.Sprintf("Completed: %d succeeded, %d failed out of %d total",
|
||||||
|
totalSummary.Succeeded, totalSummary.Failed, totalSummary.Total)
|
||||||
|
|
||||||
|
if totalSummary.Failed == totalSummary.Total {
|
||||||
|
status = manager.JobStatusFailed
|
||||||
|
message = "All user creations failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
jobManager.UpdateJob(jobID, status, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserServiceImpl) GetBulkJobStatus(ctx context.Context, jobID uuid.UUID) (*manager.BulkJobResult, error) {
|
||||||
|
jobManager := manager.GetJobManager()
|
||||||
|
job, exists := jobManager.GetJob(jobID)
|
||||||
|
if !exists {
|
||||||
|
return nil, fmt.Errorf("job not found: %s", jobID)
|
||||||
|
}
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserServiceImpl) processBulkUserBatch(ctx context.Context, batch []contract.BulkUserRequest) (*contract.BulkCreateUsersResponse, error) {
|
||||||
|
response := &contract.BulkCreateUsersResponse{
|
||||||
|
Created: []contract.UserResponse{},
|
||||||
|
Failed: []contract.BulkUserErrorResult{},
|
||||||
|
Summary: contract.BulkCreationSummary{
|
||||||
|
Total: len(batch),
|
||||||
|
Succeeded: 0,
|
||||||
|
Failed: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use transaction for batch processing
|
||||||
|
created, failed, err := s.userProcessor.BulkCreateUsersWithTransaction(ctx, batch)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Created = created
|
||||||
|
response.Failed = failed
|
||||||
|
response.Summary.Succeeded = len(created)
|
||||||
|
response.Summary.Failed = len(failed)
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@ -56,7 +239,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers
|
|||||||
limit = 10
|
limit = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
userResponses, totalCount, err := s.userProcessor.ListUsers(ctx, page, limit)
|
userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -72,7 +255,14 @@ func (s *UserServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
||||||
return s.userProcessor.GetUserProfile(ctx, userID)
|
prof, err := s.userProcessor.GetUserProfile(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if roles, err := s.userProcessor.GetUserRoles(ctx, userID); err == nil {
|
||||||
|
prof.Roles = roles
|
||||||
|
}
|
||||||
|
return prof, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserServiceImpl) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) {
|
func (s *UserServiceImpl) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) {
|
||||||
@ -89,3 +279,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)
|
||||||
|
}
|
||||||
|
|||||||
286
internal/service/vote_event_service.go
Normal file
286
internal/service/vote_event_service.go
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
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)
|
||||||
|
GetVotedCount(ctx context.Context, eventID uuid.UUID) (int64, error)
|
||||||
|
GetTotalActiveUsersCount(ctx context.Context) (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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *VoteEventServiceImpl) GetVoteEventDetails(ctx context.Context, eventID uuid.UUID) (*contract.VoteEventDetailsResponse, error) {
|
||||||
|
voteEvent, err := s.voteEventRepo.GetByID(ctx, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalParticipants, err := s.voteEventRepo.GetTotalActiveUsersCount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalVoted, err := s.voteEventRepo.GetVotedCount(ctx, eventID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
totalNotVoted := totalParticipants - totalVoted
|
||||||
|
|
||||||
|
return &contract.VoteEventDetailsResponse{
|
||||||
|
VoteEvent: *transformer.VoteEventToContract(voteEvent),
|
||||||
|
TotalParticipants: totalParticipants,
|
||||||
|
TotalVoted: totalVoted,
|
||||||
|
TotalNotVoted: totalNotVoted,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@ -78,17 +78,21 @@ func RolesToContract(roles []entities.Role) []contract.RoleResponse {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func PositionsToContract(positions []entities.Position) []contract.PositionResponse {
|
func DepartmentsToContract(positions []entities.Department) []contract.DepartmentResponse {
|
||||||
if positions == nil {
|
if positions == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
res := make([]contract.PositionResponse, 0, len(positions))
|
res := make([]contract.DepartmentResponse, 0, len(positions))
|
||||||
for _, p := range positions {
|
for _, p := range positions {
|
||||||
res = append(res, contract.PositionResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path})
|
res = append(res, contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path})
|
||||||
}
|
}
|
||||||
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
|
||||||
@ -170,3 +174,106 @@ func TitlesToContract(titles []entities.Title) []contract.TitleResponse {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PermissionsToContract(perms []entities.Permission) []contract.PermissionResponse {
|
||||||
|
out := make([]contract.PermissionResponse, 0, len(perms))
|
||||||
|
for _, p := range perms {
|
||||||
|
out = append(out, contract.PermissionResponse{ID: p.ID, Code: p.Code, Description: &p.Description, CreatedAt: p.CreatedAt, UpdatedAt: p.UpdatedAt})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func RoleWithPermissionsToContract(role entities.Role, perms []entities.Permission) contract.RoleWithPermissionsResponse {
|
||||||
|
return contract.RoleWithPermissionsResponse{
|
||||||
|
ID: role.ID,
|
||||||
|
Name: role.Name,
|
||||||
|
Code: role.Code,
|
||||||
|
Description: &role.Description,
|
||||||
|
Permissions: PermissionsToContract(perms),
|
||||||
|
CreatedAt: role.CreatedAt,
|
||||||
|
UpdatedAt: role.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func LabelsToContract(list []entities.Label) []contract.LabelResponse {
|
||||||
|
out := make([]contract.LabelResponse, 0, len(list))
|
||||||
|
for _, e := range list {
|
||||||
|
out = append(out, contract.LabelResponse{ID: e.ID.String(), Name: e.Name, Color: e.Color, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrioritiesToContract(list []entities.Priority) []contract.PriorityResponse {
|
||||||
|
out := make([]contract.PriorityResponse, 0, len(list))
|
||||||
|
for _, e := range list {
|
||||||
|
out = append(out, contract.PriorityResponse{ID: e.ID.String(), Name: e.Name, Level: e.Level, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func InstitutionsToContract(list []entities.Institution) []contract.InstitutionResponse {
|
||||||
|
out := make([]contract.InstitutionResponse, 0, len(list))
|
||||||
|
for _, e := range list {
|
||||||
|
out = append(out, contract.InstitutionResponse{ID: e.ID.String(), Name: e.Name, Type: string(e.Type), Address: e.Address, ContactPerson: e.ContactPerson, Phone: e.Phone, Email: e.Email, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionActionsToContract(list []entities.DispositionAction) []contract.DispositionActionResponse {
|
||||||
|
out := make([]contract.DispositionActionResponse, 0, len(list))
|
||||||
|
for _, e := range list {
|
||||||
|
out = append(out, contract.DispositionActionResponse{
|
||||||
|
ID: e.ID.String(),
|
||||||
|
Code: e.Code,
|
||||||
|
Label: e.Label,
|
||||||
|
Description: e.Description,
|
||||||
|
RequiresNote: e.RequiresNote,
|
||||||
|
GroupName: e.GroupName,
|
||||||
|
SortOrder: e.SortOrder,
|
||||||
|
IsActive: e.IsActive,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.DispositionRouteResponse {
|
||||||
|
out := make([]contract.DispositionRouteResponse, 0, len(list))
|
||||||
|
for _, e := range list {
|
||||||
|
var allowed map[string]interface{}
|
||||||
|
if e.AllowedActions != nil {
|
||||||
|
allowed = map[string]interface{}(e.AllowedActions)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := contract.DispositionRouteResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
FromDepartmentID: e.FromDepartmentID,
|
||||||
|
ToDepartmentID: e.ToDepartmentID,
|
||||||
|
IsActive: e.IsActive,
|
||||||
|
AllowedActions: allowed,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add department information if available
|
||||||
|
if e.FromDepartment.ID != uuid.Nil {
|
||||||
|
resp.FromDepartment = contract.DepartmentInfo{
|
||||||
|
ID: e.FromDepartment.ID,
|
||||||
|
Name: e.FromDepartment.Name,
|
||||||
|
Code: e.FromDepartment.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.ToDepartment.ID != uuid.Nil {
|
||||||
|
resp.ToDepartment = contract.DepartmentInfo{
|
||||||
|
ID: e.ToDepartment.ID,
|
||||||
|
Name: e.ToDepartment.Name,
|
||||||
|
Code: e.ToDepartment.Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, resp)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
388
internal/transformer/letter_transformer.go
Normal file
388
internal/transformer/letter_transformer.go
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
package transformer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"eslogad-be/internal/contract"
|
||||||
|
"eslogad-be/internal/entities"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment, refs ...interface{}) *contract.IncomingLetterResponse {
|
||||||
|
resp := &contract.IncomingLetterResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
LetterNumber: e.LetterNumber,
|
||||||
|
ReferenceNumber: e.ReferenceNumber,
|
||||||
|
Subject: e.Subject,
|
||||||
|
Description: e.Description,
|
||||||
|
ReceivedDate: e.ReceivedDate,
|
||||||
|
DueDate: e.DueDate,
|
||||||
|
Status: string(e.Status),
|
||||||
|
CreatedBy: e.CreatedBy,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// optional refs: allow passing already-fetched related objects
|
||||||
|
// expected ordering (if provided): *entities.Priority, *entities.Institution
|
||||||
|
for _, r := range refs {
|
||||||
|
switch v := r.(type) {
|
||||||
|
case *entities.Priority:
|
||||||
|
if v != nil {
|
||||||
|
resp.Priority = &contract.PriorityResponse{
|
||||||
|
ID: v.ID.String(),
|
||||||
|
Name: v.Name,
|
||||||
|
Level: v.Level,
|
||||||
|
CreatedAt: v.CreatedAt,
|
||||||
|
UpdatedAt: v.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *entities.Institution:
|
||||||
|
if v != nil {
|
||||||
|
resp.SenderInstitution = &contract.InstitutionResponse{
|
||||||
|
ID: v.ID.String(),
|
||||||
|
Name: v.Name,
|
||||||
|
Type: string(v.Type),
|
||||||
|
Address: v.Address,
|
||||||
|
ContactPerson: v.ContactPerson,
|
||||||
|
Phone: v.Phone,
|
||||||
|
Email: v.Email,
|
||||||
|
CreatedAt: v.CreatedAt,
|
||||||
|
UpdatedAt: v.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, a := range attachments {
|
||||||
|
resp.Attachments = append(resp.Attachments, contract.IncomingLetterAttachmentResponse{
|
||||||
|
ID: a.ID,
|
||||||
|
FileURL: a.FileURL,
|
||||||
|
FileName: a.FileName,
|
||||||
|
FileType: a.FileType,
|
||||||
|
UploadedAt: a.UploadedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispositionsToContract(list []entities.LetterIncomingDisposition) []contract.DispositionResponse {
|
||||||
|
out := make([]contract.DispositionResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
out = append(out, DispoToContract(d))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func DispoToContract(d entities.LetterIncomingDisposition) contract.DispositionResponse {
|
||||||
|
return contract.DispositionResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
LetterID: d.LetterID,
|
||||||
|
DepartmentID: d.DepartmentID,
|
||||||
|
Notes: d.Notes,
|
||||||
|
ReadAt: d.ReadAt,
|
||||||
|
CreatedBy: d.CreatedBy,
|
||||||
|
CreatedAt: d.CreatedAt,
|
||||||
|
UpdatedAt: d.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnhancedDispositionsToContract(list []entities.LetterIncomingDisposition) []contract.EnhancedDispositionResponse {
|
||||||
|
out := make([]contract.EnhancedDispositionResponse, 0, len(list))
|
||||||
|
for _, d := range list {
|
||||||
|
resp := contract.EnhancedDispositionResponse{
|
||||||
|
ID: d.ID,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiscussionEntityToContract(e *entities.LetterDiscussion) *contract.LetterDiscussionResponse {
|
||||||
|
var mentions map[string]interface{}
|
||||||
|
if e.Mentions != nil {
|
||||||
|
mentions = map[string]interface{}(e.Mentions)
|
||||||
|
}
|
||||||
|
return &contract.LetterDiscussionResponse{
|
||||||
|
ID: e.ID,
|
||||||
|
LetterID: e.LetterID,
|
||||||
|
ParentID: e.ParentID,
|
||||||
|
UserID: e.UserID,
|
||||||
|
Message: e.Message,
|
||||||
|
Mentions: mentions,
|
||||||
|
CreatedAt: e.CreatedAt,
|
||||||
|
UpdatedAt: e.UpdatedAt,
|
||||||
|
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
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ func CreateUserRequestToEntity(req *contract.CreateUserRequest, passwordHash str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &entities.User{
|
return &entities.User{
|
||||||
|
Username: req.Email,
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
PasswordHash: passwordHash,
|
PasswordHash: passwordHash,
|
||||||
@ -26,6 +27,7 @@ func UpdateUserEntity(existing *entities.User, req *contract.UpdateUserRequest)
|
|||||||
}
|
}
|
||||||
if req.Email != nil {
|
if req.Email != nil {
|
||||||
existing.Email = *req.Email
|
existing.Email = *req.Email
|
||||||
|
existing.Username = *req.Email
|
||||||
}
|
}
|
||||||
if req.IsActive != nil {
|
if req.IsActive != nil {
|
||||||
existing.IsActive = *req.IsActive
|
existing.IsActive = *req.IsActive
|
||||||
@ -37,14 +39,23 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
|
|||||||
if user == nil {
|
if user == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return &contract.UserResponse{
|
|
||||||
|
resp := &contract.UserResponse{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
Name: user.Name,
|
Name: user.Name,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
IsActive: user.IsActive,
|
IsActive: user.IsActive,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
if user.Profile != nil {
|
||||||
|
resp.Profile = ProfileEntityToContract(user.Profile)
|
||||||
|
}
|
||||||
|
if user.Departments != nil && len(user.Departments) > 0 {
|
||||||
|
resp.DepartmentResponse = DepartmentsToContract(user.Departments)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func EntitiesToContracts(users []*entities.User) []contract.UserResponse {
|
func EntitiesToContracts(users []*entities.User) []contract.UserResponse {
|
||||||
|
|||||||
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,18 +1,5 @@
|
|||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
-- =========================
|
|
||||||
-- Departments (as requested)
|
|
||||||
-- =========================
|
|
||||||
-- Root org namespace is "eslogad" in ltree path
|
|
||||||
INSERT INTO departments (name, code, path) VALUES
|
|
||||||
('RENBINMINLOG', 'renbinminlog', 'eslogad.renbinminlog'),
|
|
||||||
('FASKON BMN', 'faskon_bmn', 'eslogad.faskon_bmn'),
|
|
||||||
('BEKPALKES', 'bekpalkes', 'eslogad.bekpalkes')
|
|
||||||
ON CONFLICT (code) DO UPDATE
|
|
||||||
SET name = EXCLUDED.name,
|
|
||||||
path = EXCLUDED.path,
|
|
||||||
updated_at = CURRENT_TIMESTAMP;
|
|
||||||
|
|
||||||
-- =========================
|
-- =========================
|
||||||
-- Positions (hierarchy)
|
-- Positions (hierarchy)
|
||||||
-- =========================
|
-- =========================
|
||||||
@ -20,7 +7,7 @@ INSERT INTO departments (name, code, path) VALUES
|
|||||||
-- - superadmin is a separate root
|
-- - superadmin is a separate root
|
||||||
-- - eslogad.aslog is head; waaslog_* under aslog
|
-- - eslogad.aslog is head; waaslog_* under aslog
|
||||||
-- - paban_* under each waaslog_*; pabandya_* under its paban_*
|
-- - paban_* under each waaslog_*; pabandya_* under its paban_*
|
||||||
INSERT INTO positions (name, code, path) VALUES
|
INSERT INTO departments (name, code, path) VALUES
|
||||||
-- ROOTS
|
-- ROOTS
|
||||||
('SUPERADMIN', 'superadmin', 'superadmin'),
|
('SUPERADMIN', 'superadmin', 'superadmin'),
|
||||||
('ASLOG', 'aslog', 'eslogad.aslog'),
|
('ASLOG', 'aslog', 'eslogad.aslog'),
|
||||||
|
|||||||
@ -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');
|
|
||||||
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;
|
||||||
8
migrations/000014_user_performance_indexes.down.sql
Normal file
8
migrations/000014_user_performance_indexes.down.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
-- Drop performance indexes
|
||||||
|
DROP INDEX IF EXISTS idx_users_name_trgm;
|
||||||
|
DROP INDEX IF EXISTS idx_users_email_trgm;
|
||||||
|
DROP INDEX IF EXISTS idx_users_created_at;
|
||||||
|
DROP INDEX IF EXISTS idx_users_active_name;
|
||||||
|
DROP INDEX IF EXISTS idx_users_is_active;
|
||||||
|
DROP INDEX IF EXISTS idx_users_name;
|
||||||
|
DROP INDEX IF EXISTS idx_users_email;
|
||||||
20
migrations/000014_user_performance_indexes.up.sql
Normal file
20
migrations/000014_user_performance_indexes.up.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- Add performance indexes for user queries
|
||||||
|
-- Index for email lookup (already exists as unique, but let's ensure it's there)
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
|
||||||
|
-- Index for name searches
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_name ON users(name);
|
||||||
|
|
||||||
|
-- Index for active status filtering
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_is_active ON users(is_active);
|
||||||
|
|
||||||
|
-- Composite index for common query patterns
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_active_name ON users(is_active, name);
|
||||||
|
|
||||||
|
-- Index for created_at for sorting
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at DESC);
|
||||||
|
|
||||||
|
-- GIN index for full-text search on name and email
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_name_trgm ON users USING gin (name gin_trgm_ops);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_email_trgm ON users USING gin (email gin_trgm_ops);
|
||||||
Loading…
x
Reference in New Issue
Block a user