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) GetByReferenceNumber(ctx context.Context, refNumber *string) (*entities.LetterIncoming, error) { db := DBFromContext(ctx, r.db) var e entities.LetterIncoming if err := db.WithContext(ctx). Where("reference_number = ? AND deleted_at IS NULL", refNumber). First(&e).Error; err != nil { return nil, err } return &e, nil } func (r *LetterIncomingRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncoming, error) { return r.Get(ctx, id) } 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 } func (r *LetterIncomingRepository) BulkArchive(ctx context.Context, letterIDs []uuid.UUID) (int64, error) { db := DBFromContext(ctx, r.db) now := time.Now() result := db.WithContext(ctx). Model(&entities.LetterIncoming{}). Where("id IN ? AND deleted_at IS NULL", letterIDs). Updates(map[string]interface{}{ "is_archived": true, "archived_at": now, }) return result.RowsAffected, result.Error } func (r *LetterIncomingRepository) Archive(ctx context.Context, letterID uuid.UUID) error { db := DBFromContext(ctx, r.db) now := time.Now() return db.WithContext(ctx). Model(&entities.LetterIncoming{}). Where("id = ? AND deleted_at IS NULL", letterID). Updates(map[string]interface{}{ "is_archived": true, "archived_at": now, }).Error } // BulkArchiveForUser archives letters for a specific user only func (r *LetterIncomingRepository) BulkArchiveForUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (int64, error) { db := DBFromContext(ctx, r.db) // Archive only the recipient records for the specific user // Note: letter_incoming_recipients uses recipient_user_id column result := db.WithContext(ctx). Model(&entities.LetterIncomingRecipient{}). Where("letter_id IN ? AND recipient_user_id = ?", letterIDs, userID). Update("is_archived", true) return result.RowsAffected, result.Error } type ListIncomingLettersFilter struct { Status *string Query *string DepartmentID *uuid.UUID UserID *uuid.UUID IsRead *bool PriorityIDs []uuid.UUID IsDispositioned *bool IsArchived *bool } 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") joinedRecipients := false needsGroupBy := false if filter.DepartmentID != nil { query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id"). Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID) joinedRecipients = true needsGroupBy = true } if filter.UserID != nil && filter.IsRead != nil { if !joinedRecipients { query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id") joinedRecipients = true needsGroupBy = true } query = query.Where("letter_incoming_recipients.recipient_user_id = ?", *filter.UserID) if *filter.IsRead { query = query.Where("letter_incoming_recipients.read_at IS NOT NULL") } else { query = query.Where("letter_incoming_recipients.read_at IS NULL") } } if filter.DepartmentID != nil && filter.IsDispositioned != nil { query = query.Joins("LEFT JOIN letter_incoming_dispositions_department lidd ON lidd.letter_incoming_id = letters_incoming.id AND lidd.department_id = ?", *filter.DepartmentID) if *filter.IsDispositioned { query = query.Where("lidd.id IS NOT NULL AND lidd.status != 'pending'") } else { query = query.Where("lidd.id IS NULL OR lidd.status = 'pending'") } } if len(filter.PriorityIDs) > 0 { query = query.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs) } if filter.IsArchived != nil { if *filter.IsArchived { query = query.Where("letter_incoming_recipients.is_archived = ?", true) } else { query = query.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false) } } if filter.Status != nil { query = query.Where("letters_incoming.status = ?", *filter.Status) } if filter.Query != nil { q := "%" + *filter.Query + "%" query = query.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q) } if needsGroupBy { query = query.Group("letters_incoming.id") } var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } // For the actual data fetch, we need to select all columns var list []entities.LetterIncoming dataQuery := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL") if filter.DepartmentID != nil { dataQuery = dataQuery.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id"). Where("letter_incoming_recipients.recipient_department_id = ?", *filter.DepartmentID) } if filter.UserID != nil && filter.IsRead != nil { if filter.DepartmentID == nil { dataQuery = dataQuery.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id") } dataQuery = dataQuery.Where("letter_incoming_recipients.recipient_user_id = ?", *filter.UserID) if *filter.IsRead { dataQuery = dataQuery.Where("letter_incoming_recipients.read_at IS NOT NULL") } else { dataQuery = dataQuery.Where("letter_incoming_recipients.read_at IS NULL") } } if filter.DepartmentID != nil && filter.IsDispositioned != nil { dataQuery = dataQuery.Joins("LEFT JOIN letter_incoming_dispositions_department lidd ON lidd.letter_incoming_id = letters_incoming.id AND lidd.department_id = ?", *filter.DepartmentID) if *filter.IsDispositioned { dataQuery = dataQuery.Where("lidd.id IS NOT NULL AND lidd.status != 'pending'") } else { dataQuery = dataQuery.Where("lidd.id IS NULL OR lidd.status = 'pending'") } } if len(filter.PriorityIDs) > 0 { dataQuery = dataQuery.Where("letters_incoming.priority_id IN ?", filter.PriorityIDs) } // Apply is_archived filter based on recipient's is_archived field //if filter.IsArchived != nil { // if *filter.IsArchived { // dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ?", true) // } else { // dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false) // } //} if filter.IsArchived != nil { if *filter.IsArchived { dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ?", true) } else { dataQuery = dataQuery.Where("letter_incoming_recipients.is_archived = ? OR letter_incoming_recipients.is_archived IS NULL", false) } } if filter.Status != nil { dataQuery = dataQuery.Where("letters_incoming.status = ?", *filter.Status) } if filter.Query != nil { q := "%" + *filter.Query + "%" dataQuery = dataQuery.Where("letters_incoming.subject ILIKE ? OR letters_incoming.reference_number ILIKE ?", q, q) } if needsGroupBy { dataQuery = dataQuery.Group("letters_incoming.id, letters_incoming.letter_number, letters_incoming.reference_number, letters_incoming.subject, letters_incoming.description, letters_incoming.priority_id, letters_incoming.sender_institution_id, letters_incoming.received_date, letters_incoming.due_date, letters_incoming.status, letters_incoming.created_by, letters_incoming.created_at, letters_incoming.updated_at, letters_incoming.deleted_at") } if err := dataQuery.Order("letters_incoming.created_at DESC").Limit(limit).Offset(offset).Find(&list).Error; err != nil { return nil, 0, err } return list, total, nil } func (r *LetterIncomingRepository) Search(ctx context.Context, filters map[string]interface{}, limit, offset int, sortBy, sortOrder string) ([]entities.LetterIncoming, int64, error) { db := DBFromContext(ctx, r.db) query := db.WithContext(ctx).Model(&entities.LetterIncoming{}).Where("deleted_at IS NULL") joinedRecipients := false needsGroupBy := false // Apply search filters if q, ok := filters["query"]; ok && q != "" { searchTerm := "%" + q.(string) + "%" query = query.Where("subject ILIKE ? OR reference_number ILIKE ? OR letter_number ILIKE ? OR description ILIKE ? OR sender_name ILIKE ?", searchTerm, searchTerm, searchTerm, searchTerm, searchTerm) } if letterNumber, ok := filters["letter_number"]; ok && letterNumber != "" { query = query.Where("letter_number ILIKE ?", "%"+letterNumber.(string)+"%") } if subject, ok := filters["subject"]; ok && subject != "" { query = query.Where("subject ILIKE ?", "%"+subject.(string)+"%") } if status, ok := filters["status"]; ok && status != "" { query = query.Where("status = ?", status) } if priorityID, ok := filters["priority_id"]; ok { query = query.Where("priority_id = ?", priorityID) } if institutionID, ok := filters["sender_institution_id"]; ok { query = query.Where("sender_institution_id = ?", institutionID) } if createdBy, ok := filters["created_by"]; ok { query = query.Where("created_by = ?", createdBy) } if dateFrom, ok := filters["date_from"]; ok { query = query.Where("received_date >= ?", dateFrom) } if dateTo, ok := filters["date_to"]; ok { query = query.Where("received_date <= ?", dateTo) } // Apply user context filters if present if userContext, ok := filters["user_context"]; ok { if ctx, ok := userContext.(map[string]interface{}); ok { if userID, ok := ctx["user_id"]; ok { // User can see letters where they are recipients query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id"). Where("letter_incoming_recipients.recipient_user_id = ?", userID) joinedRecipients = true needsGroupBy = true } if departmentID, ok := ctx["department_id"]; ok { // Also include letters for user's department if !joinedRecipients { query = query.Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id") joinedRecipients = true needsGroupBy = true } query = query.Where("letter_incoming_recipients.recipient_department_id = ?", departmentID) } } } // Count total results var total int64 if needsGroupBy { // For grouped queries, count distinct letter IDs if err := db.WithContext(ctx).Model(&entities.LetterIncoming{}). Joins("JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id"). Where("letters_incoming.deleted_at IS NULL"). Distinct("letters_incoming.id"). Count(&total).Error; err != nil { return nil, 0, err } } else { if err := query.Count(&total).Error; err != nil { return nil, 0, err } } // Apply sorting if sortBy == "" { sortBy = "created_at" } if sortOrder == "" { sortOrder = "desc" } validSortFields := map[string]bool{ "letter_number": true, "subject": true, "received_date": true, "status": true, "created_at": true, "updated_at": true, } if !validSortFields[sortBy] { sortBy = "created_at" } if sortOrder != "asc" && sortOrder != "desc" { sortOrder = "desc" } orderBy := "letters_incoming." + sortBy + " " + sortOrder // Apply grouping if necessary if needsGroupBy { query = query.Group("letters_incoming.id, letters_incoming.letter_number, letters_incoming.reference_number, " + "letters_incoming.subject, letters_incoming.description, letters_incoming.priority_id, " + "letters_incoming.sender_institution_id, letters_incoming.received_date, letters_incoming.due_date, " + "letters_incoming.status, letters_incoming.created_by, letters_incoming.created_at, " + "letters_incoming.updated_at, letters_incoming.deleted_at") } // Execute query var letters []entities.LetterIncoming if err := query. Order(orderBy). Limit(limit). Offset(offset). Find(&letters).Error; err != nil { return nil, 0, err } return letters, 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 } func (r *LetterIncomingAttachmentRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) { if len(letterIDs) == 0 { return make(map[uuid.UUID][]entities.LetterIncomingAttachment), nil } db := DBFromContext(ctx, r.db) var attachments []entities.LetterIncomingAttachment if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs).Order("uploaded_at ASC").Find(&attachments).Error; err != nil { return nil, err } // Group attachments by letter ID result := make(map[uuid.UUID][]entities.LetterIncomingAttachment) for _, att := range attachments { result[att.LetterID] = append(result[att.LetterID], att) } return result, 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) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingDisposition, error) { db := DBFromContext(ctx, r.db) var e entities.LetterIncomingDisposition if err := db.WithContext(ctx). Preload("Department"). Preload("Departments.Department"). Preload("ActionSelections.Action"). Preload("DispositionNotes.User"). First(&e, "id = ?", id).Error; err != nil { return nil, err } return &e, nil } 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 } func (r *LetterIncomingDispositionRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, error) { if len(letterIDs) == 0 { return make(map[uuid.UUID][]entities.LetterIncomingDisposition), nil } db := DBFromContext(ctx, r.db) var dispositions []entities.LetterIncomingDisposition if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs). Preload("Department"). Preload("Departments.Department"). Preload("ActionSelections.Action"). Preload("DispositionNotes.User"). Order("created_at ASC"). Find(&dispositions).Error; err != nil { return nil, err } // Group by letter ID result := make(map[uuid.UUID][]entities.LetterIncomingDisposition) for i := range dispositions { // Gunakan index, bukan value letterID := dispositions[i].LetterID result[letterID] = append(result[letterID], dispositions[i]) } return result, nil } func (r *LetterIncomingDispositionRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDisposition, error) { db := DBFromContext(ctx, r.db) var list []entities.LetterIncomingDisposition if err := db.WithContext(ctx). Where("letter_id = ?", letterIncomingID). Preload("Department"). Preload("Departments.Department"). 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) DB(ctx context.Context) *gorm.DB { return DBFromContext(ctx, r.db) } func (r *LetterIncomingDispositionDepartmentRepository) Create(ctx context.Context, e *entities.LetterIncomingDispositionDepartment) error { db := DBFromContext(ctx, r.db) return db.WithContext(ctx).Create(e).Error } 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) Update(ctx context.Context, e *entities.LetterIncomingDispositionDepartment) error { db := DBFromContext(ctx, r.db) return db.WithContext(ctx).Save(e).Error } func (r *LetterIncomingDispositionDepartmentRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingDispositionDepartment, error) { db := DBFromContext(ctx, r.db) var e entities.LetterIncomingDispositionDepartment if err := db.WithContext(ctx). Preload("Department"). Preload("LetterIncoming"). Preload("LetterIncomingDisposition"). First(&e, "id = ?", id).Error; err != nil { return nil, err } return &e, nil } func (r *LetterIncomingDispositionDepartmentRepository) GetByDispositionAndDepartment(ctx context.Context, letterIncomingID, departmentID uuid.UUID) (*entities.LetterIncomingDispositionDepartment, error) { db := DBFromContext(ctx, r.db) var e entities.LetterIncomingDispositionDepartment if err := db.WithContext(ctx). Where("letter_incoming_id = ? AND department_id = ?", letterIncomingID, departmentID). Preload("Department"). Preload("LetterIncoming"). Preload("LetterIncomingDisposition"). First(&e).Error; err != nil { return nil, err } return &e, nil } func (r *LetterIncomingDispositionDepartmentRepository) GetByLetterAndDepartment(ctx context.Context, letterID, departmentID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) { db := DBFromContext(ctx, r.db) var list []entities.LetterIncomingDispositionDepartment if err := db.WithContext(ctx). Where("letter_incoming_id = ? AND department_id = ?", letterID, departmentID). Preload("Department"). Preload("LetterIncoming"). Preload("LetterIncomingDisposition"). Find(&list).Error; err != nil { return nil, err } return list, nil } func (r *LetterIncomingDispositionDepartmentRepository) ListByDepartmentWithPagination(ctx context.Context, departmentID uuid.UUID, status *string, offset, limit int) ([]entities.LetterIncomingDispositionDepartment, int64, error) { db := DBFromContext(ctx, r.db) query := db.WithContext(ctx).Where("department_id = ?", departmentID) if status != nil && *status != "" { query = query.Where("status = ?", *status) } var total int64 if err := query.Model(&entities.LetterIncomingDispositionDepartment{}).Count(&total).Error; err != nil { return nil, 0, err } var list []entities.LetterIncomingDispositionDepartment if err := query. Preload("Department"). Preload("LetterIncoming"). Preload("LetterIncomingDisposition.Department"). Offset(offset). Limit(limit). Order("created_at DESC"). Find(&list).Error; err != nil { return nil, 0, err } return list, total, nil } func (r *LetterIncomingDispositionDepartmentRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDispositionDepartment, error) { db := DBFromContext(ctx, r.db) var list []entities.LetterIncomingDispositionDepartment if err := db.WithContext(ctx). Where("letter_incoming_id = ?", letterIncomingID). Preload("Department"). Preload("LetterIncoming"). Preload("LetterIncomingDisposition.Department"). Order("created_at DESC"). Find(&list).Error; err != nil { return nil, err } return list, nil } func (r *LetterIncomingDispositionDepartmentRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.LetterIncomingDispositionDepartmentStatus, notes string, readAt, completedAt *time.Time) error { db := DBFromContext(ctx, r.db) updates := map[string]interface{}{ "status": status, } if readAt != nil { updates["read_at"] = readAt } if completedAt != nil { updates["completed_at"] = completedAt } if notes != "" { updates["notes"] = notes } return db.WithContext(ctx).Model(&entities.LetterIncomingDispositionDepartment{}). Where("id = ?", id). Updates(updates).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 } // recipients type LetterIncomingRecipientRepository struct{ db *gorm.DB } func NewLetterIncomingRecipientRepository(db *gorm.DB) *LetterIncomingRecipientRepository { return &LetterIncomingRecipientRepository{db: db} } func (r *LetterIncomingRecipientRepository) DB(ctx context.Context) *gorm.DB { return DBFromContext(ctx, r.db) } func (r *LetterIncomingRecipientRepository) CreateBulk(ctx context.Context, recs []entities.LetterIncomingRecipient) error { db := DBFromContext(ctx, r.db) return db.WithContext(ctx).Create(&recs).Error } func (r *LetterIncomingRecipientRepository) Create(ctx context.Context, recipient *entities.LetterIncomingRecipient) error { return r.DB(ctx).Create(recipient).Error } func (r *LetterIncomingRecipientRepository) Update(ctx context.Context, recipient *entities.LetterIncomingRecipient) error { return r.DB(ctx).Save(recipient).Error } func (r *LetterIncomingRecipientRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.LetterIncomingRecipient, error) { var recipient entities.LetterIncomingRecipient if err := r.DB(ctx).Where("id = ?", id).First(&recipient).Error; err != nil { return nil, err } return &recipient, nil } func (r *LetterIncomingRecipientRepository) GetByLetterAndDepartment(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (*entities.LetterIncomingRecipient, error) { var recipient entities.LetterIncomingRecipient if err := r.DB(ctx).Where("letter_id = ? AND recipient_department_id = ?", letterID, departmentID).First(&recipient).Error; err != nil { return nil, err } return &recipient, nil } func (r *LetterIncomingRecipientRepository) GetByLetterAndUser(ctx context.Context, letterID uuid.UUID, userID uuid.UUID) (*entities.LetterIncomingRecipient, error) { var recipient entities.LetterIncomingRecipient if err := r.DB(ctx).Where("letter_id = ? AND recipient_user_id = ?", letterID, userID).First(&recipient).Error; err != nil { return nil, err } return &recipient, nil } func (r *LetterIncomingRecipientRepository) ListByLetter(ctx context.Context, letterID uuid.UUID) ([]entities.LetterIncomingRecipient, error) { var recipients []entities.LetterIncomingRecipient if err := r.DB(ctx).Where("letter_id = ?", letterID).Find(&recipients).Error; err != nil { return nil, err } return recipients, nil } func (r *LetterIncomingRecipientRepository) ListByDepartment(ctx context.Context, departmentID uuid.UUID) ([]entities.LetterIncomingRecipient, error) { var recipients []entities.LetterIncomingRecipient if err := r.DB(ctx).Where("recipient_department_id = ?", departmentID).Find(&recipients).Error; err != nil { return nil, err } return recipients, nil } func (r *LetterIncomingRecipientRepository) GetLetterIDsByDepartment(ctx context.Context, departmentID uuid.UUID) ([]uuid.UUID, error) { db := DBFromContext(ctx, r.db) var letterIDs []uuid.UUID if err := db.WithContext(ctx). Model(&entities.LetterIncomingRecipient{}). Where("recipient_department_id = ?", departmentID). Distinct("letter_id"). Pluck("letter_id", &letterIDs).Error; err != nil { return nil, err } return letterIDs, nil } func (r *LetterIncomingRecipientRepository) CountReadByLetter(ctx context.Context, letterID uuid.UUID) (int, error) { db := DBFromContext(ctx, r.db) var count int64 if err := db.WithContext(ctx). Model(&entities.LetterIncomingRecipient{}). Where("letter_id = ? AND read_at IS NOT NULL", letterID). Count(&count).Error; err != nil { return 0, err } return int(count), nil } func (r *LetterIncomingRecipientRepository) CountUnreadByUser(ctx context.Context, userID uuid.UUID) (int, error) { db := DBFromContext(ctx, r.db) var count int64 sql := ` SELECT COUNT(*) FROM ( SELECT * FROM letter_incoming_recipients WHERE recipient_user_id = ? AND read_at IS NULL AND id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY letter_id ORDER BY created_at DESC) as rn FROM letter_incoming_recipients WHERE recipient_user_id = ? ) ranked WHERE rn = 1 ) ) result ` if err := db.WithContext(ctx).Raw(sql, userID, userID).Scan(&count).Error; err != nil { return 0, err } return int(count), nil } func (r *LetterIncomingRecipientRepository) MarkAsRead(ctx context.Context, letterID, userID uuid.UUID) error { db := DBFromContext(ctx, r.db) now := time.Now() return db.WithContext(ctx). Model(&entities.LetterIncomingRecipient{}). Where("letter_id = ? AND recipient_user_id = ?", letterID, userID). Update("read_at", now).Error } func (r *LetterIncomingRecipientRepository) GetByLetterIDsAndUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) { if len(letterIDs) == 0 { return make(map[uuid.UUID]*entities.LetterIncomingRecipient), nil } db := DBFromContext(ctx, r.db) var recipients []entities.LetterIncomingRecipient if err := db.WithContext(ctx). Where(`id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER (PARTITION BY letter_id ORDER BY created_at DESC) as rn FROM letter_incoming_recipients WHERE letter_id IN ? AND recipient_user_id = ? ) t WHERE rn = 1 )`, letterIDs, userID). Find(&recipients).Error; err != nil { return nil, err } result := make(map[uuid.UUID]*entities.LetterIncomingRecipient) for i := range recipients { result[recipients[i].LetterID] = &recipients[i] } return result, nil } func (r *LetterIncomingRecipientRepository) HasDepartmentAccess(ctx context.Context, letterID uuid.UUID, departmentID uuid.UUID) (bool, error) { db := DBFromContext(ctx, r.db) var count int64 if err := db.WithContext(ctx). Model(&entities.LetterIncomingRecipient{}). Where("letter_id = ? AND recipient_department_id = ?", letterID, departmentID). Count(&count).Error; err != nil { return false, err } return count > 0, nil }