mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-21 13:55:43 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b219bf829f | |||
| 0d2cdef10f | |||
| 128c8e0d08 | |||
| cf4e723f64 | |||
| a3156a156f |
@@ -238,17 +238,17 @@ func (u *DailyChecklistController) GetReport(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.DailyChecklistService.GetReport(c, query)
|
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 {
|
if err != nil {
|
||||||
return err
|
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))
|
responseData := make([]dto.DailyChecklistReportDTO, len(result))
|
||||||
for i, item := range result {
|
for i, item := range result {
|
||||||
responseData[i] = dto.DailyChecklistReportDTO{
|
responseData[i] = dto.DailyChecklistReportDTO{
|
||||||
|
|||||||
@@ -72,13 +72,14 @@ type DailyChecklistPerformanceOverviewDTO struct {
|
|||||||
ActivityLeft int `json:"activity_left"`
|
ActivityLeft int `json:"activity_left"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type DailyChecklistReportDTO struct {
|
type DailyChecklistReportDTO struct {
|
||||||
Area DailyChecklistReportEntityDTO `json:"area"`
|
Area DailyChecklistReportEntityDTO `json:"area"`
|
||||||
Farm DailyChecklistReportEntityDTO `json:"farm"`
|
Farm DailyChecklistReportEntityDTO `json:"farm"`
|
||||||
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
|
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
|
||||||
ABK DailyChecklistReportEntityDTO `json:"abk"`
|
ABK DailyChecklistReportEntityDTO `json:"abk"`
|
||||||
Phase string `json:"phase"`
|
Phase string `json:"phase"`
|
||||||
DailyActivities map[string]int `json:"daily_activities"`
|
DailyActivities map[string]any `json:"daily_activities"`
|
||||||
Summary DailyChecklistReportSummaryDTO `json:"summary"`
|
Summary DailyChecklistReportSummaryDTO `json:"summary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ type DailyChecklistReportItem struct {
|
|||||||
EmployeeID uint
|
EmployeeID uint
|
||||||
EmployeeName string
|
EmployeeName string
|
||||||
PhaseName string
|
PhaseName string
|
||||||
DailyActivities map[string]int
|
DailyActivities map[string]any
|
||||||
Summary DailyChecklistReportSummary
|
Summary DailyChecklistReportSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,12 +123,13 @@ type DailyChecklistReportCategory struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
dailyChecklistDateLayout = "2006-01-02"
|
dailyChecklistDateLayout = "2006-01-02"
|
||||||
dailyChecklistCategoryEmptyKandang = "empty_kandang"
|
dailyChecklistCategoryEmptyKandang = "empty_kandang"
|
||||||
dailyChecklistStatusRejected = "REJECTED"
|
dailyChecklistStatusRejected = "REJECTED"
|
||||||
dailyChecklistStatusDraft = "DRAFT"
|
dailyChecklistStatusDraft = "DRAFT"
|
||||||
dailyChecklistErrEmptyKandangExist = "DailyChecklist cannot be created because empty_kandang already exists for at least one date in range"
|
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"
|
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 {
|
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
|
status := req.Status
|
||||||
category := req.Category
|
category := req.Category
|
||||||
endDate := date
|
|
||||||
|
|
||||||
if req.EmptyKandang {
|
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
|
category = dailyChecklistCategoryEmptyKandang
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,15 +532,17 @@ func (s *dailyChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.EmptyKandang {
|
if category == dailyChecklistCategoryEmptyKandang {
|
||||||
if err := s.validateNoChecklistOverlapForEmptyKandang(tx, req.KandangId, date, endDate); err != nil {
|
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 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)
|
return s.createOrReuseSingleDailyChecklist(tx, req.KandangId, date, category, status, &targetID)
|
||||||
@@ -615,6 +605,22 @@ func (s *dailyChecklistService) validateNoEmptyKandangConflict(tx *gorm.DB, kand
|
|||||||
return nil
|
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 {
|
func (s *dailyChecklistService) createOrReuseSingleDailyChecklist(tx *gorm.DB, kandangID uint, date time.Time, category, status string, targetID *uint) error {
|
||||||
existing := new(entity.DailyChecklist)
|
existing := new(entity.DailyChecklist)
|
||||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
@@ -1659,7 +1665,7 @@ func (s dailyChecklistService) GetReport(c *fiber.Ctx, params *validation.Report
|
|||||||
|
|
||||||
totalChecklist := 0
|
totalChecklist := 0
|
||||||
categoryCounts := DailyChecklistReportCategory{}
|
categoryCounts := DailyChecklistReportCategory{}
|
||||||
activityOutput := make(map[string]int, len(activities))
|
activityOutput := make(map[string]any, len(activities))
|
||||||
|
|
||||||
for day, stat := range activities {
|
for day, stat := range activities {
|
||||||
activityOutput[day] = stat.Completed
|
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
|
return items, total, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ type Create struct {
|
|||||||
KandangId uint `json:"kandang_id" validate:"required"`
|
KandangId uint `json:"kandang_id" validate:"required"`
|
||||||
Category string `json:"category" validate:"required"`
|
Category string `json:"category" validate:"required"`
|
||||||
Status string `json:"status" validate:"required"`
|
Status string `json:"status" validate:"required"`
|
||||||
EmptyKandang bool `json:"empty_kandang"`
|
EmptyKandang bool `json:"empty_kandang"`
|
||||||
EmptyKandangEndDate string `json:"empty_kandang_end_date"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
|
|||||||
@@ -91,8 +91,10 @@ func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]en
|
|||||||
LOWER(COALESCE(notes, '')) LIKE ? OR
|
LOWER(COALESCE(notes, '')) LIKE ? OR
|
||||||
LOWER(COALESCE(customers.name, '')) LIKE ? OR
|
LOWER(COALESCE(customers.name, '')) LIKE ? OR
|
||||||
LOWER(COALESCE(suppliers.name, '')) LIKE ? OR
|
LOWER(COALESCE(suppliers.name, '')) LIKE ? OR
|
||||||
LOWER(COALESCE(banks.name, '')) LIKE ?`,
|
LOWER(COALESCE(banks.name, '')) LIKE ? OR
|
||||||
like, like, like, like, like, like, like, like,
|
CAST(payments.nominal AS TEXT) LIKE ? OR
|
||||||
|
TO_CHAR(payments.payment_date, 'YYYY-MM-DD') LIKE ?`,
|
||||||
|
like, like, like, like, like, like, like, like, like, like,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-36
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -166,42 +165,16 @@ func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []s
|
|||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackCategoryCodes := utils.LegacyProductCategoryCodesForFlags(flags)
|
|
||||||
|
|
||||||
db = db.
|
|
||||||
Joins("JOIN products p_flag ON p_flag.id = product_warehouses.product_id").
|
|
||||||
Joins("LEFT JOIN product_categories pc_flag ON pc_flag.id = p_flag.product_category_id")
|
|
||||||
|
|
||||||
actualFlagFilter := `
|
|
||||||
EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM flags f_flag
|
|
||||||
WHERE f_flag.flagable_id = p_flag.id
|
|
||||||
AND f_flag.flagable_type = ?
|
|
||||||
AND f_flag.name IN ?
|
|
||||||
)
|
|
||||||
`
|
|
||||||
|
|
||||||
if len(fallbackCategoryCodes) == 0 {
|
|
||||||
return db.Where(actualFlagFilter, entity.FlagableTypeProduct, flags).Distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.
|
return db.
|
||||||
Where(
|
Where(`
|
||||||
`(`+actualFlagFilter+`) OR (
|
EXISTS (
|
||||||
NOT EXISTS (
|
SELECT 1
|
||||||
SELECT 1
|
FROM flags f_flag
|
||||||
FROM flags f_any
|
WHERE f_flag.flagable_id = product_warehouses.product_id
|
||||||
WHERE f_any.flagable_id = p_flag.id
|
AND f_flag.flagable_type = ?
|
||||||
AND f_any.flagable_type = ?
|
AND f_flag.name IN ?
|
||||||
)
|
)
|
||||||
AND pc_flag.code IN ?
|
`, entity.FlagableTypeProduct, flags).
|
||||||
)`,
|
|
||||||
entity.FlagableTypeProduct,
|
|
||||||
flags,
|
|
||||||
entity.FlagableTypeProduct,
|
|
||||||
fallbackCategoryCodes,
|
|
||||||
).
|
|
||||||
Distinct()
|
Distinct()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-5
@@ -117,7 +117,7 @@ func insertProductWarehouseTestFixtures(t *testing.T, db *gorm.DB) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyFlagsFilterIncludesLegacyCategoryFallback(t *testing.T) {
|
func TestApplyFlagsFilterOnlyIncludesFlaggedProducts(t *testing.T) {
|
||||||
db := setupProductWarehouseFlagFilterTestDB(t)
|
db := setupProductWarehouseFlagFilterTestDB(t)
|
||||||
repo := NewProductWarehouseRepository(db)
|
repo := NewProductWarehouseRepository(db)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -131,12 +131,14 @@ func TestApplyFlagsFilterIncludesLegacyCategoryFallback(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ids) != 2 || ids[0] != 1 || ids[1] != 2 {
|
// Only PW 1 (product 10, flagged PAKAN) should match.
|
||||||
t.Fatalf("expected flagged and legacy RAW rows to match, got %v", ids)
|
// PW 2 (product 20, no flags, RAW category) must not appear — legacy fallback removed.
|
||||||
|
if len(ids) != 1 || ids[0] != 1 {
|
||||||
|
t.Fatalf("expected only flagged row to match, got %v", ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestApplyFlagsFilterDoesNotFallbackWhenProductAlreadyHasDifferentFlags(t *testing.T) {
|
func TestApplyFlagsFilterExcludesWrongFlaggedProducts(t *testing.T) {
|
||||||
db := setupProductWarehouseFlagFilterTestDB(t)
|
db := setupProductWarehouseFlagFilterTestDB(t)
|
||||||
repo := NewProductWarehouseRepository(db)
|
repo := NewProductWarehouseRepository(db)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -150,8 +152,9 @@ func TestApplyFlagsFilterDoesNotFallbackWhenProductAlreadyHasDifferentFlags(t *t
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PW 3 belongs to an OVK-flagged product — must not appear when filtering for PAKAN.
|
||||||
if len(ids) != 0 {
|
if len(ids) != 0 {
|
||||||
t.Fatalf("expected OVK-flagged product not to match PAKAN fallback, got %v", ids)
|
t.Fatalf("expected OVK-flagged product not to match PAKAN filter, got %v", ids)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user