add sender and receiver

This commit is contained in:
Aditya Siregar
2025-09-21 20:13:12 +07:00
parent 90da195a2e
commit 24e1d265d3
23 changed files with 854 additions and 0 deletions
+159
View File
@@ -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,
+203
View File
@@ -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