diff --git a/internal/entities/phase.go b/internal/entities/phase.go index 178ed695..0d924a1a 100644 --- a/internal/entities/phase.go +++ b/internal/entities/phase.go @@ -7,12 +7,13 @@ import ( ) type Phases struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null"` - IsActive bool `gorm:"not null;default:true"` - Category string `gorm:"type:category_code;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + IsActive bool `gorm:"not null;default:true"` + Category string `gorm:"type:category_code;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + ActivityCount int `gorm:"-" json:"-"` Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"` } diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index 7c92664a..5819d03e 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -74,6 +74,7 @@ func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error { Name: name, Status: status, Category: item.Category, + RejectReason: item.RejectReason, Date: item.Date, Kandang: kandang, CreatedUser: nil, @@ -303,12 +304,22 @@ func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error { return err } + documentDTOs := make([]dto.DailyChecklistDocumentDTO, len(detail.DocumentURLs)) + for i, doc := range detail.DocumentURLs { + documentDTOs[i] = dto.DailyChecklistDocumentDTO{ + Id: doc.ID, + Name: doc.Name, + Size: doc.Size, + URL: doc.URL, + } + } + return c.Status(fiber.StatusOK). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", Message: "Get dailyChecklist successfully", - Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress), + Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress, documentDTOs), }) } @@ -342,6 +353,12 @@ func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } + form, err := c.MultipartForm() + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form") + } + req.Documents = form.File["documents"] + if err := c.BodyParser(req); err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } diff --git a/internal/modules/daily-checklists/dto/daily-checklist.dto.go b/internal/modules/daily-checklists/dto/daily-checklist.dto.go index d133b76e..62195382 100644 --- a/internal/modules/daily-checklists/dto/daily-checklist.dto.go +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -31,6 +31,7 @@ type DailyChecklistListDTO struct { TotalPhase int `json:"total_phase"` TotalActivity int `json:"total_activity"` Progress int `json:"progress"` + RejectReason *string `json:"reject_reason"` } type DailyChecklistDetailDTO struct { @@ -40,6 +41,14 @@ type DailyChecklistDetailDTO struct { AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"` TotalActivity int `json:"total_activity"` Progress float64 `json:"progress"` + DocumentURLs []DailyChecklistDocumentDTO `json:"document_urls"` +} + +type DailyChecklistDocumentDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Size float64 `json:"size"` + URL string `json:"url"` } type DailyChecklistSummaryDTO struct { @@ -165,10 +174,11 @@ func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO { TotalPhase: 0, TotalActivity: 0, Progress: 0, + RejectReason: e.RejectReason, } } -func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO { +func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64, documentURLs []DailyChecklistDocumentDTO) DailyChecklistDetailDTO { phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases)) for _, phase := range phases { phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{ @@ -228,5 +238,6 @@ func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity. AssignedEmployees: assignedDTOs, TotalActivity: totalActivities, Progress: progress, + DocumentURLs: documentURLs, } } diff --git a/internal/modules/daily-checklists/module.go b/internal/modules/daily-checklists/module.go index bc82d5f6..a1455501 100644 --- a/internal/modules/daily-checklists/module.go +++ b/internal/modules/daily-checklists/module.go @@ -1,10 +1,15 @@ package dailyChecklists import ( + "context" + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services" rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" @@ -19,8 +24,13 @@ func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db) phasesRepo := rPhases.NewPhasesRepository(db) userRepo := rUser.NewUserRepository(db) + documentRepo := commonRepo.NewDocumentRepository(db) + documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo) + if err != nil { + panic(fmt.Sprintf("failed to create document service: %v", err)) + } - dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate) + dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate, documentSvc) userService := sUser.NewUserService(userRepo, validate) DailyChecklistRoutes(router, userService, dailyChecklistService) diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 2ed15fad..f306c74d 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -9,6 +9,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations" phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories" @@ -17,6 +18,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -39,10 +41,18 @@ type DailyChecklistService interface { } type dailyChecklistService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.DailyChecklistRepository - PhaseRepo phaseRepo.PhasesRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.DailyChecklistRepository + PhaseRepo phaseRepo.PhasesRepository + DocumentSvc commonSvc.DocumentService +} + +type DailyChecklistDocument struct { + ID uint + Name string + Size float64 + URL string } type DailyChecklistDetail struct { @@ -52,6 +62,7 @@ type DailyChecklistDetail struct { AssignedEmployees []entity.Employee TotalActivities int Progress float64 + DocumentURLs []DailyChecklistDocument } type DailyChecklistListItem struct { @@ -60,6 +71,7 @@ type DailyChecklistListItem struct { Date time.Time Category string Status *string + RejectReason *string CreatedAt time.Time UpdatedAt time.Time Kandang entity.Kandang @@ -108,12 +120,13 @@ type DailyChecklistReportCategory struct { Baik int } -func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) DailyChecklistService { +func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService { return &dailyChecklistService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - PhaseRepo: phaseRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + PhaseRepo: phaseRepo, + DocumentSvc: documentSvc, } } @@ -158,7 +171,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ if params.Search != "" { like := "%" + params.Search + "%" - db = db.Where("(k.name ILIKE ? OR dc.category ILIKE ?)", like, like) + db = db.Where("(k.name ILIKE ? OR dc.category::text ILIKE ?)", like, like) } countDB := db.Session(&gorm.Session{}) @@ -174,6 +187,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ Date time.Time Category string Status *string + RejectReason *string CreatedAt time.Time UpdatedAt time.Time KandangID uint @@ -192,6 +206,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ dc.date, dc.category, dc.status, + dc.reject_reason, dc.created_at, dc.updated_at, dc.kandang_id, @@ -265,6 +280,7 @@ func (s dailyChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([ Date: row.Date, Category: row.Category, Status: row.Status, + RejectReason: row.RejectReason, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, Kandang: kandangMap[row.KandangID], @@ -345,6 +361,29 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist progress = math.Round((float64(completedAssignments) / float64(totalAssignments)) * 100) } + documentURLs := make([]DailyChecklistDocument, 0) + if s.DocumentSvc != nil { + documents, err := s.DocumentSvc.ListByTarget(c.Context(), string(utils.DocumentTypeDailyChecklist), uint64(id)) + if err != nil { + s.Log.Errorf("Failed to list documents for daily checklist %d: %+v", id, err) + return nil, err + } + + for _, doc := range documents { + url, err := s.DocumentSvc.PresignURL(c.Context(), doc, 0) + if err != nil { + s.Log.Errorf("Failed to presign document %d for daily checklist %d: %+v", doc.Id, id, err) + continue + } + documentURLs = append(documentURLs, DailyChecklistDocument{ + ID: doc.Id, + Name: doc.Name, + Size: doc.Size, + URL: url, + }) + } + } + return &DailyChecklistDetail{ Checklist: *checklist, Phases: phases, @@ -352,6 +391,7 @@ func (s dailyChecklistService) GetDetail(c *fiber.Ctx, id uint) (*DailyChecklist AssignedEmployees: assignedEmployees, TotalActivities: totalActivities, Progress: progress, + DocumentURLs: documentURLs, }, nil } @@ -377,7 +417,7 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) err = s.Repository.DB().WithContext(c.Context()).Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "date"}, {Name: "kandang_id"}, {Name: "category"}}, - DoUpdates: clause.Assignments(map[string]any{"status": status, "updated_at": time.Now()}), + DoUpdates: clause.Assignments(map[string]any{"updated_at": time.Now()}), }).Create(createBody).Error if err != nil { s.Log.Errorf("Failed to upsert dailyChecklist: %+v", err) @@ -392,6 +432,22 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, err } + deletedIDs := make([]uint, 0) + if req.DeletedDocumentIDs != nil { + parts := strings.Split(*req.DeletedDocumentIDs, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + parsedID, err := strconv.ParseUint(part, 10, 64) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid deleted_document_ids") + } + deletedIDs = append(deletedIDs, uint(parsedID)) + } + } + updateBody := map[string]any{ "status": req.Status, } @@ -400,6 +456,40 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i updateBody["reject_reason"] = *req.RejectReason } + actorID, err := middleware.ActorIDFromContext(c) + if err != nil { + return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context") + } + + if len(deletedIDs) > 0 && s.DocumentSvc != nil { + if err := s.DocumentSvc.DeleteDocuments(c.Context(), deletedIDs, true); err != nil { + s.Log.Errorf("Failed to delete daily checklist documents: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete daily checklist documents") + } + } + + if len(req.Documents) > 0 { + documentFiles := make([]commonSvc.DocumentFile, 0, len(req.Documents)) + for idx, file := range req.Documents { + documentFiles = append(documentFiles, commonSvc.DocumentFile{ + File: file, + Type: string(utils.DocumentTypeDailyChecklist), + Index: &idx, + }) + } + + _, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{ + DocumentableType: string(utils.DocumentTypeDailyChecklist), + DocumentableID: uint64(id), + CreatedBy: &actorID, + Files: documentFiles, + }) + if err != nil { + s.Log.Errorf("Failed to upload daily checklist documents: %+v", err) + return &entity.DailyChecklist{}, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload daily checklist documents") + } + } + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") @@ -869,7 +959,8 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Joins("JOIN areas a ON a.id = loc.area_id"). Joins("JOIN phases p ON p.id = dcat.phase_id"). Where("EXTRACT(MONTH FROM dc.date) = ?", params.Month). - Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year) + Where("EXTRACT(YEAR FROM dc.date) = ?", params.Year). + Where("dc.status = ?", "APPROVED") if params.AreaID != nil { db = db.Where("a.id = ?", *params.AreaID) diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index 81bb5eff..35ef8bb9 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -1,5 +1,9 @@ package validation +import ( + "mime/multipart" +) + type Create struct { Date string `json:"date" validate:"required"` KandangId uint `json:"kandang_id" validate:"required"` @@ -8,8 +12,10 @@ type Create struct { } type Update struct { - Status string `json:"status" validate:"required"` - RejectReason *string `json:"reject_reason"` + Status string `form:"status" json:"status" validate:"required"` + RejectReason *string `form:"reject_reason" json:"reject_reason"` + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` + DeletedDocumentIDs *string `form:"deleted_document_ids" json:"deleted_document_ids"` } type Query struct { diff --git a/internal/modules/master/phasess/dto/phases.dto.go b/internal/modules/master/phasess/dto/phases.dto.go index 51724556..79a2db72 100644 --- a/internal/modules/master/phasess/dto/phases.dto.go +++ b/internal/modules/master/phasess/dto/phases.dto.go @@ -15,12 +15,13 @@ type PhasesRelationDTO struct { } type PhasesListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Category string `json:"category"` - IsActive bool `json:"is_active"` - CreatedUser *userDTO.UserRelationDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` + Id uint `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + IsActive bool `json:"is_active"` + ActivityCount int `json:"activity_count"` + CreatedUser *userDTO.UserRelationDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` } type PhasesDetailDTO struct { @@ -44,12 +45,13 @@ func ToPhasesListDTO(e entity.Phases) PhasesListDTO { // } return PhasesListDTO{ - Id: e.Id, - Name: e.Name, - Category: e.Category, - IsActive: e.IsActive, - CreatedAt: e.CreatedAt, - CreatedUser: createdUser, + Id: e.Id, + Name: e.Name, + Category: e.Category, + IsActive: e.IsActive, + ActivityCount: e.ActivityCount, + CreatedAt: e.CreatedAt, + CreatedUser: createdUser, } } diff --git a/internal/modules/master/phasess/services/phases.service.go b/internal/modules/master/phasess/services/phases.service.go index 98e73bef..cfcba600 100644 --- a/internal/modules/master/phasess/services/phases.service.go +++ b/internal/modules/master/phasess/services/phases.service.go @@ -63,6 +63,40 @@ func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity. s.Log.Errorf("Failed to get phasess: %+v", err) return nil, 0, err } + + if len(phasess) > 0 { + ids := make([]uint, 0, len(phasess)) + for _, phase := range phasess { + ids = append(ids, phase.Id) + } + + type activityCountRow struct { + PhaseID uint + Count int64 + } + + var rows []activityCountRow + if err := s.Repository.DB().WithContext(c.Context()). + Table("phase_activities"). + Select("phase_id, COUNT(*) AS count"). + Where("phase_id IN ? AND deleted_at IS NULL", ids). + Group("phase_id"). + Scan(&rows).Error; err != nil { + s.Log.Errorf("Failed to count phase activities: %+v", err) + return nil, 0, err + } + + countMap := make(map[uint]int64, len(rows)) + for _, row := range rows { + countMap[row.PhaseID] = row.Count + } + + for i := range phasess { + if count, ok := countMap[phasess[i].Id]; ok { + phasess[i].ActivityCount = int(count) + } + } + } return phasess, total, nil } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 6ec50447..35ce3132 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -432,6 +432,8 @@ const ( DocumentableTypeExpense DocumentableType = "EXPENSE" DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION" DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM" + + DocumentTypeDailyChecklist DocumentType = "DAILY_CHECKLIST_DOCUMENT" ) // -------------------------------------------------------------------