diff --git a/eslogad-backend b/eslogad-backend deleted file mode 100755 index 789f59a..0000000 Binary files a/eslogad-backend and /dev/null differ diff --git a/eslogad-be b/eslogad-be deleted file mode 100755 index 5285e4a..0000000 Binary files a/eslogad-be and /dev/null differ diff --git a/internal/contract/letter_contract.go b/internal/contract/letter_contract.go index 36e2cbb..333f899 100644 --- a/internal/contract/letter_contract.go +++ b/internal/contract/letter_contract.go @@ -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"` diff --git a/internal/contract/letter_outgoing_contract.go b/internal/contract/letter_outgoing_contract.go index 4ce134a..8317247 100644 --- a/internal/contract/letter_outgoing_contract.go +++ b/internal/contract/letter_outgoing_contract.go @@ -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"` } diff --git a/internal/entities/letter_incoming.go b/internal/entities/letter_incoming.go index 76319f0..44de3f9 100644 --- a/internal/entities/letter_incoming.go +++ b/internal/entities/letter_incoming.go @@ -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"` diff --git a/internal/entities/letter_outgoing.go b/internal/entities/letter_outgoing.go index 66f265a..6d81244 100644 --- a/internal/entities/letter_outgoing.go +++ b/internal/entities/letter_outgoing.go @@ -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"` diff --git a/internal/handler/letter_handler.go b/internal/handler/letter_handler.go index 455c0fa..116915c 100644 --- a/internal/handler/letter_handler.go +++ b/internal/handler/letter_handler.go @@ -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 { diff --git a/internal/handler/letter_outgoing_handler.go b/internal/handler/letter_outgoing_handler.go index ce2740e..d3e61bd 100644 --- a/internal/handler/letter_outgoing_handler.go +++ b/internal/handler/letter_outgoing_handler.go @@ -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 { diff --git a/internal/processor/letter_outgoing_processor.go b/internal/processor/letter_outgoing_processor.go index 9f6d470..469dede 100644 --- a/internal/processor/letter_outgoing_processor.go +++ b/internal/processor/letter_outgoing_processor.go @@ -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) } diff --git a/internal/processor/letter_processor.go b/internal/processor/letter_processor.go index bffdcc2..97091bc 100644 --- a/internal/processor/letter_processor.go +++ b/internal/processor/letter_processor.go @@ -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) diff --git a/internal/repository/letter_outgoing_repository.go b/internal/repository/letter_outgoing_repository.go index dc97165..a974998 100644 --- a/internal/repository/letter_outgoing_repository.go +++ b/internal/repository/letter_outgoing_repository.go @@ -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 diff --git a/internal/repository/letter_repository.go b/internal/repository/letter_repository.go index 9d72b46..415e4da 100644 --- a/internal/repository/letter_repository.go +++ b/internal/repository/letter_repository.go @@ -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 { diff --git a/internal/router/health_handler.go b/internal/router/health_handler.go index 3f4c9e4..9ae7404 100644 --- a/internal/router/health_handler.go +++ b/internal/router/health_handler.go @@ -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) diff --git a/internal/router/router.go b/internal/router/router.go index 97210e5..4591b38 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -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) diff --git a/internal/service/letter_outgoing_service.go b/internal/service/letter_outgoing_service.go index af02f9c..a49a5e4 100644 --- a/internal/service/letter_outgoing_service.go +++ b/internal/service/letter_outgoing_service.go @@ -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, diff --git a/internal/service/letter_service.go b/internal/service/letter_service.go index cd80419..b93063d 100644 --- a/internal/service/letter_service.go +++ b/internal/service/letter_service.go @@ -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 diff --git a/internal/transformer/letter_transformer.go b/internal/transformer/letter_transformer.go index 359ff29..8da9f8e 100644 --- a/internal/transformer/letter_transformer.go +++ b/internal/transformer/letter_transformer.go @@ -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), diff --git a/main b/main deleted file mode 100755 index b1a15b3..0000000 Binary files a/main and /dev/null differ diff --git a/migrations/000036_add_search_indexes.down.sql b/migrations/000036_add_search_indexes.down.sql new file mode 100644 index 0000000..7dbba09 --- /dev/null +++ b/migrations/000036_add_search_indexes.down.sql @@ -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; \ No newline at end of file diff --git a/migrations/000036_add_search_indexes.up.sql b/migrations/000036_add_search_indexes.up.sql new file mode 100644 index 0000000..adcf49a --- /dev/null +++ b/migrations/000036_add_search_indexes.up.sql @@ -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; \ No newline at end of file diff --git a/migrations/000037_add_sender_receiver_names.down.sql b/migrations/000037_add_sender_receiver_names.down.sql new file mode 100644 index 0000000..85c4676 --- /dev/null +++ b/migrations/000037_add_sender_receiver_names.down.sql @@ -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; \ No newline at end of file diff --git a/migrations/000037_add_sender_receiver_names.up.sql b/migrations/000037_add_sender_receiver_names.up.sql new file mode 100644 index 0000000..3dca968 --- /dev/null +++ b/migrations/000037_add_sender_receiver_names.up.sql @@ -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, ''))); \ No newline at end of file diff --git a/server b/server deleted file mode 100755 index 1076ea1..0000000 Binary files a/server and /dev/null differ