diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index fdbe4c8b..2f0c3157 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -1459,11 +1459,12 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report Group("a.id, a.name, loc.id, loc.name, k.id, k.name, e.id, e.name, p.id, p.name") } - var total int64 + // --- Count approved rows --- + var approvedTotal int64 groupedForCount := buildGroupedQuery() if err := s.Repository.DB().WithContext(c.Context()). Table("(?) AS grouped", groupedForCount). - Count(&total).Error; err != nil { + Count(&approvedTotal).Error; err != nil { s.Log.Errorf("Failed to count report data: %+v", err) return nil, 0, err } @@ -1483,19 +1484,246 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report TotalAssignments int64 } - rows := make([]reportRow, 0) - if err := buildGroupedQuery(). - Order("a.name, loc.name, k.name, e.name"). - Offset(offset). - Limit(params.Limit). - Scan(&rows).Error; err != nil { - s.Log.Errorf("Failed to fetch report data: %+v", err) + type fallbackRowType struct { + AreaID uint + AreaName string + LocationID uint + LocationName string + KandangID uint + KandangName string + EmployeeID uint + EmployeeName string + } + + // buildFallbackQ returns employees in kandangs that have NO approved checklist data + // for the filtered period. Applies the same scope/area/location/kandang/employee filters. + buildFallbackQ := func() *gorm.DB { + approvedKandangSubQ := buildBase().Select("DISTINCT dc.kandang_id") + q := s.Repository.DB().WithContext(c.Context()). + Table("employee_kandangs ek"). + Joins("JOIN employees e ON e.id = ek.employee_id AND e.deleted_at IS NULL"). + Joins("JOIN kandang_groups k ON k.id = ek.kandang_id AND k.deleted_at IS NULL"). + Joins("JOIN locations loc ON loc.id = k.location_id AND loc.deleted_at IS NULL"). + Joins("JOIN areas a ON a.id = loc.area_id AND a.deleted_at IS NULL"). + Where("ek.kandang_id NOT IN (?)", approvedKandangSubQ). + Select("e.id AS employee_id, e.name AS employee_name, k.id AS kandang_id, k.name AS kandang_name, loc.id AS location_id, loc.name AS location_name, a.id AS area_id, a.name AS area_name") + q = m.ApplyScopeFilter(q, locationScope, "loc.id") + q = m.ApplyScopeFilter(q, areaScope, "a.id") + if params.AreaID != nil { + q = q.Where("a.id = ?", *params.AreaID) + } + if params.LocationID != nil { + q = q.Where("loc.id = ?", *params.LocationID) + } + if params.KandangID != nil { + q = q.Where("ek.kandang_id = ?", *params.KandangID) + } + if params.EmployeeID != nil { + q = q.Where("ek.employee_id = ?", *params.EmployeeID) + } + // PhaseID not applied: fallback rows have no phase data + return q + } + + // --- Count fallback rows --- + var fallbackTotal int64 + if err := s.Repository.DB().WithContext(c.Context()). + Table("(?) AS fb", buildFallbackQ()). + Count(&fallbackTotal).Error; err != nil { + s.Log.Errorf("Failed to count fallback report data: %+v", err) return nil, 0, err } - if len(rows) == 0 { + total := approvedTotal + fallbackTotal + + // --- Fetch ALL approved rows (pagination done in Go after merging with fallback) --- + allApprovedRows := make([]reportRow, 0) + if approvedTotal > 0 { + if err := buildGroupedQuery(). + Order("a.name, loc.name, k.name, e.name"). + Scan(&allApprovedRows).Error; err != nil { + s.Log.Errorf("Failed to fetch report data: %+v", err) + return nil, 0, err + } + } + + // --- Fetch ALL fallback rows --- + allFallbackRows := make([]fallbackRowType, 0) + if fallbackTotal > 0 { + if err := buildFallbackQ(). + Order("a.name, loc.name, k.name, e.name"). + Scan(&allFallbackRows).Error; err != nil { + s.Log.Errorf("Failed to fetch fallback report data: %+v", err) + return nil, 0, err + } + } + + // --- Merge approved + fallback and sort consistently --- + type mergedEntry struct { + AreaName string + LocationName string + KandangName string + EmployeeName string + IsApproved bool + Idx int + } + + merged := make([]mergedEntry, 0, len(allApprovedRows)+len(allFallbackRows)) + for i, r := range allApprovedRows { + merged = append(merged, mergedEntry{ + AreaName: r.AreaName, LocationName: r.LocationName, + KandangName: r.KandangName, EmployeeName: r.EmployeeName, + IsApproved: true, Idx: i, + }) + } + for i, r := range allFallbackRows { + merged = append(merged, mergedEntry{ + AreaName: r.AreaName, LocationName: r.LocationName, + KandangName: r.KandangName, EmployeeName: r.EmployeeName, + IsApproved: false, Idx: i, + }) + } + sort.Slice(merged, func(i, j int) bool { + a, b := merged[i], merged[j] + if a.AreaName != b.AreaName { + return a.AreaName < b.AreaName + } + if a.LocationName != b.LocationName { + return a.LocationName < b.LocationName + } + if a.KandangName != b.KandangName { + return a.KandangName < b.KandangName + } + return a.EmployeeName < b.EmployeeName + }) + + // --- Apply Go-level pagination --- + end := offset + params.Limit + if end > len(merged) { + end = len(merged) + } + if offset >= len(merged) { return []DailyChecklistReportItem{}, total, nil } + pageData := merged[offset:end] + + // --- Split page into approved vs fallback rows --- + pageApproved := make([]reportRow, 0) + pageFallback := make([]fallbackRowType, 0) + for _, entry := range pageData { + if entry.IsApproved { + pageApproved = append(pageApproved, allApprovedRows[entry.Idx]) + } else { + pageFallback = append(pageFallback, allFallbackRows[entry.Idx]) + } + } + + applyEmptyKandangFlags := func(items []DailyChecklistReportItem, kandangIDs []uint) error { + if len(kandangIDs) == 0 { + return nil + } + 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 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 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 nil + } type comboKey struct { EmployeeID uint @@ -1517,7 +1745,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report kandangSet := make(map[uint]struct{}) phaseSet := make(map[uint]struct{}) - for _, row := range rows { + for _, row := range pageApproved { key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID} comboSet[key] = struct{}{} if _, ok := employeeSet[row.EmployeeID]; !ok { @@ -1658,8 +1886,9 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report return selected } - items := make([]DailyChecklistReportItem, len(rows)) - for i, row := range rows { + // --- Build approved items (existing logic) --- + approvedItems := make([]DailyChecklistReportItem, len(pageApproved)) + for i, row := range pageApproved { key := comboKey{EmployeeID: row.EmployeeID, KandangID: row.KandangID, PhaseID: row.PhaseID} activities := dailyActivityMap[key] @@ -1706,7 +1935,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report kandangPercentage = int(math.Round(float64(kandangStat.Completed) / float64(kandangStat.Total) * 100)) } - items[i] = DailyChecklistReportItem{ + approvedItems[i] = DailyChecklistReportItem{ AreaID: row.AreaID, AreaName: row.AreaName, LocationID: row.LocationID, @@ -1727,109 +1956,55 @@ 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" - } - } + // --- Build fallback items (kandangs with no approved data) --- + fallbackItems := make([]DailyChecklistReportItem, len(pageFallback)) + for i, fb := range pageFallback { + fallbackItems[i] = DailyChecklistReportItem{ + AreaID: fb.AreaID, + AreaName: fb.AreaName, + LocationID: fb.LocationID, + LocationName: fb.LocationName, + KandangID: fb.KandangID, + KandangName: fb.KandangName, + EmployeeID: fb.EmployeeID, + EmployeeName: fb.EmployeeName, + PhaseName: "", + DailyActivities: map[string]any{}, + Summary: DailyChecklistReportSummary{}, } } - return items, total, nil + // --- Reconstruct allItems in the sorted pageData order --- + allItems := make([]DailyChecklistReportItem, len(pageData)) + approvedIdx := 0 + fallbackIdx := 0 + for i, entry := range pageData { + if entry.IsApproved { + allItems[i] = approvedItems[approvedIdx] + approvedIdx++ + } else { + allItems[i] = fallbackItems[fallbackIdx] + fallbackIdx++ + } + } + + // --- Collect all kandangIDs on this page (approved + fallback) for empty_kandang flags --- + allKandangSet := make(map[uint]struct{}) + for _, id := range kandangIDs { + allKandangSet[id] = struct{}{} + } + for _, fb := range pageFallback { + allKandangSet[fb.KandangID] = struct{}{} + } + allKandangIDs := make([]uint, 0, len(allKandangSet)) + for id := range allKandangSet { + allKandangIDs = append(allKandangIDs, id) + } + + // --- Flag empty kandang days within this report month --- + if err := applyEmptyKandangFlags(allItems, allKandangIDs); err != nil { + return nil, 0, err + } + + return allItems, total, nil }