diff --git a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go index 4c41a6b1..de4b49d0 100644 --- a/internal/modules/daily-checklists/controllers/daily-checklist.controller.go +++ b/internal/modules/daily-checklists/controllers/daily-checklist.controller.go @@ -351,6 +351,31 @@ func (u *DailyChecklistController) CreateOne(c *fiber.Ctx) error { }) } +func (u *DailyChecklistController) BulkUpdate(c *fiber.Ctx) error { + req := new(validation.BulkStatusUpdate) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.DailyChecklistService.BulkUpdate(c, req) + if err != nil { + return err + } + + responseData := make([]dto.DailyChecklistListDTO, len(results)) + for i, item := range results { + responseData[i] = dto.ToDailyChecklistListDTO(item) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Bulk update dailyChecklist successfully", + Data: responseData, + }) +} + func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error { req := new(validation.Update) param := c.Params("idDailyChecklist") diff --git a/internal/modules/daily-checklists/repositories/daily-checklist.repository.go b/internal/modules/daily-checklists/repositories/daily-checklist.repository.go index e653ba3b..8d664d31 100644 --- a/internal/modules/daily-checklists/repositories/daily-checklist.repository.go +++ b/internal/modules/daily-checklists/repositories/daily-checklist.repository.go @@ -1,13 +1,22 @@ package repository import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "context" + "time" + + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + + "github.com/gofiber/fiber/v2" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) type DailyChecklistRepository interface { repository.BaseRepository[entity.DailyChecklist] + ListScopedChecklistIDs(c *fiber.Ctx, ids []uint) ([]uint, error) + BulkUpdateStatus(ctx context.Context, ids []uint, status string, rejectReason *string) error + ListByIDsWithKandang(ctx context.Context, ids []uint) ([]entity.DailyChecklist, error) } type DailyChecklistRepositoryImpl struct { @@ -19,3 +28,70 @@ func NewDailyChecklistRepository(db *gorm.DB) DailyChecklistRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.DailyChecklist](db), } } + +func (r *DailyChecklistRepositoryImpl) ListScopedChecklistIDs(c *fiber.Ctx, ids []uint) ([]uint, error) { + if len(ids) == 0 { + return []uint{}, nil + } + + db := r.DB().WithContext(c.Context()). + Table("daily_checklists dc"). + Select("dc.id"). + Joins("JOIN kandang_groups k ON k.id = dc.kandang_id"). + Joins("JOIN locations loc ON loc.id = k.location_id"). + Joins("JOIN areas a ON a.id = loc.area_id"). + Where("dc.id IN ?", ids) + + db, err := m.ApplyLocationAreaScope(c, db, "loc.id", "a.id") + if err != nil { + return nil, err + } + + var scopedIDs []uint + if err := db.Pluck("dc.id", &scopedIDs).Error; err != nil { + return nil, err + } + + return scopedIDs, nil +} + +func (r *DailyChecklistRepositoryImpl) BulkUpdateStatus(ctx context.Context, ids []uint, status string, rejectReason *string) error { + if len(ids) == 0 { + return nil + } + + updateBody := map[string]any{ + "status": status, + "reject_reason": rejectReason, + "updated_at": time.Now(), + } + + return r.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + result := tx.Model(&entity.DailyChecklist{}). + Where("id IN ?", ids). + Updates(updateBody) + if result.Error != nil { + return result.Error + } + if result.RowsAffected != int64(len(ids)) { + return gorm.ErrRecordNotFound + } + return nil + }) +} + +func (r *DailyChecklistRepositoryImpl) ListByIDsWithKandang(ctx context.Context, ids []uint) ([]entity.DailyChecklist, error) { + if len(ids) == 0 { + return []entity.DailyChecklist{}, nil + } + + var items []entity.DailyChecklist + if err := r.DB().WithContext(ctx). + Where("id IN ?", ids). + Preload("Kandang"). + Find(&items).Error; err != nil { + return nil, err + } + + return items, nil +} diff --git a/internal/modules/daily-checklists/route.go b/internal/modules/daily-checklists/route.go index 0927486a..e8965697 100644 --- a/internal/modules/daily-checklists/route.go +++ b/internal/modules/daily-checklists/route.go @@ -58,6 +58,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist. */ route.Post("/assignment", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateAssignment) + route.Patch("/bulk-update", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.BulkUpdate) route.Patch("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.UpdateOne) route.Delete("/:idDailyChecklist", m.RequirePermissions(m.P_DailyChecklistCreateOne), ctrl.DeleteOne) } diff --git a/internal/modules/daily-checklists/services/daily-checklist.service.go b/internal/modules/daily-checklists/services/daily-checklist.service.go index 14937e8b..5fc8c0bc 100644 --- a/internal/modules/daily-checklists/services/daily-checklist.service.go +++ b/internal/modules/daily-checklists/services/daily-checklist.service.go @@ -29,6 +29,7 @@ type DailyChecklistService interface { GetOne(ctx *fiber.Ctx, id uint) (*entity.DailyChecklist, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.DailyChecklist, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DailyChecklist, error) + BulkUpdate(ctx *fiber.Ctx, req *validation.BulkStatusUpdate) ([]entity.DailyChecklist, error) DeleteOne(ctx *fiber.Ctx, id uint) error AssignPhases(ctx *fiber.Ctx, id uint, req *validation.AssignPhases) error AssignTasks(ctx *fiber.Ctx, id uint, req *validation.AssignTask) error @@ -646,6 +647,67 @@ func (s dailyChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return s.GetOne(c, id) } +func (s dailyChecklistService) BulkUpdate(c *fiber.Ctx, req *validation.BulkStatusUpdate) ([]entity.DailyChecklist, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + status := strings.ToUpper(strings.TrimSpace(req.Status)) + if status != "APPROVED" && status != "REJECTED" { + return nil, fiber.NewError(fiber.StatusBadRequest, "status must be APPROVED or REJECTED") + } + + ids, err := parseChecklistIDs(req.IDs) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + if len(ids) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "ids cannot be empty") + } + + scopedIDs, err := s.Repository.ListScopedChecklistIDs(c, ids) + if err != nil { + s.Log.Errorf("Failed to validate daily checklist scope for bulk update: %+v", err) + return nil, err + } + if len(scopedIDs) != len(ids) { + return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + + var rejectReason *string + if status == "REJECTED" { + rejectReason = req.RejectReason + } + + if err := s.Repository.BulkUpdateStatus(c.Context(), ids, status, rejectReason); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + s.Log.Errorf("Failed to bulk update daily checklist status: %+v", err) + return nil, err + } + + updated, err := s.Repository.ListByIDsWithKandang(c.Context(), ids) + if err != nil { + s.Log.Errorf("Failed to fetch updated daily checklists: %+v", err) + return nil, err + } + if len(updated) != len(ids) { + return nil, fiber.NewError(fiber.StatusNotFound, "DailyChecklist not found") + } + + orderByID := make(map[uint]int, len(ids)) + for idx, id := range ids { + orderByID[id] = idx + } + + sort.Slice(updated, func(i, j int) bool { + return orderByID[updated[i].Id] < orderByID[updated[j].Id] + }) + + return updated, nil +} + func (s dailyChecklistService) DeleteOne(c *fiber.Ctx, id uint) error { if err := s.ensureChecklistAccess(c, id); err != nil { return err @@ -908,6 +970,33 @@ func parsePhaseIDs(raw string) ([]uint, error) { return result, nil } +func parseChecklistIDs(raw string) ([]uint, error) { + parts := strings.Split(raw, ",") + result := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + + num, err := strconv.ParseUint(value, 10, 64) + if err != nil || num == 0 { + return nil, errors.New("invalid daily checklist id: " + value) + } + + u := uint(num) + if _, ok := seen[u]; ok { + continue + } + seen[u] = struct{}{} + result = append(result, u) + } + + return result, nil +} + func parseIDs(raw string) ([]uint, error) { parts := strings.Split(raw, ",") result := make([]uint, 0, len(parts)) diff --git a/internal/modules/daily-checklists/validations/daily-checklist.validation.go b/internal/modules/daily-checklists/validations/daily-checklist.validation.go index 353aeaa5..c5bc32a5 100644 --- a/internal/modules/daily-checklists/validations/daily-checklist.validation.go +++ b/internal/modules/daily-checklists/validations/daily-checklist.validation.go @@ -18,6 +18,12 @@ type Update struct { DeletedDocumentIDs *string `form:"deleted_document_ids" json:"deleted_document_ids"` } +type BulkStatusUpdate struct { + IDs string `json:"ids" validate:"required_strict"` + Status string `json:"status" validate:"required,oneof=APPROVED REJECTED"` + RejectReason *string `json:"reject_reason"` +} + type Query struct { Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 572a2317..c5e89e3c 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -2366,8 +2366,8 @@ func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangRes } if hppCost != nil { eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir - // eggHpp = hppCost.Estimation.HargaKg - eggHpp = hppCost.Real.HargaKg + eggHpp = hppCost.Estimation.HargaKg + // eggHpp = hppCost.Real.HargaKg eggTotalPiecesFloat = hppCost.Estimation.Butir eggWeightFloat = hppCost.Estimation.Kg if eggTotalPiecesFloat > 0 {