From 3fc330d8f78b95c76cc6103b6d64d624ee7b5060 Mon Sep 17 00:00:00 2001 From: ragilap Date: Fri, 21 Nov 2025 10:50:30 +0700 Subject: [PATCH] fix merging --- .../controllers/projectflock.controller.go | 4 +- .../repositories/projectflock.repository.go | 101 ++++++++++++++-- .../production/project_flocks/route.go | 3 +- .../services/projectflock.service.go | 110 +++++------------- .../purchases/services/purchase.service.go | 33 ++++-- 5 files changed, 151 insertions(+), 100 deletions(-) diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 5d51b985..6d78520e 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -261,7 +261,7 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { }) } -func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { +func (u *ProjectflockController) GetPeriodSummary(c *fiber.Ctx) error { param := c.Params("location_id") id, err := strconv.Atoi(param) @@ -269,7 +269,7 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id") } - summaries, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) + summaries, err := u.ProjectflockService.GetPeriodSummary(c, uint(id)) if err != nil { return err } diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 7221ebd0..de4df25d 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -11,17 +11,24 @@ import ( "gorm.io/gorm" ) -const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))" type ProjectflockRepository interface { repository.BaseRepository[entity.ProjectFlock] GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) WithDefaultRelations() func(*gorm.DB) *gorm.DB ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) + GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) + GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) + GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) AreaExists(ctx context.Context, id uint) (bool, error) FcrExists(ctx context.Context, id uint) (bool, error) LocationExists(ctx context.Context, id uint) (bool, error) } +type KandangPeriodRow struct { + Id uint + Name string + LatestPeriod int +} type ProjectflockRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.ProjectFlock] @@ -35,19 +42,13 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository { func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { - db = r.withDefaultRelations(db) return r.applyQueryFilters(db, params) }) } func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { - return r.withDefaultRelations(db) - } -} - -func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB { - return db. + return db. Preload("CreatedUser"). Preload("Area"). Preload("Fcr"). @@ -55,8 +56,10 @@ func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB Preload("Kandangs"). Preload("KandangHistory"). Preload("KandangHistory.Kandang") + } } + func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB { if params == nil { return db @@ -163,6 +166,88 @@ func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint return repository.Exists[entity.Location](ctx, r.DB(), id) } +func (r *ProjectflockRepositoryImpl) GetNextPeriodsForKandangs(ctx context.Context, kandangIDs []uint) (map[uint]int, error) { + result := make(map[uint]int) + if len(kandangIDs) == 0 { + return result, nil + } + + unique := uniqueUintSlice(kandangIDs) + for _, id := range unique { + result[id] = 1 + } + + type periodRow struct { + KandangID uint + MaxPeriod int + } + + var rows []periodRow + if err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Select("kandang_id, COALESCE(MAX(period), 0) AS max_period"). + Where("kandang_id IN ?", unique). + Group("kandang_id"). + Scan(&rows).Error; err != nil { + return nil, err + } + + for _, row := range rows { + if row.MaxPeriod > 0 { + result[row.KandangID] = row.MaxPeriod + 1 + } + } + + return result, nil +} + +func (r *ProjectflockRepositoryImpl) GetCurrentProjectPeriod(ctx context.Context, projectFlockID uint) (int, error) { + if projectFlockID == 0 { + return 0, nil + } + + var currentPeriod int + if err := r.DB().WithContext(ctx). + Table("project_flock_kandangs"). + Where("project_flock_id = ?", projectFlockID). + Select("COALESCE(MAX(period), 0)"). + Scan(¤tPeriod).Error; err != nil { + return 0, err + } + + return currentPeriod, nil +} + +func (r *ProjectflockRepositoryImpl) GetKandangPeriodSummaryRows(ctx context.Context, locationID uint) ([]KandangPeriodRow, error) { + rows := make([]KandangPeriodRow, 0) + if err := r.DB().WithContext(ctx). + Table("kandangs AS k"). + Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period"). + Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id"). + Where("k.location_id = ?", locationID). + Where("k.deleted_at IS NULL"). + Group("k.id, k.name"). + Order("k.id ASC"). + Scan(&rows).Error; err != nil { + return nil, err + } + + return rows, nil +} + +func uniqueUintSlice(values []uint) []uint { + seen := make(map[uint]struct{}, len(values)) + result := make([]uint, 0, len(values)) + for _, v := range values { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + result = append(result, v) + } + return result +} + func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder string) []string { direction := "ASC" if strings.ToLower(sortOrder) == "desc" { diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index deec1a05..eb806129 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -20,9 +20,8 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) - route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:location_id/periods", ctrl.GetFlockPeriodSummary) + route.Get("/locations/:location_id/periods", ctrl.GetPeriodSummary) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index f40ea97b..19b07447 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -37,7 +37,7 @@ type ProjectflockService interface { GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) - GetFlockPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) + GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) } @@ -85,18 +85,6 @@ func NewProjectflockService( } } -func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { - return db. - Preload("CreatedUser"). - Preload("Flock"). - Preload("Area"). - Preload("Fcr"). - Preload("Location"). - Preload("Kandangs"). - Preload("KandangHistory"). - Preload("KandangHistory.Kandang") -} - func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, nil, err @@ -124,9 +112,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e ids[i] = item.Id } - latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB { - return s.withRelations(db) - }) + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.Repository.WithDefaultRelations()) if err != nil { s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) } else if len(latestMap) > 0 { @@ -170,9 +156,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr } if s.ApprovalSvc != nil { - approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { - return s.withRelations(db) - }) + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations()) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) } else if len(approvals) > 0 { @@ -199,9 +183,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock } if s.ApprovalSvc != nil { - approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { - return s.withRelations(db) - }) + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations()) if err != nil { s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) } else if len(approvals) > 0 { @@ -320,13 +302,12 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return err } - // Compute period based on location history (max period in that location + 1), - // and store it on project_flock_kandangs only. - nextPeriod, err := s.nextLocationPeriod(c.Context(), dbTransaction, req.LocationId) + // Compute period per kandang so every kandang maintains its own cycle history. + periods, err := projectRepo.GetNextPeriodsForKandangs(c.Context(), kandangIDs) if err != nil { return err } - if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, nextPeriod); err != nil { + if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs, periods); err != nil { return err } @@ -544,19 +525,24 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if len(toAttach) > 0 { - var currentPeriod int - if err := dbTransaction.WithContext(c.Context()). - Table("project_flock_kandangs"). - Where("project_flock_id = ?", id). - Select("COALESCE(MAX(period), 0)"). - Scan(¤tPeriod).Error; err != nil { + currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id) + if err != nil { return err } - if currentPeriod <= 0 { - currentPeriod = 1 + + periods := make(map[uint]int, len(toAttach)) + if currentPeriod > 0 { + for _, kid := range toAttach { + periods[kid] = currentPeriod + } + } else { + periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach) + if err != nil { + return err + } } - if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, currentPeriod); err != nil { + if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil { return err } } @@ -832,32 +818,6 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } -// nextLocationPeriod computes the next period number for a given location -// based on the maximum period that has ever been used by any kandang in that location. -func (s projectflockService) nextLocationPeriod(ctx context.Context, tx *gorm.DB, locationID uint) (int, error) { - if locationID == 0 { - return 0, fiber.NewError(fiber.StatusBadRequest, "location_id is required to compute period") - } - - db := s.Repository.DB() - if tx != nil { - db = tx - } - - var maxPeriod int - if err := db.WithContext(ctx). - Table("project_flock_kandangs pfk"). - Joins("JOIN kandangs k ON k.id = pfk.kandang_id"). - Where("k.location_id = ?", locationID). - Select("COALESCE(MAX(pfk.period), 0)"). - Scan(&maxPeriod).Error; err != nil { - s.Log.Errorf("Failed to compute max period for location %d: %+v", locationID, err) - return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute period for location") - } - - return maxPeriod + 1, nil -} - func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) { if len(projectIDs) == 0 { return map[uint]int{}, nil @@ -865,7 +825,7 @@ func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs) } -func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { +func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) { if locationID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required") } @@ -879,24 +839,8 @@ func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, locationID uint return nil, fiber.NewError(fiber.StatusNotFound, "Location not found") } - type kandangPeriodRow struct { - Id uint - Name string - LatestPeriod int - } - - var rows []kandangPeriodRow - - db := s.Repository.DB().WithContext(c.Context()) - if err := db. - Table("kandangs AS k"). - Select("k.id, k.name, COALESCE(MAX(pfk.period), 0) AS latest_period"). - Joins("LEFT JOIN project_flock_kandangs AS pfk ON pfk.kandang_id = k.id"). - Where("k.location_id = ?", locationID). - Where("k.deleted_at IS NULL"). - Group("k.id, k.name"). - Order("k.id ASC"). - Scan(&rows).Error; err != nil { + rows, err := s.Repository.GetKandangPeriodSummaryRows(c.Context(), locationID) + if err != nil { s.Log.Errorf("Failed to fetch kandang period summary for location %d: %+v", locationID, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandang period summary") } @@ -991,7 +935,7 @@ func (s projectflockService) ensureFlockByName(ctx context.Context, actorID uint return newFlock, nil } -func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, period int) error { +func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, periods map[uint]int) error { if len(kandangIDs) == 0 { return nil } @@ -1026,6 +970,10 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction * records := make([]*entity.ProjectFlockKandang, 0, len(toAttach)) for _, id := range toAttach { + period := periods[id] + if period <= 0 { + period = 1 + } records = append(records, &entity.ProjectFlockKandang{ ProjectFlockId: projectFlockID, KandangId: id, diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 5035cbc6..b0d5311d 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -168,9 +168,9 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase return nil, err } - user, ok := authmiddleware.AuthenticatedUser(c) - if !ok || user == nil || user.Id == 0 { - return nil, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err } ctx := c.Context() @@ -263,7 +263,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase DueDate: dueDate, GrandTotal: 0, Notes: req.Notes, - CreatedBy: uint64(user.Id), + CreatedBy: uint64(actorID), } items := make([]*entity.PurchaseItem, 0, len(aggregated)) @@ -332,7 +332,10 @@ func (s *purchaseService) processStaffPurchaseApproval(c *fiber.Ctx, id uint64, return nil, err } - actorID := uint(1) // TODO: replace with authenticated user id once available + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err + } ctx := c.Context() purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) @@ -476,6 +479,11 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint64, req *v return nil, err } + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err + } + ctx := c.Context() purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) @@ -496,7 +504,6 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint64, req *v return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must reach staff purchase step before manager approval") } - actorID := uint(1) action := entity.ApprovalActionApproved now := time.Now().UTC() hasExistingPO := purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" @@ -577,6 +584,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati return nil, err } + actorID, err := actorIDFromContext(c) + if err != nil { + return nil, err + } + ctx := c.Context() purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id) @@ -674,7 +686,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint64, req *validati receivingAction := entity.ApprovalActionApproved completedAction := entity.ApprovalActionApproved - actorID := uint(1) approvalSvc := s.approvalServiceForDB(nil) if approvalSvc != nil { @@ -1059,6 +1070,14 @@ func (s *purchaseService) notifyExpenseItemsDeleted(ctx context.Context, purchas } } +func actorIDFromContext(c *fiber.Ctx) (uint, error) { + user, ok := authmiddleware.AuthenticatedUser(c) + if !ok || user == nil || user.Id == 0 { + return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") + } + return user.Id, nil +} + func (s *purchaseService) buildStaffAdjustmentPayload( ctx context.Context, purchase *entity.Purchase,