From 3d75251c9611694a604d9efd199cf0b55b978b44 Mon Sep 17 00:00:00 2001 From: giovanni Date: Fri, 10 Apr 2026 14:09:31 +0700 Subject: [PATCH 1/6] adjust api get all project flock kandang with periode --- .../project_flock_kandang.controller.go | 47 +++++++-- .../dto/project_flock_kandang.dto.go | 29 +++++- .../services/project_flock_kandang.service.go | 37 +++++++ .../project_flock_kandang.validation.go | 23 ++--- .../projectflock_kandang.repository.go | 98 +++++++++++++++++++ 5 files changed, 210 insertions(+), 24 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 32ac0e38..9333410f 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -24,22 +24,49 @@ func NewProjectFlockKandangController(projectFlockKandangService service.Project func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), - ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)), - KandangId: uint(c.QueryInt("kandang_id", 0)), - Category: c.Query("category", ""), - AreaId: uint(c.QueryInt("area_id", 0)), - SortBy: c.Query("sort_by", ""), - SortOrder: c.Query("sort_order", ""), - StepName: c.Query("step_name", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + NameWithPeriode: c.QueryBool("name_with_periode", false), + ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)), + KandangId: uint(c.QueryInt("kandang_id", 0)), + Category: c.Query("category", ""), + AreaId: uint(c.QueryInt("area_id", 0)), + SortBy: c.Query("sort_by", ""), + SortOrder: c.Query("sort_order", ""), + StepName: c.Query("step_name", ""), } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } + if query.NameWithPeriode { + results, totalResults, err := u.ProjectFlockKandangService.GetAllNameWithPeriode(c, query) + if err != nil { + return err + } + + data := make([]dto.ProjectFlockKandangNameWithPeriodDTO, 0, len(results)) + for _, result := range results { + data = append(data, dto.ToProjectFlockKandangNameWithPeriodDTOValues(result.Id, result.KandangName, result.Period)) + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangNameWithPeriodDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all projectFlockKandangs successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: data, + }) + } + results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) if err != nil { return err diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 8231a551..b914aa10 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -60,6 +60,11 @@ type ProjectFlockKandangListDTO struct { ChickinApproval *approvalDTO.ApprovalRelationDTO `json:"chickin_approval,omitempty"` } +type ProjectFlockKandangNameWithPeriodDTO struct { + Id uint `json:"id"` + NameWithPeriod string `json:"name_with_period"` +} + type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO Chickins []chickinDTO.ChickinRelationDTO `json:"chickins,omitempty"` @@ -129,13 +134,17 @@ func toKandangRelation(kandang entity.Kandang) *kandangDTO.KandangRelationDTO { } func toNameWithPeriod(kandang entity.Kandang, period int) string { - if kandang.Name == "" { + return toNameWithPeriodValue(kandang.Name, period) +} + +func toNameWithPeriodValue(kandangName string, period int) string { + if kandangName == "" { return "" } if period == 0 { - return kandang.Name + return kandangName } - return kandang.Name + " Period " + strconv.Itoa(period) + return kandangName + " Period " + strconv.Itoa(period) } func toApprovalDTOSelector( @@ -167,6 +176,20 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand } } +func ToProjectFlockKandangNameWithPeriodDTO(e entity.ProjectFlockKandang) ProjectFlockKandangNameWithPeriodDTO { + return ProjectFlockKandangNameWithPeriodDTO{ + Id: e.Id, + NameWithPeriod: toNameWithPeriod(e.Kandang, e.Period), + } +} + +func ToProjectFlockKandangNameWithPeriodDTOValues(id uint, kandangName string, period int) ProjectFlockKandangNameWithPeriodDTO { + return ProjectFlockKandangNameWithPeriodDTO{ + Id: id, + NameWithPeriod: toNameWithPeriodValue(kandangName, period), + } +} + func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserRelationDTO { if pf.CreatedUser.Id != 0 { mapped := userDTO.ToUserRelationDTO(pf.CreatedUser) diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 1dc62062..f80022b4 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -26,6 +26,7 @@ import ( type ProjectFlockKandangService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetAllNameWithPeriode(ctx *fiber.Ctx, params *validation.Query) ([]ProjectFlockKandangNameWithPeriode, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error) Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) @@ -51,6 +52,12 @@ type ClosingCheckResult struct { Expenses []ExpenseSummary `json:"expenses"` } +type ProjectFlockKandangNameWithPeriode struct { + Id uint + KandangName string + Period int +} + type StockRemainingDetail struct { FlagName string `json:"flag_name"` ProductWarehouseId uint `json:"product_warehouse_id"` @@ -133,6 +140,36 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return projectFlockKandangs, total, nil } +func (s projectFlockKandangService) GetAllNameWithPeriode(c *fiber.Ctx, params *validation.Query) ([]ProjectFlockKandangNameWithPeriode, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + scope, err := m.ResolveLocationScope(c, s.Repository.DB()) + if err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + rows, total, err := s.Repository.GetAllNameWithPeriodeScoped(c.Context(), offset, params.Limit, params, scope.IDs, scope.Restrict) + if err != nil { + s.Log.Errorf("Failed to get projectFlockKandangs name_with_periode: %+v", err) + return nil, 0, err + } + + results := make([]ProjectFlockKandangNameWithPeriode, 0, len(rows)) + for _, row := range rows { + results = append(results, ProjectFlockKandangNameWithPeriode{ + Id: row.Id, + KandangName: row.KandangName, + Period: row.Period, + }) + } + + return results, total, nil +} + func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { scope, err := m.ResolveLocationScope(c, s.Repository.DB()) if err != nil { diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go index 729d8329..1fc392ec 100644 --- a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -11,19 +11,20 @@ type Update struct { } 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"` - Search string `query:"search" validate:"omitempty,max=50"` - ProjectFlockId uint `query:"project_flock_id" validate:"omitempty"` - KandangId uint `query:"kandang_id" validate:"omitempty"` - Category string `query:"category" validate:"omitempty,oneof=Growing Laying"` - AreaId uint `query:"area_id" validate:"omitempty"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` - SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` - StepName string `query:"step_name" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` + NameWithPeriode bool `query:"name_with_periode"` + ProjectFlockId uint `query:"project_flock_id" validate:"omitempty"` + KandangId uint `query:"kandang_id" validate:"omitempty"` + Category string `query:"category" validate:"omitempty,oneof=Growing Laying"` + AreaId uint `query:"area_id" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` + StepName string `query:"step_name" validate:"omitempty,max=50"` } type Closing struct { Action string `json:"action" validate:"required,oneof=close unclose"` ClosedDate *string `json:"closed_date,omitempty"` -} \ No newline at end of file +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index b200cb18..29b06fe4 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -22,6 +22,7 @@ type ProjectFlockKandangRepository interface { GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) GetAllWithFiltersScoped(ctx context.Context, offset int, limit int, params interface{}, locationIDs []uint, restrict bool) ([]entity.ProjectFlockKandang, int64, error) + GetAllNameWithPeriodeScoped(ctx context.Context, offset int, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]ProjectFlockKandangNameWithPeriode, int64, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) @@ -40,6 +41,12 @@ type projectFlockKandangRepositoryImpl struct { db *gorm.DB } +type ProjectFlockKandangNameWithPeriode struct { + Id uint `gorm:"column:id"` + KandangName string `gorm:"column:kandang_name"` + Period int `gorm:"column:period"` +} + const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))" func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository { @@ -297,6 +304,97 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context. return records, total, nil } +func (r *projectFlockKandangRepositoryImpl) GetAllNameWithPeriodeScoped(ctx context.Context, offset int, limit int, params *validation.Query, locationIDs []uint, restrict bool) ([]ProjectFlockKandangNameWithPeriode, int64, error) { + var records []ProjectFlockKandangNameWithPeriode + var total int64 + + q := r.db.WithContext(ctx). + Table("project_flock_kandangs"). + Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). + Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\"") + + if restrict { + if len(locationIDs) == 0 { + return []ProjectFlockKandangNameWithPeriode{}, 0, nil + } + q = q.Where("\"project_flocks\".\"location_id\" IN ?", locationIDs) + } + + if params != nil && params.StepName != "" { + q = q.Where(` + EXISTS ( + SELECT 1 FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + AND LOWER("approvals"."step_name") = LOWER(?) + AND "approvals"."id" IN ( + SELECT "approvals"."id" FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + ORDER BY "approvals"."id" DESC + LIMIT 1 + ) + ) + `, "PROJECT_FLOCK_KANDANGS", params.StepName, "PROJECT_FLOCK_KANDANGS") + } + + if params != nil { + if params.Search != "" { + escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(params.Search) + q = q.Where( + r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). + Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), + ) + } + + if params.ProjectFlockId > 0 { + q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", params.ProjectFlockId) + } + + if params.KandangId > 0 { + q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", params.KandangId) + } + + if params.Category != "" { + q = q.Where("\"project_flocks\".\"category\" = ?", params.Category) + } + + if params.AreaId > 0 { + q = q.Where("\"project_flocks\".\"area_id\" = ?", params.AreaId) + } + } + + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + + sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + if params != nil && params.SortBy != "" { + sortOrder := "DESC" + if params.SortOrder == "ASC" { + sortOrder = "ASC" + } + + switch params.SortBy { + case "created_at": + sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder + case "period": + sortBy = "\"project_flocks\".\"period\" " + sortOrder + } + } + + if err := q. + Select("\"project_flock_kandangs\".\"id\", \"project_flock_kandangs\".\"period\", \"kandangs\".\"name\" AS kandang_name"). + Order(sortBy). + Offset(offset). + Limit(limit). + Scan(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} + func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } From 3eb225cca896eb4ee94c1d048a8463f6defeb592 Mon Sep 17 00:00:00 2001 From: giovanni Date: Thu, 9 Apr 2026 17:00:03 +0700 Subject: [PATCH 2/6] adjust validation from week 19 --- .../services/uniformity.service.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go index 5c28ce78..1bb72ae4 100644 --- a/internal/modules/production/uniformities/services/uniformity.service.go +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -386,10 +386,10 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file weekBase = config.LayingWeekStart() } if req.Week < weekBase { - if isLayingCategory { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) + if !isLayingCategory { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") } - return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") + // return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) } var latestWeek int @@ -401,10 +401,10 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity week sequence") } if latestWeek == 0 && req.Week != weekBase { - if isLayingCategory { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) + if !isLayingCategory { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") } - return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") + // return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) } // if latestWeek > 0 && req.Week > latestWeek+1 { // return nil, fiber.NewError(fiber.StatusBadRequest, "week must be sequential without skipping") @@ -582,10 +582,10 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui weekBase = config.LayingWeekStart() } if targetWeek < weekBase { - if isLayingCategory { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) + if !isLayingCategory { + return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") } - return nil, fiber.NewError(fiber.StatusBadRequest, "week must start from 1 for growing projects") + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("week must start from %d for laying projects", weekBase)) } } if targetDate != nil { From b79738dbe1ff3138cd00653f1c5f16d766dd8254 Mon Sep 17 00:00:00 2001 From: giovanni Date: Fri, 10 Apr 2026 17:16:28 +0700 Subject: [PATCH 3/6] fix calculate egg mass and hen house recordings --- .../main.go | 380 ++++++++++++++++++ .../repositories/recording.repository.go | 30 +- .../recordings/services/recording.service.go | 6 +- 3 files changed, 410 insertions(+), 6 deletions(-) create mode 100644 cmd/normalize-data-egg-mass-and-hen-house/main.go diff --git a/cmd/normalize-data-egg-mass-and-hen-house/main.go b/cmd/normalize-data-egg-mass-and-hen-house/main.go new file mode 100644 index 00000000..b34fb7e2 --- /dev/null +++ b/cmd/normalize-data-egg-mass-and-hen-house/main.go @@ -0,0 +1,380 @@ +package main + +import ( + "context" + "flag" + "fmt" + "log" + "math" + "os" + "strings" + "time" + + "gitlab.com/mbugroup/lti-api.git/internal/config" + "gitlab.com/mbugroup/lti-api.git/internal/database" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" + "gorm.io/gorm" +) + +const metricEpsilon = 1e-9 + +type normalizeOptions struct { + Apply bool + RecordingID uint + ProjectFlockKandangID uint + From *time.Time + To *time.Time + BatchSize int + Limit int +} + +type normalizeStats struct { + Processed int + Changed int + Updated int + Skipped int + Failed int +} + +type recordingMetricRow struct { + ID uint `gorm:"column:id"` + ProjectFlockKandangID uint `gorm:"column:project_flock_kandangs_id"` + RecordDatetime time.Time `gorm:"column:record_datetime"` + HenHouse *float64 `gorm:"column:hen_house"` + EggMass *float64 `gorm:"column:egg_mass"` +} + +func main() { + var ( + apply bool + recordingID uint + projectFlockKandangID uint + fromRaw string + toRaw string + batchSize int + limit int + ) + + flag.BoolVar(&apply, "apply", false, "Apply update. If false, run as dry-run") + flag.UintVar(&recordingID, "recording-id", 0, "Target a single recording ID") + flag.UintVar(&projectFlockKandangID, "project-flock-kandang-id", 0, "Filter by project_flock_kandangs_id") + flag.StringVar(&fromRaw, "from", "", "Lower bound record_datetime (RFC3339 / YYYY-MM-DD)") + flag.StringVar(&toRaw, "to", "", "Upper bound record_datetime (RFC3339 / YYYY-MM-DD)") + flag.IntVar(&batchSize, "batch-size", 200, "Batch size when scanning recordings") + flag.IntVar(&limit, "limit", 0, "Max recordings to process (0 = no limit)") + flag.Parse() + + if batchSize <= 0 { + log.Fatal("--batch-size must be > 0") + } + if limit < 0 { + log.Fatal("--limit cannot be negative") + } + + from, err := parseTimeBound(strings.TrimSpace(fromRaw), false) + if err != nil { + log.Fatalf("invalid --from: %v", err) + } + to, err := parseTimeBound(strings.TrimSpace(toRaw), true) + if err != nil { + log.Fatalf("invalid --to: %v", err) + } + if from != nil && to != nil && to.Before(*from) { + log.Fatal("--to cannot be before --from") + } + + opts := normalizeOptions{ + Apply: apply, + RecordingID: recordingID, + ProjectFlockKandangID: projectFlockKandangID, + From: from, + To: to, + BatchSize: batchSize, + Limit: limit, + } + + ctx := context.Background() + db := database.Connect(config.DBHost, config.DBName) + repo := recordingRepo.NewRecordingRepository(db) + + fmt.Printf("Mode: %s\n", modeLabel(opts.Apply)) + fmt.Printf("Filter recording_id: %s\n", displayUint(opts.RecordingID)) + fmt.Printf("Filter project_flock_kandangs_id: %s\n", displayUint(opts.ProjectFlockKandangID)) + fmt.Printf("Filter from: %s\n", displayTime(opts.From)) + fmt.Printf("Filter to: %s\n", displayTime(opts.To)) + fmt.Printf("Batch size: %d\n", opts.BatchSize) + fmt.Printf("Limit: %d\n\n", opts.Limit) + + stats, err := normalizeRecordings(ctx, db, repo, opts) + if err != nil { + log.Fatalf("normalize failed: %v", err) + } + + fmt.Println() + fmt.Printf( + "Summary: processed=%d changed=%d updated=%d skipped=%d failed=%d\n", + stats.Processed, + stats.Changed, + stats.Updated, + stats.Skipped, + stats.Failed, + ) + + if stats.Failed > 0 { + os.Exit(1) + } +} + +func normalizeRecordings( + ctx context.Context, + db *gorm.DB, + repo recordingRepo.RecordingRepository, + opts normalizeOptions, +) (normalizeStats, error) { + stats := normalizeStats{} + lastID := uint(0) + initialChickCache := make(map[uint]float64) + + for { + batchLimit := opts.BatchSize + if opts.Limit > 0 { + remaining := opts.Limit - stats.Processed + if remaining <= 0 { + break + } + if remaining < batchLimit { + batchLimit = remaining + } + } + + rows, err := loadRecordingBatch(ctx, db, opts, lastID, batchLimit) + if err != nil { + return stats, err + } + if len(rows) == 0 { + break + } + + for _, row := range rows { + stats.Processed++ + lastID = row.ID + + initialChick, ok := initialChickCache[row.ProjectFlockKandangID] + if !ok { + initialChick, err = repo.GetTotalChickinByProjectFlockKandang(db.WithContext(ctx), row.ProjectFlockKandangID) + if err != nil { + fmt.Printf("FAIL rec=%d error=getTotalChickinByProjectFlockKandang: %v\n", row.ID, err) + stats.Failed++ + continue + } + initialChickCache[row.ProjectFlockKandangID] = initialChick + } + + _, totalEggWeightGrams, err := repo.GetEggSummaryByRecording(db.WithContext(ctx), row.ID) + if err != nil { + fmt.Printf("FAIL rec=%d error=getEggSummaryByRecording: %v\n", row.ID, err) + stats.Failed++ + continue + } + + cumulativeEggQty, err := repo.GetCumulativeEggQtyByProjectFlockKandang(db.WithContext(ctx), row.ProjectFlockKandangID, row.RecordDatetime) + if err != nil { + fmt.Printf("FAIL rec=%d error=getCumulativeEggQtyByProjectFlockKandang: %v\n", row.ID, err) + stats.Failed++ + continue + } + + newHenHouse, newEggMass := computeNormalizedMetrics(initialChick, cumulativeEggQty, totalEggWeightGrams) + henHouseChanged := metricChanged(row.HenHouse, newHenHouse) + eggMassChanged := metricChanged(row.EggMass, newEggMass) + + if !henHouseChanged && !eggMassChanged { + stats.Skipped++ + continue + } + + stats.Changed++ + fmt.Printf( + "PLAN rec=%d pfk=%d at=%s hen_house:%s->%s egg_mass:%s->%s\n", + row.ID, + row.ProjectFlockKandangID, + row.RecordDatetime.UTC().Format(time.RFC3339), + displayFloat(row.HenHouse), + displayFloat(newHenHouse), + displayFloat(row.EggMass), + displayFloat(newEggMass), + ) + + if !opts.Apply { + continue + } + + if err := updateRecordingMetrics(ctx, db, row.ID, newHenHouse, newEggMass); err != nil { + fmt.Printf("FAIL rec=%d error=updateRecordingMetrics: %v\n", row.ID, err) + stats.Failed++ + continue + } + + fmt.Printf( + "DONE rec=%d hen_house=%s egg_mass=%s\n", + row.ID, + displayFloat(newHenHouse), + displayFloat(newEggMass), + ) + stats.Updated++ + } + + if opts.RecordingID > 0 { + break + } + } + + return stats, nil +} + +func loadRecordingBatch( + ctx context.Context, + db *gorm.DB, + opts normalizeOptions, + lastID uint, + limit int, +) ([]recordingMetricRow, error) { + query := db.WithContext(ctx). + Table("recordings"). + Select("id, project_flock_kandangs_id, record_datetime, hen_house, egg_mass"). + Where("recordings.deleted_at IS NULL") + + if opts.RecordingID > 0 { + query = query.Where("recordings.id = ?", opts.RecordingID) + } + if opts.ProjectFlockKandangID > 0 { + query = query.Where("recordings.project_flock_kandangs_id = ?", opts.ProjectFlockKandangID) + } + if opts.From != nil { + query = query.Where("recordings.record_datetime >= ?", *opts.From) + } + if opts.To != nil { + query = query.Where("recordings.record_datetime <= ?", *opts.To) + } + if opts.RecordingID == 0 && lastID > 0 { + query = query.Where("recordings.id > ?", lastID) + } + + var rows []recordingMetricRow + err := query. + Order("recordings.id ASC"). + Limit(limit). + Scan(&rows).Error + return rows, err +} + +func computeNormalizedMetrics(initialChick, cumulativeEggQty, totalEggWeightGrams float64) (*float64, *float64) { + var henHouse *float64 + if initialChick > 0 && cumulativeEggQty >= 0 { + value := cumulativeEggQty / initialChick + henHouse = &value + } + + var eggMass *float64 + if initialChick > 0 && totalEggWeightGrams > 0 { + value := totalEggWeightGrams / initialChick + eggMass = &value + } + + return henHouse, eggMass +} + +func updateRecordingMetrics(ctx context.Context, db *gorm.DB, recordingID uint, henHouse, eggMass *float64) error { + updates := map[string]any{} + if henHouse == nil { + updates["hen_house"] = gorm.Expr("NULL") + } else { + updates["hen_house"] = *henHouse + } + if eggMass == nil { + updates["egg_mass"] = gorm.Expr("NULL") + } else { + updates["egg_mass"] = *eggMass + } + + return db.WithContext(ctx). + Table("recordings"). + Where("id = ?", recordingID). + Updates(updates).Error +} + +func metricChanged(oldValue, newValue *float64) bool { + if oldValue == nil && newValue == nil { + return false + } + if oldValue == nil || newValue == nil { + return true + } + return !nearlyEqual(*oldValue, *newValue) +} + +func nearlyEqual(a, b float64) bool { + scale := math.Max(1, math.Max(math.Abs(a), math.Abs(b))) + return math.Abs(a-b) <= metricEpsilon*scale +} + +func parseTimeBound(raw string, isUpper bool) (*time.Time, error) { + if raw == "" { + return nil, nil + } + + layouts := []string{ + time.RFC3339Nano, + time.RFC3339, + "2006-01-02 15:04:05", + "2006-01-02", + } + + for _, layout := range layouts { + parsed, err := time.Parse(layout, raw) + if err != nil { + continue + } + if layout == "2006-01-02" { + if isUpper { + endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), time.UTC) + return &endOfDay, nil + } + startOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 0, 0, 0, 0, time.UTC) + return &startOfDay, nil + } + + t := parsed.UTC() + return &t, nil + } + + return nil, fmt.Errorf("unsupported format %q", raw) +} + +func modeLabel(apply bool) string { + if apply { + return "APPLY" + } + return "DRY-RUN" +} + +func displayFloat(v *float64) string { + if v == nil { + return "NULL" + } + return fmt.Sprintf("%.6f", *v) +} + +func displayTime(v *time.Time) string { + if v == nil { + return "" + } + return v.UTC().Format(time.RFC3339) +} + +func displayUint(v uint) string { + if v == 0 { + return "" + } + return fmt.Sprintf("%d", v) +} diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 6ea4c473..1f01f50b 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -758,15 +758,39 @@ func (r *RecordingRepositoryImpl) GetCumulativeEggQtyByProjectFlockKandang( return 0, nil } - var result float64 + var cumulativeEggQty float64 err := tx. Table("recording_eggs"). Select("COALESCE(SUM(recording_eggs.qty), 0)"). Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id"). Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId). Where("recordings.record_datetime <= ?", recordTime). - Scan(&result).Error - return result, err + Scan(&cumulativeEggQty).Error + if err != nil { + return 0, err + } + + productWarehouseSubQuery := tx. + Table("recording_eggs"). + Select("DISTINCT recording_eggs.product_warehouse_id"). + Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id"). + Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId). + Where("recordings.record_datetime <= ?", recordTime) + + var adjustmentEggQty float64 + err = tx. + Table("adjustment_stocks"). + Select("COALESCE(SUM(adjustment_stocks.total_qty), 0)"). + Where("adjustment_stocks.product_warehouse_id IN (?)", productWarehouseSubQuery). + Where("adjustment_stocks.function_code = ?", "RECORDING_EGG_IN"). + Where("adjustment_stocks.transaction_type = ?", "RECORDING"). + Where("adjustment_stocks.created_at <= ?", recordTime). + Scan(&adjustmentEggQty).Error + if err != nil { + return 0, err + } + + return cumulativeEggQty + adjustmentEggQty, nil } func (r *RecordingRepositoryImpl) GetProductionWeightAndQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeight float64, totalQty float64, err error) { // Body-weight tracking is removed; keep stub for report compatibility. diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 6f5fddb6..5c4d6a9c 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1989,9 +1989,9 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm } var eggMass float64 - if remainingChick > 0 && totalEggWeightGrams > 0 { - // totalEggWeightGrams is in grams; egg mass is grams per hen. - eggMass = totalEggWeightGrams / remainingChick + if initialChickin > 0 && totalEggWeightGrams > 0 { + // totalEggWeightGrams is in grams; egg mass uses initial chick population. + eggMass = totalEggWeightGrams / initialChickin updates["egg_mass"] = eggMass recording.EggMass = &eggMass } else { From d1612e5c659544d04379e29a8f9d4460a1fd5bbf Mon Sep 17 00:00:00 2001 From: giovanni Date: Mon, 13 Apr 2026 10:51:12 +0700 Subject: [PATCH 4/6] add query param location id --- .../controllers/project_flock_kandang.controller.go | 1 + .../validations/project_flock_kandang.validation.go | 1 + .../repositories/projectflock_kandang.repository.go | 12 ++++++++++++ 3 files changed, 14 insertions(+) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 9333410f..8593b992 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -32,6 +32,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { KandangId: uint(c.QueryInt("kandang_id", 0)), Category: c.Query("category", ""), AreaId: uint(c.QueryInt("area_id", 0)), + LocationId: uint(c.QueryInt("location_id", 0)), SortBy: c.Query("sort_by", ""), SortOrder: c.Query("sort_order", ""), StepName: c.Query("step_name", ""), diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go index 1fc392ec..a1d71596 100644 --- a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -19,6 +19,7 @@ type Query struct { KandangId uint `query:"kandang_id" validate:"omitempty"` Category string `query:"category" validate:"omitempty,oneof=Growing Laying"` AreaId uint `query:"area_id" validate:"omitempty"` + LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` StepName string `query:"step_name" validate:"omitempty,max=50"` diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 29b06fe4..194d1157 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -178,6 +178,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex if query.AreaId > 0 { q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) } + + if query.LocationId > 0 { + q = q.Where("\"kandangs\".\"location_id\" = ?", query.LocationId) + } } if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { @@ -276,6 +280,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context. if query.AreaId > 0 { q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) } + + if query.LocationId > 0 { + q = q.Where("\"kandangs\".\"location_id\" = ?", query.LocationId) + } } if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { @@ -362,6 +370,10 @@ func (r *projectFlockKandangRepositoryImpl) GetAllNameWithPeriodeScoped(ctx cont if params.AreaId > 0 { q = q.Where("\"project_flocks\".\"area_id\" = ?", params.AreaId) } + + if params.LocationId > 0 { + q = q.Where("\"kandangs\".\"location_id\" = ?", params.LocationId) + } } if err := q.Count(&total).Error; err != nil { From 5e2187c46bfe6e11b3ee838fde8ea6cc5467cc5e Mon Sep 17 00:00:00 2001 From: giovanni Date: Mon, 13 Apr 2026 11:49:41 +0700 Subject: [PATCH 5/6] adjust default order by dan sort by --- .../repositories/projectflock_kandang.repository.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 194d1157..e70a77fb 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -188,7 +188,7 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Contex return nil, 0, err } - sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + sortBy := "\"project_flock_kandangs\".\"id\" ASC" if ok && query != nil && query.SortBy != "" { sortOrder := "DESC" if query.SortOrder == "ASC" { @@ -290,7 +290,7 @@ func (r *projectFlockKandangRepositoryImpl) GetAllWithFiltersScoped(ctx context. return nil, 0, err } - sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + sortBy := "\"project_flock_kandangs\".\"id\" ASC" if ok && query != nil && query.SortBy != "" { sortOrder := "DESC" if query.SortOrder == "ASC" { From cff5837ff946cfd599a182d2500c5638e682f266 Mon Sep 17 00:00:00 2001 From: giovanni Date: Mon, 13 Apr 2026 12:14:54 +0700 Subject: [PATCH 6/6] adjust default order dan sort by --- internal/modules/master/areas/services/area.service.go | 4 ++-- .../modules/master/locations/services/location.service.go | 2 +- .../project_flocks/repositories/projectflock.repository.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/modules/master/areas/services/area.service.go b/internal/modules/master/areas/services/area.service.go index 6110aaef..c0e351d7 100644 --- a/internal/modules/master/areas/services/area.service.go +++ b/internal/modules/master/areas/services/area.service.go @@ -55,9 +55,9 @@ func (s areaService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Ar db = s.withRelations(db) db, scopeErr = m.ApplyAreaScope(c, db, "id") if params.Search != "" { - return db.Where("name ILIKE ?", "%"+params.Search+"%") + db = db.Where("name ILIKE ?", "%"+params.Search+"%") } - return db.Order("created_at DESC").Order("updated_at DESC") + return db.Order("name ASC").Order("id ASC") }) if scopeErr != nil { diff --git a/internal/modules/master/locations/services/location.service.go b/internal/modules/master/locations/services/location.service.go index 8aa01dbf..138c33b9 100644 --- a/internal/modules/master/locations/services/location.service.go +++ b/internal/modules/master/locations/services/location.service.go @@ -71,7 +71,7 @@ func (s locationService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit ) `, utils.ProjectFlockCategoryLaying) } - return db.Order("created_at DESC").Order("updated_at DESC") + return db.Order("locations.name ASC").Order("locations.id ASC") }) if scopeErr != nil { diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 6fab653f..60b5fc01 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -322,8 +322,8 @@ func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder str } default: return []string{ - "project_flocks.created_at DESC", - "project_flocks.updated_at DESC", + "project_flocks.flock_name ASC", + "project_flocks.id ASC", } } }