Compare commits
No commits in common. "main" and "master" have entirely different histories.
@ -1,130 +0,0 @@
|
||||
# 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"
|
||||
DB_USERNAME :=metidb
|
||||
DB_PASSWORD :=metipassword%23123
|
||||
DB_USERNAME :=eslogad_user
|
||||
DB_PASSWORD :=M9u%24e%23jT2%40qR4pX%21zL
|
||||
DB_HOST :=103.191.71.2
|
||||
DB_PORT :=5433
|
||||
DB_NAME :=mydb
|
||||
DB_PORT :=5432
|
||||
DB_NAME :=eslogad_db
|
||||
|
||||
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
|
||||
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
# 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
|
||||
|
||||
APP_NAME="meti-backend"
|
||||
APP_NAME="eslogad"
|
||||
PORT="4000"
|
||||
|
||||
echo "🔄 Pulling latest code..."
|
||||
@ -15,7 +15,7 @@ docker rm $APP_NAME 2>/dev/null
|
||||
|
||||
echo "🚀 Running new container..."
|
||||
docker run -d --name $APP_NAME \
|
||||
-p 4001:$PORT \
|
||||
-p $PORT:$PORT \
|
||||
-v "$(pwd)/infra":/infra:ro \
|
||||
-v "$(pwd)/templates":/templates:ro \
|
||||
$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/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
@ -64,7 +64,7 @@ require (
|
||||
github.com/aws/aws-sdk-go v1.55.7
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.uber.org/zap v1.21.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
gorm.io/driver/postgres v1.5.0
|
||||
|
||||
6
go.sum
6
go.sum
@ -236,9 +236,8 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
||||
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@ -248,9 +247,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
|
||||
@ -10,11 +10,11 @@ jwt:
|
||||
|
||||
postgresql:
|
||||
host: 103.191.71.2
|
||||
port: 5433
|
||||
port: 5432
|
||||
driver: postgres
|
||||
db: mydb
|
||||
username: metidb
|
||||
password: 'metipassword#123'
|
||||
db: eslogad_db
|
||||
username: eslogad_user
|
||||
password: 'M9u$e#jT2@qR4pX!zL'
|
||||
ssl-mode: disable
|
||||
max-idle-connections-in-second: 600
|
||||
max-open-connections-in-second: 600
|
||||
|
||||
@ -43,24 +43,14 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
middlewares := a.initMiddleware(services)
|
||||
healthHandler := handler.NewHealthHandler()
|
||||
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(
|
||||
cfg,
|
||||
handler.NewAuthHandler(services.authService),
|
||||
middlewares.authMiddleware,
|
||||
healthHandler,
|
||||
handler.NewUserHandler(services.userService, validator.NewUserValidator()),
|
||||
handler.NewUserHandler(services.userService, &validator.UserValidatorImpl{}),
|
||||
fileHandler,
|
||||
rbacHandler,
|
||||
masterHandler,
|
||||
letterHandler,
|
||||
dispositionRouteHandler,
|
||||
voteEventHandler,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -109,26 +99,6 @@ type repositories struct {
|
||||
userRepo *repository.UserRepositoryImpl
|
||||
userProfileRepo *repository.UserProfileRepository
|
||||
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 {
|
||||
@ -136,41 +106,16 @@ func (a *App) initRepositories() *repositories {
|
||||
userRepo: repository.NewUserRepository(a.db),
|
||||
userProfileRepo: repository.NewUserProfileRepository(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 {
|
||||
userProcessor *processor.UserProcessorImpl
|
||||
letterProcessor *processor.LetterProcessorImpl
|
||||
activityLogger *processor.ActivityLogProcessorImpl
|
||||
}
|
||||
|
||||
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
|
||||
txMgr := repository.NewTxManager(a.db)
|
||||
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
|
||||
return &processors{
|
||||
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
|
||||
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispositionDeptRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo),
|
||||
activityLogger: activity,
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,11 +123,6 @@ type services struct {
|
||||
userService *service.UserServiceImpl
|
||||
authService *service.AuthServiceImpl
|
||||
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 {
|
||||
@ -192,27 +132,15 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
|
||||
|
||||
userSvc := service.NewUserService(processors.userProcessor, repos.titleRepo)
|
||||
|
||||
// File storage client and service
|
||||
fileCfg := cfg.S3Config
|
||||
s3Client := client.NewFileClient(fileCfg)
|
||||
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{
|
||||
userService: userSvc,
|
||||
authService: authService,
|
||||
fileService: fileSvc,
|
||||
rbacService: rbacSvc,
|
||||
masterService: masterSvc,
|
||||
letterService: letterSvc,
|
||||
dispositionRouteService: dispRouteSvc,
|
||||
voteEventService: voteEventSvc,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ const (
|
||||
CorrelationIDKey = key("CorrelationID")
|
||||
OrganizationIDKey = key("OrganizationIDKey")
|
||||
UserIDKey = key("UserID")
|
||||
DepartmentIDKey = key("DepartmentID")
|
||||
OutletIDKey = key("OutletID")
|
||||
RoleIDKey = key("RoleID")
|
||||
AppVersionKey = key("AppVersion")
|
||||
AppIDKey = key("AppID")
|
||||
@ -27,7 +27,7 @@ func LogFields(ctx interface{}) map[string]interface{} {
|
||||
fields := make(map[string]interface{})
|
||||
fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey)
|
||||
fields[string(OrganizationIDKey)] = value(ctx, OrganizationIDKey)
|
||||
fields[string(DepartmentIDKey)] = value(ctx, DepartmentIDKey)
|
||||
fields[string(OutletIDKey)] = value(ctx, OutletIDKey)
|
||||
fields[string(AppVersionKey)] = value(ctx, AppVersionKey)
|
||||
fields[string(AppIDKey)] = value(ctx, AppIDKey)
|
||||
fields[string(AppTypeKey)] = value(ctx, AppTypeKey)
|
||||
|
||||
@ -19,7 +19,8 @@ var log *Logger
|
||||
type ContextInfo struct {
|
||||
CorrelationID string
|
||||
UserID uuid.UUID
|
||||
DepartmentID uuid.UUID
|
||||
OrganizationID uuid.UUID
|
||||
OutletID uuid.UUID
|
||||
AppVersion string
|
||||
AppID string
|
||||
AppType string
|
||||
@ -60,7 +61,8 @@ func FromGinContext(ctx context.Context) *ContextInfo {
|
||||
return &ContextInfo{
|
||||
CorrelationID: value(ctx, CorrelationIDKey),
|
||||
UserID: uuidValue(ctx, UserIDKey),
|
||||
DepartmentID: uuidValue(ctx, DepartmentIDKey),
|
||||
OutletID: uuidValue(ctx, OutletIDKey),
|
||||
OrganizationID: uuidValue(ctx, OrganizationIDKey),
|
||||
AppVersion: value(ctx, AppVersionKey),
|
||||
AppID: value(ctx, AppIDKey),
|
||||
AppType: value(ctx, AppTypeKey),
|
||||
|
||||
@ -9,7 +9,7 @@ const (
|
||||
XAppIDHeader = "x-appid"
|
||||
XPhoneModelHeader = "X-PhoneModel"
|
||||
OrganizationID = "x_organization_id"
|
||||
DepartmentID = "x_department_id"
|
||||
OutletID = "x_owner_id"
|
||||
CountryCodeHeader = "country-code"
|
||||
AcceptedLanguageHeader = "accept-language"
|
||||
XUserLocaleHeader = "x-user-locale"
|
||||
|
||||
@ -2,12 +2,6 @@ package contract
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
SettingIncomingLetterPrefix = "INCOMING_LETTER_PREFIX"
|
||||
SettingIncomingLetterSequence = "INCOMING_LETTER_SEQUENCE"
|
||||
SettingIncomingLetterRecipients = "INCOMING_LETTER_RECIPIENTS"
|
||||
)
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
@ -53,118 +47,3 @@ type HealthResponse struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
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,20 +44,16 @@ type LoginResponse struct {
|
||||
User UserResponse `json:"user"`
|
||||
Roles []RoleResponse `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Departments []DepartmentResponse `json:"departments"`
|
||||
Positions []PositionResponse `json:"positions"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
IsActive bool `json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Roles []RoleResponse `json:"roles,omitempty"`
|
||||
DepartmentResponse []DepartmentResponse `json:"department_response"`
|
||||
Profile *UserProfileResponse `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
type ListUsersRequest struct {
|
||||
@ -65,8 +61,6 @@ type ListUsersRequest struct {
|
||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||
Role *string `json:"role,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
Search *string `json:"search,omitempty"`
|
||||
RoleCode *string `json:"role_code,omitempty"`
|
||||
}
|
||||
|
||||
type ListUsersResponse struct {
|
||||
@ -80,7 +74,7 @@ type RoleResponse struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
type DepartmentResponse struct {
|
||||
type PositionResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
@ -103,7 +97,6 @@ type UserProfileResponse struct {
|
||||
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Roles []RoleResponse `json:"roles"`
|
||||
}
|
||||
|
||||
type UpdateUserProfileRequest struct {
|
||||
@ -130,55 +123,3 @@ type TitleResponse struct {
|
||||
type ListTitlesResponse struct {
|
||||
Titles []TitleResponse `json:"titles"`
|
||||
}
|
||||
|
||||
// MentionUsersRequest represents the request for getting users for mention purposes
|
||||
type MentionUsersRequest struct {
|
||||
Search *string `json:"search,omitempty" form:"search"` // Optional search term for username
|
||||
Limit *int `json:"limit,omitempty" form:"limit"` // Optional limit, defaults to 50, max 100
|
||||
}
|
||||
|
||||
// MentionUsersResponse represents the response for getting users for mention purposes
|
||||
type MentionUsersResponse struct {
|
||||
Users []UserResponse `json:"users"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
@ -1,118 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
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" }
|
||||
@ -1,22 +0,0 @@
|
||||
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" }
|
||||
@ -1,23 +0,0 @@
|
||||
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" }
|
||||
@ -1,30 +0,0 @@
|
||||
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" }
|
||||
@ -1,17 +0,0 @@
|
||||
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" }
|
||||
@ -1,24 +0,0 @@
|
||||
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" }
|
||||
@ -1,65 +0,0 @@
|
||||
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" }
|
||||
@ -1,45 +0,0 @@
|
||||
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" }
|
||||
@ -1,23 +0,0 @@
|
||||
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" }
|
||||
@ -1,28 +0,0 @@
|
||||
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" }
|
||||
@ -1,17 +0,0 @@
|
||||
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" }
|
||||
@ -1,10 +0,0 @@
|
||||
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" }
|
||||
@ -1,14 +0,0 @@
|
||||
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,15 +41,12 @@ func (p *Permissions) Scan(value interface{}) error {
|
||||
|
||||
type User struct {
|
||||
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"`
|
||||
Email string `gorm:"uniqueIndex;not null;size:255" json:"email" validate:"required,email"`
|
||||
PasswordHash string `gorm:"not null;size:255" json:"-"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
|
||||
Departments []Department `gorm:"many2many:user_department;foreignKey:ID;joinForeignKey:user_id;References:ID;joinReferences:department_id" json:"departments,omitempty"`
|
||||
}
|
||||
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
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"
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
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"})
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@ -1,252 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@ -1,137 +0,0 @@
|
||||
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,10 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"eslogad-be/internal/appcontext"
|
||||
"eslogad-be/internal/constants"
|
||||
@ -53,102 +51,6 @@ func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
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) {
|
||||
userIDStr := c.Param("id")
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
@ -264,24 +166,10 @@ func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
var roleParam *string
|
||||
if role := c.Query("role"); role != "" {
|
||||
roleParam = &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 isActive, err := strconv.ParseBool(isActiveStr); err == nil {
|
||||
req.IsActive = &isActive
|
||||
@ -383,48 +271,12 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *UserHandler) ListTitles(c *gin.Context) {
|
||||
titles, err := h.userService.ListTitles(c.Request.Context())
|
||||
resp, err := h.userService.ListTitles(c.Request.Context())
|
||||
if err != nil {
|
||||
logger.FromContext(c).WithError(err).Error("UserHandler::ListTitles -> Failed to get titles from service")
|
||||
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||
}
|
||||
|
||||
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
|
||||
|
||||
@ -3,16 +3,12 @@ package handler
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/manager"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
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)
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*contract.UserResponse, error)
|
||||
@ -24,6 +20,4 @@ type UserService interface {
|
||||
UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
|
||||
|
||||
ListTitles(ctx context.Context) (*contract.ListTitlesResponse, error)
|
||||
|
||||
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
|
||||
}
|
||||
|
||||
@ -1,344 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,117 +0,0 @@
|
||||
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,12 +41,6 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
|
||||
if len(userResponse.DepartmentResponse) > 0 {
|
||||
departmentID := userResponse.DepartmentResponse[0].ID.String()
|
||||
setKeyInContext(c, appcontext.DepartmentIDKey, departmentID)
|
||||
} else {
|
||||
setKeyInContext(c, appcontext.DepartmentIDKey, "")
|
||||
}
|
||||
|
||||
if roles, perms, err := m.authService.ExtractAccess(token); err == nil {
|
||||
c.Set("user_roles", roles)
|
||||
|
||||
@ -13,7 +13,7 @@ func PopulateContext() gin.HandlerFunc {
|
||||
setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c))
|
||||
setKeyInContext(c, appcontext.AppTypeKey, getAppType(c))
|
||||
setKeyInContext(c, appcontext.OrganizationIDKey, getOrganizationID(c))
|
||||
setKeyInContext(c, appcontext.DepartmentIDKey, getDepartmentID(c))
|
||||
setKeyInContext(c, appcontext.OutletIDKey, getOutletID(c))
|
||||
setKeyInContext(c, appcontext.DeviceOSKey, getDeviceOS(c))
|
||||
setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c))
|
||||
setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c))
|
||||
@ -37,8 +37,8 @@ func getOrganizationID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.OrganizationID)
|
||||
}
|
||||
|
||||
func getDepartmentID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.DepartmentID)
|
||||
func getOutletID(c *gin.Context) string {
|
||||
return c.GetHeader(constants.OutletID)
|
||||
}
|
||||
|
||||
func getDeviceOS(c *gin.Context) string {
|
||||
|
||||
@ -5,13 +5,11 @@ import (
|
||||
)
|
||||
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
return gin.HandlerFunc(func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
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, X-Correlation-ID")
|
||||
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")
|
||||
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-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
@ -19,5 +17,5 @@ func CORS() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,492 +0,0 @@
|
||||
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,6 +53,7 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
// create default user profile
|
||||
defaultFullName := userEntity.Name
|
||||
profile := &entities.UserProfile{
|
||||
UserID: userEntity.ID,
|
||||
@ -62,7 +63,6 @@ func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *contract.Create
|
||||
Preferences: entities.JSONB{},
|
||||
NotificationPrefs: entities.JSONB{},
|
||||
}
|
||||
|
||||
_ = p.profileRepo.Create(ctx, profile)
|
||||
|
||||
return transformer.EntityToContract(userEntity), nil
|
||||
@ -110,15 +110,8 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
resp := transformer.EntityToContract(user)
|
||||
if resp != nil {
|
||||
// Roles are loaded separately since they're not preloaded
|
||||
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
|
||||
resp.Roles = transformer.RolesToContract(roles)
|
||||
}
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
}
|
||||
return resp, nil
|
||||
|
||||
return transformer.EntityToContract(user), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*contract.UserResponse, error) {
|
||||
@ -127,41 +120,20 @@ func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
return transformer.EntityToContract(user), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
func (p *UserProcessorImpl) ListUsers(ctx context.Context, page, limit int) ([]contract.UserResponse, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
users, totalCount, err := p.userRepo.ListWithFilters(ctx, req.Search, req.RoleCode, req.IsActive, limit, offset)
|
||||
filters := map[string]interface{}{}
|
||||
|
||||
users, totalCount, err := p.userRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -247,12 +219,12 @@ func (p *UserProcessorImpl) GetUserPermissionCodes(ctx context.Context, userID u
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error) {
|
||||
departments, err := p.userRepo.GetDepartmentsByUserID(ctx, userID)
|
||||
func (p *UserProcessorImpl) GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error) {
|
||||
positions, err := p.userRepo.GetPositionsByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transformer.DepartmentsToContract(departments), nil
|
||||
return transformer.PositionsToContract(positions), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) GetUserProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
||||
@ -277,126 +249,3 @@ func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.U
|
||||
}
|
||||
return transformer.ProfileEntityToContract(entity), nil
|
||||
}
|
||||
|
||||
// GetActiveUsersForMention retrieves active users for mention purposes with optional username search
|
||||
func (p *UserProcessorImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
||||
if limit <= 0 {
|
||||
limit = 50 // Default limit for mention suggestions
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100 // Max limit for mention suggestions
|
||||
}
|
||||
|
||||
// Set isActive to true to only get active users
|
||||
isActive := true
|
||||
users, _, err := p.userRepo.ListWithFilters(ctx, search, nil, &isActive, limit, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active users: %w", err)
|
||||
}
|
||||
|
||||
responses := transformer.EntitiesToContracts(users)
|
||||
userIDs := make([]uuid.UUID, 0, len(responses))
|
||||
for i := range responses {
|
||||
userIDs = append(userIDs, responses[i].ID)
|
||||
}
|
||||
|
||||
// Load roles for the users
|
||||
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
|
||||
if err == nil {
|
||||
for i := range responses {
|
||||
if roles, ok := rolesMap[responses[i].ID]; ok {
|
||||
responses[i].Roles = transformer.RolesToContract(roles)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@ -1,250 +0,0 @@
|
||||
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,7 +3,6 @@ package processor
|
||||
import (
|
||||
"context"
|
||||
"eslogad-be/internal/entities"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@ -22,13 +21,5 @@ type UserRepository interface {
|
||||
|
||||
GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error)
|
||||
GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, 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
|
||||
GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error)
|
||||
}
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,307 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,147 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
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,25 +10,22 @@ import (
|
||||
)
|
||||
|
||||
type UserRepositoryImpl struct {
|
||||
b *gorm.DB
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepositoryImpl {
|
||||
return &UserRepositoryImpl{
|
||||
b: db,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) error {
|
||||
return r.b.WithContext(ctx).Create(user).Error
|
||||
return r.db.WithContext(ctx).Create(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
|
||||
var user entities.User
|
||||
err := r.b.WithContext(ctx).
|
||||
Preload("Profile").
|
||||
Preload("Departments").
|
||||
First(&user, "id = ?", id).Error
|
||||
err := r.db.WithContext(ctx).First(&user, "id = ?", id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -37,10 +34,7 @@ func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entiti
|
||||
|
||||
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
var user entities.User
|
||||
err := r.b.WithContext(ctx).
|
||||
Preload("Profile").
|
||||
Preload("Departments").
|
||||
Where("email = ?", email).First(&user).Error
|
||||
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -49,36 +43,34 @@ func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*ent
|
||||
|
||||
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
err := r.b.WithContext(ctx).Preload("Profile").Preload("Departments").Where("role = ?", role).Find(&users).Error
|
||||
err := r.db.WithContext(ctx).Where("role = ?", role).Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
|
||||
var users []*entities.User
|
||||
err := r.b.WithContext(ctx).
|
||||
err := r.db.WithContext(ctx).
|
||||
Where(" is_active = ?", organizationID, true).
|
||||
Preload("Profile").
|
||||
Preload("Departments").
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) Update(ctx context.Context, user *entities.User) error {
|
||||
return r.b.WithContext(ctx).Save(user).Error
|
||||
return r.db.WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return r.b.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error
|
||||
return r.db.WithContext(ctx).Delete(&entities.User{}, "id = ?", id).Error
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
|
||||
return r.b.WithContext(ctx).Model(&entities.User{}).
|
||||
return r.db.WithContext(ctx).Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("password_hash", passwordHash).Error
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
|
||||
return r.b.WithContext(ctx).Model(&entities.User{}).
|
||||
return r.db.WithContext(ctx).Model(&entities.User{}).
|
||||
Where("id = ?", id).
|
||||
Update("is_active", isActive).Error
|
||||
}
|
||||
@ -87,7 +79,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
|
||||
var users []*entities.User
|
||||
var total int64
|
||||
|
||||
query := r.b.WithContext(ctx).Model(&entities.User{})
|
||||
query := r.db.WithContext(ctx).Model(&entities.User{})
|
||||
|
||||
for key, value := range filters {
|
||||
query = query.Where(key+" = ?", value)
|
||||
@ -97,13 +89,13 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := query.Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error
|
||||
err := query.Limit(limit).Offset(offset).Find(&users).Error
|
||||
return users, total, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
|
||||
var count int64
|
||||
query := r.b.WithContext(ctx).Model(&entities.User{})
|
||||
query := r.db.WithContext(ctx).Model(&entities.User{})
|
||||
|
||||
for key, value := range filters {
|
||||
query = query.Where(key+" = ?", value)
|
||||
@ -116,7 +108,7 @@ func (r *UserRepositoryImpl) Count(ctx context.Context, filters map[string]inter
|
||||
// RBAC helpers
|
||||
func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
|
||||
var roles []entities.Role
|
||||
err := r.b.WithContext(ctx).
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("roles as r").
|
||||
Select("r.*").
|
||||
Joins("JOIN user_role ur ON ur.role_id = r.id AND ur.removed_at IS NULL").
|
||||
@ -127,7 +119,7 @@ func (r *UserRepositoryImpl) GetRolesByUserID(ctx context.Context, userID uuid.U
|
||||
|
||||
func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
|
||||
var perms []entities.Permission
|
||||
err := r.b.WithContext(ctx).
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("permissions as p").
|
||||
Select("DISTINCT p.*").
|
||||
Joins("JOIN role_permissions rp ON rp.permission_id = p.id").
|
||||
@ -137,99 +129,13 @@ func (r *UserRepositoryImpl) GetPermissionsByUserID(ctx context.Context, userID
|
||||
return perms, err
|
||||
}
|
||||
|
||||
func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error) {
|
||||
var departments []entities.Department
|
||||
err := r.b.WithContext(ctx).
|
||||
Table("departments as d").
|
||||
Select("d.*").
|
||||
Joins("JOIN user_department ud ON ud.department_id = d.id AND ud.removed_at IS NULL").
|
||||
Where("ud.user_id = ?", userID).
|
||||
Find(&departments).Error
|
||||
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
|
||||
func (r *UserRepositoryImpl) GetPositionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Position, error) {
|
||||
var positions []entities.Position
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("positions as p").
|
||||
Select("p.*").
|
||||
Joins("JOIN user_position up ON up.position_id = p.id AND up.removed_at IS NULL").
|
||||
Where("up.user_id = ?", userID).
|
||||
Find(&positions).Error
|
||||
return positions, err
|
||||
}
|
||||
|
||||
@ -1,140 +0,0 @@
|
||||
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,86 +12,9 @@ type UserHandler interface {
|
||||
UpdateProfile(c *gin.Context)
|
||||
ChangePassword(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 {
|
||||
UploadProfileAvatar(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,11 +14,6 @@ type Router struct {
|
||||
authMiddleware AuthMiddleware
|
||||
userHandler UserHandler
|
||||
fileHandler FileHandler
|
||||
rbacHandler RBACHandler
|
||||
masterHandler MasterHandler
|
||||
letterHandler LetterHandler
|
||||
dispRouteHandler DispositionRouteHandler
|
||||
voteEventHandler VoteEventHandler
|
||||
}
|
||||
|
||||
func NewRouter(
|
||||
@ -28,11 +23,6 @@ func NewRouter(
|
||||
healthHandler HealthHandler,
|
||||
userHandler UserHandler,
|
||||
fileHandler FileHandler,
|
||||
rbacHandler RBACHandler,
|
||||
masterHandler MasterHandler,
|
||||
letterHandler LetterHandler,
|
||||
dispRouteHandler DispositionRouteHandler,
|
||||
voteEventHandler VoteEventHandler,
|
||||
) *Router {
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -41,11 +31,6 @@ func NewRouter(
|
||||
healthHandler: healthHandler,
|
||||
userHandler: userHandler,
|
||||
fileHandler: fileHandler,
|
||||
rbacHandler: rbacHandler,
|
||||
masterHandler: masterHandler,
|
||||
letterHandler: letterHandler,
|
||||
dispRouteHandler: dispRouteHandler,
|
||||
voteEventHandler: voteEventHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +38,6 @@ func (r *Router) Init() *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(
|
||||
middleware.CORS(),
|
||||
middleware.JsonAPI(),
|
||||
middleware.CorrelationID(),
|
||||
middleware.Recover(),
|
||||
@ -80,15 +64,11 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
users := v1.Group("/users")
|
||||
users.Use(r.authMiddleware.RequireAuth())
|
||||
{
|
||||
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("", r.authMiddleware.RequirePermissions("user.view"), r.userHandler.ListUsers)
|
||||
users.GET("/profile", r.userHandler.GetProfile)
|
||||
users.PUT("/profile", r.userHandler.UpdateProfile)
|
||||
users.PUT(":id/password", r.userHandler.ChangePassword)
|
||||
users.GET("/titles", r.userHandler.ListTitles)
|
||||
users.GET("/mention", r.userHandler.GetActiveUsersForMention)
|
||||
users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar)
|
||||
}
|
||||
|
||||
@ -97,96 +77,5 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
{
|
||||
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,9 +56,10 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
// fetch roles, permissions, positions for response and token
|
||||
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
|
||||
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
|
||||
// Departments are now preloaded, so they're already in userResponse
|
||||
positions, _ := s.userProcessor.GetUserPositions(ctx, userResponse.ID)
|
||||
|
||||
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
|
||||
if err != nil {
|
||||
@ -71,7 +72,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
|
||||
User: *userResponse,
|
||||
Roles: roles,
|
||||
Permissions: permCodes,
|
||||
Departments: userResponse.DepartmentResponse,
|
||||
Positions: positions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -90,7 +91,6 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in the response
|
||||
return userResponse, nil
|
||||
}
|
||||
|
||||
@ -116,14 +116,14 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
// Departments are now preloaded, so they're already in userResponse
|
||||
positions, _ := s.userProcessor.GetUserPositions(ctx, userResponse.ID)
|
||||
return &contract.LoginResponse{
|
||||
Token: newToken,
|
||||
ExpiresAt: expiresAt,
|
||||
User: *userResponse,
|
||||
Roles: roles,
|
||||
Permissions: permCodes,
|
||||
Departments: userResponse.DepartmentResponse,
|
||||
Positions: positions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,214 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
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,22 +14,14 @@ type UserProcessor interface {
|
||||
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||
GetUserByID(ctx context.Context, id uuid.UUID) (*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)
|
||||
ChangePassword(ctx context.Context, userID uuid.UUID, req *contract.ChangePasswordRequest) error
|
||||
|
||||
GetUserRoles(ctx context.Context, userID uuid.UUID) ([]contract.RoleResponse, error)
|
||||
GetUserPermissionCodes(ctx context.Context, userID uuid.UUID) ([]string, error)
|
||||
GetUserDepartments(ctx context.Context, userID uuid.UUID) ([]contract.DepartmentResponse, error)
|
||||
GetUserPositions(ctx context.Context, userID uuid.UUID) ([]contract.PositionResponse, error)
|
||||
|
||||
GetUserProfile(ctx context.Context, userID uuid.UUID) (*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,12 +2,9 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"eslogad-be/internal/contract"
|
||||
"eslogad-be/internal/entities"
|
||||
"eslogad-be/internal/manager"
|
||||
"eslogad-be/internal/transformer"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -33,186 +30,6 @@ func (s *UserServiceImpl) CreateUser(ctx context.Context, req *contract.CreateUs
|
||||
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) {
|
||||
return s.userProcessor.UpdateUser(ctx, id, req)
|
||||
}
|
||||
@ -239,7 +56,7 @@ func (s *UserServiceImpl) ListUsers(ctx context.Context, req *contract.ListUsers
|
||||
limit = 10
|
||||
}
|
||||
|
||||
userResponses, totalCount, err := s.userProcessor.ListUsersWithFilters(ctx, req)
|
||||
userResponses, totalCount, err := s.userProcessor.ListUsers(ctx, page, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -255,14 +72,7 @@ func (s *UserServiceImpl) ChangePassword(ctx context.Context, userID uuid.UUID,
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) GetProfile(ctx context.Context, userID uuid.UUID) (*contract.UserProfileResponse, error) {
|
||||
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
|
||||
return s.userProcessor.GetUserProfile(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *UserServiceImpl) UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error) {
|
||||
@ -279,8 +89,3 @@ func (s *UserServiceImpl) ListTitles(ctx context.Context) (*contract.ListTitlesR
|
||||
}
|
||||
return &contract.ListTitlesResponse{Titles: transformer.TitlesToContract(titles)}, nil
|
||||
}
|
||||
|
||||
// GetActiveUsersForMention retrieves active users for mention purposes
|
||||
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
|
||||
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
|
||||
}
|
||||
|
||||
@ -1,286 +0,0 @@
|
||||
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,21 +78,17 @@ func RolesToContract(roles []entities.Role) []contract.RoleResponse {
|
||||
return res
|
||||
}
|
||||
|
||||
func DepartmentsToContract(positions []entities.Department) []contract.DepartmentResponse {
|
||||
func PositionsToContract(positions []entities.Position) []contract.PositionResponse {
|
||||
if positions == nil {
|
||||
return nil
|
||||
}
|
||||
res := make([]contract.DepartmentResponse, 0, len(positions))
|
||||
res := make([]contract.PositionResponse, 0, len(positions))
|
||||
for _, p := range positions {
|
||||
res = append(res, contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path})
|
||||
res = append(res, contract.PositionResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path})
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func DepartmentToContract(p entities.Department) contract.DepartmentResponse {
|
||||
return contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path}
|
||||
}
|
||||
|
||||
func ProfileEntityToContract(p *entities.UserProfile) *contract.UserProfileResponse {
|
||||
if p == nil {
|
||||
return nil
|
||||
@ -174,106 +170,3 @@ func TitlesToContract(titles []entities.Title) []contract.TitleResponse {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,388 +0,0 @@
|
||||
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,7 +10,6 @@ func CreateUserRequestToEntity(req *contract.CreateUserRequest, passwordHash str
|
||||
return nil
|
||||
}
|
||||
return &entities.User{
|
||||
Username: req.Email,
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
PasswordHash: passwordHash,
|
||||
@ -27,7 +26,6 @@ func UpdateUserEntity(existing *entities.User, req *contract.UpdateUserRequest)
|
||||
}
|
||||
if req.Email != nil {
|
||||
existing.Email = *req.Email
|
||||
existing.Username = *req.Email
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
existing.IsActive = *req.IsActive
|
||||
@ -39,23 +37,14 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
|
||||
if user == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
resp := &contract.UserResponse{
|
||||
return &contract.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
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 {
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
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,5 +1,18 @@
|
||||
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)
|
||||
-- =========================
|
||||
@ -7,7 +20,7 @@ BEGIN;
|
||||
-- - superadmin is a separate root
|
||||
-- - eslogad.aslog is head; waaslog_* under aslog
|
||||
-- - paban_* under each waaslog_*; pabandya_* under its paban_*
|
||||
INSERT INTO departments (name, code, path) VALUES
|
||||
INSERT INTO positions (name, code, path) VALUES
|
||||
-- ROOTS
|
||||
('SUPERADMIN', 'superadmin', 'superadmin'),
|
||||
('ASLOG', 'aslog', 'eslogad.aslog'),
|
||||
|
||||
0
migrations/000005_title_table.down.sql
Normal file
0
migrations/000005_title_table.down.sql
Normal file
60
migrations/000005_title_table.up.sql
Normal file
60
migrations/000005_title_table.up.sql
Normal file
@ -0,0 +1,60 @@
|
||||
-- =======================
|
||||
-- TITLES
|
||||
-- =======================
|
||||
CREATE TABLE IF NOT EXISTS titles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL, -- e.g., "Senior Software Engineer"
|
||||
code TEXT UNIQUE, -- e.g., "senior-software-engineer"
|
||||
description TEXT, -- optional: extra details
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Trigger for updated_at
|
||||
CREATE TRIGGER trg_titles_updated_at
|
||||
BEFORE UPDATE ON titles
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
-- Perwira Tinggi (High-ranking Officers)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Jenderal', 'jenderal', 'Pangkat tertinggi di TNI AD'),
|
||||
('Letnan Jenderal', 'letnan-jenderal', 'Pangkat tinggi di bawah Jenderal'),
|
||||
('Mayor Jenderal', 'mayor-jenderal', 'Pangkat tinggi di bawah Letnan Jenderal'),
|
||||
('Brigadir Jenderal', 'brigadir-jenderal', 'Pangkat tinggi di bawah Mayor Jenderal');
|
||||
|
||||
-- Perwira Menengah (Middle-ranking Officers)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Kolonel', 'kolonel', 'Pangkat perwira menengah tertinggi'),
|
||||
('Letnan Kolonel', 'letnan-kolonel', 'Pangkat perwira menengah di bawah Kolonel'),
|
||||
('Mayor', 'mayor', 'Pangkat perwira menengah di bawah Letnan Kolonel');
|
||||
|
||||
-- Perwira Pertama (Junior Officers)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Kapten', 'kapten', 'Pangkat perwira pertama tertinggi'),
|
||||
('Letnan Satu', 'letnan-satu', 'Pangkat perwira pertama di bawah Kapten'),
|
||||
('Letnan Dua', 'letnan-dua', 'Pangkat perwira pertama di bawah Letnan Satu');
|
||||
|
||||
-- Bintara Tinggi (Senior NCOs)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Pembantu Letnan Satu', 'pembantu-letnan-satu', 'Pangkat bintara tinggi tertinggi'),
|
||||
('Pembantu Letnan Dua', 'pembantu-letnan-dua', 'Pangkat bintara tinggi di bawah Pelda');
|
||||
|
||||
-- Bintara (NCOs)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Sersan Mayor', 'sersan-mayor', 'Pangkat bintara di bawah Pelda'),
|
||||
('Sersan Kepala', 'sersan-kepala', 'Pangkat bintara di bawah Serma'),
|
||||
('Sersan Satu', 'sersan-satu', 'Pangkat bintara di bawah Serka'),
|
||||
('Sersan Dua', 'sersan-dua', 'Pangkat bintara di bawah Sertu');
|
||||
|
||||
-- Tamtama Tinggi (Senior Enlisted)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Kopral Kepala', 'kopral-kepala', 'Pangkat tamtama tinggi tertinggi'),
|
||||
('Kopral Satu', 'kopral-satu', 'Pangkat tamtama tinggi di bawah Kopka'),
|
||||
('Kopral Dua', 'kopral-dua', 'Pangkat tamtama tinggi di bawah Koptu');
|
||||
|
||||
-- Tamtama (Enlisted)
|
||||
INSERT INTO titles (name, code, description) VALUES
|
||||
('Prajurit Kepala', 'prajurit-kepala', 'Pangkat tamtama di bawah Kopda'),
|
||||
('Prajurit Satu', 'prajurit-satu', 'Pangkat tamtama di bawah Prada'),
|
||||
('Prajurit Dua', 'prajurit-dua', 'Pangkat tamtama terendah');
|
||||
@ -1,5 +0,0 @@
|
||||
-- Drop Voting System Tables
|
||||
|
||||
DROP TABLE IF EXISTS votes;
|
||||
DROP TABLE IF EXISTS candidates;
|
||||
DROP TABLE IF EXISTS vote_events;
|
||||
@ -1,51 +0,0 @@
|
||||
-- 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);
|
||||
@ -1,3 +0,0 @@
|
||||
-- Remove results_open column from vote_events table
|
||||
ALTER TABLE vote_events
|
||||
DROP COLUMN IF EXISTS results_open;
|
||||
@ -1,3 +0,0 @@
|
||||
-- Add results_open column to vote_events table
|
||||
ALTER TABLE vote_events
|
||||
ADD COLUMN results_open BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@ -1,8 +0,0 @@
|
||||
-- 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;
|
||||
@ -1,20 +0,0 @@
|
||||
-- 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