add sender and receiver
This commit is contained in:
parent
90da195a2e
commit
24e1d265d3
BIN
eslogad-backend
BIN
eslogad-backend
Binary file not shown.
BIN
eslogad-be
BIN
eslogad-be
Binary file not shown.
@ -6,6 +6,29 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SearchIncomingLettersRequest struct {
|
||||
Query string `json:"query" form:"query"`
|
||||
LetterNumber string `json:"letter_number" form:"letter_number"`
|
||||
Subject string `json:"subject" form:"subject"`
|
||||
Status string `json:"status" form:"status"`
|
||||
PriorityID *uuid.UUID `json:"priority_id" form:"priority_id"`
|
||||
InstitutionID *uuid.UUID `json:"institution_id" form:"institution_id"`
|
||||
CreatedBy *uuid.UUID `json:"created_by" form:"created_by"`
|
||||
DateFrom *time.Time `json:"date_from" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to" form:"date_to"`
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
SortBy string `json:"sort_by" form:"sort_by"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
||||
}
|
||||
|
||||
type SearchIncomingLettersResponse struct {
|
||||
Letters []IncomingLetterResponse `json:"letters"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type CreateIncomingLetterAttachment struct {
|
||||
FileURL string `json:"file_url"`
|
||||
FileName string `json:"file_name"`
|
||||
@ -19,6 +42,7 @@ type CreateIncomingLetterRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||
SenderName *string `json:"sender_name,omitempty"`
|
||||
ReceivedDate time.Time `json:"received_date"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Attachments []CreateIncomingLetterAttachment `json:"attachments,omitempty"`
|
||||
@ -40,6 +64,7 @@ type IncomingLetterResponse struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
Priority *PriorityResponse `json:"priority,omitempty"`
|
||||
SenderInstitution *InstitutionResponse `json:"sender_institution,omitempty"`
|
||||
SenderName *string `json:"sender_name,omitempty"`
|
||||
ReceivedDate time.Time `json:"received_date"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Status string `json:"status"`
|
||||
@ -56,6 +81,7 @@ type UpdateIncomingLetterRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||
SenderName *string `json:"sender_name,omitempty"`
|
||||
ReceivedDate *time.Time `json:"received_date,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
|
||||
@ -6,6 +6,29 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SearchOutgoingLettersRequest struct {
|
||||
Query string `json:"query" form:"query"`
|
||||
LetterNumber string `json:"letter_number" form:"letter_number"`
|
||||
Subject string `json:"subject" form:"subject"`
|
||||
Status string `json:"status" form:"status"`
|
||||
PriorityID *uuid.UUID `json:"priority_id" form:"priority_id"`
|
||||
InstitutionID *uuid.UUID `json:"institution_id" form:"institution_id"`
|
||||
CreatedBy *uuid.UUID `json:"created_by" form:"created_by"`
|
||||
DateFrom *time.Time `json:"date_from" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to" form:"date_to"`
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
SortBy string `json:"sort_by" form:"sort_by"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
||||
}
|
||||
|
||||
type SearchOutgoingLettersResponse struct {
|
||||
Letters []OutgoingLetterResponse `json:"letters"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
type CreateOutgoingLetterRecipient struct {
|
||||
LetterID uuid.UUID `json:"letter_id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||
@ -28,6 +51,7 @@ type CreateOutgoingLetterRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
||||
IssueDate time.Time `json:"issue_date" validate:"required"`
|
||||
Attachments []CreateOutgoingLetterAttachment `json:"attachments,omitempty"`
|
||||
UserID uuid.UUID
|
||||
@ -78,6 +102,7 @@ type OutgoingLetterResponse struct {
|
||||
Priority *PriorityResponse `json:"priority,omitempty"`
|
||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||
ReceiverInstitution *InstitutionResponse `json:"receiver_institution,omitempty"`
|
||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
||||
IssueDate time.Time `json:"issue_date"`
|
||||
Status string `json:"status"`
|
||||
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
||||
@ -95,6 +120,7 @@ type UpdateOutgoingLetterRequest struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
||||
IssueDate *time.Time `json:"issue_date,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,7 @@ type LetterIncoming struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
SenderInstitutionID *uuid.UUID `json:"sender_institution_id,omitempty"`
|
||||
SenderName *string `json:"sender_name,omitempty"`
|
||||
ReceivedDate time.Time `json:"received_date"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
Status LetterIncomingStatus `gorm:"not null;default:'new'" json:"status"`
|
||||
|
||||
@ -24,6 +24,7 @@ type LetterOutgoing struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
PriorityID *uuid.UUID `json:"priority_id,omitempty"`
|
||||
ReceiverInstitutionID *uuid.UUID `json:"receiver_institution_id,omitempty"`
|
||||
ReceiverName *string `json:"receiver_name,omitempty"`
|
||||
IssueDate time.Time `json:"issue_date"`
|
||||
Status LetterOutgoingStatus `gorm:"not null;default:'draft'" json:"status"`
|
||||
ApprovalFlowID *uuid.UUID `json:"approval_flow_id,omitempty"`
|
||||
|
||||
@ -17,6 +17,7 @@ type LetterService interface {
|
||||
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||
ListIncomingLetters(ctx context.Context, req *contract.ListIncomingLettersRequest) (*contract.ListIncomingLettersResponse, error)
|
||||
SearchIncomingLetters(ctx context.Context, req *contract.SearchIncomingLettersRequest) (*contract.SearchIncomingLettersResponse, error)
|
||||
GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error)
|
||||
MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
||||
MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
||||
@ -358,6 +359,34 @@ func (h *LetterHandler) UpdateDiscussion(c *gin.Context) {
|
||||
h.respondSuccess(c, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *LetterHandler) SearchIncomingLetters(c *gin.Context) {
|
||||
var req contract.SearchIncomingLettersRequest
|
||||
if !h.bindQuery(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
if req.SortOrder == "" {
|
||||
req.SortOrder = "desc"
|
||||
}
|
||||
if req.SortBy == "" {
|
||||
req.SortBy = "created_at"
|
||||
}
|
||||
|
||||
resp, err := h.svc.SearchIncomingLetters(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.handleServiceError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
h.respondSuccess(c, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (h *LetterHandler) GetDepartmentDispositionStatus(c *gin.Context) {
|
||||
letterID, ok := h.parseUUID(c, "letter_id")
|
||||
if !ok {
|
||||
|
||||
@ -15,6 +15,7 @@ type LetterOutgoingService interface {
|
||||
CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||
GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error)
|
||||
ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error)
|
||||
SearchOutgoingLetters(ctx context.Context, req *contract.SearchOutgoingLettersRequest) (*contract.SearchOutgoingLettersResponse, error)
|
||||
UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
@ -402,6 +403,35 @@ func (h *LetterOutgoingHandler) DeleteDiscussion(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, &contract.SuccessResponse{Message: "discussion deleted"})
|
||||
}
|
||||
|
||||
func (h *LetterOutgoingHandler) SearchOutgoingLetters(c *gin.Context) {
|
||||
var req contract.SearchOutgoingLettersRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, &contract.ErrorResponse{Error: "invalid query parameters", Code: http.StatusBadRequest})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
if req.SortOrder == "" {
|
||||
req.SortOrder = "desc"
|
||||
}
|
||||
if req.SortBy == "" {
|
||||
req.SortBy = "created_at"
|
||||
}
|
||||
|
||||
resp, err := h.svc.SearchOutgoingLetters(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, &contract.ErrorResponse{Error: err.Error(), Code: http.StatusInternalServerError})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, contract.BuildSuccessResponse(resp))
|
||||
}
|
||||
|
||||
func (h *LetterOutgoingHandler) GetLetterApprovalInfo(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
|
||||
@ -16,6 +16,7 @@ type LetterOutgoingProcessor interface {
|
||||
CreateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, attachments []entities.LetterOutgoingAttachment, userID, departmentID uuid.UUID) error
|
||||
GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*entities.LetterOutgoing, error)
|
||||
ListOutgoingLetters(ctx context.Context, filter repository.ListOutgoingLettersFilter, limit, offset int) ([]entities.LetterOutgoing, int64, error)
|
||||
SearchOutgoingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error)
|
||||
UpdateOutgoingLetter(ctx context.Context, letter *entities.LetterOutgoing, userID uuid.UUID) error
|
||||
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID, userID uuid.UUID) error
|
||||
|
||||
@ -780,6 +781,11 @@ func (p *LetterOutgoingProcessorImpl) GetUsersByIDs(ctx context.Context, userIDs
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (p *LetterOutgoingProcessorImpl) SearchOutgoingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error) {
|
||||
offset := (page - 1) * limit
|
||||
return p.letterRepo.Search(ctx, filters, limit, offset, sortBy, sortOrder)
|
||||
}
|
||||
|
||||
func (p *LetterOutgoingProcessorImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) {
|
||||
return p.letterRepo.BulkArchive(ctx, letterIDs)
|
||||
}
|
||||
|
||||
@ -67,6 +67,7 @@ func (p *LetterProcessorImpl) CreateIncomingLetter(ctx context.Context, req *con
|
||||
Description: req.Description,
|
||||
PriorityID: req.PriorityID,
|
||||
SenderInstitutionID: req.SenderInstitutionID,
|
||||
SenderName: req.SenderName,
|
||||
ReceivedDate: req.ReceivedDate,
|
||||
DueDate: req.DueDate,
|
||||
Status: entities.LetterIncomingStatusNew,
|
||||
@ -252,6 +253,9 @@ func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid.
|
||||
if req.SenderInstitutionID != nil {
|
||||
entity.SenderInstitutionID = req.SenderInstitutionID
|
||||
}
|
||||
if req.SenderName != nil {
|
||||
entity.SenderName = req.SenderName
|
||||
}
|
||||
if req.ReceivedDate != nil {
|
||||
entity.ReceivedDate = *req.ReceivedDate
|
||||
}
|
||||
@ -577,6 +581,11 @@ func (p *LetterProcessorImpl) createAttachments(ctx context.Context, letterID uu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *LetterProcessorImpl) SearchIncomingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterIncoming, int64, error) {
|
||||
offset := (page - 1) * limit
|
||||
return p.letterRepo.Search(ctx, filters, limit, offset, sortBy, sortOrder)
|
||||
}
|
||||
|
||||
func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) {
|
||||
savedAttachments, _ := p.attachRepo.ListByLetter(ctx, entity.ID)
|
||||
|
||||
|
||||
@ -188,6 +188,109 @@ func (r *LetterOutgoingRepository) List(ctx context.Context, filter ListOutgoing
|
||||
return list, total, nil
|
||||
}
|
||||
|
||||
func (r *LetterOutgoingRepository) Search(ctx context.Context, filters map[string]interface{}, limit, offset int, sortBy, sortOrder string) ([]entities.LetterOutgoing, int64, error) {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
query := db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("deleted_at IS NULL")
|
||||
|
||||
// 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 receiver_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["receiver_institution_id"]; ok {
|
||||
query = query.Where("receiver_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("issue_date >= ?", dateFrom)
|
||||
}
|
||||
|
||||
if dateTo, ok := filters["date_to"]; ok {
|
||||
query = query.Where("issue_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 created by them OR letters where they are recipients
|
||||
subQuery := db.Model(&entities.LetterOutgoingRecipient{}).Select("letter_id").Where("user_id = ?", userID)
|
||||
query = query.Where("created_by = ? OR id IN (?)", userID, subQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count total results
|
||||
var total int64
|
||||
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,
|
||||
"issue_date": true,
|
||||
"status": true,
|
||||
"created_at": true,
|
||||
"updated_at": true,
|
||||
}
|
||||
|
||||
if !validSortFields[sortBy] {
|
||||
sortBy = "created_at"
|
||||
}
|
||||
|
||||
if sortOrder != "asc" && sortOrder != "desc" {
|
||||
sortOrder = "desc"
|
||||
}
|
||||
|
||||
orderBy := sortBy + " " + sortOrder
|
||||
|
||||
// Execute query with preloads
|
||||
var letters []entities.LetterOutgoing
|
||||
if err := query.
|
||||
Preload("Priority").
|
||||
Preload("ReceiverInstitution").
|
||||
Preload("Creator").
|
||||
Preload("Creator.Profile").
|
||||
Order(orderBy).
|
||||
Limit(limit).
|
||||
Offset(offset).
|
||||
Find(&letters).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return letters, total, nil
|
||||
}
|
||||
|
||||
func (r *LetterOutgoingRepository) UpdateStatus(ctx context.Context, id uuid.UUID, status entities.LetterOutgoingStatus) error {
|
||||
db := DBFromContext(ctx, r.db)
|
||||
return db.WithContext(ctx).Model(&entities.LetterOutgoing{}).Where("id = ? AND deleted_at IS NULL", id).Update("status", status).Error
|
||||
|
||||
@ -210,6 +210,139 @@ func (r *LetterIncomingRepository) List(ctx context.Context, filter ListIncoming
|
||||
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 {
|
||||
|
||||
@ -74,6 +74,7 @@ type LetterHandler interface {
|
||||
CreateIncomingLetter(c *gin.Context)
|
||||
GetIncomingLetter(c *gin.Context)
|
||||
ListIncomingLetters(c *gin.Context)
|
||||
SearchIncomingLetters(c *gin.Context)
|
||||
GetLetterUnreadCounts(c *gin.Context)
|
||||
MarkIncomingLetterAsRead(c *gin.Context)
|
||||
MarkOutgoingLetterAsRead(c *gin.Context)
|
||||
@ -95,6 +96,7 @@ type LetterOutgoingHandler interface {
|
||||
CreateOutgoingLetter(c *gin.Context)
|
||||
GetOutgoingLetter(c *gin.Context)
|
||||
ListOutgoingLetters(c *gin.Context)
|
||||
SearchOutgoingLetters(c *gin.Context)
|
||||
UpdateOutgoingLetter(c *gin.Context)
|
||||
DeleteOutgoingLetter(c *gin.Context)
|
||||
|
||||
|
||||
@ -169,6 +169,7 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
lettersch.GET("/unread-counts", r.letterHandler.GetLetterUnreadCounts)
|
||||
|
||||
lettersch.GET("/incoming", r.letterHandler.ListIncomingLetters)
|
||||
lettersch.GET("/incoming/search", r.letterHandler.SearchIncomingLetters)
|
||||
lettersch.POST("/incoming", r.letterHandler.CreateIncomingLetter)
|
||||
lettersch.GET("/incoming/cta/:letter_id", r.letterHandler.GetLetterCTA)
|
||||
lettersch.GET("/incoming/:id", r.letterHandler.GetIncomingLetter)
|
||||
@ -178,8 +179,10 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
lettersch.POST("/incoming/archive", r.letterHandler.BulkArchiveIncomingLetters)
|
||||
|
||||
lettersch.POST("/outgoing", r.letterOutgoingHandler.CreateOutgoingLetter)
|
||||
lettersch.GET("/outgoing/search", r.letterOutgoingHandler.SearchOutgoingLetters)
|
||||
lettersch.GET("/outgoing/:id", r.letterOutgoingHandler.GetOutgoingLetter)
|
||||
lettersch.GET("/outgoing", r.letterOutgoingHandler.ListOutgoingLetters)
|
||||
|
||||
lettersch.PUT("/outgoing/:id", r.letterOutgoingHandler.UpdateOutgoingLetter)
|
||||
lettersch.PUT("/outgoing/:id/read", r.letterHandler.MarkOutgoingLetterAsRead)
|
||||
lettersch.DELETE("/outgoing/:id", r.letterOutgoingHandler.DeleteOutgoingLetter)
|
||||
|
||||
@ -21,6 +21,7 @@ type LetterOutgoingService interface {
|
||||
CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||
GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error)
|
||||
ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error)
|
||||
SearchOutgoingLetters(ctx context.Context, req *contract.SearchOutgoingLettersRequest) (*contract.SearchOutgoingLettersResponse, error)
|
||||
UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error)
|
||||
DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error
|
||||
|
||||
@ -99,6 +100,7 @@ func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, re
|
||||
Description: req.Description,
|
||||
PriorityID: req.PriorityID,
|
||||
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
||||
ReceiverName: req.ReceiverName,
|
||||
IssueDate: req.IssueDate,
|
||||
CreatedBy: userID,
|
||||
}
|
||||
@ -352,6 +354,159 @@ func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) SearchOutgoingLetters(ctx context.Context, req *contract.SearchOutgoingLettersRequest) (*contract.SearchOutgoingLettersResponse, error) {
|
||||
userID := getUserIDFromContext(ctx)
|
||||
departmentID := getDepartmentIDFromContext(ctx)
|
||||
|
||||
// Build search filters
|
||||
filters := buildOutgoingSearchFilters(req, userID, departmentID)
|
||||
|
||||
// Execute search with pagination
|
||||
letters, total, err := s.processor.SearchOutgoingLetters(ctx, filters, req.Page, req.Limit, req.SortBy, req.SortOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect IDs for batch loading
|
||||
letterIDs := make([]uuid.UUID, len(letters))
|
||||
priorityIDMap := make(map[uuid.UUID]bool)
|
||||
institutionIDMap := make(map[uuid.UUID]bool)
|
||||
|
||||
for i, letter := range letters {
|
||||
letterIDs[i] = letter.ID
|
||||
if letter.PriorityID != nil {
|
||||
priorityIDMap[*letter.PriorityID] = true
|
||||
}
|
||||
if letter.ReceiverInstitutionID != nil {
|
||||
institutionIDMap[*letter.ReceiverInstitutionID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert maps to slices
|
||||
priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDMap))
|
||||
for id := range priorityIDMap {
|
||||
priorityIDSlice = append(priorityIDSlice, id)
|
||||
}
|
||||
|
||||
institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDMap))
|
||||
for id := range institutionIDMap {
|
||||
institutionIDSlice = append(institutionIDSlice, id)
|
||||
}
|
||||
|
||||
// Parallel batch loading
|
||||
type batchLoadResult struct {
|
||||
attachments map[uuid.UUID][]entities.LetterOutgoingAttachment
|
||||
recipients map[uuid.UUID][]entities.LetterOutgoingRecipient
|
||||
priorities map[uuid.UUID]*entities.Priority
|
||||
institutions map[uuid.UUID]*entities.Institution
|
||||
}
|
||||
|
||||
var result batchLoadResult
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
// Load attachments
|
||||
go func() {
|
||||
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load recipients
|
||||
go func() {
|
||||
result.recipients, err = s.processor.GetBatchRecipients(ctx, letterIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load priorities
|
||||
go func() {
|
||||
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDSlice)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load institutions
|
||||
go func() {
|
||||
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDSlice)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Wait for all goroutines and check for errors
|
||||
for i := 0; i < 4; i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Transform letters with batch loaded data
|
||||
items := make([]contract.OutgoingLetterResponse, len(letters))
|
||||
for i, letter := range letters {
|
||||
// Attach batch loaded data to letter
|
||||
if attachments, ok := result.attachments[letter.ID]; ok {
|
||||
letter.Attachments = attachments
|
||||
}
|
||||
if recipients, ok := result.recipients[letter.ID]; ok {
|
||||
letter.Recipients = recipients
|
||||
}
|
||||
if letter.PriorityID != nil {
|
||||
if priority, ok := result.priorities[*letter.PriorityID]; ok {
|
||||
letter.Priority = priority
|
||||
}
|
||||
}
|
||||
if letter.ReceiverInstitutionID != nil {
|
||||
if institution, ok := result.institutions[*letter.ReceiverInstitutionID]; ok {
|
||||
letter.ReceiverInstitution = institution
|
||||
}
|
||||
}
|
||||
|
||||
items[i] = *transformLetterToResponse(&letter)
|
||||
}
|
||||
|
||||
return &contract.SearchOutgoingLettersResponse{
|
||||
Letters: items,
|
||||
TotalCount: total,
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildOutgoingSearchFilters(req *contract.SearchOutgoingLettersRequest, userID, departmentID uuid.UUID) map[string]interface{} {
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
if req.Query != "" {
|
||||
filters["query"] = req.Query
|
||||
}
|
||||
if req.LetterNumber != "" {
|
||||
filters["letter_number"] = req.LetterNumber
|
||||
}
|
||||
if req.Subject != "" {
|
||||
filters["subject"] = req.Subject
|
||||
}
|
||||
if req.Status != "" {
|
||||
filters["status"] = req.Status
|
||||
}
|
||||
if req.PriorityID != nil {
|
||||
filters["priority_id"] = *req.PriorityID
|
||||
}
|
||||
if req.InstitutionID != nil {
|
||||
filters["receiver_institution_id"] = *req.InstitutionID
|
||||
}
|
||||
if req.CreatedBy != nil {
|
||||
filters["created_by"] = *req.CreatedBy
|
||||
}
|
||||
if req.DateFrom != nil {
|
||||
filters["date_from"] = *req.DateFrom
|
||||
}
|
||||
if req.DateTo != nil {
|
||||
filters["date_to"] = *req.DateTo
|
||||
}
|
||||
|
||||
// Add user/department context filters
|
||||
filters["user_context"] = map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"department_id": departmentID,
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id uuid.UUID, req *contract.UpdateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) {
|
||||
userID := getUserIDFromContext(ctx)
|
||||
|
||||
@ -382,6 +537,9 @@ func (s *LetterOutgoingServiceImpl) UpdateOutgoingLetter(ctx context.Context, id
|
||||
if req.ReferenceNumber != nil {
|
||||
letter.ReferenceNumber = req.ReferenceNumber
|
||||
}
|
||||
if req.ReceiverName != nil {
|
||||
letter.ReceiverName = req.ReceiverName
|
||||
}
|
||||
|
||||
err = s.processor.UpdateOutgoingLetter(ctx, letter, userID)
|
||||
if err != nil {
|
||||
@ -1245,6 +1403,7 @@ func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.Outgoi
|
||||
Description: letter.Description,
|
||||
PriorityID: letter.PriorityID,
|
||||
ReceiverInstitutionID: letter.ReceiverInstitutionID,
|
||||
ReceiverName: letter.ReceiverName,
|
||||
IssueDate: letter.IssueDate,
|
||||
Status: string(letter.Status),
|
||||
ApprovalFlowID: letter.ApprovalFlowID,
|
||||
|
||||
@ -25,6 +25,7 @@ type LetterProcessor interface {
|
||||
CreateIncomingLetter(ctx context.Context, req *contract.CreateIncomingLetterRequest) (*contract.IncomingLetterResponse, error)
|
||||
GetIncomingLetterByID(ctx context.Context, id uuid.UUID) (*contract.IncomingLetterResponse, error)
|
||||
ListIncomingLetters(ctx context.Context, filter repository.ListIncomingLettersFilter, page, limit int) ([]entities.LetterIncoming, int64, error)
|
||||
SearchIncomingLetters(ctx context.Context, filters map[string]interface{}, page, limit int, sortBy, sortOrder string) ([]entities.LetterIncoming, int64, error)
|
||||
GetLetterUnreadCounts(ctx context.Context) (*contract.LetterUnreadCountResponse, error)
|
||||
MarkIncomingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
||||
MarkOutgoingLetterAsRead(ctx context.Context, letterID uuid.UUID) (*contract.MarkLetterReadResponse, error)
|
||||
@ -448,6 +449,208 @@ func (s *LetterServiceImpl) SoftDeleteIncomingLetter(ctx context.Context, id uui
|
||||
return s.processor.SoftDeleteIncomingLetter(ctx, id)
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) SearchIncomingLetters(ctx context.Context, req *contract.SearchIncomingLettersRequest) (*contract.SearchIncomingLettersResponse, error) {
|
||||
appCtx := appcontext.FromGinContext(ctx)
|
||||
userID := appCtx.UserID
|
||||
departmentID := appCtx.DepartmentID
|
||||
|
||||
// Build search filters
|
||||
filters := buildIncomingSearchFilters(req, userID, departmentID)
|
||||
|
||||
// Execute search with pagination
|
||||
letters, total, err := s.processor.SearchIncomingLetters(ctx, filters, req.Page, req.Limit, req.SortBy, req.SortOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Collect IDs for batch loading
|
||||
letterIDs := make([]uuid.UUID, len(letters))
|
||||
priorityIDMap := make(map[uuid.UUID]bool)
|
||||
institutionIDMap := make(map[uuid.UUID]bool)
|
||||
|
||||
for i, letter := range letters {
|
||||
letterIDs[i] = letter.ID
|
||||
if letter.PriorityID != nil {
|
||||
priorityIDMap[*letter.PriorityID] = true
|
||||
}
|
||||
if letter.SenderInstitutionID != nil {
|
||||
institutionIDMap[*letter.SenderInstitutionID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert maps to slices
|
||||
priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDMap))
|
||||
for id := range priorityIDMap {
|
||||
priorityIDSlice = append(priorityIDSlice, id)
|
||||
}
|
||||
|
||||
institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDMap))
|
||||
for id := range institutionIDMap {
|
||||
institutionIDSlice = append(institutionIDSlice, id)
|
||||
}
|
||||
|
||||
// Parallel batch loading
|
||||
type batchLoadResult struct {
|
||||
attachments map[uuid.UUID][]entities.LetterIncomingAttachment
|
||||
recipients map[uuid.UUID]*entities.LetterIncomingRecipient
|
||||
priorities map[uuid.UUID]*entities.Priority
|
||||
institutions map[uuid.UUID]*entities.Institution
|
||||
}
|
||||
|
||||
var result batchLoadResult
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
// Load attachments
|
||||
go func() {
|
||||
result.attachments, err = s.processor.GetBatchAttachments(ctx, letterIDs)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load recipients for user
|
||||
go func() {
|
||||
result.recipients, err = s.processor.GetBatchRecipientsByUser(ctx, letterIDs, userID)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load priorities
|
||||
go func() {
|
||||
result.priorities, err = s.processor.GetBatchPriorities(ctx, priorityIDSlice)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Load institutions
|
||||
go func() {
|
||||
result.institutions, err = s.processor.GetBatchInstitutions(ctx, institutionIDSlice)
|
||||
errChan <- err
|
||||
}()
|
||||
|
||||
// Wait for all goroutines and check for errors
|
||||
for i := 0; i < 4; i++ {
|
||||
if err := <-errChan; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Transform letters with batch loaded data
|
||||
items := make([]contract.IncomingLetterResponse, len(letters))
|
||||
for i, letter := range letters {
|
||||
// Attach batch loaded data
|
||||
attachmentResponses := []contract.IncomingLetterAttachmentResponse{}
|
||||
if attachments, ok := result.attachments[letter.ID]; ok {
|
||||
for _, att := range attachments {
|
||||
attachmentResponses = append(attachmentResponses, contract.IncomingLetterAttachmentResponse{
|
||||
ID: att.ID,
|
||||
FileURL: att.FileURL,
|
||||
FileName: att.FileName,
|
||||
FileType: att.FileType,
|
||||
UploadedAt: att.UploadedAt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var priorityResp *contract.PriorityResponse
|
||||
if letter.PriorityID != nil {
|
||||
if priority, ok := result.priorities[*letter.PriorityID]; ok {
|
||||
priorityResp = &contract.PriorityResponse{
|
||||
ID: priority.ID.String(),
|
||||
Name: priority.Name,
|
||||
Level: priority.Level,
|
||||
CreatedAt: priority.CreatedAt,
|
||||
UpdatedAt: priority.UpdatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var institutionResp *contract.InstitutionResponse
|
||||
if letter.SenderInstitutionID != nil {
|
||||
if institution, ok := result.institutions[*letter.SenderInstitutionID]; ok {
|
||||
institutionResp = &contract.InstitutionResponse{
|
||||
ID: institution.ID.String(),
|
||||
Name: institution.Name,
|
||||
Type: string(institution.Type),
|
||||
Address: institution.Address,
|
||||
ContactPerson: institution.ContactPerson,
|
||||
Phone: institution.Phone,
|
||||
Email: institution.Email,
|
||||
CreatedAt: institution.CreatedAt,
|
||||
UpdatedAt: institution.UpdatedAt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isRead := false
|
||||
if recipient, ok := result.recipients[letter.ID]; ok && recipient.ReadAt != nil {
|
||||
isRead = true
|
||||
}
|
||||
|
||||
items[i] = contract.IncomingLetterResponse{
|
||||
ID: letter.ID,
|
||||
LetterNumber: letter.LetterNumber,
|
||||
ReferenceNumber: letter.ReferenceNumber,
|
||||
Subject: letter.Subject,
|
||||
Description: letter.Description,
|
||||
Priority: priorityResp,
|
||||
SenderInstitution: institutionResp,
|
||||
SenderName: letter.SenderName,
|
||||
ReceivedDate: letter.ReceivedDate,
|
||||
DueDate: letter.DueDate,
|
||||
Status: string(letter.Status),
|
||||
CreatedBy: letter.CreatedBy,
|
||||
CreatedAt: letter.CreatedAt,
|
||||
UpdatedAt: letter.UpdatedAt,
|
||||
Attachments: attachmentResponses,
|
||||
IsRead: isRead,
|
||||
}
|
||||
}
|
||||
|
||||
return &contract.SearchIncomingLettersResponse{
|
||||
Letters: items,
|
||||
TotalCount: total,
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildIncomingSearchFilters(req *contract.SearchIncomingLettersRequest, userID, departmentID uuid.UUID) map[string]interface{} {
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
if req.Query != "" {
|
||||
filters["query"] = req.Query
|
||||
}
|
||||
if req.LetterNumber != "" {
|
||||
filters["letter_number"] = req.LetterNumber
|
||||
}
|
||||
if req.Subject != "" {
|
||||
filters["subject"] = req.Subject
|
||||
}
|
||||
if req.Status != "" {
|
||||
filters["status"] = req.Status
|
||||
}
|
||||
if req.PriorityID != nil {
|
||||
filters["priority_id"] = *req.PriorityID
|
||||
}
|
||||
if req.InstitutionID != nil {
|
||||
filters["sender_institution_id"] = *req.InstitutionID
|
||||
}
|
||||
if req.CreatedBy != nil {
|
||||
filters["created_by"] = *req.CreatedBy
|
||||
}
|
||||
if req.DateFrom != nil {
|
||||
filters["date_from"] = *req.DateFrom
|
||||
}
|
||||
if req.DateTo != nil {
|
||||
filters["date_to"] = *req.DateTo
|
||||
}
|
||||
|
||||
// Add user/department context filters
|
||||
filters["user_context"] = map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"department_id": departmentID,
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contract.CreateLetterDispositionRequest) (*contract.ListDispositionsResponse, error) {
|
||||
log.Printf("[DEBUG] CreateDispositions START - LetterID: %s\n", req.LetterID.String())
|
||||
userID := appcontext.FromGinContext(ctx).UserID
|
||||
|
||||
@ -14,6 +14,7 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L
|
||||
ReferenceNumber: e.ReferenceNumber,
|
||||
Subject: e.Subject,
|
||||
Description: e.Description,
|
||||
SenderName: e.SenderName,
|
||||
ReceivedDate: e.ReceivedDate,
|
||||
DueDate: e.DueDate,
|
||||
Status: string(e.Status),
|
||||
@ -75,6 +76,7 @@ func LetterIncomingEntityToContract(e *entities.LetterIncoming) *contract.Incomi
|
||||
ReferenceNumber: e.ReferenceNumber,
|
||||
Subject: e.Subject,
|
||||
Description: e.Description,
|
||||
SenderName: e.SenderName,
|
||||
ReceivedDate: e.ReceivedDate,
|
||||
DueDate: e.DueDate,
|
||||
Status: string(e.Status),
|
||||
|
||||
31
migrations/000036_add_search_indexes.down.sql
Normal file
31
migrations/000036_add_search_indexes.down.sql
Normal file
@ -0,0 +1,31 @@
|
||||
-- Drop functions
|
||||
DROP FUNCTION IF EXISTS search_outgoing_letters(text);
|
||||
DROP FUNCTION IF EXISTS search_incoming_letters(text);
|
||||
|
||||
-- Drop indexes for outgoing letters
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_letter_number_text;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_subject_text;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_description_text;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_reference_number_text;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_status_created;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_priority_created;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_institution_created;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_created_by;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_issue_date;
|
||||
|
||||
-- Drop indexes for incoming letters
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_letter_number_text;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_subject_text;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_description_text;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_reference_number_text;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_status_created;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_priority_created;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_institution_created;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_created_by;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_received_date;
|
||||
|
||||
-- Drop recipient indexes
|
||||
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_user;
|
||||
DROP INDEX IF EXISTS idx_letter_outgoing_recipients_dept;
|
||||
DROP INDEX IF EXISTS idx_letter_incoming_recipients_user;
|
||||
DROP INDEX IF EXISTS idx_letter_incoming_recipients_dept;
|
||||
63
migrations/000036_add_search_indexes.up.sql
Normal file
63
migrations/000036_add_search_indexes.up.sql
Normal file
@ -0,0 +1,63 @@
|
||||
-- Add indexes for optimized search on outgoing letters
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_letter_number_text ON letters_outgoing USING gin(to_tsvector('simple', letter_number));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_subject_text ON letters_outgoing USING gin(to_tsvector('simple', subject));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_description_text ON letters_outgoing USING gin(to_tsvector('simple', COALESCE(description, '')));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_reference_number_text ON letters_outgoing USING gin(to_tsvector('simple', COALESCE(reference_number, '')));
|
||||
|
||||
-- Composite indexes for common query patterns on outgoing letters
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_status_created ON letters_outgoing(status, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_priority_created ON letters_outgoing(priority_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_institution_created ON letters_outgoing(receiver_institution_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_created_by ON letters_outgoing(created_by, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_issue_date ON letters_outgoing(issue_date DESC) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Add indexes for optimized search on incoming letters
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_letter_number_text ON letters_incoming USING gin(to_tsvector('simple', letter_number));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_subject_text ON letters_incoming USING gin(to_tsvector('simple', subject));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_description_text ON letters_incoming USING gin(to_tsvector('simple', COALESCE(description, '')));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_reference_number_text ON letters_incoming USING gin(to_tsvector('simple', COALESCE(reference_number, '')));
|
||||
|
||||
-- Composite indexes for common query patterns on incoming letters
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_status_created ON letters_incoming(status, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_priority_created ON letters_incoming(priority_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_institution_created ON letters_incoming(sender_institution_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_created_by ON letters_incoming(created_by, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_received_date ON letters_incoming(received_date DESC) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Indexes for recipient lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_user ON letter_outgoing_recipients(user_id, letter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_outgoing_recipients_dept ON letter_outgoing_recipients(department_id, letter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_user ON letter_incoming_recipients(recipient_user_id, letter_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_letter_incoming_recipients_dept ON letter_incoming_recipients(recipient_department_id, letter_id);
|
||||
|
||||
-- Create a function for full-text search on outgoing letters
|
||||
CREATE OR REPLACE FUNCTION search_outgoing_letters(search_query text)
|
||||
RETURNS SETOF letters_outgoing AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT * FROM letters_outgoing
|
||||
WHERE deleted_at IS NULL
|
||||
AND (
|
||||
to_tsvector('simple', letter_number) @@ plainto_tsquery('simple', search_query)
|
||||
OR to_tsvector('simple', subject) @@ plainto_tsquery('simple', search_query)
|
||||
OR to_tsvector('simple', COALESCE(description, '')) @@ plainto_tsquery('simple', search_query)
|
||||
OR to_tsvector('simple', COALESCE(reference_number, '')) @@ plainto_tsquery('simple', search_query)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create a function for full-text search on incoming letters
|
||||
CREATE OR REPLACE FUNCTION search_incoming_letters(search_query text)
|
||||
RETURNS SETOF letters_incoming AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT * FROM letters_incoming
|
||||
WHERE deleted_at IS NULL
|
||||
AND (
|
||||
to_tsvector('simple', letter_number) @@ plainto_tsquery('simple', search_query)
|
||||
OR to_tsvector('simple', subject) @@ plainto_tsquery('simple', search_query)
|
||||
OR to_tsvector('simple', COALESCE(description, '')) @@ plainto_tsquery('simple', search_query)
|
||||
OR to_tsvector('simple', COALESCE(reference_number, '')) @@ plainto_tsquery('simple', search_query)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
12
migrations/000037_add_sender_receiver_names.down.sql
Normal file
12
migrations/000037_add_sender_receiver_names.down.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- Drop indexes
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_sender_name_text;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_receiver_name_text;
|
||||
DROP INDEX IF EXISTS idx_letters_incoming_sender_name;
|
||||
DROP INDEX IF EXISTS idx_letters_outgoing_receiver_name;
|
||||
|
||||
-- Remove columns
|
||||
ALTER TABLE letters_incoming
|
||||
DROP COLUMN IF EXISTS sender_name;
|
||||
|
||||
ALTER TABLE letters_outgoing
|
||||
DROP COLUMN IF EXISTS receiver_name;
|
||||
15
migrations/000037_add_sender_receiver_names.up.sql
Normal file
15
migrations/000037_add_sender_receiver_names.up.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Add sender_name to incoming letters
|
||||
ALTER TABLE letters_incoming
|
||||
ADD COLUMN IF NOT EXISTS sender_name VARCHAR(255);
|
||||
|
||||
-- Add receiver_name to outgoing letters
|
||||
ALTER TABLE letters_outgoing
|
||||
ADD COLUMN IF NOT EXISTS receiver_name VARCHAR(255);
|
||||
|
||||
-- Add indexes for the new fields to support searching
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_sender_name ON letters_incoming(sender_name) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_receiver_name ON letters_outgoing(receiver_name) WHERE deleted_at IS NULL;
|
||||
|
||||
-- Add GIN indexes for full-text search
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_incoming_sender_name_text ON letters_incoming USING gin(to_tsvector('simple', COALESCE(sender_name, '')));
|
||||
CREATE INDEX IF NOT EXISTS idx_letters_outgoing_receiver_name_text ON letters_outgoing USING gin(to_tsvector('simple', COALESCE(receiver_name, '')));
|
||||
Loading…
x
Reference in New Issue
Block a user