2018 lines
61 KiB
Go
2018 lines
61 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"time"
|
|
|
|
"eslogad-be/internal/appcontext"
|
|
"eslogad-be/internal/contract"
|
|
"eslogad-be/internal/entities"
|
|
"eslogad-be/internal/processor"
|
|
"eslogad-be/internal/repository"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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
|
|
|
|
SubmitForApproval(ctx context.Context, letterID uuid.UUID) error
|
|
ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error
|
|
RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error
|
|
SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
|
ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error
|
|
|
|
AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error
|
|
UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error
|
|
RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error
|
|
|
|
AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error
|
|
RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error
|
|
|
|
CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error)
|
|
UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error
|
|
DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error
|
|
|
|
GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error)
|
|
|
|
// GetLetterApprovals returns all approvals and their status for a letter
|
|
GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error)
|
|
|
|
// GetApprovalDiscussions returns both approvals and discussions for an outgoing letter
|
|
GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error)
|
|
|
|
// GetApprovalTimeline returns a chronological timeline of all events for a letter
|
|
GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error)
|
|
}
|
|
|
|
type LetterOutgoingServiceImpl struct {
|
|
processor processor.LetterOutgoingProcessor
|
|
txManager *repository.TxManager
|
|
validationProcessor processor.LetterValidationProcessor
|
|
creationProcessor processor.LetterCreationProcessor
|
|
approvalProcessor processor.LetterApprovalProcessor
|
|
attachmentProcessor processor.LetterAttachmentProcessor
|
|
recipientProcessor processor.LetterOutgoingRecipientProcessor
|
|
notificationProcessor processor.NotificationProcessor
|
|
activityProcessor processor.LetterActivityProcessor
|
|
}
|
|
|
|
func NewLetterOutgoingService(
|
|
processor processor.LetterOutgoingProcessor,
|
|
txManager *repository.TxManager,
|
|
validationProcessor processor.LetterValidationProcessor,
|
|
creationProcessor processor.LetterCreationProcessor,
|
|
approvalProcessor processor.LetterApprovalProcessor,
|
|
attachmentProcessor processor.LetterAttachmentProcessor,
|
|
recipientProcessor processor.LetterOutgoingRecipientProcessor,
|
|
notificationProcessor processor.NotificationProcessor,
|
|
activityProcessor processor.LetterActivityProcessor,
|
|
) *LetterOutgoingServiceImpl {
|
|
return &LetterOutgoingServiceImpl{
|
|
processor: processor,
|
|
txManager: txManager,
|
|
validationProcessor: validationProcessor,
|
|
creationProcessor: creationProcessor,
|
|
approvalProcessor: approvalProcessor,
|
|
attachmentProcessor: attachmentProcessor,
|
|
recipientProcessor: recipientProcessor,
|
|
notificationProcessor: notificationProcessor,
|
|
activityProcessor: activityProcessor,
|
|
}
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) CreateOutgoingLetter(ctx context.Context, req *contract.CreateOutgoingLetterRequest) (*contract.OutgoingLetterResponse, error) {
|
|
departmentID := getDepartmentIDFromContext(ctx)
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
// Create letter entity
|
|
letter := &entities.LetterOutgoing{
|
|
Subject: req.Subject,
|
|
Description: req.Description,
|
|
PriorityID: req.PriorityID,
|
|
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
|
ReceiverName: req.ReceiverName,
|
|
IssueDate: req.IssueDate,
|
|
CreatedBy: userID,
|
|
ApprovalFlowID: req.ApprovalFlowID,
|
|
}
|
|
|
|
if req.ReferenceNumber != nil {
|
|
letter.ReferenceNumber = req.ReferenceNumber
|
|
}
|
|
|
|
// Prepare attachments
|
|
var attachments []entities.LetterOutgoingAttachment
|
|
if len(req.Attachments) > 0 {
|
|
attachments = make([]entities.LetterOutgoingAttachment, len(req.Attachments))
|
|
for i, a := range req.Attachments {
|
|
attachments[i] = entities.LetterOutgoingAttachment{
|
|
FileURL: a.FileURL,
|
|
FileName: a.FileName,
|
|
FileType: a.FileType,
|
|
UploadedBy: &userID,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Execute creation with transaction in service layer
|
|
err := s.txManager.WithTransaction(ctx, func(txCtx context.Context) error {
|
|
// Step 1: Validate letter
|
|
if err := s.validationProcessor.ValidateCreateOutgoingLetter(txCtx, letter); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 2: Prepare letter for creation (assign approval flow, set initial status)
|
|
if err := s.creationProcessor.PrepareLetterForCreation(txCtx, letter, departmentID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 3: Generate letter number
|
|
if err := s.creationProcessor.GenerateLetterNumber(txCtx, letter); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 4: Create the letter
|
|
if err := s.creationProcessor.CreateLetter(txCtx, letter); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 5: Create approval steps if needed
|
|
if err := s.approvalProcessor.CreateApprovalSteps(txCtx, letter); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 6: Create initial recipients (approval workflow users + department members)
|
|
if err := s.recipientProcessor.CreateInitialRecipients(txCtx, letter, departmentID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 7: Create attachments
|
|
if err := s.attachmentProcessor.CreateAttachments(txCtx, letter.ID, attachments); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 8: Log the activity
|
|
return s.activityProcessor.LogActivity(txCtx, letter.ID, entities.LetterOutgoingActionCreated, userID, nil)
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get the created letter with all relationships
|
|
result, err := s.processor.GetOutgoingLetterByID(ctx, letter.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Send notifications if letter needs approval
|
|
log.Printf("[DEBUG] createOutgoingLetter Finsig")
|
|
log.Printf("[DEBUG] NotificationProcessor is nil: %v", s.notificationProcessor == nil)
|
|
if s.notificationProcessor != nil && len(result.Approvals) > 0 {
|
|
log.Printf("[DEBUG] sendFirstStepApprovalNotifications start")
|
|
go s.sendStepApprovalNotifications(context.Background(), result.ID, result.Subject, 1)
|
|
}
|
|
|
|
return transformLetterToResponse(result), nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) GetOutgoingLetterByID(ctx context.Context, id uuid.UUID) (*contract.OutgoingLetterResponse, error) {
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transformLetterToResponse(letter), nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) ListOutgoingLetters(ctx context.Context, req *contract.ListOutgoingLettersRequest) (*contract.ListOutgoingLettersResponse, error) {
|
|
// Extract user context from gin context
|
|
appCtx := appcontext.FromGinContext(ctx)
|
|
userID := appCtx.UserID
|
|
departmentID := appCtx.DepartmentID
|
|
|
|
offset := (req.Page - 1) * req.Limit
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
|
|
filter := repository.ListOutgoingLettersFilter{
|
|
CreatedBy: req.CreatedBy,
|
|
DepartmentID: req.DepartmentID,
|
|
ReceiverInstitutionID: req.ReceiverInstitutionID,
|
|
PriorityID: req.PriorityID,
|
|
PriorityIDs: req.PriorityIDs,
|
|
UserID: &userID,
|
|
IsRead: req.IsRead,
|
|
}
|
|
|
|
if departmentID != uuid.Nil {
|
|
filter.DepartmentID = &departmentID
|
|
}
|
|
|
|
if len(req.PriorityIDs) > 0 {
|
|
filter.PriorityIDs = req.PriorityIDs
|
|
}
|
|
|
|
if req.Status != "" {
|
|
filter.Status = &req.Status
|
|
}
|
|
if req.Query != "" {
|
|
filter.Query = &req.Query
|
|
}
|
|
if req.SortBy != "" {
|
|
filter.SortBy = &req.SortBy
|
|
}
|
|
if req.SortOrder != "" {
|
|
filter.SortOrder = &req.SortOrder
|
|
}
|
|
|
|
if req.FromDate != "" {
|
|
if date, err := time.Parse("2006-01-02", req.FromDate); err == nil {
|
|
filter.FromDate = &date
|
|
}
|
|
}
|
|
|
|
if req.ToDate != "" {
|
|
if date, err := time.Parse("2006-01-02", req.ToDate); err == nil {
|
|
endOfDay := date.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
|
filter.ToDate = &endOfDay
|
|
}
|
|
}
|
|
|
|
archived := true
|
|
filter.IsArchived = &archived
|
|
if filter.IsArchived != nil {
|
|
filter.IsArchived = req.IsArchived
|
|
}
|
|
|
|
if filter.IsRead != nil {
|
|
filter.IsRead = req.IsRead
|
|
}
|
|
|
|
fmt.Printf("[DEBUG] filter: %v\n", filter)
|
|
|
|
// Get raw letters data
|
|
letters, total, err := s.processor.ListOutgoingLetters(ctx, filter, req.Limit, offset)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Collect IDs for batch loading
|
|
letterIDs := make([]uuid.UUID, len(letters))
|
|
priorityIDs := make(map[uuid.UUID]bool)
|
|
institutionIDs := make(map[uuid.UUID]bool)
|
|
|
|
for i, letter := range letters {
|
|
letterIDs[i] = letter.ID
|
|
if letter.PriorityID != nil {
|
|
priorityIDs[*letter.PriorityID] = true
|
|
}
|
|
if letter.ReceiverInstitutionID != nil {
|
|
institutionIDs[*letter.ReceiverInstitutionID] = true
|
|
}
|
|
}
|
|
|
|
// Convert maps to slices
|
|
priorityIDSlice := make([]uuid.UUID, 0, len(priorityIDs))
|
|
for id := range priorityIDs {
|
|
priorityIDSlice = append(priorityIDSlice, id)
|
|
}
|
|
|
|
institutionIDSlice := make([]uuid.UUID, 0, len(institutionIDs))
|
|
for id := range institutionIDs {
|
|
institutionIDSlice = append(institutionIDSlice, id)
|
|
}
|
|
|
|
// Parallel batch loading
|
|
type batchResult 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
|
|
err error
|
|
}
|
|
|
|
result := batchResult{}
|
|
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
|
|
}
|
|
}
|
|
|
|
isRead := false
|
|
recipientByUser := make(map[uuid.UUID]*entities.LetterOutgoingRecipient)
|
|
recipientByUser, err = s.processor.GetBatchOutgoingRecipientsByUser(ctx, letterIDs, userID)
|
|
if err != nil {
|
|
// Handle error
|
|
return nil, err
|
|
}
|
|
|
|
// Ambil isRead dari recipientByUser berdasarkan letter.ID
|
|
if recipient, exists := recipientByUser[letter.ID]; exists && recipient != nil {
|
|
isRead = recipient.ReadAt != nil
|
|
}
|
|
|
|
response := transformLetterToResponse(&letter)
|
|
response.IsRead = isRead
|
|
items[i] = response
|
|
}
|
|
|
|
return &contract.ListOutgoingLettersResponse{
|
|
Items: items,
|
|
Total: total,
|
|
}, 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)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return nil, gorm.ErrInvalidData
|
|
}
|
|
|
|
if req.Subject != nil {
|
|
letter.Subject = *req.Subject
|
|
}
|
|
if req.Description != nil {
|
|
letter.Description = req.Description
|
|
}
|
|
if req.PriorityID != nil {
|
|
letter.PriorityID = req.PriorityID
|
|
}
|
|
if req.ReceiverInstitutionID != nil {
|
|
letter.ReceiverInstitutionID = req.ReceiverInstitutionID
|
|
}
|
|
if req.IssueDate != nil {
|
|
letter.IssueDate = *req.IssueDate
|
|
}
|
|
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 {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := s.processor.GetOutgoingLetterByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return transformLetterToResponse(result), nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) DeleteOutgoingLetter(ctx context.Context, id uuid.UUID) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
return s.processor.DeleteOutgoingLetter(ctx, id, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) SubmitForApproval(ctx context.Context, letterID uuid.UUID) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
if letter.ApprovalFlowID == nil {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
return s.processor.ProcessApprovalSubmission(ctx, letterID, *letter.ApprovalFlowID, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) ApproveOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ApproveLetterRequest) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusPendingApproval {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
// Get approvals for the current revision only
|
|
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var currentApproval *entities.LetterOutgoingApproval
|
|
for i := range approvals {
|
|
if approvals[i].Status == entities.ApprovalStatusPending {
|
|
step := approvals[i].Step
|
|
if (step.ApproverUserID != nil && *step.ApproverUserID == userID) ||
|
|
(step.ApproverRoleID != nil && userHasRole(ctx, *step.ApproverRoleID)) {
|
|
currentApproval = &approvals[i]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if currentApproval == nil {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
currentApproval.Remarks = req.Remarks
|
|
|
|
err = s.processor.ProcessApproval(ctx, letterID, currentApproval, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send notifications after successful approval
|
|
if s.notificationProcessor != nil {
|
|
// Step approved but not final - notify creator about step completion AND next approvers
|
|
creatorMessage := fmt.Sprintf("Surat keluar '%s' telah disetujui pada tahap %d, menunggu persetujuan tahap berikutnya", letter.Subject, currentApproval.StepOrder)
|
|
go s.sendApprovalNotificationToCreator(context.Background(), letterID, letter.CreatedBy, "Surat Keluar Disetujui Tahap "+fmt.Sprintf("%d", currentApproval.StepOrder), creatorMessage)
|
|
|
|
// Notify next step approvers
|
|
nextStepOrder := currentApproval.StepOrder + 1
|
|
go s.sendStepApprovalNotifications(context.Background(), letterID, letter.Subject, nextStepOrder)
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) RejectOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.RejectLetterRequest) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusPendingApproval {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
// Get approvals for the current revision only
|
|
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var currentApproval *entities.LetterOutgoingApproval
|
|
for i := range approvals {
|
|
if approvals[i].Status == entities.ApprovalStatusPending {
|
|
step := approvals[i].Step
|
|
if (step.ApproverUserID != nil && *step.ApproverUserID == userID) ||
|
|
(step.ApproverRoleID != nil && userHasRole(ctx, *step.ApproverRoleID)) {
|
|
currentApproval = &approvals[i]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if currentApproval == nil {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
currentApproval.Remarks = &req.Reason
|
|
|
|
err = s.processor.ProcessRejection(ctx, letterID, currentApproval, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Send notification to letter creator (rejection always notifies creator)
|
|
if s.notificationProcessor != nil {
|
|
message := fmt.Sprintf("Surat keluar '%s' ditolak pada tahap %d dengan alasan: %s", letter.Subject, currentApproval.StepOrder, req.Reason)
|
|
go s.sendApprovalNotificationToCreator(context.Background(), letterID, letter.CreatedBy, "Surat Keluar Ditolak", message)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) ReviseOutgoingLetter(ctx context.Context, letterID uuid.UUID, req *contract.ReviseLetterRequest) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Can only revise rejected letters
|
|
if letter.Status != entities.LetterOutgoingStatusRejected {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
attachment := entities.LetterOutgoingAttachment{
|
|
LetterID: letterID,
|
|
FileURL: req.FileURL,
|
|
FileName: req.FileName,
|
|
FileType: req.FileType,
|
|
UploadedBy: &userID,
|
|
}
|
|
|
|
err = s.processor.ProcessRevision(ctx, letterID, attachment, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) SendOutgoingLetter(ctx context.Context, letterID uuid.UUID) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusApproved {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
fromStatus := string(entities.LetterOutgoingStatusApproved)
|
|
toStatus := string(entities.LetterOutgoingStatusSent)
|
|
return s.processor.UpdateLetterStatus(ctx, letterID, entities.LetterOutgoingStatusSent, userID, &fromStatus, &toStatus)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) ArchiveOutgoingLetter(ctx context.Context, letterID uuid.UUID) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Can only archive sent letters
|
|
if letter.Status != entities.LetterOutgoingStatusSent {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
// Use the new archive method instead of changing status
|
|
return s.processor.ArchiveOutgoingLetter(ctx, letterID, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) AddRecipients(ctx context.Context, letterID uuid.UUID, req *contract.AddRecipientsRequest) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
recipients := make([]entities.LetterOutgoingRecipient, len(req.Recipients))
|
|
for i, r := range req.Recipients {
|
|
recipients[i] = entities.LetterOutgoingRecipient{
|
|
LetterID: letterID,
|
|
UserID: r.UserID,
|
|
DepartmentID: r.DepartmentID,
|
|
IsPrimary: r.IsPrimary,
|
|
Status: r.Status,
|
|
Flag: r.Flag,
|
|
IsArchived: r.IsArchived,
|
|
}
|
|
}
|
|
|
|
return s.processor.AddRecipients(ctx, letterID, recipients, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) UpdateRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID, req *contract.UpdateRecipientRequest) error {
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
recipient := &entities.LetterOutgoingRecipient{
|
|
ID: recipientID,
|
|
IsPrimary: req.IsPrimary,
|
|
}
|
|
|
|
if req.UserID != nil {
|
|
recipient.UserID = req.UserID
|
|
}
|
|
if req.DepartmentID != nil {
|
|
recipient.DepartmentID = req.DepartmentID
|
|
}
|
|
if req.Status != nil {
|
|
recipient.Status = *req.Status
|
|
}
|
|
if req.Flag != nil {
|
|
recipient.Flag = req.Flag
|
|
}
|
|
if req.IsArchived != nil {
|
|
recipient.IsArchived = *req.IsArchived
|
|
}
|
|
|
|
return s.processor.UpdateRecipient(ctx, recipient)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) RemoveRecipient(ctx context.Context, letterID uuid.UUID, recipientID uuid.UUID) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
return s.processor.RemoveRecipient(ctx, letterID, recipientID, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) AddAttachments(ctx context.Context, letterID uuid.UUID, req *contract.AddAttachmentsRequest) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
attachments := make([]entities.LetterOutgoingAttachment, len(req.Attachments))
|
|
for i, a := range req.Attachments {
|
|
attachments[i] = entities.LetterOutgoingAttachment{
|
|
LetterID: letterID,
|
|
FileURL: a.FileURL,
|
|
FileName: a.FileName,
|
|
FileType: a.FileType,
|
|
UploadedBy: &userID,
|
|
}
|
|
}
|
|
|
|
return s.processor.AddAttachments(ctx, letterID, attachments, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) RemoveAttachment(ctx context.Context, letterID uuid.UUID, attachmentID uuid.UUID) error {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if letter.Status != entities.LetterOutgoingStatusDraft {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
return s.processor.RemoveAttachment(ctx, letterID, attachmentID, userID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) CreateDiscussion(ctx context.Context, letterID uuid.UUID, req *contract.CreateDiscussionRequest) (*contract.DiscussionResponse, error) {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
discussion := &entities.LetterOutgoingDiscussion{
|
|
LetterID: letterID,
|
|
ParentID: req.ParentID,
|
|
UserID: userID,
|
|
Message: req.Message,
|
|
}
|
|
|
|
if req.Mentions != nil {
|
|
discussion.Mentions = req.Mentions
|
|
}
|
|
|
|
var attachments []entities.LetterOutgoingDiscussionAttachment
|
|
if len(req.Attachments) > 0 {
|
|
attachments = make([]entities.LetterOutgoingDiscussionAttachment, len(req.Attachments))
|
|
for i, a := range req.Attachments {
|
|
attachments[i] = entities.LetterOutgoingDiscussionAttachment{
|
|
DiscussionID: discussion.ID,
|
|
FileURL: a.FileURL,
|
|
FileName: a.FileName,
|
|
FileType: a.FileType,
|
|
UploadedBy: &userID,
|
|
}
|
|
}
|
|
}
|
|
|
|
err := s.processor.CreateDiscussion(ctx, discussion, attachments, userID)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := s.processor.GetDiscussionByID(ctx, discussion.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if s.notificationProcessor != nil && req.Mentions != nil {
|
|
go s.sendOutgoingDiscussionMentionNotifications(context.Background(), letterID, userID, req.Mentions, req.Message)
|
|
}
|
|
|
|
return transformDiscussionToResponse(result), nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) UpdateDiscussion(ctx context.Context, discussionID uuid.UUID, req *contract.UpdateDiscussionRequest) error {
|
|
discussion, err := s.processor.GetDiscussionByID(ctx, discussionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userID := getUserIDFromContext(ctx)
|
|
if discussion.UserID != userID {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
discussion.Message = req.Message
|
|
if req.Mentions != nil {
|
|
discussion.Mentions = req.Mentions
|
|
}
|
|
|
|
return s.processor.UpdateDiscussion(ctx, discussion)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) DeleteDiscussion(ctx context.Context, discussionID uuid.UUID) error {
|
|
discussion, err := s.processor.GetDiscussionByID(ctx, discussionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
userID := getUserIDFromContext(ctx)
|
|
if discussion.UserID != userID {
|
|
return gorm.ErrInvalidData
|
|
}
|
|
|
|
return s.processor.DeleteDiscussion(ctx, discussionID)
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) GetLetterApprovalInfo(ctx context.Context, letterID uuid.UUID) (*contract.LetterApprovalInfoResponse, error) {
|
|
userID := getUserIDFromContext(ctx)
|
|
|
|
// Verify letter exists
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get all approvals for this letter's current revision
|
|
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Group approvals by step order and parallel group to understand the workflow
|
|
approvalsByStepAndGroup := make(map[int]map[int][]entities.LetterOutgoingApproval)
|
|
for _, approval := range approvals {
|
|
if approvalsByStepAndGroup[approval.StepOrder] == nil {
|
|
approvalsByStepAndGroup[approval.StepOrder] = make(map[int][]entities.LetterOutgoingApproval)
|
|
}
|
|
approvalsByStepAndGroup[approval.StepOrder][approval.ParallelGroup] = append(
|
|
approvalsByStepAndGroup[approval.StepOrder][approval.ParallelGroup],
|
|
approval,
|
|
)
|
|
}
|
|
|
|
// Find the current active step (lowest step order with pending approvals)
|
|
var currentStepOrder int = -1
|
|
var userApproval *entities.LetterOutgoingApproval
|
|
var canApprove bool
|
|
|
|
// Find the minimum step order that has pending approvals
|
|
for stepOrder, groupApprovals := range approvalsByStepAndGroup {
|
|
stepHasPending := false
|
|
|
|
// Check each parallel group in this step
|
|
for _, groupMembers := range groupApprovals {
|
|
groupHasPending := false
|
|
var userApprovalInGroup *entities.LetterOutgoingApproval
|
|
|
|
// Check if this group has pending approvals and if user is in this group
|
|
for i := range groupMembers {
|
|
approval := groupMembers[i]
|
|
if approval.Status == entities.ApprovalStatusPending {
|
|
groupHasPending = true
|
|
stepHasPending = true
|
|
|
|
// Check if this user is an approver in this group
|
|
if approval.ApproverID != nil && *approval.ApproverID == userID {
|
|
userApprovalInGroup = &approval
|
|
}
|
|
}
|
|
}
|
|
|
|
// If this is the earliest step with pending approvals and user is in a pending group
|
|
if groupHasPending && userApprovalInGroup != nil {
|
|
if currentStepOrder == -1 || stepOrder < currentStepOrder {
|
|
currentStepOrder = stepOrder
|
|
userApproval = userApprovalInGroup
|
|
// User can approve if they're in a parallel group with pending approvals at the current active step
|
|
canApprove = true
|
|
} else if stepOrder == currentStepOrder {
|
|
// Same step order - user can still approve if in parallel group
|
|
userApproval = userApprovalInGroup
|
|
canApprove = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track the lowest step with pending approvals
|
|
if stepHasPending && (currentStepOrder == -1 || stepOrder < currentStepOrder) {
|
|
currentStepOrder = stepOrder
|
|
// Reset canApprove if we found a lower step and user is not in it
|
|
if userApproval == nil || userApproval.StepOrder != stepOrder {
|
|
canApprove = false
|
|
userApproval = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build actions based on eligibility
|
|
var actions []contract.ApprovalAction
|
|
if canApprove && userApproval != nil {
|
|
actions = []contract.ApprovalAction{
|
|
{
|
|
Type: "APPROVE",
|
|
Href: fmt.Sprintf("/api/v1/letters/outgoing/%s/approve", letterID),
|
|
Method: "POST",
|
|
},
|
|
{
|
|
Type: "REJECT",
|
|
Href: fmt.Sprintf("/api/v1/letters/outgoing/%s/reject", letterID),
|
|
Method: "POST",
|
|
},
|
|
}
|
|
}
|
|
|
|
// Add REVISE action if letter is rejected and user is the creator
|
|
if letter.Status == entities.LetterOutgoingStatusRejected && letter.CreatedBy == userID {
|
|
actions = append(actions, contract.ApprovalAction{
|
|
Type: "REVISE",
|
|
Href: fmt.Sprintf("/api/v1/letters/outgoing/%s/revise", letterID),
|
|
Method: "POST",
|
|
})
|
|
}
|
|
|
|
// Determine overall decision status
|
|
decisionStatus := "PENDING"
|
|
|
|
// Check if all required approvals are completed
|
|
allCompleted := true
|
|
hasRejection := false
|
|
for _, approval := range approvals {
|
|
// Check required approvals only
|
|
if approval.IsRequired {
|
|
if approval.Status == entities.ApprovalStatusPending || approval.Status == entities.ApprovalStatusNotStarted {
|
|
allCompleted = false
|
|
}
|
|
if approval.Status == entities.ApprovalStatusRejected {
|
|
hasRejection = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasRejection {
|
|
decisionStatus = "REJECTED"
|
|
} else if allCompleted {
|
|
decisionStatus = "COMPLETED"
|
|
} else if letter.Status == entities.LetterOutgoingStatusPendingApproval {
|
|
decisionStatus = "PENDING"
|
|
}
|
|
|
|
// Determine notes visibility
|
|
notesVisibility := "READONLY"
|
|
if canApprove {
|
|
notesVisibility = "FULL"
|
|
}
|
|
|
|
info := &contract.LetterApprovalInfoResponse{
|
|
IsApproverOnActiveStep: canApprove, // User can approve means they're on the active step
|
|
DecisionStatus: decisionStatus,
|
|
CanApprove: canApprove,
|
|
Actions: actions,
|
|
NotesVisibility: notesVisibility,
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) GetLetterApprovals(ctx context.Context, letterID uuid.UUID) (*contract.GetLetterApprovalsResponse, error) {
|
|
// Get letter details
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get all approvals for this letter (all revisions)
|
|
approvals, err := s.processor.GetAllApprovalsByLetter(ctx, letterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Group approvals by revision number from approval itself
|
|
revisionMap := make(map[int][]entities.LetterOutgoingApproval)
|
|
for _, approval := range approvals {
|
|
revisionMap[approval.RevisionNumber] = append(revisionMap[approval.RevisionNumber], approval)
|
|
}
|
|
|
|
// Get sorted revision numbers
|
|
revisionNumbers := make([]int, 0, len(revisionMap))
|
|
for revNum := range revisionMap {
|
|
revisionNumbers = append(revisionNumbers, revNum)
|
|
}
|
|
sort.Sort(sort.Reverse(sort.IntSlice(revisionNumbers)))
|
|
|
|
// Process each revision
|
|
revisionResponses := make([]contract.OutgoingLetterApprovalRevisionNumberResponse, 0, len(revisionNumbers))
|
|
|
|
totalSteps := 0
|
|
currentStep := 0
|
|
|
|
for _, revNum := range revisionNumbers {
|
|
revisionApprovals := revisionMap[revNum]
|
|
|
|
// Sort approvals within this revision by step order and parallel group
|
|
sort.Slice(revisionApprovals, func(i, j int) bool {
|
|
if revisionApprovals[i].StepOrder != revisionApprovals[j].StepOrder {
|
|
return revisionApprovals[i].StepOrder < revisionApprovals[j].StepOrder
|
|
}
|
|
return revisionApprovals[i].ParallelGroup < revisionApprovals[j].ParallelGroup
|
|
})
|
|
|
|
// Transform to response format
|
|
approvalResponses := make([]contract.EnhancedOutgoingLetterApprovalResponse, 0, len(revisionApprovals))
|
|
|
|
// Only calculate totalSteps and currentStep for the current letter's revision
|
|
if revNum == letter.RevisionNumber {
|
|
stepOrdersSeen := make(map[int]bool)
|
|
|
|
for _, approval := range revisionApprovals {
|
|
// Count unique step orders for total steps
|
|
if !stepOrdersSeen[approval.StepOrder] {
|
|
stepOrdersSeen[approval.StepOrder] = true
|
|
totalSteps++
|
|
}
|
|
|
|
// Determine current step (lowest step with pending status)
|
|
if approval.Status == entities.ApprovalStatusPending && (currentStep == 0 || approval.StepOrder < currentStep) {
|
|
currentStep = approval.StepOrder
|
|
}
|
|
}
|
|
|
|
// If no current step found but there are approvals, check if all are completed
|
|
if currentStep == 0 && len(revisionApprovals) > 0 {
|
|
allCompleted := true
|
|
for _, approval := range revisionApprovals {
|
|
if approval.IsRequired && approval.Status != entities.ApprovalStatusApproved {
|
|
allCompleted = false
|
|
break
|
|
}
|
|
}
|
|
if allCompleted {
|
|
currentStep = totalSteps // All steps completed
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, approval := range revisionApprovals {
|
|
approvalResp := contract.EnhancedOutgoingLetterApprovalResponse{
|
|
ID: approval.ID,
|
|
LetterID: approval.LetterID,
|
|
StepID: approval.StepID,
|
|
StepOrder: approval.StepOrder,
|
|
ParallelGroup: approval.ParallelGroup,
|
|
IsRequired: approval.IsRequired,
|
|
ApproverID: approval.ApproverID,
|
|
RevisionNumber: approval.RevisionNumber,
|
|
Status: string(approval.Status),
|
|
Remarks: approval.Remarks,
|
|
ActedAt: approval.ActedAt,
|
|
CreatedAt: approval.CreatedAt,
|
|
}
|
|
|
|
// Add step details if available
|
|
if approval.Step != nil {
|
|
approvalResp.Step = &contract.ApprovalFlowStepResponse{
|
|
ID: approval.Step.ID,
|
|
StepOrder: approval.Step.StepOrder,
|
|
ParallelGroup: approval.Step.ParallelGroup,
|
|
Required: approval.Step.Required,
|
|
CreatedAt: approval.Step.CreatedAt,
|
|
UpdatedAt: approval.Step.UpdatedAt,
|
|
}
|
|
|
|
// Add approver role if available
|
|
if approval.Step.ApproverRole != nil {
|
|
approvalResp.Step.ApproverRole = &contract.RoleResponse{
|
|
ID: approval.Step.ApproverRole.ID,
|
|
Name: approval.Step.ApproverRole.Name,
|
|
Code: approval.Step.ApproverRole.Code,
|
|
}
|
|
}
|
|
|
|
// Add approver user if available
|
|
if approval.Step.ApproverUser != nil {
|
|
approvalResp.Step.ApproverUser = &contract.UserResponse{
|
|
ID: approval.Step.ApproverUser.ID,
|
|
Name: approval.Step.ApproverUser.Name,
|
|
Email: approval.Step.ApproverUser.Email,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add approver details if available
|
|
if approval.Approver != nil {
|
|
approvalResp.Approver = &contract.UserResponse{
|
|
ID: approval.Approver.ID,
|
|
Name: approval.Approver.Name,
|
|
Email: approval.Approver.Email,
|
|
}
|
|
}
|
|
|
|
approvalResponses = append(approvalResponses, approvalResp)
|
|
}
|
|
|
|
// Add revision response
|
|
revisionResponses = append(revisionResponses, contract.OutgoingLetterApprovalRevisionNumberResponse{
|
|
RevisionNumber: revNum,
|
|
Approvals: approvalResponses,
|
|
})
|
|
}
|
|
|
|
response := &contract.GetLetterApprovalsResponse{
|
|
LetterID: letter.ID,
|
|
LetterNumber: letter.LetterNumber,
|
|
LetterStatus: string(letter.Status),
|
|
TotalSteps: totalSteps,
|
|
CurrentStep: currentStep,
|
|
CurrentRevisionNumber: letter.RevisionNumber,
|
|
Approvals: revisionResponses,
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
func getUserIDFromContext(ctx context.Context) uuid.UUID {
|
|
appCtx := appcontext.FromGinContext(ctx)
|
|
if appCtx != nil {
|
|
return appCtx.UserID
|
|
}
|
|
return uuid.New()
|
|
}
|
|
|
|
func getDepartmentIDFromContext(ctx context.Context) uuid.UUID {
|
|
appCtx := appcontext.FromGinContext(ctx)
|
|
if appCtx != nil {
|
|
return appCtx.DepartmentID
|
|
}
|
|
return uuid.Nil
|
|
}
|
|
|
|
func userHasRole(ctx context.Context, roleID uuid.UUID) bool {
|
|
return false
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) GetApprovalDiscussions(ctx context.Context, letterID uuid.UUID) (*contract.OutgoingLetterApprovalDiscussionsResponse, error) {
|
|
// Get the letter with all related data
|
|
letter, err := s.processor.GetOutgoingLetterWithDetails(ctx, letterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Transform approvals
|
|
approvals := make([]contract.EnhancedOutgoingLetterApprovalResponse, 0, len(letter.Approvals))
|
|
for _, approval := range letter.Approvals {
|
|
approvalResp := contract.EnhancedOutgoingLetterApprovalResponse{
|
|
ID: approval.ID,
|
|
LetterID: approval.LetterID,
|
|
StepID: approval.StepID,
|
|
StepOrder: approval.StepOrder,
|
|
ParallelGroup: approval.ParallelGroup,
|
|
IsRequired: approval.IsRequired,
|
|
ApproverID: approval.ApproverID,
|
|
RevisionNumber: approval.RevisionNumber,
|
|
Status: string(approval.Status),
|
|
Remarks: approval.Remarks,
|
|
ActedAt: approval.ActedAt,
|
|
CreatedAt: approval.CreatedAt,
|
|
}
|
|
|
|
// Add step details if available
|
|
if approval.Step != nil {
|
|
approvalResp.Step = &contract.ApprovalFlowStepResponse{
|
|
ID: approval.Step.ID,
|
|
StepOrder: approval.Step.StepOrder,
|
|
ParallelGroup: approval.Step.ParallelGroup,
|
|
Required: approval.Step.Required,
|
|
CreatedAt: approval.Step.CreatedAt,
|
|
UpdatedAt: approval.Step.UpdatedAt,
|
|
}
|
|
|
|
if approval.Step.ApproverRoleID != nil {
|
|
approvalResp.Step.ApproverRoleID = approval.Step.ApproverRoleID
|
|
}
|
|
if approval.Step.ApproverUserID != nil {
|
|
approvalResp.Step.ApproverUserID = approval.Step.ApproverUserID
|
|
}
|
|
|
|
// Add role information if available
|
|
if approval.Step.ApproverRole != nil {
|
|
approvalResp.Step.ApproverRole = &contract.RoleResponse{
|
|
ID: approval.Step.ApproverRole.ID,
|
|
Name: approval.Step.ApproverRole.Name,
|
|
Code: approval.Step.ApproverRole.Code,
|
|
}
|
|
}
|
|
|
|
// Add user information if available
|
|
if approval.Step.ApproverUser != nil {
|
|
approvalResp.Step.ApproverUser = &contract.UserResponse{
|
|
ID: approval.Step.ApproverUser.ID,
|
|
Name: approval.Step.ApproverUser.Name,
|
|
Email: approval.Step.ApproverUser.Email,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add approver details if available
|
|
if approval.Approver != nil {
|
|
approvalResp.Approver = &contract.UserResponse{
|
|
ID: approval.Approver.ID,
|
|
Name: approval.Approver.Name,
|
|
Email: approval.Approver.Email,
|
|
}
|
|
|
|
// Add profile if available
|
|
if approval.Approver.Profile != nil {
|
|
approvalResp.Approver.Profile = &contract.UserProfileResponse{
|
|
UserID: approval.Approver.Profile.UserID,
|
|
FullName: approval.Approver.Profile.FullName,
|
|
DisplayName: approval.Approver.Profile.DisplayName,
|
|
Phone: approval.Approver.Profile.Phone,
|
|
AvatarURL: approval.Approver.Profile.AvatarURL,
|
|
JobTitle: approval.Approver.Profile.JobTitle,
|
|
EmployeeNo: approval.Approver.Profile.EmployeeNo,
|
|
Bio: approval.Approver.Profile.Bio,
|
|
Timezone: approval.Approver.Profile.Timezone,
|
|
Locale: approval.Approver.Profile.Locale,
|
|
}
|
|
}
|
|
}
|
|
|
|
approvals = append(approvals, approvalResp)
|
|
}
|
|
|
|
// Transform discussions
|
|
discussions := make([]contract.OutgoingLetterDiscussionResponse, 0, len(letter.Discussions))
|
|
for _, discussion := range letter.Discussions {
|
|
// Extract mentioned user IDs from mentions
|
|
mentionedUserIDs := extractMentionedUserIDs(discussion.Mentions)
|
|
|
|
discussionResp := contract.OutgoingLetterDiscussionResponse{
|
|
ID: discussion.ID,
|
|
LetterID: discussion.LetterID,
|
|
ParentID: discussion.ParentID,
|
|
UserID: discussion.UserID,
|
|
Message: discussion.Message,
|
|
Mentions: discussion.Mentions,
|
|
CreatedAt: discussion.CreatedAt,
|
|
UpdatedAt: discussion.UpdatedAt,
|
|
EditedAt: discussion.EditedAt,
|
|
}
|
|
|
|
// Add user details if available
|
|
if discussion.User != nil {
|
|
discussionResp.User = &contract.UserResponse{
|
|
ID: discussion.User.ID,
|
|
Name: discussion.User.Name,
|
|
Email: discussion.User.Email,
|
|
IsActive: discussion.User.IsActive,
|
|
CreatedAt: discussion.User.CreatedAt,
|
|
UpdatedAt: discussion.User.UpdatedAt,
|
|
}
|
|
|
|
// Add profile if available
|
|
if discussion.User.Profile != nil {
|
|
discussionResp.User.Profile = &contract.UserProfileResponse{
|
|
UserID: discussion.User.Profile.UserID,
|
|
FullName: discussion.User.Profile.FullName,
|
|
DisplayName: discussion.User.Profile.DisplayName,
|
|
Phone: discussion.User.Profile.Phone,
|
|
AvatarURL: discussion.User.Profile.AvatarURL,
|
|
JobTitle: discussion.User.Profile.JobTitle,
|
|
EmployeeNo: discussion.User.Profile.EmployeeNo,
|
|
Bio: discussion.User.Profile.Bio,
|
|
Timezone: discussion.User.Profile.Timezone,
|
|
Locale: discussion.User.Profile.Locale,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get mentioned users details
|
|
if len(mentionedUserIDs) > 0 {
|
|
mentionedUsers, _ := s.processor.GetUsersByIDs(ctx, mentionedUserIDs)
|
|
for _, user := range mentionedUsers {
|
|
mentionedUserResp := contract.UserResponse{
|
|
ID: user.ID,
|
|
Name: user.Name,
|
|
Email: user.Email,
|
|
IsActive: user.IsActive,
|
|
CreatedAt: user.CreatedAt,
|
|
UpdatedAt: user.UpdatedAt,
|
|
}
|
|
|
|
if user.Profile != nil {
|
|
mentionedUserResp.Profile = &contract.UserProfileResponse{
|
|
UserID: user.Profile.UserID,
|
|
FullName: user.Profile.FullName,
|
|
DisplayName: user.Profile.DisplayName,
|
|
Timezone: user.Profile.Timezone,
|
|
Locale: user.Profile.Locale,
|
|
}
|
|
}
|
|
|
|
discussionResp.MentionedUsers = append(discussionResp.MentionedUsers, mentionedUserResp)
|
|
}
|
|
}
|
|
|
|
// Add attachments if available
|
|
for _, attachment := range discussion.Attachments {
|
|
attachmentResp := contract.OutgoingLetterDiscussionAttachmentResponse{
|
|
ID: attachment.ID,
|
|
DiscussionID: attachment.DiscussionID,
|
|
FileURL: attachment.FileURL,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
UploadedBy: attachment.UploadedBy,
|
|
UploadedAt: attachment.UploadedAt,
|
|
}
|
|
discussionResp.Attachments = append(discussionResp.Attachments, attachmentResp)
|
|
}
|
|
|
|
discussions = append(discussions, discussionResp)
|
|
}
|
|
|
|
return &contract.OutgoingLetterApprovalDiscussionsResponse{
|
|
Approvals: approvals,
|
|
Discussions: discussions,
|
|
}, nil
|
|
}
|
|
|
|
// Helper function to extract user IDs from mentions
|
|
func extractMentionedUserIDs(mentions map[string]interface{}) []uuid.UUID {
|
|
var userIDs []uuid.UUID
|
|
|
|
if mentions == nil {
|
|
return userIDs
|
|
}
|
|
|
|
if userIDsInterface, ok := mentions["user_ids"]; ok {
|
|
if userIDsList, ok := userIDsInterface.([]interface{}); ok {
|
|
for _, id := range userIDsList {
|
|
if idStr, ok := id.(string); ok {
|
|
if userID, err := uuid.Parse(idStr); err == nil {
|
|
userIDs = append(userIDs, userID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return userIDs
|
|
}
|
|
|
|
func transformLetterToResponse(letter *entities.LetterOutgoing) *contract.OutgoingLetterResponse {
|
|
resp := &contract.OutgoingLetterResponse{
|
|
ID: letter.ID,
|
|
LetterNumber: letter.LetterNumber,
|
|
ReferenceNumber: letter.ReferenceNumber,
|
|
Subject: letter.Subject,
|
|
Description: letter.Description,
|
|
PriorityID: letter.PriorityID,
|
|
ReceiverInstitutionID: letter.ReceiverInstitutionID,
|
|
ReceiverName: letter.ReceiverName,
|
|
IssueDate: letter.IssueDate,
|
|
Status: string(letter.Status),
|
|
ApprovalFlowID: letter.ApprovalFlowID,
|
|
RevisionNumber: letter.RevisionNumber,
|
|
CreatedBy: letter.CreatedBy,
|
|
CreatedAt: letter.CreatedAt,
|
|
UpdatedAt: letter.UpdatedAt,
|
|
}
|
|
|
|
if letter.Priority != nil {
|
|
resp.Priority = &contract.PriorityResponse{
|
|
ID: letter.Priority.ID.String(),
|
|
Name: letter.Priority.Name,
|
|
Level: letter.Priority.Level,
|
|
CreatedAt: letter.Priority.CreatedAt,
|
|
UpdatedAt: letter.Priority.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
if letter.ReceiverInstitution != nil {
|
|
resp.ReceiverInstitution = &contract.InstitutionResponse{
|
|
ID: letter.ReceiverInstitution.ID.String(),
|
|
Name: letter.ReceiverInstitution.Name,
|
|
Type: string(letter.ReceiverInstitution.Type),
|
|
Address: letter.ReceiverInstitution.Address,
|
|
ContactPerson: letter.ReceiverInstitution.ContactPerson,
|
|
Phone: letter.ReceiverInstitution.Phone,
|
|
Email: letter.ReceiverInstitution.Email,
|
|
CreatedAt: letter.ReceiverInstitution.CreatedAt,
|
|
UpdatedAt: letter.ReceiverInstitution.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
if len(letter.Recipients) > 0 {
|
|
resp.Recipients = make([]contract.OutgoingLetterRecipientResponse, len(letter.Recipients))
|
|
for i, recipient := range letter.Recipients {
|
|
recipResp := contract.OutgoingLetterRecipientResponse{
|
|
ID: recipient.ID,
|
|
LetterID: recipient.LetterID,
|
|
UserID: recipient.UserID,
|
|
DepartmentID: recipient.DepartmentID,
|
|
IsPrimary: recipient.IsPrimary,
|
|
Status: recipient.Status,
|
|
ReadAt: recipient.ReadAt,
|
|
Flag: recipient.Flag,
|
|
IsArchived: recipient.IsArchived,
|
|
CreatedAt: recipient.CreatedAt,
|
|
}
|
|
|
|
if recipient.User != nil {
|
|
recipResp.User = &contract.UserResponse{
|
|
ID: recipient.User.ID,
|
|
Name: recipient.User.Name,
|
|
Email: recipient.User.Email,
|
|
}
|
|
}
|
|
|
|
if recipient.Department != nil {
|
|
recipResp.Department = &contract.DepartmentResponse{
|
|
ID: recipient.Department.ID,
|
|
Name: recipient.Department.Name,
|
|
Code: recipient.Department.Code,
|
|
}
|
|
}
|
|
|
|
resp.Recipients[i] = recipResp
|
|
}
|
|
}
|
|
|
|
if len(letter.Attachments) > 0 {
|
|
resp.Attachments = make([]contract.OutgoingLetterAttachmentResponse, len(letter.Attachments))
|
|
for i, attachment := range letter.Attachments {
|
|
resp.Attachments[i] = contract.OutgoingLetterAttachmentResponse{
|
|
ID: attachment.ID,
|
|
FileURL: attachment.FileURL,
|
|
FileName: attachment.FileName,
|
|
FileType: attachment.FileType,
|
|
UploadedAt: attachment.UploadedAt,
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(letter.Approvals) > 0 {
|
|
resp.Approvals = make([]contract.OutgoingLetterApprovalResponse, len(letter.Approvals))
|
|
for i, approval := range letter.Approvals {
|
|
approvalResp := contract.OutgoingLetterApprovalResponse{
|
|
ID: approval.ID,
|
|
StepOrder: approval.StepOrder,
|
|
ParallelGroup: approval.ParallelGroup,
|
|
IsRequired: approval.IsRequired,
|
|
ApproverID: approval.ApproverID,
|
|
Status: string(approval.Status),
|
|
Remarks: approval.Remarks,
|
|
ActedAt: approval.ActedAt,
|
|
CreatedAt: approval.CreatedAt,
|
|
}
|
|
|
|
resp.Approvals[i] = approvalResp
|
|
}
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
func transformDiscussionToResponse(discussion *entities.LetterOutgoingDiscussion) *contract.DiscussionResponse {
|
|
return &contract.DiscussionResponse{
|
|
ID: discussion.ID,
|
|
UserID: discussion.UserID,
|
|
Message: discussion.Message,
|
|
CreatedAt: discussion.CreatedAt,
|
|
UpdatedAt: discussion.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// GetApprovalTimeline generates a chronological timeline of all events for a letter
|
|
func (s *LetterOutgoingServiceImpl) GetApprovalTimeline(ctx context.Context, letterID uuid.UUID) (*contract.ApprovalTimelineResponse, error) {
|
|
// Get letter details
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get approvals and discussions
|
|
approvalDiscussions, err := s.GetApprovalDiscussions(ctx, letterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Create timeline events
|
|
timeline := make([]contract.TimelineEvent, 0)
|
|
|
|
// Add letter creation event
|
|
timeline = append(timeline, contract.TimelineEvent{
|
|
ID: letter.ID.String(),
|
|
Type: "submission",
|
|
Timestamp: letter.CreatedAt,
|
|
Actor: nil, // Could add creator info here if needed
|
|
Action: "created",
|
|
Description: "Letter was created",
|
|
Status: "created",
|
|
})
|
|
|
|
// Add approval events
|
|
for _, approval := range approvalDiscussions.Approvals {
|
|
if approval.ActedAt != nil {
|
|
eventType := "approval"
|
|
action := "approved"
|
|
status := "approved"
|
|
|
|
if approval.Status == "rejected" {
|
|
eventType = "rejection"
|
|
action = "rejected"
|
|
status = "rejected"
|
|
} else if approval.Status == "pending" {
|
|
continue // Skip pending approvals as they haven't happened yet
|
|
}
|
|
|
|
description := fmt.Sprintf("Step %d: %s by %s",
|
|
approval.StepOrder,
|
|
action,
|
|
getApproverName(approval.Approver))
|
|
|
|
timeline = append(timeline, contract.TimelineEvent{
|
|
ID: approval.ID.String(),
|
|
Type: eventType,
|
|
Timestamp: *approval.ActedAt,
|
|
Actor: approval.Approver,
|
|
Action: action,
|
|
Description: description,
|
|
Status: status,
|
|
StepOrder: approval.StepOrder,
|
|
Message: getLetterStringValue(approval.Remarks),
|
|
Data: approval,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Add discussion events
|
|
for _, discussion := range approvalDiscussions.Discussions {
|
|
timeline = append(timeline, contract.TimelineEvent{
|
|
ID: discussion.ID.String(),
|
|
Type: "discussion",
|
|
Timestamp: discussion.CreatedAt,
|
|
Actor: discussion.User,
|
|
Action: "commented",
|
|
Description: fmt.Sprintf("%s added a comment", getUserName(discussion.User)),
|
|
Message: discussion.Message,
|
|
Data: discussion,
|
|
})
|
|
}
|
|
|
|
// Sort timeline by timestamp
|
|
sort.Slice(timeline, func(i, j int) bool {
|
|
return timeline[i].Timestamp.Before(timeline[j].Timestamp)
|
|
})
|
|
|
|
// Calculate summary statistics
|
|
summary := s.calculateTimelineSummary(letter, approvalDiscussions.Approvals, timeline)
|
|
|
|
return &contract.ApprovalTimelineResponse{
|
|
LetterID: letter.ID,
|
|
LetterNumber: letter.LetterNumber,
|
|
Subject: letter.Subject,
|
|
Status: string(letter.Status),
|
|
CreatedAt: letter.CreatedAt,
|
|
Timeline: timeline,
|
|
Summary: summary,
|
|
}, nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) calculateTimelineSummary(
|
|
letter *entities.LetterOutgoing,
|
|
approvals []contract.EnhancedOutgoingLetterApprovalResponse,
|
|
timeline []contract.TimelineEvent,
|
|
) contract.TimelineSummary {
|
|
totalSteps := 0
|
|
completedSteps := 0
|
|
pendingSteps := 0
|
|
currentStep := 0
|
|
|
|
// Count unique step orders
|
|
stepMap := make(map[int]string)
|
|
for _, approval := range approvals {
|
|
if _, exists := stepMap[approval.StepOrder]; !exists {
|
|
stepMap[approval.StepOrder] = approval.Status
|
|
totalSteps++
|
|
}
|
|
|
|
switch approval.Status {
|
|
case "approved":
|
|
if stepMap[approval.StepOrder] == "approved" {
|
|
completedSteps++
|
|
currentStep = approval.StepOrder + 1
|
|
}
|
|
case "pending":
|
|
pendingSteps++
|
|
if currentStep == 0 {
|
|
currentStep = approval.StepOrder
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculate duration
|
|
totalDuration := ""
|
|
averageStepTime := ""
|
|
|
|
if len(timeline) > 0 {
|
|
lastEvent := timeline[len(timeline)-1]
|
|
duration := lastEvent.Timestamp.Sub(letter.CreatedAt)
|
|
totalDuration = formatDuration(duration)
|
|
|
|
if completedSteps > 0 {
|
|
avgDuration := duration / time.Duration(completedSteps)
|
|
averageStepTime = formatDuration(avgDuration)
|
|
}
|
|
}
|
|
|
|
status := "in_progress"
|
|
if letter.Status == entities.LetterOutgoingStatusApproved {
|
|
status = "completed"
|
|
} else if letter.Status == "rejected" {
|
|
status = "rejected"
|
|
}
|
|
|
|
return contract.TimelineSummary{
|
|
TotalSteps: totalSteps,
|
|
CompletedSteps: completedSteps,
|
|
PendingSteps: pendingSteps,
|
|
CurrentStep: currentStep,
|
|
TotalDuration: totalDuration,
|
|
AverageStepTime: averageStepTime,
|
|
Status: status,
|
|
}
|
|
}
|
|
|
|
func getApproverName(user *contract.UserResponse) string {
|
|
if user == nil {
|
|
return "Unknown"
|
|
}
|
|
if user.Name != "" {
|
|
return user.Name
|
|
}
|
|
return user.Email
|
|
}
|
|
|
|
func getUserName(user *contract.UserResponse) string {
|
|
if user == nil {
|
|
return "Unknown"
|
|
}
|
|
if user.Name != "" {
|
|
return user.Name
|
|
}
|
|
return user.Email
|
|
}
|
|
|
|
func getLetterStringValue(s *string) string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return *s
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
days := int(d.Hours() / 24)
|
|
hours := int(d.Hours()) % 24
|
|
minutes := int(d.Minutes()) % 60
|
|
|
|
if days > 0 {
|
|
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
|
|
} else if hours > 0 {
|
|
return fmt.Sprintf("%dh %dm", hours, minutes)
|
|
}
|
|
return fmt.Sprintf("%dm", minutes)
|
|
}
|
|
|
|
func ApplyLetterFilterOverrides(ctx context.Context, filter *repository.ListOutgoingLettersFilter) {
|
|
appCtx := appcontext.FromGinContext(ctx)
|
|
if appCtx == nil {
|
|
return
|
|
}
|
|
|
|
isSuperAdmin := false
|
|
if appCtx.UserRole == "superadmin" || appCtx.UserRole == "admin" {
|
|
isSuperAdmin = true
|
|
}
|
|
|
|
if !isSuperAdmin && appCtx.UserID != uuid.Nil {
|
|
filter.UserID = &appCtx.UserID
|
|
}
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) BulkArchiveOutgoingLetters(ctx context.Context, letterIDs []uuid.UUID) (*contract.BulkArchiveLettersResponse, error) {
|
|
userID := appcontext.FromGinContext(ctx).UserID
|
|
|
|
archivedCount, err := s.processor.BulkArchiveIncomingLettersForUser(ctx, letterIDs, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &contract.BulkArchiveLettersResponse{
|
|
Success: true,
|
|
Message: "Letters archived successfully",
|
|
ArchivedCount: int(archivedCount),
|
|
}, nil
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) sendStepApprovalNotifications(ctx context.Context, letterID uuid.UUID, subject string, stepOrder int) {
|
|
log.Printf("[DEBUG] sendStepApprovalNotifications START - LetterID: %s, StepOrder: %d", letterID.String(), stepOrder)
|
|
|
|
// Get the letter to know the current revision
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to get letter: %v", err)
|
|
return
|
|
}
|
|
|
|
// Get approvals for the current revision only
|
|
approvals, err := s.processor.GetApprovalsByLetterAndRevision(ctx, letterID, letter.RevisionNumber)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to get approvals: %v", err)
|
|
return
|
|
}
|
|
|
|
log.Printf("[DEBUG] Found %d approvals", len(approvals))
|
|
|
|
// Find approvers for the specified step
|
|
for _, approval := range approvals {
|
|
log.Printf("[DEBUG] Checking approval: Step=%d, Status=%s, ApproverID=%v",
|
|
approval.StepOrder, approval.Status, approval.ApproverID)
|
|
|
|
if approval.StepOrder == stepOrder {
|
|
log.Printf("[DEBUG] Sending notification to approver %s for step %d", approval.ApproverID.String(), stepOrder)
|
|
|
|
err := s.notificationProcessor.SendOutgoingLetterNotification(
|
|
ctx,
|
|
letterID,
|
|
*approval.ApproverID,
|
|
"Surat Keluar Perlu Persetujuan",
|
|
fmt.Sprintf("Surat keluar '%s' memerlukan persetujuan Anda pada tahap %d", subject, stepOrder))
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to send notification to approver %s: %v", approval.ApproverID.String(), err)
|
|
} else {
|
|
log.Printf("[DEBUG] Successfully sent notification to approver %s", approval.ApproverID.String())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Kirim notifikasi ke creator
|
|
func (s *LetterOutgoingServiceImpl) sendApprovalNotificationToCreator(ctx context.Context, letterID uuid.UUID, creatorID uuid.UUID, title string, message string) {
|
|
log.Printf("[DEBUG] sendApprovalNotificationToCreator START - LetterID: %s, CreatorID: %s", letterID.String(), creatorID.String())
|
|
|
|
err := s.notificationProcessor.SendOutgoingLetterNotification(
|
|
ctx,
|
|
letterID,
|
|
creatorID,
|
|
title,
|
|
message)
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to send notification to creator %s: %v", creatorID.String(), err)
|
|
} else {
|
|
log.Printf("[DEBUG] Successfully sent notification to creator %s", creatorID.String())
|
|
}
|
|
}
|
|
|
|
func (s *LetterOutgoingServiceImpl) sendOutgoingDiscussionMentionNotifications(ctx context.Context, letterID uuid.UUID, senderUserID uuid.UUID, mentions map[string]interface{}, message string) {
|
|
log.Printf("[DEBUG] sendOutgoingDiscussionMentionNotifications START - LetterID: %s", letterID.String())
|
|
|
|
// Extract user_ids dari mentions
|
|
userIDs := s.extractUserIDsFromMentions(mentions)
|
|
if len(userIDs) == 0 {
|
|
log.Printf("[DEBUG] No user IDs found in mentions")
|
|
return
|
|
}
|
|
|
|
log.Printf("[DEBUG] Found %d mentioned users", len(userIDs))
|
|
|
|
// Get letter details untuk notification
|
|
letter, err := s.processor.GetOutgoingLetterByID(ctx, letterID)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to get letter details: %v", err)
|
|
return
|
|
}
|
|
|
|
// Get sender user name dari context (bisa juga dari user service)
|
|
appContext := appcontext.FromGinContext(ctx)
|
|
senderName := appContext.UserName
|
|
if senderName == "" {
|
|
senderName = "Seseorang" // fallback jika nama tidak tersedia
|
|
}
|
|
|
|
// Kirim notification ke setiap mentioned user
|
|
for _, mentionedUserID := range userIDs {
|
|
// Jangan kirim notification ke sender sendiri
|
|
if mentionedUserID == senderUserID {
|
|
continue
|
|
}
|
|
|
|
subject := "Anda Disebutkan dalam Diskusi Surat Keluar"
|
|
notificationMessage := fmt.Sprintf("%s menyebutkan Anda dalam diskusi surat keluar: %s", senderName, letter.Subject)
|
|
|
|
err := s.notificationProcessor.SendOutgoingLetterNotification(
|
|
ctx,
|
|
letterID,
|
|
mentionedUserID,
|
|
subject,
|
|
notificationMessage)
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to send mention notification to user %s: %v", mentionedUserID.String(), err)
|
|
} else {
|
|
log.Printf("[DEBUG] Successfully sent mention notification to user %s", mentionedUserID.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function untuk extract user IDs dari mentions map
|
|
func (s *LetterOutgoingServiceImpl) extractUserIDsFromMentions(mentions map[string]interface{}) []uuid.UUID {
|
|
userIDs := make([]uuid.UUID, 0)
|
|
|
|
if mentions == nil {
|
|
return userIDs
|
|
}
|
|
|
|
if userIDsInterface, exists := mentions["user_ids"]; exists {
|
|
switch userIDsValue := userIDsInterface.(type) {
|
|
case []interface{}:
|
|
for _, userIDInterface := range userIDsValue {
|
|
if userIDStr, ok := userIDInterface.(string); ok {
|
|
if userID, err := uuid.Parse(userIDStr); err == nil {
|
|
userIDs = append(userIDs, userID)
|
|
}
|
|
}
|
|
}
|
|
case []string:
|
|
for _, userIDStr := range userIDsValue {
|
|
if userID, err := uuid.Parse(userIDStr); err == nil {
|
|
userIDs = append(userIDs, userID)
|
|
}
|
|
}
|
|
case []uuid.UUID:
|
|
userIDs = userIDsValue
|
|
}
|
|
}
|
|
|
|
return userIDs
|
|
}
|