fix merging

This commit is contained in:
ragilap
2025-11-21 10:50:30 +07:00
parent 53b226f243
commit 3fc330d8f7
5 changed files with 151 additions and 100 deletions
@@ -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
}
@@ -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(&currentPeriod).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" {
@@ -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)
}
@@ -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(&currentPeriod).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,
@@ -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,