Merge branch 'feat/bulk' into 'development'

[FEAT][BE]: add api bulk update status daily checklist; change hpp real to estimate

See merge request mbugroup/lti-api!428
This commit is contained in:
Adnan Zahir
2026-04-21 11:37:57 +07:00
6 changed files with 200 additions and 3 deletions
@@ -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")
@@ -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
}
@@ -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)
}
@@ -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))
@@ -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"`
@@ -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 {