adjust empty kandang daily checklist

This commit is contained in:
giovanni
2026-04-30 11:15:11 +07:00
parent 128c8e0d08
commit 0d2cdef10f
4 changed files with 148 additions and 38 deletions
@@ -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{
@@ -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"`
}
@@ -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
}
@@ -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 {