diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index de4b49d0..c2f258fd 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -238,17 +238,17 @@ func (u *DailyChecklistController) GetReport(c *fiber.Ctx) error { } result, totalResults, err := u.DailyChecklistService.GetReport(c, query) - withoutActivities := func(src map[string]int) map[string]int { - if src == nil { - return map[string]int{} - } - return src - } - if err != nil { return err } + withoutActivities := func(src map[string]any) map[string]any { + if src == nil { + return map[string]any{} + } + return src + } + responseData := make([]dto.DailyChecklistReportDTO, len(result)) for i, item := range result { responseData[i] = dto.DailyChecklistReportDTO{ diff --git a/internal/modules/daily-checklists/dto/daily-checklist.dto.go b/internal/modules/daily-checklists/dto/daily-checklist.dto.go index 6869894f..abc1cea1 100644 --- a/internal/modules/daily-checklists/dto/daily-checklist.dto.go +++ b/internal/modules/daily-checklists/dto/daily-checklist.dto.go @@ -72,13 +72,14 @@ type DailyChecklistPerformanceOverviewDTO struct { ActivityLeft int `json:"activity_left"` } + type DailyChecklistReportDTO struct { Area DailyChecklistReportEntityDTO `json:"area"` Farm DailyChecklistReportEntityDTO `json:"farm"` Kandang DailyChecklistReportEntityDTO `json:"kandang"` ABK DailyChecklistReportEntityDTO `json:"abk"` Phase string `json:"phase"` - DailyActivities map[string]int `json:"daily_activities"` + DailyActivities map[string]any `json:"daily_activities"` Summary DailyChecklistReportSummaryDTO `json:"summary"` } diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 45533994..8e60ef8b 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -104,7 +104,7 @@ type DailyChecklistReportItem struct { EmployeeID uint EmployeeName string PhaseName string - DailyActivities map[string]int + DailyActivities map[string]any Summary DailyChecklistReportSummary } @@ -123,12 +123,13 @@ type DailyChecklistReportCategory struct { } const ( - dailyChecklistDateLayout = "2006-01-02" - dailyChecklistCategoryEmptyKandang = "empty_kandang" - dailyChecklistStatusRejected = "REJECTED" - dailyChecklistStatusDraft = "DRAFT" - dailyChecklistErrEmptyKandangExist = "DailyChecklist cannot be created because empty_kandang already exists for at least one date in range" - dailyChecklistErrDateOverlapExist = "DailyChecklist cannot be created because at least one date in range already has a checklist" + dailyChecklistDateLayout = "2006-01-02" + dailyChecklistCategoryEmptyKandang = "empty_kandang" + dailyChecklistStatusRejected = "REJECTED" + dailyChecklistStatusDraft = "DRAFT" + dailyChecklistErrEmptyKandangExist = "DailyChecklist cannot be created because empty_kandang already exists for at least one date in range" + dailyChecklistErrDateOverlapExist = "DailyChecklist cannot be created because at least one date in range already has a checklist" + dailyChecklistErrDeletedNonEmptyKandangExists = "DailyChecklist cannot be created as empty_kandang because a deleted non-empty_kandang checklist exists for this date" ) func NewDailyChecklistService(repo repository.DailyChecklistRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate, documentSvc commonSvc.DocumentService) DailyChecklistService { @@ -519,21 +520,8 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) status := req.Status category := req.Category - endDate := date if req.EmptyKandang { - if strings.TrimSpace(req.EmptyKandangEndDate) == "" { - return nil, fiber.NewError(fiber.StatusBadRequest, "empty_kandang_end_date is required when empty_kandang is true") - } - - endDate, err = time.Parse(dailyChecklistDateLayout, strings.TrimSpace(req.EmptyKandangEndDate)) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "invalid empty_kandang_end_date format, use YYYY-MM-DD") - } - if endDate.Before(date) { - return nil, fiber.NewError(fiber.StatusBadRequest, "empty_kandang_end_date must be greater than or equal to date") - } - category = dailyChecklistCategoryEmptyKandang } @@ -544,15 +532,17 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) return err } - if req.EmptyKandang { - if err := s.validateNoChecklistOverlapForEmptyKandang(tx, req.KandangId, date, endDate); err != nil { + if category == dailyChecklistCategoryEmptyKandang { + if err := s.validateNoChecklistOverlapForEmptyKandang(tx, req.KandangId, date, date); err != nil { + return err + } + if err := s.validateNoDeletedNonEmptyKandangForDate(tx, req.KandangId, date); err != nil { + return err + } + } else { + if err := s.validateNoEmptyKandangConflict(tx, req.KandangId, date, date); err != nil { return err } - return s.createBulkDailyChecklists(tx, req.KandangId, date, endDate, category, status, &targetID) - } - - if err := s.validateNoEmptyKandangConflict(tx, req.KandangId, date, endDate); err != nil { - return err } return s.createOrReuseSingleDailyChecklist(tx, req.KandangId, date, category, status, &targetID) @@ -615,6 +605,22 @@ func (s *dailyChecklistService) validateNoEmptyKandangConflict(tx *gorm.DB, kand return nil } +func (s *dailyChecklistService) validateNoDeletedNonEmptyKandangForDate(tx *gorm.DB, kandangID uint, date time.Time) error { + var conflictCount int64 + if err := tx.Model(&entity.DailyChecklist{}). + Unscoped(). + Where("kandang_id = ? AND date = ? AND deleted_at IS NULL AND category != ?", kandangID, date, dailyChecklistCategoryEmptyKandang). + Count(&conflictCount).Error; err != nil { + return err + } + + if conflictCount > 0 { + return fiber.NewError(fiber.StatusBadRequest, dailyChecklistErrDeletedNonEmptyKandangExists) + } + + return nil +} + func (s *dailyChecklistService) createOrReuseSingleDailyChecklist(tx *gorm.DB, kandangID uint, date time.Time, category, status string, targetID *uint) error { existing := new(entity.DailyChecklist) err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). @@ -1659,7 +1665,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report totalChecklist := 0 categoryCounts := DailyChecklistReportCategory{} - activityOutput := make(map[string]int, len(activities)) + activityOutput := make(map[string]any, len(activities)) for day, stat := range activities { activityOutput[day] = stat.Completed @@ -1717,5 +1723,109 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report } } + // Flag empty kandang days within this report month + if len(kandangIDs) > 0 { + firstDay := time.Date(params.Year, time.Month(params.Month), 1, 0, 0, 0, 0, time.UTC) + lastDay := firstDay.AddDate(0, 1, 0).AddDate(0, 0, -1) + today := time.Now().UTC().Truncate(24 * time.Hour) + + type emptyKandangRec struct { + KandangID uint + Date time.Time + } + var emptyRecs []emptyKandangRec + if err := s.Repository.DB().WithContext(c.Context()). + Model(&entity.DailyChecklist{}). + Where("kandang_id IN ? AND category = ? AND date <= ? AND deleted_at IS NULL", + kandangIDs, dailyChecklistCategoryEmptyKandang, lastDay). + Select("kandang_id, date"). + Scan(&emptyRecs).Error; err != nil { + s.Log.Errorf("Failed to get empty kandang records for report: %+v", err) + return nil, 0, err + } + + emptyDaysByKandang := make(map[uint]map[int]struct{}) + + if len(emptyRecs) > 0 { + minEmptyDate := emptyRecs[0].Date + for _, rec := range emptyRecs[1:] { + if rec.Date.Before(minEmptyDate) { + minEmptyDate = rec.Date + } + } + + type checklistDateRec struct { + KandangID uint + Date time.Time + } + var nextDates []checklistDateRec + if err := s.Repository.DB().WithContext(c.Context()). + Model(&entity.DailyChecklist{}). + Where("kandang_id IN ? AND category != ? AND date > ? AND (status IS NULL OR status != ?) AND deleted_at IS NULL", + kandangIDs, dailyChecklistCategoryEmptyKandang, minEmptyDate, dailyChecklistStatusRejected). + Select("kandang_id, date"). + Order("kandang_id ASC, date ASC"). + Scan(&nextDates).Error; err != nil { + s.Log.Errorf("Failed to get next checklist dates for empty kandang: %+v", err) + return nil, 0, err + } + + nextDatesByKandang := make(map[uint][]time.Time) + for _, row := range nextDates { + nextDatesByKandang[row.KandangID] = append(nextDatesByKandang[row.KandangID], row.Date) + } + + for _, rec := range emptyRecs { + var nextDate time.Time + for _, d := range nextDatesByKandang[rec.KandangID] { + if d.After(rec.Date) { + nextDate = d + break + } + } + + // If no next checklist, cap empty period at today (not end of month) + ceiling := lastDay + if today.Before(lastDay) { + ceiling = today + } + periodEnd := ceiling + if !nextDate.IsZero() { + periodEnd = nextDate.AddDate(0, 0, -1) + } + + effectiveStart := rec.Date + if effectiveStart.Before(firstDay) { + effectiveStart = firstDay + } + effectiveEnd := periodEnd + if effectiveEnd.After(lastDay) { + effectiveEnd = lastDay + } + + if effectiveStart.After(effectiveEnd) { + continue + } + + if _, ok := emptyDaysByKandang[rec.KandangID]; !ok { + emptyDaysByKandang[rec.KandangID] = make(map[int]struct{}) + } + for d := effectiveStart; !d.After(effectiveEnd); d = d.AddDate(0, 0, 1) { + emptyDaysByKandang[rec.KandangID][d.Day()] = struct{}{} + } + } + } + + for i, item := range items { + daySet := emptyDaysByKandang[item.KandangID] + for day := range daySet { + key := strconv.Itoa(day) + if _, exists := items[i].DailyActivities[key]; !exists { + items[i].DailyActivities[key] = "Kandang kosong" + } + } + } + } + return items, total, nil } diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index 3738a58d..fdf19dbe 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -9,8 +9,7 @@ type Create struct { KandangId uint `json:"kandang_id" validate:"required"` Category string `json:"category" validate:"required"` Status string `json:"status" validate:"required"` - EmptyKandang bool `json:"empty_kandang"` - EmptyKandangEndDate string `json:"empty_kandang_end_date"` + EmptyKandang bool `json:"empty_kandang"` } type Update struct {