From 92440953cb8ba2678a60fef1b97ca8c0d2466953 Mon Sep 17 00:00:00 2001 From: efrilm Date: Fri, 17 Oct 2025 00:04:21 +0700 Subject: [PATCH] update api incoming letter --- internal/contract/letter_contract.go | 27 +++--- internal/processor/letter_processor.go | 17 +++- internal/repository/letter_repository.go | 27 ++++++ internal/service/letter_service.go | 96 ++++++++++++---------- internal/transformer/letter_transformer.go | 20 ++++- 5 files changed, 128 insertions(+), 59 deletions(-) diff --git a/internal/contract/letter_contract.go b/internal/contract/letter_contract.go index 70122b1..8960054 100644 --- a/internal/contract/letter_contract.go +++ b/internal/contract/letter_contract.go @@ -7,19 +7,19 @@ import ( ) 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"` + 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 { @@ -77,6 +77,7 @@ type IncomingLetterResponse struct { UpdatedAt time.Time `json:"updated_at"` Attachments []IncomingLetterAttachmentResponse `json:"attachments"` IsRead bool `json:"is_read"` + Dispositions []EnhancedDispositionResponse `json:"dispositions"` } type UpdateIncomingLetterRequest struct { diff --git a/internal/processor/letter_processor.go b/internal/processor/letter_processor.go index 3d9fb82..97aac5b 100644 --- a/internal/processor/letter_processor.go +++ b/internal/processor/letter_processor.go @@ -101,6 +101,7 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid return nil, err } atts, _ := p.attachRepo.ListByLetter(ctx, id) + dispo, _ := p.dispositionRepo.ListByLetter(ctx, id) var pr *entities.Priority if entity.PriorityID != nil && p.priorityRepo != nil { if got, err := p.priorityRepo.Get(ctx, *entity.PriorityID); err == nil { @@ -119,10 +120,11 @@ func (p *LetterProcessorImpl) GetIncomingLetterByID(ctx context.Context, id uuid if p.recipientRepo != nil { if recipient, err := p.recipientRepo.GetByLetterAndUser(ctx, id, userID); err == nil { isRead = recipient.ReadAt != nil + fmt.Printf("Recipient: %+v\n", recipient) } } - resp := transformer.LetterEntityToContract(entity, atts, pr, inst) + resp := transformer.LetterEntityToContract(entity, atts, dispo, pr, inst) resp.IsRead = isRead // Include created_by if the current user is the creator @@ -209,6 +211,13 @@ func (p *LetterProcessorImpl) GetBatchAttachments(ctx context.Context, letterIDs return p.attachRepo.ListByLetterIDs(ctx, letterIDs) } +func (p *LetterProcessorImpl) GetBatchDispositions(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, error) { + if p.dispositionRepo == nil || len(letterIDs) == 0 { + return make(map[uuid.UUID][]entities.LetterIncomingDisposition), nil + } + return p.dispositionRepo.ListByLetterIDs(ctx, letterIDs) +} + func (p *LetterProcessorImpl) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) { if p.priorityRepo == nil || len(priorityIDs) == 0 { return make(map[uuid.UUID]*entities.Priority), nil @@ -290,6 +299,7 @@ func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid. } } atts, _ := p.attachRepo.ListByLetter(txCtx, id) + dispo, _ := p.dispositionRepo.ListByLetter(txCtx, id) var pr *entities.Priority if entity.PriorityID != nil && p.priorityRepo != nil { if got, err := p.priorityRepo.Get(txCtx, *entity.PriorityID); err == nil { @@ -302,7 +312,7 @@ func (p *LetterProcessorImpl) UpdateIncomingLetter(ctx context.Context, id uuid. inst = got } } - out = transformer.LetterEntityToContract(entity, atts, pr, inst) + out = transformer.LetterEntityToContract(entity, atts, dispo, pr, inst) return nil }) if err != nil { @@ -601,6 +611,7 @@ func (p *LetterProcessorImpl) SearchIncomingLetters(ctx context.Context, filters func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *entities.LetterIncoming) (*contract.IncomingLetterResponse, error) { savedAttachments, _ := p.attachRepo.ListByLetter(ctx, entity.ID) + dispo, _ := p.dispositionRepo.ListByLetter(ctx, entity.ID) var pr *entities.Priority if entity.PriorityID != nil && p.priorityRepo != nil { @@ -616,7 +627,7 @@ func (p *LetterProcessorImpl) buildLetterResponse(ctx context.Context, entity *e } } - return transformer.LetterEntityToContract(entity, savedAttachments, pr, inst), nil + return transformer.LetterEntityToContract(entity, savedAttachments, dispo, pr, inst), nil } func (p *LetterProcessorImpl) BulkArchiveIncomingLetters(ctx context.Context, letterIDs []uuid.UUID) (int64, error) { diff --git a/internal/repository/letter_repository.go b/internal/repository/letter_repository.go index 786c702..6369295 100644 --- a/internal/repository/letter_repository.go +++ b/internal/repository/letter_repository.go @@ -463,6 +463,33 @@ func (r *LetterIncomingDispositionRepository) ListByLetter(ctx context.Context, return list, nil } +func (r *LetterIncomingDispositionRepository) ListByLetterIDs(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, error) { + if len(letterIDs) == 0 { + return make(map[uuid.UUID][]entities.LetterIncomingDisposition), nil + } + + db := DBFromContext(ctx, r.db) + var dispositions []entities.LetterIncomingDisposition + if err := db.WithContext(ctx).Where("letter_id IN ?", letterIDs). + Preload("Department"). + Preload("Departments.Department"). + Preload("ActionSelections.Action"). + Preload("DispositionNotes.User"). + Order("created_at ASC"). + Find(&dispositions).Error; err != nil { + return nil, err + } + + // Group by letter ID + result := make(map[uuid.UUID][]entities.LetterIncomingDisposition) + for i := range dispositions { // Gunakan index, bukan value + letterID := dispositions[i].LetterID + result[letterID] = append(result[letterID], dispositions[i]) + } + + return result, nil +} + func (r *LetterIncomingDispositionRepository) GetByLetterIncomingID(ctx context.Context, letterIncomingID uuid.UUID) ([]entities.LetterIncomingDisposition, error) { db := DBFromContext(ctx, r.db) var list []entities.LetterIncomingDisposition diff --git a/internal/service/letter_service.go b/internal/service/letter_service.go index 7ec599f..ca18654 100644 --- a/internal/service/letter_service.go +++ b/internal/service/letter_service.go @@ -37,6 +37,7 @@ type LetterProcessor interface { // Batch loading methods GetBatchAttachments(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingAttachment, error) + GetBatchDispositions(ctx context.Context, letterIDs []uuid.UUID) (map[uuid.UUID][]entities.LetterIncomingDisposition, error) GetBatchPriorities(ctx context.Context, priorityIDs []uuid.UUID) (map[uuid.UUID]*entities.Priority, error) GetBatchInstitutions(ctx context.Context, institutionIDs []uuid.UUID) (map[uuid.UUID]*entities.Institution, error) GetBatchRecipientsByUser(ctx context.Context, letterIDs []uuid.UUID, userID uuid.UUID) (map[uuid.UUID]*entities.LetterIncomingRecipient, error) @@ -347,6 +348,7 @@ func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contra priorities map[uuid.UUID]*entities.Priority institutions map[uuid.UUID]*entities.Institution recipients map[uuid.UUID]*entities.LetterIncomingRecipient + dispostions map[uuid.UUID][]entities.LetterIncomingDisposition err error } @@ -358,9 +360,10 @@ func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contra priorities: make(map[uuid.UUID]*entities.Priority), institutions: make(map[uuid.UUID]*entities.Institution), recipients: make(map[uuid.UUID]*entities.LetterIncomingRecipient), + dispostions: make(map[uuid.UUID][]entities.LetterIncomingDisposition), } - errChan := make(chan error, 4) + errChan := make(chan error, 5) go func() { var err error @@ -386,7 +389,13 @@ func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contra errChan <- err }() - for i := 0; i < 4; i++ { + go func() { + var err error + result.dispostions, err = s.processor.GetBatchDispositions(ctx, letterIDs) + errChan <- err + }() + + for i := 0; i < 5; i++ { if err := <-errChan; err != nil { // Batch load error, continue anyway } @@ -414,12 +423,17 @@ func (s *LetterServiceImpl) ListIncomingLetters(ctx context.Context, req *contra institution = batchData.institutions[*letter.SenderInstitutionID] } + dispositions := batchData.dispostions[letter.ID] + if dispositions == nil { + dispositions = []entities.LetterIncomingDisposition{} + } + isRead := false if recipient, exists := batchData.recipients[letter.ID]; exists && recipient != nil { isRead = recipient.ReadAt != nil } - resp := transformer.LetterEntityToContract(&letter, attachments, priority, institution) + resp := transformer.LetterEntityToContract(&letter, attachments, dispositions, priority, institution) resp.IsRead = isRead respList = append(respList, *resp) } @@ -697,7 +711,7 @@ func (s *LetterServiceImpl) CreateDispositions(ctx context.Context, req *contrac if len(recipients) > 0 { go s.sendDispositionNotifications(context.Background(), req.LetterID, recipients) } - + // Send notification to letter creator about new disposition go s.sendDispositionCreatorNotification(context.Background(), req.LetterID, userID) } @@ -711,7 +725,7 @@ func (s *LetterServiceImpl) GetEnhancedDispositionsByLetter(ctx context.Context, func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { userID := appcontext.FromGinContext(ctx).UserID - + var result *contract.LetterDiscussionResponse err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error @@ -719,34 +733,34 @@ func (s *LetterServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid. if err != nil { return err } - + // Log activity for discussion creation if s.activityLogger != nil && result != nil { if err := s.activityLogger.LogLetterDispositionStatusUpdate(txCtx, letterID, userID, "discussion_created"); err != nil { // Don't fail the transaction for logging errors } } - + return nil }) - + if err != nil { return nil, err } - + // Send notifications to mentioned users asynchronously if s.notificationProcessor != nil && req.Mentions != nil { go s.sendDiscussionMentionNotifications(context.Background(), letterID, userID, req.Mentions, req.Message) } - + return result, nil } func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid.UUID, discussionID uuid.UUID, req *contract.UpdateLetterDiscussionRequest) (*contract.LetterDiscussionResponse, error) { userID := appcontext.FromGinContext(ctx).UserID - + var result *contract.LetterDiscussionResponse - + err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error var oldMessage string @@ -754,7 +768,7 @@ func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid. if err != nil { return err } - + // Log activity for discussion update (could use oldMessage for more detailed logging) if s.activityLogger != nil && result != nil { // Create a simple activity log - oldMessage could be included in a more detailed log @@ -763,14 +777,14 @@ func (s *LetterServiceImpl) UpdateDiscussion(ctx context.Context, letterID uuid. // Don't fail the transaction for logging errors } } - + return nil }) - + if err != nil { return nil, err } - + return result, nil } @@ -780,14 +794,14 @@ func (s *LetterServiceImpl) GetDepartmentDispositionStatus(ctx context.Context, func (s *LetterServiceImpl) UpdateDispositionStatus(ctx context.Context, req *contract.UpdateDispositionStatusRequest) (*contract.DepartmentDispositionStatusResponse, error) { var result *contract.DepartmentDispositionStatusResponse - + err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error { var err error result, err = s.processor.UpdateDispositionStatus(txCtx, req) if err != nil { return err } - + // Log activity for disposition status update if s.activityLogger != nil && result != nil { userID := appcontext.FromGinContext(txCtx).UserID @@ -795,19 +809,19 @@ func (s *LetterServiceImpl) UpdateDispositionStatus(ctx context.Context, req *co // Don't fail the transaction for logging errors } } - + return nil }) - + if err != nil { return nil, err } - + // Send notification to letter creator asynchronously if s.notificationProcessor != nil && result != nil { go s.sendDispositionStatusUpdateNotification(context.Background(), req.LetterIncomingID, req.Status) } - + return result, nil } @@ -840,34 +854,34 @@ func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Conte if len(userIDs) == 0 { return } - + // Get letter details for notification letter, err := s.processor.GetIncomingLetterByID(ctx, letterID) if err != nil { return } - + // Get sender user name (you might need to implement this) appContext := appcontext.FromGinContext(ctx) senderName := appContext.UserName // or get from user service - + // Send notification to each mentioned user for _, mentionedUserID := range userIDs { // Don't send notification to the sender themselves if mentionedUserID == senderUserID { continue } - + subject := "Anda Disebutkan dalam Diskusi" notificationMessage := fmt.Sprintf("%s menyebutkan Anda dalam diskusi surat: %s", senderName, letter.Subject) - + err := s.notificationProcessor.SendIncomingLetterNotification( ctx, letterID, mentionedUserID, subject, notificationMessage) - + if err != nil { // Log error but continue with other notifications } @@ -876,7 +890,7 @@ func (s *LetterServiceImpl) sendDiscussionMentionNotifications(ctx context.Conte func (s *LetterServiceImpl) extractUserIDsFromMentions(mentions map[string]interface{}) []uuid.UUID { userIDs := make([]uuid.UUID, 0) - + if userIDsInterface, exists := mentions["user_ids"]; exists { switch userIDsValue := userIDsInterface.(type) { case []interface{}: @@ -895,7 +909,7 @@ func (s *LetterServiceImpl) extractUserIDsFromMentions(mentions map[string]inter } } } - + return userIDs } @@ -909,8 +923,6 @@ func (s *LetterServiceImpl) sendDispositionCreatorNotification(ctx context.Conte fmt.Printf("[DEBUG] Starting sendDispositionCreatorNotification for letterID: %s\n", letterID.String()) fmt.Printf("[DEBUG] Successfully retrieved letter: %s\n", letter.Subject) fmt.Printf("[DEBUG] Successfully retrieved letter: %s\n", letter.CreatedBy) - - letterCreatorID := letter.CreatedBy @@ -924,7 +936,7 @@ func (s *LetterServiceImpl) sendDispositionCreatorNotification(ctx context.Conte dispositionCreatorName := appContext.UserName subject := "Disposisi Baru pada Surat Anda" - message := fmt.Sprintf("Surat yang Anda buat telah didisposisikan %s: %s", + message := fmt.Sprintf("Surat yang Anda buat telah didisposisikan %s: %s", dispositionCreatorName, letter.Subject) err = s.notificationProcessor.SendIncomingLetterNotification( @@ -947,19 +959,19 @@ func (s *LetterServiceImpl) sendDispositionStatusUpdateNotification(ctx context. // Log error but don't fail return } - + // Get current user context (the one updating the status) appContext := appcontext.FromGinContext(ctx) updaterUserID := appContext.UserID updaterName := appContext.UserName - + letterCreatorID := letter.CreatedBy - + // Don't send notification if the updater is the same as letter creator if letterCreatorID == updaterUserID { return } - + // Create status-specific notification message var statusMessage string switch newStatus { @@ -974,20 +986,20 @@ func (s *LetterServiceImpl) sendDispositionStatusUpdateNotification(ctx context. default: statusMessage = fmt.Sprintf("diubah statusnya menjadi %s", newStatus) } - + subject := "Status Disposisi Surat Diperbarui" - message := fmt.Sprintf("Disposisi surat '%s' %s %s", + message := fmt.Sprintf("Disposisi surat '%s' %s %s", letter.Subject, statusMessage, updaterName) - + err = s.notificationProcessor.SendIncomingLetterNotification( ctx, letterID, letterCreatorID, subject, message) - + if err != nil { // Log error but don't fail the operation // You might want to add proper logging here } -} \ No newline at end of file +} diff --git a/internal/transformer/letter_transformer.go b/internal/transformer/letter_transformer.go index 7a24f35..cf95154 100644 --- a/internal/transformer/letter_transformer.go +++ b/internal/transformer/letter_transformer.go @@ -7,7 +7,7 @@ import ( "github.com/google/uuid" ) -func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment, refs ...interface{}) *contract.IncomingLetterResponse { +func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.LetterIncomingAttachment, dispositions []entities.LetterIncomingDisposition, refs ...interface{}) *contract.IncomingLetterResponse { resp := &contract.IncomingLetterResponse{ ID: e.ID, LetterNumber: e.LetterNumber, @@ -24,6 +24,7 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, Attachments: make([]contract.IncomingLetterAttachmentResponse, 0, len(attachments)), + Dispositions: make([]contract.EnhancedDispositionResponse, 0, len(dispositions)), } // optional refs: allow passing already-fetched related objects @@ -65,6 +66,23 @@ func LetterEntityToContract(e *entities.LetterIncoming, attachments []entities.L UploadedAt: a.UploadedAt, }) } + + for _, d := range dispositions { + resp.Dispositions = append(resp.Dispositions, contract.EnhancedDispositionResponse{ + ID: d.ID, + LetterID: d.LetterID, + DepartmentID: d.DepartmentID, + Notes: d.Notes, + ReadAt: d.ReadAt, + CreatedBy: d.CreatedBy, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + Departments: DispositionDepartmentsWithDetailsToContract(d.Departments), + Actions: DispositionActionSelectionsWithDetailsToContract(d.ActionSelections), + DispositionNotes: DispositionNotesWithDetailsToContract(d.DispositionNotes), + Department: DepartmentToContract(d.Department), + }) + } return resp }