This commit is contained in:
Aditya Siregar
2025-08-16 20:37:02 +07:00
parent de60983e4e
commit 1964fe50de
45 changed files with 1732 additions and 251 deletions
+31 -29
View File
@@ -117,38 +117,40 @@ type repositories struct {
activityLogRepo *repository.LetterIncomingActivityLogRepository
dispositionRouteRepo *repository.DispositionRouteRepository
// new repos
letterDispositionRepo *repository.LetterDispositionRepository
letterDispActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository
letterDiscussionRepo *repository.LetterDiscussionRepository
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
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
}
func (a *App) initRepositories() *repositories {
return &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.NewLetterDispositionRepository(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),
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),
}
}
@@ -163,7 +165,7 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
activity := processor.NewActivityLogProcessor(repos.activityLogRepo)
return &processors{
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.userProfileRepo),
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo),
letterProcessor: processor.NewLetterProcessor(repos.letterRepo, repos.letterAttachRepo, txMgr, activity, repos.letterDispositionRepo, repos.letterDispositionDeptRepo, repos.letterDispActionSelRepo, repos.dispositionNoteRepo, repos.letterDiscussionRepo, repos.settingRepo, repos.recipientRepo, repos.departmentRepo, repos.userDeptRepo, repos.priorityRepo, repos.institutionRepo, repos.dispRepo),
activityLogger: activity,
}
}
+2 -2
View File
@@ -12,7 +12,7 @@ const (
CorrelationIDKey = key("CorrelationID")
OrganizationIDKey = key("OrganizationIDKey")
UserIDKey = key("UserID")
OutletIDKey = key("OutletID")
DepartmentIDKey = key("DepartmentID")
RoleIDKey = key("RoleID")
AppVersionKey = key("AppVersion")
AppIDKey = key("AppID")
@@ -27,7 +27,7 @@ func LogFields(ctx interface{}) map[string]interface{} {
fields := make(map[string]interface{})
fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey)
fields[string(OrganizationIDKey)] = value(ctx, OrganizationIDKey)
fields[string(OutletIDKey)] = value(ctx, OutletIDKey)
fields[string(DepartmentIDKey)] = value(ctx, DepartmentIDKey)
fields[string(AppVersionKey)] = value(ctx, AppVersionKey)
fields[string(AppIDKey)] = value(ctx, AppIDKey)
fields[string(AppTypeKey)] = value(ctx, AppTypeKey)
+20 -22
View File
@@ -17,17 +17,16 @@ type Logger struct {
var log *Logger
type ContextInfo struct {
CorrelationID string
UserID uuid.UUID
OrganizationID uuid.UUID
OutletID uuid.UUID
AppVersion string
AppID string
AppType string
Platform string
DeviceOS string
UserLocale string
UserRole string
CorrelationID string
UserID uuid.UUID
DepartmentID uuid.UUID
AppVersion string
AppID string
AppType string
Platform string
DeviceOS string
UserLocale string
UserRole string
}
type ctxKeyType struct{}
@@ -59,17 +58,16 @@ func NewContext(ctx context.Context, baseFields map[string]interface{}) context.
func FromGinContext(ctx context.Context) *ContextInfo {
return &ContextInfo{
CorrelationID: value(ctx, CorrelationIDKey),
UserID: uuidValue(ctx, UserIDKey),
OutletID: uuidValue(ctx, OutletIDKey),
OrganizationID: uuidValue(ctx, OrganizationIDKey),
AppVersion: value(ctx, AppVersionKey),
AppID: value(ctx, AppIDKey),
AppType: value(ctx, AppTypeKey),
Platform: value(ctx, PlatformKey),
DeviceOS: value(ctx, DeviceOSKey),
UserLocale: value(ctx, UserLocaleKey),
UserRole: value(ctx, UserRoleKey),
CorrelationID: value(ctx, CorrelationIDKey),
UserID: uuidValue(ctx, UserIDKey),
DepartmentID: uuidValue(ctx, DepartmentIDKey),
AppVersion: value(ctx, AppVersionKey),
AppID: value(ctx, AppIDKey),
AppType: value(ctx, AppTypeKey),
Platform: value(ctx, PlatformKey),
DeviceOS: value(ctx, DeviceOSKey),
UserLocale: value(ctx, UserLocaleKey),
UserRole: value(ctx, UserRoleKey),
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ const (
XAppIDHeader = "x-appid"
XPhoneModelHeader = "X-PhoneModel"
OrganizationID = "x_organization_id"
OutletID = "x_owner_id"
DepartmentID = "x_department_id"
CountryCodeHeader = "country-code"
AcceptedLanguageHeader = "accept-language"
XUserLocaleHeader = "x-user-locale"
+4
View File
@@ -132,6 +132,10 @@ type ListInstitutionsResponse struct {
Institutions []InstitutionResponse `json:"institutions"`
}
type ListInstitutionsRequest struct {
Search *string `json:"search,omitempty" form:"search"`
}
type DispositionActionResponse struct {
ID string `json:"id"`
Code string `json:"code"`
@@ -6,6 +6,12 @@ import (
"github.com/google/uuid"
)
type DepartmentInfo struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Code string `json:"code,omitempty"`
}
type DispositionRouteResponse struct {
ID uuid.UUID `json:"id"`
FromDepartmentID uuid.UUID `json:"from_department_id"`
@@ -14,6 +20,10 @@ type DispositionRouteResponse struct {
AllowedActions map[string]interface{} `json:"allowed_actions,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Department information
FromDepartment DepartmentInfo `json:"from_department"`
ToDepartment DepartmentInfo `json:"to_department"`
}
type CreateDispositionRouteRequest struct {
+73 -22
View File
@@ -32,20 +32,20 @@ type IncomingLetterAttachmentResponse struct {
}
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"`
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 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"`
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 {
@@ -77,6 +77,7 @@ type CreateDispositionActionSelection struct {
}
type CreateLetterDispositionRequest struct {
FromDepartment uuid.UUID `json:"from_department"`
LetterID uuid.UUID `json:"letter_id"`
ToDepartmentIDs []uuid.UUID `json:"to_department_ids"`
Notes *string `json:"notes,omitempty"`
@@ -84,20 +85,64 @@ type CreateLetterDispositionRequest struct {
}
type DispositionResponse struct {
ID uuid.UUID `json:"id"`
LetterID uuid.UUID `json:"letter_id"`
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
Notes *string `json:"notes,omitempty"`
Status string `json:"status"`
CreatedBy uuid.UUID `json:"created_by"`
CreatedAt time.Time `json:"created_at"`
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"`
@@ -119,4 +164,10 @@ type LetterDiscussionResponse struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
EditedAt *time.Time `json:"edited_at,omitempty"`
// Preloaded user profile who created the discussion
User *UserResponse `json:"user,omitempty"`
// Preloaded user profiles for mentions
MentionedUsers []UserResponse `json:"mentioned_users,omitempty"`
}
+21 -8
View File
@@ -48,14 +48,15 @@ type LoginResponse struct {
}
type UserResponse struct {
ID uuid.UUID `json:"id"`
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"`
Profile *UserProfileResponse `json:"profile,omitempty"`
ID uuid.UUID `json:"id"`
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 {
@@ -128,3 +129,15 @@ type TitleResponse struct {
type ListTitlesResponse struct {
Titles []TitleResponse `json:"titles"`
}
// MentionUsersRequest represents the request for getting users for mention purposes
type MentionUsersRequest struct {
Search *string `json:"search,omitempty" form:"search"` // Optional search term for username
Limit *int `json:"limit,omitempty" form:"limit"` // Optional limit, defaults to 50, max 100
}
// MentionUsersResponse represents the response for getting users for mention purposes
type MentionUsersResponse struct {
Users []UserResponse `json:"users"`
Count int `json:"count"`
}
+4
View File
@@ -14,6 +14,10 @@ type DispositionRoute struct {
AllowedActions JSONB `gorm:"type:jsonb" json:"allowed_actions,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
// Relationships
FromDepartment Department `gorm:"foreignKey:FromDepartmentID;references:ID" json:"from_department,omitempty"`
ToDepartment Department `gorm:"foreignKey:ToDepartmentID;references:ID" json:"to_department,omitempty"`
}
func (DispositionRoute) TableName() string { return "disposition_routes" }
+3
View File
@@ -16,6 +16,9 @@ type LetterDiscussion struct {
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
EditedAt *time.Time `json:"edited_at,omitempty"`
// Relationships
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
}
func (LetterDiscussion) TableName() string { return "letter_incoming_discussions" }
+34 -24
View File
@@ -6,32 +6,36 @@ import (
"github.com/google/uuid"
)
type LetterDispositionStatus string
const (
DispositionPending LetterDispositionStatus = "pending"
DispositionRead LetterDispositionStatus = "read"
DispositionRejected LetterDispositionStatus = "rejected"
DispositionCompleted LetterDispositionStatus = "completed"
)
type LetterDisposition struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterID uuid.UUID `gorm:"type:uuid;not null" json:"letter_id"`
FromUserID *uuid.UUID `json:"from_user_id,omitempty"`
FromDepartmentID *uuid.UUID `json:"from_department_id,omitempty"`
ToUserID *uuid.UUID `json:"to_user_id,omitempty"`
ToDepartmentID *uuid.UUID `json:"to_department_id,omitempty"`
Notes *string `json:"notes,omitempty"`
Status LetterDispositionStatus `gorm:"not null;default:'pending'" json:"status"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
ReadAt *time.Time `json:"read_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
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 (LetterDisposition) TableName() string { return "letter_dispositions" }
func (LetterIncomingDisposition) TableName() string { return "letter_incoming_dispositions" }
type LetterIncomingDispositionDepartment struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
LetterIncomingDispositionID uuid.UUID `gorm:"type:uuid;not null" json:"letter_incoming_disposition_id"`
DepartmentID uuid.UUID `gorm:"type:uuid;not null" json:"department_id"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
// Relationships
Department *Department `gorm:"foreignKey:DepartmentID;references:ID" json:"department,omitempty"`
}
func (LetterIncomingDispositionDepartment) TableName() string {
return "letter_incoming_dispositions_department"
}
type DispositionNote struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
@@ -39,6 +43,9 @@ type DispositionNote struct {
UserID *uuid.UUID `json:"user_id,omitempty"`
Note string `gorm:"not null" json:"note"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
// Relationships
User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"`
}
func (DispositionNote) TableName() string { return "disposition_notes" }
@@ -50,6 +57,9 @@ type LetterDispositionActionSelection struct {
Note *string `json:"note,omitempty"`
CreatedBy uuid.UUID `gorm:"not null" json:"created_by"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
// Relationships
Action *DispositionAction `gorm:"foreignKey:ActionID;references:ID" json:"action,omitempty"`
}
func (LetterDispositionActionSelection) TableName() string { return "letter_disposition_actions" }
+1
View File
@@ -48,6 +48,7 @@ type User struct {
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Profile *UserProfile `gorm:"foreignKey:UserID;references:ID" json:"profile,omitempty"`
Departments []Department `gorm:"many2many:user_department;foreignKey:ID;joinForeignKey:user_id;References:ID;joinReferences:department_id" json:"departments,omitempty"`
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
@@ -2,6 +2,7 @@ package handler
import (
"context"
"eslogad-be/internal/appcontext"
"eslogad-be/internal/contract"
@@ -71,12 +72,9 @@ func (h *DispositionRouteHandler) Get(c *gin.Context) {
}
func (h *DispositionRouteHandler) ListByFromDept(c *gin.Context) {
fromID, err := uuid.Parse(c.Param("from_department_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid from_department_id", Code: 400})
return
}
resp, err := h.svc.ListByFromDept(c.Request.Context(), fromID)
appCtx := appcontext.FromGinContext(c.Request.Context())
resp, err := h.svc.ListByFromDept(c.Request.Context(), appCtx.DepartmentID)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
+6 -3
View File
@@ -2,6 +2,7 @@ package handler
import (
"context"
"eslogad-be/internal/appcontext"
"net/http"
"strconv"
@@ -19,7 +20,7 @@ type LetterService interface {
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error)
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
@@ -112,11 +113,13 @@ func (h *LetterHandler) DeleteIncomingLetter(c *gin.Context) {
}
func (h *LetterHandler) CreateDispositions(c *gin.Context) {
appCtx := appcontext.FromGinContext(c.Request.Context())
var req contract.CreateLetterDispositionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid body", Code: 400})
return
}
req.FromDepartment = appCtx.DepartmentID
resp, err := h.svc.CreateDispositions(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
@@ -125,13 +128,13 @@ func (h *LetterHandler) CreateDispositions(c *gin.Context) {
c.JSON(201, contract.BuildSuccessResponse(resp))
}
func (h *LetterHandler) ListDispositionsByLetter(c *gin.Context) {
func (h *LetterHandler) GetEnhancedDispositionsByLetter(c *gin.Context) {
letterID, err := uuid.Parse(c.Param("letter_id"))
if err != nil {
c.JSON(400, &contract.ErrorResponse{Error: "invalid letter_id", Code: 400})
return
}
resp, err := h.svc.ListDispositionsByLetter(c.Request.Context(), letterID)
resp, err := h.svc.GetEnhancedDispositionsByLetter(c.Request.Context(), letterID)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
+8 -2
View File
@@ -24,7 +24,7 @@ type MasterService interface {
CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error)
UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error)
DeleteInstitution(ctx context.Context, id uuid.UUID) error
ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error)
ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error)
CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error)
UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error)
@@ -190,7 +190,13 @@ func (h *MasterHandler) DeleteInstitution(c *gin.Context) {
}
func (h *MasterHandler) ListInstitutions(c *gin.Context) {
resp, err := h.svc.ListInstitutions(c.Request.Context())
var req contract.ListInstitutionsRequest
if search := c.Query("search"); search != "" {
req.Search = &search
}
resp, err := h.svc.ListInstitutions(c.Request.Context(), &req)
if err != nil {
c.JSON(500, &contract.ErrorResponse{Error: err.Error(), Code: 500})
return
+190
View File
@@ -0,0 +1,190 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"eslogad-be/internal/contract"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockMasterService is a mock implementation of MasterService
type MockMasterService struct {
mock.Mock
}
func (m *MockMasterService) CreateLabel(ctx context.Context, req *contract.CreateLabelRequest) (*contract.LabelResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*contract.LabelResponse), args.Error(1)
}
func (m *MockMasterService) UpdateLabel(ctx context.Context, id uuid.UUID, req *contract.UpdateLabelRequest) (*contract.LabelResponse, error) {
args := m.Called(ctx, id, req)
return args.Get(0).(*contract.LabelResponse), args.Error(1)
}
func (m *MockMasterService) DeleteLabel(ctx context.Context, id uuid.UUID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockMasterService) ListLabels(ctx context.Context) (*contract.ListLabelsResponse, error) {
args := m.Called(ctx)
return args.Get(0).(*contract.ListLabelsResponse), args.Error(1)
}
func (m *MockMasterService) CreatePriority(ctx context.Context, req *contract.CreatePriorityRequest) (*contract.PriorityResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*contract.PriorityResponse), args.Error(1)
}
func (m *MockMasterService) UpdatePriority(ctx context.Context, id uuid.UUID, req *contract.UpdatePriorityRequest) (*contract.PriorityResponse, error) {
args := m.Called(ctx, id, req)
return args.Get(0).(*contract.PriorityResponse), args.Error(1)
}
func (m *MockMasterService) DeletePriority(ctx context.Context, id uuid.UUID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockMasterService) ListPriorities(ctx context.Context) (*contract.ListPrioritiesResponse, error) {
args := m.Called(ctx)
return args.Get(0).(*contract.ListPrioritiesResponse), args.Error(1)
}
func (m *MockMasterService) CreateInstitution(ctx context.Context, req *contract.CreateInstitutionRequest) (*contract.InstitutionResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*contract.InstitutionResponse), args.Error(1)
}
func (m *MockMasterService) UpdateInstitution(ctx context.Context, id uuid.UUID, req *contract.UpdateInstitutionRequest) (*contract.InstitutionResponse, error) {
args := m.Called(ctx, id, req)
return args.Get(0).(*contract.InstitutionResponse), args.Error(1)
}
func (m *MockMasterService) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockMasterService) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*contract.ListInstitutionsResponse), args.Error(1)
}
func (m *MockMasterService) CreateDispositionAction(ctx context.Context, req *contract.CreateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
args := m.Called(ctx, req)
return args.Get(0).(*contract.DispositionActionResponse), args.Error(1)
}
func (m *MockMasterService) UpdateDispositionAction(ctx context.Context, id uuid.UUID, req *contract.UpdateDispositionActionRequest) (*contract.DispositionActionResponse, error) {
args := m.Called(ctx, id, req)
return args.Get(0).(*contract.DispositionActionResponse), args.Error(1)
}
func (m *MockMasterService) DeleteDispositionAction(ctx context.Context, id uuid.UUID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockMasterService) ListDispositionActions(ctx context.Context) (*contract.ListDispositionActionsResponse, error) {
args := m.Called(ctx)
return args.Get(0).(*contract.ListDispositionActionsResponse), args.Error(1)
}
func TestMasterHandler_ListInstitutions_WithSearch(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
mockService := new(MockMasterService)
handler := NewMasterHandler(mockService)
// Test data
searchTerm := "university"
expectedResponse := &contract.ListInstitutionsResponse{
Institutions: []contract.InstitutionResponse{
{
ID: "123",
Name: "Test University",
Type: "university",
},
},
}
// Setup mock expectations
mockService.On("ListInstitutions", mock.Anything, &contract.ListInstitutionsRequest{
Search: &searchTerm,
}).Return(expectedResponse, nil)
// Create request
req, _ := http.NewRequest("GET", "/institutions?search="+searchTerm, nil)
w := httptest.NewRecorder()
// Create gin context
c, _ := gin.CreateTestContext(w)
c.Request = req
// Execute
handler.ListInstitutions(c)
// Assertions
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify mock was called correctly
mockService.AssertExpectations(t)
}
func TestMasterHandler_ListInstitutions_WithoutSearch(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
mockService := new(MockMasterService)
handler := NewMasterHandler(mockService)
// Test data
expectedResponse := &contract.ListInstitutionsResponse{
Institutions: []contract.InstitutionResponse{
{
ID: "123",
Name: "Test Institution",
Type: "company",
},
},
}
// Setup mock expectations
mockService.On("ListInstitutions", mock.Anything, &contract.ListInstitutionsRequest{
Search: nil,
}).Return(expectedResponse, nil)
// Create request
req, _ := http.NewRequest("GET", "/institutions", nil)
w := httptest.NewRecorder()
// Create gin context
c, _ := gin.CreateTestContext(w)
c.Request = req
// Execute
handler.ListInstitutions(c)
// Assertions
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
// Verify mock was called correctly
mockService.AssertExpectations(t)
}
+38 -2
View File
@@ -285,12 +285,48 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
}
func (h *UserHandler) ListTitles(c *gin.Context) {
resp, err := h.userService.ListTitles(c.Request.Context())
titles, err := h.userService.ListTitles(c.Request.Context())
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::ListTitles -> Failed to get titles from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
logger.FromContext(c).Infof("UserHandler::ListTitles -> Successfully retrieved titles = %+v", titles)
c.JSON(http.StatusOK, titles)
}
func (h *UserHandler) GetActiveUsersForMention(c *gin.Context) {
search := c.Query("search")
limitStr := c.DefaultQuery("limit", "50")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 50
}
if limit > 100 {
limit = 100
}
var searchPtr *string
if search != "" {
searchPtr = &search
}
users, err := h.userService.GetActiveUsersForMention(c.Request.Context(), searchPtr, limit)
if err != nil {
logger.FromContext(c).WithError(err).Error("UserHandler::GetActiveUsersForMention -> Failed to get active users from service")
h.sendErrorResponse(c, err.Error(), http.StatusInternalServerError)
return
}
response := contract.MentionUsersResponse{
Users: users,
Count: len(users),
}
logger.FromContext(c).Infof("UserHandler::GetActiveUsersForMention -> Successfully retrieved %d active users", len(users))
c.JSON(http.StatusOK, response)
}
func (h *UserHandler) sendErrorResponse(c *gin.Context, message string, statusCode int) {
+3
View File
@@ -20,4 +20,7 @@ type UserService interface {
UpdateProfile(ctx context.Context, userID uuid.UUID, req *contract.UpdateUserProfileRequest) (*contract.UserProfileResponse, error)
ListTitles(ctx context.Context) (*contract.ListTitlesResponse, error)
// Get active users for mention purposes
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
}
+6
View File
@@ -41,6 +41,12 @@ func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
}
setKeyInContext(c, appcontext.UserIDKey, userResponse.ID.String())
if len(userResponse.DepartmentResponse) > 0 {
departmentID := userResponse.DepartmentResponse[0].ID.String()
setKeyInContext(c, appcontext.DepartmentIDKey, departmentID)
} else {
setKeyInContext(c, appcontext.DepartmentIDKey, "")
}
if roles, perms, err := m.authService.ExtractAccess(token); err == nil {
c.Set("user_roles", roles)
+3 -3
View File
@@ -13,7 +13,7 @@ func PopulateContext() gin.HandlerFunc {
setKeyInContext(c, appcontext.AppVersionKey, getAppVersion(c))
setKeyInContext(c, appcontext.AppTypeKey, getAppType(c))
setKeyInContext(c, appcontext.OrganizationIDKey, getOrganizationID(c))
setKeyInContext(c, appcontext.OutletIDKey, getOutletID(c))
setKeyInContext(c, appcontext.DepartmentIDKey, getDepartmentID(c))
setKeyInContext(c, appcontext.DeviceOSKey, getDeviceOS(c))
setKeyInContext(c, appcontext.PlatformKey, getDevicePlatform(c))
setKeyInContext(c, appcontext.UserLocaleKey, getUserLocale(c))
@@ -37,8 +37,8 @@ func getOrganizationID(c *gin.Context) string {
return c.GetHeader(constants.OrganizationID)
}
func getOutletID(c *gin.Context) string {
return c.GetHeader(constants.OutletID)
func getDepartmentID(c *gin.Context) string {
return c.GetHeader(constants.DepartmentID)
}
func getDeviceOS(c *gin.Context) string {
+182 -67
View File
@@ -15,25 +15,26 @@ import (
)
type LetterProcessorImpl struct {
letterRepo *repository.LetterIncomingRepository
attachRepo *repository.LetterIncomingAttachmentRepository
txManager *repository.TxManager
activity *ActivityLogProcessorImpl
// new repos for dispositions
dispositionRepo *repository.LetterDispositionRepository
letterRepo *repository.LetterIncomingRepository
attachRepo *repository.LetterIncomingAttachmentRepository
txManager *repository.TxManager
activity *ActivityLogProcessorImpl
dispositionRepo *repository.LetterIncomingDispositionRepository
dispositionDeptRepo *repository.LetterIncomingDispositionDepartmentRepository
dispositionActionSelRepo *repository.LetterDispositionActionSelectionRepository
dispositionNoteRepo *repository.DispositionNoteRepository
// discussion repo
discussionRepo *repository.LetterDiscussionRepository
// settings and recipients
settingRepo *repository.AppSettingRepository
recipientRepo *repository.LetterIncomingRecipientRepository
departmentRepo *repository.DepartmentRepository
userDeptRepo *repository.UserDepartmentRepository
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.LetterDispositionRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo}
func NewLetterProcessor(letterRepo *repository.LetterIncomingRepository, attachRepo *repository.LetterIncomingAttachmentRepository, txManager *repository.TxManager, activity *ActivityLogProcessorImpl, dispRepo *repository.LetterIncomingDispositionRepository, dispDeptRepo *repository.LetterIncomingDispositionDepartmentRepository, dispSelRepo *repository.LetterDispositionActionSelectionRepository, noteRepo *repository.DispositionNoteRepository, discussionRepo *repository.LetterDiscussionRepository, settingRepo *repository.AppSettingRepository, recipientRepo *repository.LetterIncomingRecipientRepository, departmentRepo *repository.DepartmentRepository, userDeptRepo *repository.UserDepartmentRepository, priorityRepo *repository.PriorityRepository, institutionRepo *repository.InstitutionRepository, dispActionRepo *repository.DispositionActionRepository) *LetterProcessorImpl {
return &LetterProcessorImpl{letterRepo: letterRepo, attachRepo: attachRepo, txManager: txManager, activity: activity, dispositionRepo: dispRepo, dispositionDeptRepo: dispDeptRepo, dispositionActionSelRepo: dispSelRepo, dispositionNoteRepo: noteRepo, discussionRepo: discussionRepo, settingRepo: settingRepo, recipientRepo: recipientRepo, departmentRepo: departmentRepo, userDeptRepo: userDeptRepo, priorityRepo: priorityRepo, institutionRepo: institutionRepo, dispActionRepo: dispActionRepo}
}
func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error) {
@@ -85,7 +86,6 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
}
}
// resolve department codes to ids using repository
depIDs := make([]uuid.UUID, 0, len(defaultDeptCodes))
for _, code := range defaultDeptCodes {
dep, err := p.departmentRepo.GetByCode(txCtx, code)
@@ -94,20 +94,19 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
}
depIDs = append(depIDs, dep.ID)
}
// query user memberships for all departments at once
userMemberships, _ := p.userDeptRepo.ListActiveByDepartmentIDs(txCtx, depIDs)
// build recipients: one department recipient per department + one user recipient per membership
recipients := make([]entities.LetterIncomingRecipient, 0, len(depIDs)+len(userMemberships))
// department recipients
for _, depID := range depIDs {
id := depID
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientDepartmentID: &id, Status: entities.RecipientStatusNew})
}
// user recipients
var recipients []entities.LetterIncomingRecipient
mapsUsers := map[string]bool{}
for _, row := range userMemberships {
uid := row.UserID
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, Status: entities.RecipientStatusNew})
if _, ok := mapsUsers[uid.String()]; !ok {
recipients = append(recipients, entities.LetterIncomingRecipient{LetterID: entity.ID, RecipientUserID: &uid, RecipientDepartmentID: &row.DepartmentID, Status: entities.RecipientStatusNew})
}
mapsUsers[uid.String()] = true
}
if len(recipients) > 0 {
if err := p.recipientRepo.CreateBulk(txCtx, recipients); err != nil {
return err
@@ -141,9 +140,26 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
}
savedAttachments, _ := p.attachRepo.ListByLetter(txCtx, entity.ID)
result = transformer.LetterEntityToContract(entity, savedAttachments)
var pr *entities.Priority
if entity.PriorityID != nil {
if p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
pr = got
}
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil {
if p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
}
result = transformer.LetterEntityToContract(entity, savedAttachments, pr, inst)
return nil
})
if err != nil {
return nil, err
}
@@ -156,7 +172,19 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid
return nil, err
}
atts, _ := p.attachRepo.ListByLetter(ctx, id)
return transformer.LetterEntityToContract(entity, atts), nil
var pr *entities.Priority
if entity.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(ctx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
return transformer.LetterEntityToContract(entity, atts, pr, inst), nil
}
func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error) {
@@ -175,7 +203,19 @@ func (p *LetterProcessorImpl) ListIncomingLetters(ctx context.Context, req *cont
respList := make([]contract.IncomingLetterResponse, 0, len(list))
for _, e := range list {
atts, _ := p.attachRepo.ListByLetter(ctx, e.ID)
resp := transformer.LetterEntityToContract(&e, atts)
var pr *entities.Priority
if e.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(ctx, *e.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if e.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(ctx, *e.SenderInstitutionID); err == nil {
inst = got
}
}
resp := transformer.LetterEntityToContract(&e, atts, pr, inst)
respList = append(respList, *resp)
}
return &contract.ListIncomingLettersResponse{Letters: respList, Pagination: transformer.CreatePaginationResponse(int(total), page, limit)}, nil
@@ -225,7 +265,19 @@ func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.
}
}
atts, _ := p.attachRepo.ListByLetter(txCtx, id)
out = transformer.LetterEntityToContract(entity, atts)
var pr *entities.Priority
if entity.PriorityID != nil && p.priorityRepo != nil {
if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil {
pr = got
}
}
var inst *entities.Institution
if entity.SenderInstitutionID != nil && p.institutionRepo != nil {
if got, err := p.institutionRepo.Get(txCtx, *entity.SenderInstitutionID); err == nil {
inst = got
}
}
out = transformer.LetterEntityToContract(entity, atts, pr, inst)
return nil
})
if err != nil {
@@ -254,48 +306,53 @@ func (p *LetterProcessorImpl) CreateDispositions(ctx context.Context, req *contr
var out *contract.ListDispositionsResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
userID := appcontext.FromGinContext(txCtx).UserID
created := make([]entities.LetterDisposition, 0, len(req.ToDepartmentIDs))
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 {
disp := entities.LetterDisposition{
LetterID: req.LetterID,
FromDepartmentID: nil,
ToDepartmentID: &toDept,
Notes: req.Notes,
Status: entities.DispositionPending,
CreatedBy: userID,
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.dispositionRepo.Create(txCtx, &disp); err != nil {
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
return err
}
created = append(created, disp)
if len(req.SelectedActions) > 0 {
selections := make([]entities.LetterDispositionActionSelection, 0, len(req.SelectedActions))
for _, sel := range req.SelectedActions {
selections = append(selections, entities.LetterDispositionActionSelection{
DispositionID: disp.ID,
ActionID: sel.ActionID,
Note: sel.Note,
CreatedBy: userID,
})
}
if err := p.dispositionActionSelRepo.CreateBulk(txCtx, selections); err != nil {
return err
}
}
if p.activity != nil {
action := "disposition.created"
for _, d := range created {
ctxMap := map[string]interface{}{"to_department_id": d.ToDepartmentID}
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &d.ID, nil, nil, ctxMap); err != nil {
return err
}
}
}
}
out = &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(created)}
if p.activity != nil {
action := "disposition.created"
ctxMap := map[string]interface{}{"to_department_id": dispDepartments}
if err := p.activity.Log(txCtx, req.LetterID, action, &userID, nil, nil, &disp.ID, nil, nil, ctxMap); err != nil {
return err
}
}
out = &contract.ListDispositionsResponse{Dispositions: []contract.DispositionResponse{transformer.DispoToContract(disp)}}
return nil
})
if err != nil {
@@ -312,6 +369,64 @@ func (p *LetterProcessorImpl) ListDispositionsByLetter(ctx context.Context, lett
return &contract.ListDispositionsResponse{Dispositions: transformer.DispositionsToContract(list)}, nil
}
func (p *LetterProcessorImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
// Get dispositions with all related data preloaded in a single query
dispositions, err := p.dispositionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Get discussions with preloaded user profiles
discussions, err := p.discussionRepo.ListByLetter(ctx, letterID)
if err != nil {
return nil, err
}
// Extract all mentioned user IDs from discussions for efficient batch fetching
var mentionedUserIDs []uuid.UUID
mentionedUserIDsMap := make(map[uuid.UUID]bool)
for _, discussion := range discussions {
if discussion.Mentions != nil {
mentions := map[string]interface{}(discussion.Mentions)
if userIDs, ok := mentions["user_ids"]; ok {
if userIDList, ok := userIDs.([]interface{}); ok {
for _, userID := range userIDList {
if userIDStr, ok := userID.(string); ok {
if userUUID, err := uuid.Parse(userIDStr); err == nil {
if !mentionedUserIDsMap[userUUID] {
mentionedUserIDsMap[userUUID] = true
mentionedUserIDs = append(mentionedUserIDs, userUUID)
}
}
}
}
}
}
}
}
// Fetch all mentioned users in a single batch query
var mentionedUsers []entities.User
if len(mentionedUserIDs) > 0 {
mentionedUsers, err = p.discussionRepo.GetUsersByIDs(ctx, mentionedUserIDs)
if err != nil {
return nil, err
}
}
// Transform dispositions
enhancedDispositions := transformer.EnhancedDispositionsWithPreloadedDataToContract(dispositions)
// Transform discussions with mentioned users
enhancedDiscussions := transformer.DiscussionsWithPreloadedDataToContract(discussions, mentionedUsers)
return &contract.ListEnhancedDispositionsResponse{
Dispositions: enhancedDispositions,
Discussions: enhancedDiscussions,
}, nil
}
func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
var out *contract.LetterDiscussionResponse
err := p.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
@@ -320,7 +435,7 @@ func (p *LetterProcessorImpl) CreateDiscussion(ctx context.Context, letterID uui
if req.Mentions != nil {
mentions = entities.JSONB(req.Mentions)
}
disc := &entities.LetterDiscussion{LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions}
disc := &entities.LetterDiscussion{ID: uuid.New(), LetterID: letterID, ParentID: req.ParentID, UserID: userID, Message: req.Message, Mentions: mentions}
if err := p.discussionRepo.Create(txCtx, disc); err != nil {
return err
}
+40
View File
@@ -112,9 +112,11 @@ func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*con
}
resp := transformer.EntityToContract(user)
if resp != nil {
// Roles are loaded separately since they're not preloaded
if roles, err := p.userRepo.GetRolesByUserID(ctx, resp.ID); err == nil {
resp.Roles = transformer.RolesToContract(roles)
}
// Departments are now preloaded, so they're already in the response
}
return resp, nil
}
@@ -125,6 +127,7 @@ func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*
return nil, fmt.Errorf("user not found: %w", err)
}
// Departments are now preloaded, so they're already in the response
return transformer.EntityToContract(user), nil
}
@@ -149,6 +152,7 @@ func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contr
for i := range responses {
userIDs = append(userIDs, responses[i].ID)
}
// Roles are loaded separately since they're not preloaded
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
if err == nil {
for i := range responses {
@@ -157,6 +161,7 @@ func (p *UserProcessorImpl) ListUsersWithFilters(ctx context.Context, req *contr
}
}
}
// Departments are now preloaded, so they're already in the responses
return responses, int(totalCount), nil
}
@@ -272,3 +277,38 @@ func (p *UserProcessorImpl) UpdateUserProfile(ctx context.Context, userID uuid.U
}
return transformer.ProfileEntityToContract(entity), nil
}
// GetActiveUsersForMention retrieves active users for mention purposes with optional username search
func (p *UserProcessorImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
if limit <= 0 {
limit = 50 // Default limit for mention suggestions
}
if limit > 100 {
limit = 100 // Max limit for mention suggestions
}
// Set isActive to true to only get active users
isActive := true
users, _, err := p.userRepo.ListWithFilters(ctx, search, nil, &isActive, limit, 0)
if err != nil {
return nil, fmt.Errorf("failed to get active users: %w", err)
}
responses := transformer.EntitiesToContracts(users)
userIDs := make([]uuid.UUID, 0, len(responses))
for i := range responses {
userIDs = append(userIDs, responses[i].ID)
}
// Load roles for the users
rolesMap, err := p.userRepo.GetRolesByUserIDs(ctx, userIDs)
if err == nil {
for i := range responses {
if roles, ok := rolesMap[responses[i].ID]; ok {
responses[i].Roles = transformer.RolesToContract(roles)
}
}
}
return responses, nil
}
+250
View File
@@ -0,0 +1,250 @@
package processor
import (
"context"
"testing"
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockUserRepository is a mock implementation of UserRepository
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Create(ctx context.Context, user *entities.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
args := m.Called(ctx, id)
return args.Get(0).(*entities.User), args.Error(1)
}
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
args := m.Called(ctx, email)
return args.Get(0).(*entities.User), args.Error(1)
}
func (m *MockUserRepository) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
args := m.Called(ctx, role)
return args.Get(0).([]*entities.User), args.Error(1)
}
func (m *MockUserRepository) GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error) {
args := m.Called(ctx, organizationID)
return args.Get(0).([]*entities.User), args.Error(1)
}
func (m *MockUserRepository) Update(ctx context.Context, user *entities.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(ctx context.Context, id uuid.UUID) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockUserRepository) UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error {
args := m.Called(ctx, id, passwordHash)
return args.Error(0)
}
func (m *MockUserRepository) UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error {
args := m.Called(ctx, id, isActive)
return args.Error(0)
}
func (m *MockUserRepository) List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.User, int64, error) {
args := m.Called(ctx, filters, limit, offset)
return args.Get(0).([]*entities.User), args.Get(1).(int64), args.Error(2)
}
func (m *MockUserRepository) Count(ctx context.Context, filters map[string]interface{}) (int64, error) {
args := m.Called(ctx, filters)
return args.Get(0).(int64), args.Error(1)
}
func (m *MockUserRepository) GetRolesByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Role, error) {
args := m.Called(ctx, userID)
return args.Get(0).([]entities.Role), args.Error(1)
}
func (m *MockUserRepository) GetPermissionsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Permission, error) {
args := m.Called(ctx, userID)
return args.Get(0).([]entities.Permission), args.Error(1)
}
func (m *MockUserRepository) GetDepartmentsByUserID(ctx context.Context, userID uuid.UUID) ([]entities.Department, error) {
args := m.Called(ctx, userID)
return args.Get(0).([]entities.Department), args.Error(1)
}
func (m *MockUserRepository) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
args := m.Called(ctx, userIDs)
return args.Get(0).(map[uuid.UUID][]entities.Role), args.Error(1)
}
func (m *MockUserRepository) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
args := m.Called(ctx, search, roleCode, isActive, limit, offset)
return args.Get(0).([]*entities.User), args.Get(1).(int64), args.Error(2)
}
// MockUserProfileRepository is a mock implementation of UserProfileRepository
type MockUserProfileRepository struct {
mock.Mock
}
func (m *MockUserProfileRepository) GetByUserID(ctx context.Context, userID uuid.UUID) (*entities.UserProfile, error) {
args := m.Called(ctx, userID)
return args.Get(0).(*entities.UserProfile), args.Error(1)
}
func (m *MockUserProfileRepository) Create(ctx context.Context, profile *entities.UserProfile) error {
args := m.Called(ctx, profile)
return args.Error(0)
}
func (m *MockUserProfileRepository) Upsert(ctx context.Context, profile *entities.UserProfile) error {
args := m.Called(ctx, profile)
return args.Error(0)
}
func (m *MockUserProfileRepository) Update(ctx context.Context, profile *entities.UserProfile) error {
args := m.Called(ctx, profile)
return args.Error(0)
}
func TestGetActiveUsersForMention(t *testing.T) {
tests := []struct {
name string
search *string
limit int
mockUsers []*entities.User
mockRoles map[uuid.UUID][]entities.Role
expectedCount int
expectedError bool
setupMocks func(*MockUserRepository, *MockUserProfileRepository)
}{
{
name: "success with search",
search: stringPtr("john"),
limit: 10,
mockUsers: []*entities.User{
{
ID: uuid.New(),
Name: "John Doe",
Email: "john@example.com",
IsActive: true,
},
},
expectedCount: 1,
expectedError: false,
setupMocks: func(mockRepo *MockUserRepository, mockProfileRepo *MockUserProfileRepository) {
mockRepo.On("ListWithFilters", mock.Anything, stringPtr("john"), (*string)(nil), boolPtr(true), 10, 0).
Return([]*entities.User{
{
ID: uuid.New(),
Name: "John Doe",
Email: "john@example.com",
IsActive: true,
},
}, int64(1), nil)
mockRepo.On("GetRolesByUserIDs", mock.Anything, mock.AnythingOfType("[]uuid.UUID")).
Return(map[uuid.UUID][]entities.Role{}, nil)
},
},
{
name: "success without search",
search: nil,
limit: 50,
mockUsers: []*entities.User{
{
ID: uuid.New(),
Name: "Jane Doe",
Email: "jane@example.com",
IsActive: true,
},
},
expectedCount: 1,
expectedError: false,
setupMocks: func(mockRepo *MockUserRepository, mockProfileRepo *MockUserProfileRepository) {
mockRepo.On("ListWithFilters", mock.Anything, (*string)(nil), (*string)(nil), boolPtr(true), 50, 0).
Return([]*entities.User{
{
ID: uuid.New(),
Name: "Jane Doe",
Email: "jane@example.com",
IsActive: true,
},
}, int64(1), nil)
mockRepo.On("GetRolesByUserIDs", mock.Anything, mock.AnythingOfType("[]uuid.UUID")).
Return(map[uuid.UUID][]entities.Role{}, nil)
},
},
{
name: "limit validation - too high",
search: nil,
limit: 150,
mockUsers: []*entities.User{},
expectedCount: 0,
expectedError: false,
setupMocks: func(mockRepo *MockUserRepository, mockProfileRepo *MockUserProfileRepository) {
mockRepo.On("ListWithFilters", mock.Anything, (*string)(nil), (*string)(nil), boolPtr(true), 100, 0).
Return([]*entities.User{}, int64(0), nil)
mockRepo.On("GetRolesByUserIDs", mock.Anything, mock.AnythingOfType("[]uuid.UUID")).
Return(map[uuid.UUID][]entities.Role{}, nil)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mocks
mockRepo := &MockUserRepository{}
mockProfileRepo := &MockUserProfileRepository{}
// Setup mocks
if tt.setupMocks != nil {
tt.setupMocks(mockRepo, mockProfileRepo)
}
// Create processor
processor := NewUserProcessor(mockRepo, mockProfileRepo)
// Call method
result, err := processor.GetActiveUsersForMention(context.Background(), tt.search, tt.limit)
// Assertions
if tt.expectedError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Len(t, result, tt.expectedCount)
}
// Verify mocks
mockRepo.AssertExpectations(t)
mockProfileRepo.AssertExpectations(t)
})
}
}
// Helper functions
func stringPtr(s string) *string {
return &s
}
func boolPtr(b bool) *bool {
return &b
}
@@ -26,7 +26,10 @@ func (r *DispositionRouteRepository) Update(ctx context.Context, e *entities.Dis
func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var e entities.DispositionRoute
if err := db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
if err := db.WithContext(ctx).
Preload("FromDepartment").
Preload("ToDepartment").
First(&e, "id = ?", id).Error; err != nil {
return nil, err
}
return &e, nil
@@ -34,11 +37,15 @@ func (r *DispositionRouteRepository) Get(ctx context.Context, id uuid.UUID) (*en
func (r *DispositionRouteRepository) ListByFromDept(ctx context.Context, fromDept uuid.UUID) ([]entities.DispositionRoute, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionRoute
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).Order("to_department_id").Find(&list).Error; err != nil {
if err := db.WithContext(ctx).Where("from_department_id = ?", fromDept).
Preload("FromDepartment").
Preload("ToDepartment").
Order("to_department_id").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *DispositionRouteRepository) SetActive(ctx context.Context, id uuid.UUID, isActive bool) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Model(&entities.DispositionRoute{}).Where("id = ?", id).Update("is_active", isActive).Error
+106 -7
View File
@@ -104,19 +104,56 @@ func (r *LetterIncomingActivityLogRepository) ListByLetter(ctx context.Context,
return list, nil
}
type LetterDispositionRepository struct{ db *gorm.DB }
type LetterIncomingDispositionRepository struct{ db *gorm.DB }
func NewLetterDispositionRepository(db *gorm.DB) *LetterDispositionRepository {
return &LetterDispositionRepository{db: db}
func NewLetterIncomingDispositionRepository(db *gorm.DB) *LetterIncomingDispositionRepository {
return &LetterIncomingDispositionRepository{db: db}
}
func (r *LetterDispositionRepository) Create(ctx context.Context, e *entities.LetterDisposition) error {
func (r *LetterIncomingDispositionRepository) Create(ctx context.Context, e *entities.LetterIncomingDisposition) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(e).Error
}
func (r *LetterDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDisposition, error) {
func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingDisposition, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDisposition
if err := db.WithContext(ctx).Where("letter_id = ?", letterID).Order("created_at ASC").Find(&list).Error; err != nil {
var list []entities.LetterIncomingDisposition
if err := db.WithContext(ctx).
Where("letter_id = ?", letterID).
Preload("Department").
Preload("Departments.Department").
Preload("ActionSelections.Action").
Preload("DispositionNotes.User").
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterIncomingDispositionDepartmentRepository struct{ db *gorm.DB }
func NewLetterIncomingDispositionDepartmentRepository(db *gorm.DB) *LetterIncomingDispositionDepartmentRepository {
return &LetterIncomingDispositionDepartmentRepository{db: db}
}
func (r *LetterIncomingDispositionDepartmentRepository) CreateBulk(ctx context.Context, list []entities.LetterIncomingDispositionDepartment) error {
db := DBFromContext(ctx, r.db)
return db.WithContext(ctx).Create(&list).Error
}
func (r *LetterIncomingDispositionDepartmentRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDispositionDepartment
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterIncomingDispositionDepartmentRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterIncomingDispositionDepartment
if len(dispositionIDs) == 0 {
return list, nil
}
if err := db.WithContext(ctx).Where("letter_incoming_disposition_id IN ?", dispositionIDs).Order("letter_incoming_disposition_id, created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
@@ -132,6 +169,27 @@ func (r *DispositionNoteRepository) Create(ctx context.Context, e *entities.Disp
return db.WithContext(ctx).Create(e).Error
}
func (r *DispositionNoteRepository) ListByDisposition(ctx context.Context, dispositionID uuid.UUID) ([]entities.DispositionNote, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionNote
if err := db.WithContext(ctx).Where("disposition_id = ?", dispositionID).Order("created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *DispositionNoteRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.DispositionNote, error) {
db := DBFromContext(ctx, r.db)
var list []entities.DispositionNote
if len(dispositionIDs) == 0 {
return list, nil
}
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterDispositionActionSelectionRepository struct{ db *gorm.DB }
func NewLetterDispositionActionSelectionRepository(db *gorm.DB) *LetterDispositionActionSelectionRepository {
@@ -150,6 +208,18 @@ func (r *LetterDispositionActionSelectionRepository) ListByDisposition(ctx conte
return list, nil
}
func (r *LetterDispositionActionSelectionRepository) ListByDispositions(ctx context.Context, dispositionIDs []uuid.UUID) ([]entities.LetterDispositionActionSelection, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDispositionActionSelection
if len(dispositionIDs) == 0 {
return list, nil
}
if err := db.WithContext(ctx).Where("disposition_id IN ?", dispositionIDs).Order("disposition_id, created_at ASC").Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
type LetterDiscussionRepository struct{ db *gorm.DB }
func NewLetterDiscussionRepository(db *gorm.DB) *LetterDiscussionRepository {
@@ -179,6 +249,35 @@ func (r *LetterDiscussionRepository) Update(ctx context.Context, e *entities.Let
Updates(map[string]interface{}{"message": e.Message, "mentions": e.Mentions, "edited_at": e.EditedAt}).Error
}
func (r *LetterDiscussionRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterDiscussion, error) {
db := DBFromContext(ctx, r.db)
var list []entities.LetterDiscussion
if err := db.WithContext(ctx).
Where("letter_id = ?", letterID).
Preload("User.Profile").
Order("created_at ASC").
Find(&list).Error; err != nil {
return nil, err
}
return list, nil
}
func (r *LetterDiscussionRepository) GetUsersByIDs(ctx context.Context, userIDs []uuid.UUID) ([]entities.User, error) {
if len(userIDs) == 0 {
return []entities.User{}, nil
}
db := DBFromContext(ctx, r.db)
var users []entities.User
if err := db.WithContext(ctx).
Where("id IN ?", userIDs).
Preload("Profile").
Find(&users).Error; err != nil {
return nil, err
}
return users, nil
}
type AppSettingRepository struct{ db *gorm.DB }
func NewAppSettingRepository(db *gorm.DB) *AppSettingRepository { return &AppSettingRepository{db: db} }
+34
View File
@@ -78,6 +78,20 @@ func (r *InstitutionRepository) List(ctx context.Context) ([]entities.Institutio
err := r.db.WithContext(ctx).Order("name ASC").Find(&list).Error
return list, err
}
func (r *InstitutionRepository) ListWithSearch(ctx context.Context, search *string) ([]entities.Institution, error) {
var list []entities.Institution
q := r.db.WithContext(ctx).Model(&entities.Institution{})
if search != nil && *search != "" {
like := "%" + *search + "%"
q = q.Where("name ILIKE ? OR type ILIKE ? OR address ILIKE ? OR contact_person ILIKE ?", like, like, like, like)
}
err := q.Order("name ASC").Find(&list).Error
return list, err
}
func (r *InstitutionRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Institution, error) {
var e entities.Institution
if err := r.db.WithContext(ctx).First(&e, "id = ?", id).Error; err != nil {
@@ -113,6 +127,17 @@ func (r *DispositionActionRepository) Get(ctx context.Context, id uuid.UUID) (*e
return &e, nil
}
func (r *DispositionActionRepository) GetByIDs(ctx context.Context, ids []uuid.UUID) ([]entities.DispositionAction, error) {
var actions []entities.DispositionAction
if len(ids) == 0 {
return actions, nil
}
if err := r.db.WithContext(ctx).Where("id IN ?", ids).Find(&actions).Error; err != nil {
return nil, err
}
return actions, nil
}
type DepartmentRepository struct{ db *gorm.DB }
func NewDepartmentRepository(db *gorm.DB) *DepartmentRepository { return &DepartmentRepository{db: db} }
@@ -125,3 +150,12 @@ func (r *DepartmentRepository) GetByCode(ctx context.Context, code string) (*ent
}
return &dep, nil
}
func (r *DepartmentRepository) Get(ctx context.Context, id uuid.UUID) (*entities.Department, error) {
db := DBFromContext(ctx, r.db)
var dep entities.Department
if err := db.WithContext(ctx).First(&dep, "id = ?", id).Error; err != nil {
return nil, err
}
return &dep, nil
}
+14 -8
View File
@@ -25,7 +25,10 @@ func (r *UserRepositoryImpl) Create(ctx context.Context, user *entities.User) er
func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error) {
var user entities.User
err := r.b.WithContext(ctx).Preload("Profile").First(&user, "id = ?", id).Error
err := r.b.WithContext(ctx).
Preload("Profile").
Preload("Departments").
First(&user, "id = ?", id).Error
if err != nil {
return nil, err
}
@@ -34,7 +37,10 @@ func (r *UserRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entiti
func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
var user entities.User
err := r.b.WithContext(ctx).Preload("Profile").Where("email = ?", email).First(&user).Error
err := r.b.WithContext(ctx).
Preload("Profile").
Preload("Departments").
Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
@@ -43,7 +49,7 @@ func (r *UserRepositoryImpl) GetByEmail(ctx context.Context, email string) (*ent
func (r *UserRepositoryImpl) GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error) {
var users []*entities.User
err := r.b.WithContext(ctx).Preload("Profile").Where("role = ?", role).Find(&users).Error
err := r.b.WithContext(ctx).Preload("Profile").Preload("Departments").Where("role = ?", role).Find(&users).Error
return users, err
}
@@ -52,6 +58,7 @@ func (r *UserRepositoryImpl) GetActiveUsers(ctx context.Context, organizationID
err := r.b.WithContext(ctx).
Where(" is_active = ?", organizationID, true).
Preload("Profile").
Preload("Departments").
Find(&users).Error
return users, err
}
@@ -90,7 +97,7 @@ func (r *UserRepositoryImpl) List(ctx context.Context, filters map[string]interf
return nil, 0, err
}
err := query.Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error
err := query.Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error
return users, total, err
}
@@ -141,19 +148,19 @@ func (r *UserRepositoryImpl) GetDepartmentsByUserID(ctx context.Context, userID
return departments, err
}
// GetRolesByUserIDs returns roles per user for a batch of user IDs
func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uuid.UUID) (map[uuid.UUID][]entities.Role, error) {
result := make(map[uuid.UUID][]entities.Role)
if len(userIDs) == 0 {
return result, nil
}
// fetch pairs user_id, role
type row struct {
UserID uuid.UUID
RoleID uuid.UUID
Name string
Code string
}
var rows []row
err := r.b.WithContext(ctx).
Table("user_role as ur").
@@ -171,7 +178,6 @@ func (r *UserRepositoryImpl) GetRolesByUserIDs(ctx context.Context, userIDs []uu
return result, nil
}
// ListWithFilters supports name search and filtering by role code
func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string, roleCode *string, isActive *bool, limit, offset int) ([]*entities.User, int64, error) {
var users []*entities.User
var total int64
@@ -194,7 +200,7 @@ func (r *UserRepositoryImpl) ListWithFilters(ctx context.Context, search *string
return nil, 0, err
}
if err := q.Select("users.*").Distinct("users.id").Limit(limit).Offset(offset).Preload("Profile").Find(&users).Error; err != nil {
if err := q.Select("users.*").Distinct("users.id").Limit(limit).Offset(offset).Preload("Profile").Preload("Departments").Find(&users).Error; err != nil {
return nil, 0, err
}
return users, total, nil
+3 -1
View File
@@ -12,6 +12,7 @@ type UserHandler interface {
UpdateProfile(c *gin.Context)
ChangePassword(c *gin.Context)
ListTitles(c *gin.Context)
GetActiveUsersForMention(c *gin.Context)
}
type FileHandler interface {
@@ -62,7 +63,8 @@ type LetterHandler interface {
DeleteIncomingLetter(c *gin.Context)
CreateDispositions(c *gin.Context)
ListDispositionsByLetter(c *gin.Context)
//ListDispositionsByLetter(c *gin.Context)
GetEnhancedDispositionsByLetter(c *gin.Context)
CreateDiscussion(c *gin.Context)
UpdateDiscussion(c *gin.Context)
+3 -2
View File
@@ -82,6 +82,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
users.PUT("/profile", r.userHandler.UpdateProfile)
users.PUT(":id/password", r.userHandler.ChangePassword)
users.GET("/titles", r.userHandler.ListTitles)
users.GET("/mention", r.userHandler.GetActiveUsersForMention)
users.POST("/profile/avatar", r.fileHandler.UploadProfileAvatar)
}
@@ -139,7 +140,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
lettersch.DELETE("/incoming/:id", r.letterHandler.DeleteIncomingLetter)
lettersch.POST("/dispositions/:letter_id", r.letterHandler.CreateDispositions)
lettersch.GET("/dispositions/:letter_id", r.letterHandler.ListDispositionsByLetter)
lettersch.GET("/dispositions/:letter_id", r.letterHandler.GetEnhancedDispositionsByLetter)
lettersch.POST("/discussions/:letter_id", r.letterHandler.CreateDiscussion)
lettersch.PUT("/discussions/:letter_id/:discussion_id", r.letterHandler.UpdateDiscussion)
@@ -151,7 +152,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
droutes.POST("", r.dispRouteHandler.Create)
droutes.GET(":id", r.dispRouteHandler.Get)
droutes.PUT(":id", r.dispRouteHandler.Update)
droutes.GET("from/:from_department_id", r.dispRouteHandler.ListByFromDept)
droutes.GET("department", r.dispRouteHandler.ListByFromDept)
droutes.PUT(":id/active", r.dispRouteHandler.SetActive)
}
}
+5 -4
View File
@@ -58,7 +58,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
roles, _ := s.userProcessor.GetUserRoles(ctx, userResponse.ID)
permCodes, _ := s.userProcessor.GetUserPermissionCodes(ctx, userResponse.ID)
departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
// Departments are now preloaded, so they're already in userResponse
token, expiresAt, err := s.generateToken(userResponse, roles, permCodes)
if err != nil {
@@ -71,7 +71,7 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest)
User: *userResponse,
Roles: roles,
Permissions: permCodes,
Departments: departments,
Departments: userResponse.DepartmentResponse,
}, nil
}
@@ -90,6 +90,7 @@ func (s *AuthServiceImpl) ValidateToken(tokenString string) (*contract.UserRespo
return nil, fmt.Errorf("user account is deactivated")
}
// Departments are now preloaded, so they're already in the response
return userResponse, nil
}
@@ -115,14 +116,14 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string)
return nil, fmt.Errorf("failed to generate token: %w", err)
}
departments, _ := s.userProcessor.GetUserDepartments(ctx, userResponse.ID)
// Departments are now preloaded, so they're already in userResponse
return &contract.LoginResponse{
Token: newToken,
ExpiresAt: expiresAt,
User: *userResponse,
Roles: roles,
Permissions: permCodes,
Departments: departments,
Departments: userResponse.DepartmentResponse,
}, nil
}
+3 -3
View File
@@ -16,7 +16,7 @@ type LetterProcessor interface {
SoftDeleteIncomingLetter(ctx context.Context, id uuid.UUID) error
CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error)
ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error)
GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error)
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error)
@@ -50,8 +50,8 @@ func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contrac
return s.processor.CreateDispositions(ctx, req)
}
func (s *LetterServiceImpl) ListDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListDispositionsResponse, error) {
return s.processor.ListDispositionsByLetter(ctx, letterID)
func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, letterID uuid.UUID) (*contract.ListEnhancedDispositionsResponse, error) {
return s.processor.GetEnhancedDispositionsByLetter(ctx, letterID)
}
func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) {
+2 -2
View File
@@ -140,8 +140,8 @@ func (s *MasterServiceImpl) UpdateInstitution(ctx context.Context, id uuid.UUID,
func (s *MasterServiceImpl) DeleteInstitution(ctx context.Context, id uuid.UUID) error {
return s.institutionRepo.Delete(ctx, id)
}
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context) (*contract.ListInstitutionsResponse, error) {
list, err := s.institutionRepo.List(ctx)
func (s *MasterServiceImpl) ListInstitutions(ctx context.Context, req *contract.ListInstitutionsRequest) (*contract.ListInstitutionsResponse, error) {
list, err := s.institutionRepo.ListWithSearch(ctx, req.Search)
if err != nil {
return nil, err
}
+3
View File
@@ -26,4 +26,7 @@ type UserProcessor interface {
// New optimized listing
ListUsersWithFilters(ctx context.Context, req *contract.ListUsersRequest) ([]contract.UserResponse, int, error)
// Get active users for mention purposes
GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error)
}
+5
View File
@@ -96,3 +96,8 @@ func (s *UserServiceImpl) ListTitles(ctx context.Context) (*contract.ListTitlesR
}
return &contract.ListTitlesResponse{Titles: transformer.TitlesToContract(titles)}, nil
}
// GetActiveUsersForMention retrieves active users for mention purposes
func (s *UserServiceImpl) GetActiveUsersForMention(ctx context.Context, search *string, limit int) ([]contract.UserResponse, error) {
return s.userProcessor.GetActiveUsersForMention(ctx, search, limit)
}
+26 -2
View File
@@ -89,6 +89,10 @@ func DepartmentsToContract(positions []entities.Department) []contract.Departmen
return res
}
func DepartmentToContract(p entities.Department) contract.DepartmentResponse {
return contract.DepartmentResponse{ID: p.ID, Name: p.Name, Code: p.Code, Path: p.Path}
}
func ProfileEntityToContract(p *entities.UserProfile) *contract.UserProfileResponse {
if p == nil {
return nil
@@ -241,7 +245,8 @@ func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.Di
if e.AllowedActions != nil {
allowed = map[string]interface{}(e.AllowedActions)
}
out = append(out, contract.DispositionRouteResponse{
resp := contract.DispositionRouteResponse{
ID: e.ID,
FromDepartmentID: e.FromDepartmentID,
ToDepartmentID: e.ToDepartmentID,
@@ -249,7 +254,26 @@ func DispositionRoutesToContract(list []entities.DispositionRoute) []contract.Di
AllowedActions: allowed,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
})
}
// Add department information if available
if e.FromDepartment.ID != uuid.Nil {
resp.FromDepartment = contract.DepartmentInfo{
ID: e.FromDepartment.ID,
Name: e.FromDepartment.Name,
Code: e.FromDepartment.Code,
}
}
if e.ToDepartment.ID != uuid.Nil {
resp.ToDepartment = contract.DepartmentInfo{
ID: e.ToDepartment.ID,
Name: e.ToDepartment.Name,
Code: e.ToDepartment.Code,
}
}
out = append(out, resp)
}
return out
}
+339 -21
View File
@@ -3,24 +3,55 @@ package transformer
import (
"eslogad-be/internal/contract"
"eslogad-be/internal/entities"
"github.com/google/uuid"
)
func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment) *contract.IncomingLetterResponse {
func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment, refs ...interface{}) *contract.IncomingLetterResponse {
resp := &contract.IncomingLetterResponse{
ID: e.ID,
LetterNumber: e.LetterNumber,
ReferenceNumber: e.ReferenceNumber,
Subject: e.Subject,
Description: e.Description,
PriorityID: e.PriorityID,
SenderInstitutionID: e.SenderInstitutionID,
ReceivedDate: e.ReceivedDate,
DueDate: e.DueDate,
Status: string(e.Status),
CreatedBy: e.CreatedBy,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)),
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{
@@ -34,19 +65,171 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
return resp
}
func DispositionsToContract(list []entities.LetterDisposition) []contract.DispositionResponse {
func DispositionsToContract(list []entities.LetterIncomingDisposition) []contract.DispositionResponse {
out := make([]contract.DispositionResponse, 0, len(list))
for _, d := range list {
out = append(out, contract.DispositionResponse{
out = append(out, DispoToContract(d))
}
return out
}
func DispoToContract(d entities.LetterIncomingDisposition) contract.DispositionResponse {
return contract.DispositionResponse{
ID: d.ID,
LetterID: d.LetterID,
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,
FromDepartmentID: d.FromDepartmentID,
ToDepartmentID: d.ToDepartmentID,
DepartmentID: d.DepartmentID,
Notes: d.Notes,
Status: string(d.Status),
ReadAt: d.ReadAt,
CreatedBy: d.CreatedBy,
CreatedAt: d.CreatedAt,
})
UpdatedAt: d.UpdatedAt,
Departments: []contract.DispositionDepartmentResponse{},
Actions: []contract.DispositionActionSelectionResponse{},
DispositionNotes: []contract.DispositionNoteResponse{},
}
out = append(out, resp)
}
return out
}
func DispositionDepartmentsToContract(list []entities.LetterIncomingDispositionDepartment) []contract.DispositionDepartmentResponse {
out := make([]contract.DispositionDepartmentResponse, 0, len(list))
for _, d := range list {
resp := contract.DispositionDepartmentResponse{
ID: d.ID,
DepartmentID: d.DepartmentID,
CreatedAt: d.CreatedAt,
}
out = append(out, resp)
}
return out
}
func DispositionDepartmentsWithDetailsToContract(list []entities.LetterIncomingDispositionDepartment) []contract.DispositionDepartmentResponse {
out := make([]contract.DispositionDepartmentResponse, 0, len(list))
for _, d := range list {
resp := contract.DispositionDepartmentResponse{
ID: d.ID,
DepartmentID: d.DepartmentID,
CreatedAt: d.CreatedAt,
}
// Include department details if preloaded
if d.Department != nil {
resp.Department = &contract.DepartmentResponse{
ID: d.Department.ID,
Name: d.Department.Name,
Code: d.Department.Code,
Path: d.Department.Path,
}
}
out = append(out, resp)
}
return out
}
func DispositionActionSelectionsToContract(list []entities.LetterDispositionActionSelection) []contract.DispositionActionSelectionResponse {
out := make([]contract.DispositionActionSelectionResponse, 0, len(list))
for _, d := range list {
resp := contract.DispositionActionSelectionResponse{
ID: d.ID,
ActionID: d.ActionID,
Action: nil, // Will be populated by processor
Note: d.Note,
CreatedBy: d.CreatedBy,
CreatedAt: d.CreatedAt,
}
out = append(out, resp)
}
return out
}
func DispositionActionSelectionsWithDetailsToContract(list []entities.LetterDispositionActionSelection) []contract.DispositionActionSelectionResponse {
out := make([]contract.DispositionActionSelectionResponse, 0, len(list))
for _, d := range list {
resp := contract.DispositionActionSelectionResponse{
ID: d.ID,
ActionID: d.ActionID,
Action: nil, // Will be populated by processor
Note: d.Note,
CreatedBy: d.CreatedBy,
CreatedAt: d.CreatedAt,
}
// Include action details if preloaded
if d.Action != nil {
resp.Action = &contract.DispositionActionResponse{
ID: d.Action.ID.String(),
Code: d.Action.Code,
Label: d.Action.Label,
Description: d.Action.Description,
RequiresNote: d.Action.RequiresNote,
GroupName: d.Action.GroupName,
SortOrder: d.Action.SortOrder,
IsActive: d.Action.IsActive,
CreatedAt: d.Action.CreatedAt,
UpdatedAt: d.Action.UpdatedAt,
}
}
out = append(out, resp)
}
return out
}
func DispositionNotesToContract(list []entities.DispositionNote) []contract.DispositionNoteResponse {
out := make([]contract.DispositionNoteResponse, 0, len(list))
for _, d := range list {
resp := contract.DispositionNoteResponse{
ID: d.ID,
UserID: d.UserID,
Note: d.Note,
CreatedAt: d.CreatedAt,
}
out = append(out, resp)
}
return out
}
func DispositionNotesWithDetailsToContract(list []entities.DispositionNote) []contract.DispositionNoteResponse {
out := make([]contract.DispositionNoteResponse, 0, len(list))
for _, d := range list {
resp := contract.DispositionNoteResponse{
ID: d.ID,
UserID: d.UserID,
Note: d.Note,
CreatedAt: d.CreatedAt,
}
// Include user details if preloaded
if d.User != nil {
resp.User = &contract.UserResponse{
ID: d.User.ID,
Name: d.User.Name,
Email: d.User.Email,
IsActive: d.User.IsActive,
CreatedAt: d.User.CreatedAt,
UpdatedAt: d.User.UpdatedAt,
}
}
out = append(out, resp)
}
return out
}
@@ -68,3 +251,138 @@ func DiscussionEntityToContract(e *entities.LetterDiscussion) *contract.LetterDi
EditedAt: e.EditedAt,
}
}
func DiscussionsWithPreloadedDataToContract(list []entities.LetterDiscussion, mentionedUsers []entities.User) []contract.LetterDiscussionResponse {
// Create a map for efficient user lookup
userMap := make(map[uuid.UUID]entities.User)
for _, user := range mentionedUsers {
userMap[user.ID] = user
}
out := make([]contract.LetterDiscussionResponse, 0, len(list))
for _, d := range list {
resp := contract.LetterDiscussionResponse{
ID: d.ID,
LetterID: d.LetterID,
ParentID: d.ParentID,
UserID: d.UserID,
Message: d.Message,
Mentions: map[string]interface{}(d.Mentions),
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
EditedAt: d.EditedAt,
}
// Include user profile if preloaded
if d.User != nil {
resp.User = &contract.UserResponse{
ID: d.User.ID,
Name: d.User.Name,
Email: d.User.Email,
IsActive: d.User.IsActive,
CreatedAt: d.User.CreatedAt,
UpdatedAt: d.User.UpdatedAt,
}
// Include user profile if available
if d.User.Profile != nil {
resp.User.Profile = &contract.UserProfileResponse{
UserID: d.User.Profile.UserID,
FullName: d.User.Profile.FullName,
DisplayName: d.User.Profile.DisplayName,
Phone: d.User.Profile.Phone,
AvatarURL: d.User.Profile.AvatarURL,
JobTitle: d.User.Profile.JobTitle,
EmployeeNo: d.User.Profile.EmployeeNo,
Bio: d.User.Profile.Bio,
Timezone: d.User.Profile.Timezone,
Locale: d.User.Profile.Locale,
}
}
}
// Process mentions to get mentioned users with profiles
if d.Mentions != nil {
mentions := map[string]interface{}(d.Mentions)
if userIDs, ok := mentions["user_ids"]; ok {
if userIDList, ok := userIDs.([]interface{}); ok {
mentionedUsersList := make([]contract.UserResponse, 0)
for _, userID := range userIDList {
if userIDStr, ok := userID.(string); ok {
if userUUID, err := uuid.Parse(userIDStr); err == nil {
if user, exists := userMap[userUUID]; exists {
userResp := contract.UserResponse{
ID: user.ID,
Name: user.Name,
Email: user.Email,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
// Include user profile if available
if user.Profile != nil {
userResp.Profile = &contract.UserProfileResponse{
UserID: user.Profile.UserID,
FullName: user.Profile.FullName,
DisplayName: user.Profile.DisplayName,
Phone: user.Profile.Phone,
AvatarURL: user.Profile.AvatarURL,
JobTitle: user.Profile.JobTitle,
EmployeeNo: user.Profile.EmployeeNo,
Bio: user.Profile.Bio,
Timezone: user.Profile.Timezone,
Locale: user.Profile.Locale,
}
}
mentionedUsersList = append(mentionedUsersList, userResp)
}
}
}
}
resp.MentionedUsers = mentionedUsersList
}
}
}
out = append(out, resp)
}
return out
}
func EnhancedDispositionsWithPreloadedDataToContract(list []entities.LetterIncomingDisposition) []contract.EnhancedDispositionResponse {
out := make([]contract.EnhancedDispositionResponse, 0, len(list))
for _, d := range list {
resp := contract.EnhancedDispositionResponse{
ID: d.ID,
LetterID: d.LetterID,
DepartmentID: d.DepartmentID,
Notes: d.Notes,
ReadAt: d.ReadAt,
CreatedBy: d.CreatedBy,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
Departments: []contract.DispositionDepartmentResponse{},
Actions: []contract.DispositionActionSelectionResponse{},
DispositionNotes: []contract.DispositionNoteResponse{},
Department: DepartmentToContract(d.Department),
}
if len(d.Departments) > 0 {
resp.Departments = DispositionDepartmentsWithDetailsToContract(d.Departments)
}
// Include preloaded action selections with details
if len(d.ActionSelections) > 0 {
resp.Actions = DispositionActionSelectionsWithDetailsToContract(d.ActionSelections)
}
// Include preloaded notes with user details
if len(d.DispositionNotes) > 0 {
resp.DispositionNotes = DispositionNotesWithDetailsToContract(d.DispositionNotes)
}
out = append(out, resp)
}
return out
}
+11 -1
View File
@@ -37,9 +37,16 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
if user == nil {
return nil
}
// Use Profile.FullName if available, otherwise fall back to user.Name
displayName := user.Name
if user.Profile != nil && user.Profile.FullName != "" {
displayName = user.Profile.FullName
}
resp := &contract.UserResponse{
ID: user.ID,
Name: user.Profile.FullName,
Name: displayName,
Email: user.Email,
IsActive: user.IsActive,
CreatedAt: user.CreatedAt,
@@ -48,6 +55,9 @@ func EntityToContract(user *entities.User) *contract.UserResponse {
if user.Profile != nil {
resp.Profile = ProfileEntityToContract(user.Profile)
}
if user.Departments != nil && len(user.Departments) > 0 {
resp.DepartmentResponse = DepartmentsToContract(user.Departments)
}
return resp
}