feat(BE-281): unfinished uniformity and create project flock triger productwarehouse and add new filtering lookup

This commit is contained in:
ragilap
2025-12-29 00:21:26 +07:00
parent cdfa77566c
commit 644896edfa
25 changed files with 2043 additions and 8 deletions
@@ -0,0 +1,195 @@
package service
import (
"io"
"mime/multipart"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/xuri/excelize/v2"
)
type BodyWeightExcelRow struct {
No int `json:"no"`
Weight float64 `json:"weight"`
Range string `json:"range,omitempty"`
}
func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) {
if file == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "file is required")
}
reader, err := file.Open()
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file")
}
defer reader.Close()
rows, err := parseBodyWeightExcelReader(reader)
if err != nil {
return nil, err
}
return rows, nil
}
func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) {
xlsx, err := excelize.OpenReader(reader)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file")
}
defer func() {
_ = xlsx.Close()
}()
sheets := xlsx.GetSheetList()
if len(sheets) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file")
}
rows, err := xlsx.GetRows(sheets[0], excelize.Options{RawCellValue: true})
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows")
}
return parseBodyWeightRows(rows)
}
func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) {
headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows)
if headerRowIdx < 0 || bwCol < 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found")
}
result := make([]BodyWeightExcelRow, 0)
lastNo := 0
for i := headerRowIdx + 1; i < len(rows); i++ {
row := rows[i]
weightStr := cellAt(row, bwCol)
weightVal, ok := parseNumber(weightStr)
if !ok {
continue
}
noVal := 0
if noCol >= 0 {
if parsed, ok := parseNumber(cellAt(row, noCol)); ok {
noVal = int(parsed)
}
}
if noVal <= 0 {
noVal = lastNo + 1
}
if noVal > lastNo {
lastNo = noVal
}
rangeVal := ""
if rangeCol >= 0 {
rangeVal = strings.TrimSpace(cellAt(row, rangeCol))
}
rowPayload := BodyWeightExcelRow{
No: noVal,
Weight: weightVal,
Range: rangeVal,
}
if rowPayload.No <= 0 || rowPayload.Weight <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data")
}
result = append(result, rowPayload)
}
if len(result) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
}
return result, nil
}
func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) {
rowIdx = -1
noCol = -1
bwCol = -1
rangeCol = -1
for i, row := range rows {
tempNo := -1
tempBW := -1
tempRange := -1
for j, cell := range row {
label := normalizeHeader(cell)
switch label {
case "no":
tempNo = j
case "bw":
tempBW = j
case "outsiderange":
tempRange = j
default:
if strings.HasPrefix(label, "bw") {
tempBW = j
} else if strings.HasPrefix(label, "no") {
tempNo = j
} else if strings.Contains(label, "range") {
tempRange = j
}
}
}
if tempBW >= 0 {
rowIdx = i
bwCol = tempBW
noCol = tempNo
rangeCol = tempRange
break
}
}
return rowIdx, noCol, bwCol, rangeCol
}
func cellAt(row []string, idx int) string {
if idx < 0 || idx >= len(row) {
return ""
}
return strings.TrimSpace(row[idx])
}
func normalizeHeader(value string) string {
trimmed := strings.ToLower(strings.TrimSpace(value))
if trimmed == "" {
return ""
}
var b strings.Builder
for _, r := range trimmed {
if r >= 'a' && r <= 'z' {
b.WriteRune(r)
}
}
return b.String()
}
func parseNumber(value string) (float64, bool) {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return 0, false
}
if strings.Contains(trimmed, ",") {
if strings.Contains(trimmed, ".") {
trimmed = strings.ReplaceAll(trimmed, ",", "")
} else {
trimmed = strings.ReplaceAll(trimmed, ",", ".")
}
}
parsed, err := strconv.ParseFloat(trimmed, 64)
if err != nil {
return 0, false
}
return parsed, true
}
@@ -0,0 +1,738 @@
package service
import (
"context"
"errors"
"fmt"
"math"
"mime/multipart"
"net/http"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type UniformityService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error)
}
type uniformityService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.UniformityRepository
DocumentSvc commonSvc.DocumentService
ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService
}
func NewUniformityService(
repo repository.UniformityRepository,
documentSvc commonSvc.DocumentService,
approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) UniformityService {
return &uniformityService{
Log: utils.Log,
Validate: validate,
Repository: repo,
DocumentSvc: documentSvc,
ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc,
}
}
func (s uniformityService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("ProjectFlockKandang.ProjectFlock.Location").
Preload("ProjectFlockKandang.Kandang.Location")
}
func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
uniformitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.ProjectFlockKandangId != 0 {
db = db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId)
}
if params.Week != 0 {
db = db.Where("week = ?", params.Week)
}
return db.Order("uniform_date DESC").Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get uniformitys: %+v", err)
return nil, 0, err
}
if err := s.attachLatestApprovals(c.Context(), uniformitys); err != nil {
return nil, 0, err
}
return uniformitys, total, nil
}
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
uniformity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
if err != nil {
s.Log.Errorf("Failed get uniformity by id: %+v", err)
return nil, err
}
if err := s.attachLatestApproval(c.Context(), uniformity); err != nil {
return nil, err
}
return uniformity, nil
}
func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
return s.GetOne(c, id)
}
func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if file == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "document is required")
}
uniformDate, err := time.Parse("2006-01-02", req.Date)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
}
if len(rows) == 0 {
parsedRows, err := s.ParseBodyWeightExcel(c, file)
if err != nil {
return nil, err
}
rows = parsedRows
}
calculation, err := s.ComputeUniformity(rows)
if err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
createBody := &entity.ProjectFlockKandangUniformity{
Uniformity: calculation.Uniformity,
Week: req.Week,
Cv: calculation.Cv,
ChickQtyOfWeight: calculation.ChickQtyOfWeight,
MeanUp: calculation.MeanUp,
MeanDown: calculation.MeanDown,
ProjectFlockKandangId: req.ProjectFlockKandangId,
UniformQty: calculation.UniformQty,
NotUniformQty: calculation.OutsideQty,
UniformDate: &uniformDate,
CreatedBy: actorID,
}
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if err := s.createUniformityApproval(
c.Context(),
tx,
createBody.Id,
utils.UniformityStepPengajuan,
entity.ApprovalActionCreated,
actorID,
nil,
); err != nil {
return err
}
return nil
}); err != nil {
s.Log.Errorf("Failed to create uniformity: %+v", err)
return nil, err
}
if s.DocumentSvc != nil {
actorIDCopy := actorID
_, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: "UNIFORMITY",
DocumentableID: uint64(createBody.Id),
CreatedBy: &actorIDCopy,
Files: []commonSvc.DocumentFile{
{
File: file,
Type: "UNIFORMITY",
},
},
})
if err != nil {
s.rollbackUniformityCreate(c.Context(), createBody.Id)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
}
}
return s.GetOne(c, createBody.Id)
}
func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Date != nil {
parsed, err := time.Parse("2006-01-02", *req.Date)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format")
}
updateBody["uniform_date"] = parsed
}
if req.ProjectFlockKandangId != nil {
updateBody["project_flock_kandang_id"] = *req.ProjectFlockKandangId
}
if req.Week != nil {
updateBody["week"] = *req.Week
}
if file != nil {
if s.DocumentSvc == nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
if len(rows) == 0 {
parsedRows, err := s.ParseBodyWeightExcel(c, file)
if err != nil {
return nil, err
}
rows = parsedRows
}
calculation, err := s.ComputeUniformity(rows)
if err != nil {
return nil, err
}
updateBody["uniformity"] = calculation.Uniformity
updateBody["cv"] = calculation.Cv
updateBody["chick_qty_of_weight"] = calculation.ChickQtyOfWeight
updateBody["mean_up"] = calculation.MeanUp
updateBody["mean_down"] = calculation.MeanDown
updateBody["uniform_qty"] = calculation.UniformQty
updateBody["not_uniform_qty"] = calculation.OutsideQty
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if file == nil {
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
s.Log.Errorf("Failed to update uniformity: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
existingDocs, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(id))
if err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
actorIDCopy := actorID
uploadResults, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{
DocumentableType: "UNIFORMITY",
DocumentableID: uint64(id),
CreatedBy: &actorIDCopy,
Files: []commonSvc.DocumentFile{
{
File: file,
Type: "UNIFORMITY",
},
},
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if len(uploadResults) > 0 {
ids := make([]uint, 0, len(uploadResults))
for _, result := range uploadResults {
if result.Document.Id != 0 {
ids = append(ids, result.Document.Id)
}
}
if len(ids) > 0 {
_ = s.DocumentSvc.DeleteDocuments(c.Context(), ids, true)
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
s.Log.Errorf("Failed to update uniformity: %+v", err)
return nil, err
}
if len(existingDocs) > 0 {
oldIDs := make([]uint, 0, len(existingDocs))
for _, doc := range existingDocs {
if doc.Id != 0 {
oldIDs = append(oldIDs, doc.Id)
}
}
if len(oldIDs) > 0 {
_ = s.DocumentSvc.DeleteDocuments(c.Context(), oldIDs, true)
}
}
return s.GetOne(c, id)
}
func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
}
s.Log.Errorf("Failed to delete uniformity: %+v", err)
return err
}
return nil
}
func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actionValue := strings.ToUpper(strings.TrimSpace(req.Action))
var action entity.ApprovalAction
switch actionValue {
case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
case string(entity.ApprovalActionRejected):
action = entity.ApprovalActionRejected
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
ids := uniqueUintSlice(req.ApprovableIds)
if len(ids) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.UniformityStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.UniformityStepDisetujui
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
ctx := c.Context()
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
for _, id := range ids {
if _, err := repoTx.GetByID(ctx, id, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Uniformity %d not found", id))
}
return err
}
if _, err := approvalSvc.CreateApproval(
ctx,
utils.ApprovalWorkflowUniformity,
id,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
}
return nil
})
if transactionErr != nil {
if fiberErr, ok := transactionErr.(*fiber.Error); ok {
return nil, fiberErr
}
s.Log.Errorf("Failed to record approvals for uniformities %+v: %+v", ids, transactionErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to submit uniformity approval")
}
results := make([]entity.ProjectFlockKandangUniformity, 0, len(ids))
for _, id := range ids {
loaded, err := s.GetOne(c, id)
if err != nil {
return nil, err
}
results = append(results, *loaded)
}
return results, nil
}
type UniformityDetailItem struct {
Id int
Weight float64
Range string
}
type UniformityCalculation struct {
ChickQtyOfWeight float64
MeanWeight float64
MeanDown float64
MeanUp float64
UniformQty float64
OutsideQty float64
Uniformity float64
Cv float64
Details []UniformityDetailItem
}
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
return computeUniformity(rows)
}
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) {
if s.DocumentSvc == nil {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
if err != nil {
return UniformityCalculation{}, nil, err
}
if len(documents) == 0 {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
}
document := documents[0]
url := s.DocumentSvc.PublicURL(document)
if url == "" {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
}
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
if err != nil {
return UniformityCalculation{}, nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return UniformityCalculation{}, nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
}
rows, err := parseBodyWeightExcelReader(resp.Body)
if err != nil {
return UniformityCalculation{}, nil, err
}
calculation, err := computeUniformity(rows)
if err != nil {
return UniformityCalculation{}, nil, err
}
return calculation, &document, nil
}
func (s *uniformityService) createUniformityApproval(
ctx context.Context,
db *gorm.DB,
uniformityID uint,
step approvalutils.ApprovalStep,
action entity.ApprovalAction,
actorID uint,
notes *string,
) error {
if uniformityID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Uniformity tidak valid untuk approval")
}
if actorID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval")
}
var svc commonSvc.ApprovalService
if db != nil {
svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
} else if s.ApprovalSvc != nil {
svc = s.ApprovalSvc
} else {
svc = commonSvc.NewApprovalService(s.ApprovalRepo)
}
_, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowUniformity, uniformityID, step, &action, actorID, notes)
return err
}
func (s *uniformityService) attachLatestApprovals(ctx context.Context, items []entity.ProjectFlockKandangUniformity) error {
if len(items) == 0 || s.ApprovalSvc == nil {
return nil
}
ids := make([]uint, 0, len(items))
visited := make(map[uint]struct{}, len(items))
for _, item := range items {
if item.Id == 0 {
continue
}
if _, ok := visited[item.Id]; ok {
continue
}
visited[item.Id] = struct{}{}
ids = append(ids, item.Id)
}
if len(ids) == 0 {
return nil
}
latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowUniformity, ids, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Unable to load latest approvals for uniformities: %+v", err)
return nil
}
if len(latestMap) == 0 {
return nil
}
for i := range items {
if items[i].Id == 0 {
continue
}
if approval, ok := latestMap[items[i].Id]; ok {
items[i].LatestApproval = approval
}
}
return nil
}
func (s *uniformityService) attachLatestApproval(ctx context.Context, item *entity.ProjectFlockKandangUniformity) error {
if item == nil || item.Id == 0 || s.ApprovalSvc == nil {
return nil
}
approvals, err := s.ApprovalSvc.ListByTarget(ctx, utils.ApprovalWorkflowUniformity, item.Id, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("Unable to load approvals for uniformity %d: %+v", item.Id, err)
return nil
}
if len(approvals) == 0 {
item.LatestApproval = nil
return nil
}
latest := approvals[len(approvals)-1]
item.LatestApproval = &latest
return nil
}
func (s *uniformityService) rollbackUniformityCreate(ctx context.Context, uniformityID uint) {
if uniformityID == 0 {
return
}
if s.ApprovalRepo != nil {
if err := s.ApprovalRepo.DeleteByTarget(ctx, utils.ApprovalWorkflowUniformity.String(), uniformityID); err != nil {
s.Log.WithError(err).Warnf("Failed to rollback uniformity approvals for %d", uniformityID)
}
}
if err := s.Repository.DeleteOne(ctx, uniformityID); err != nil {
s.Log.WithError(err).Warnf("Failed to rollback uniformity %d", uniformityID)
}
}
func uniqueUintSlice(values []uint) []uint {
if len(values) == 0 {
return nil
}
seen := make(map[uint]struct{}, len(values))
result := make([]uint, 0, len(values))
for _, v := range values {
if v == 0 {
continue
}
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
result = append(result, v)
}
return result
}
func computeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
weights := make([]float64, 0, len(rows))
details := make([]UniformityDetailItem, 0, len(rows))
hasRangeLabels := false
for idx, row := range rows {
if row.Weight <= 0 {
continue
}
id := row.No
if id <= 0 {
id = idx + 1
}
weights = append(weights, row.Weight)
rangeLabel := strings.TrimSpace(row.Range)
if rangeLabel != "" {
upper := strings.ToUpper(rangeLabel)
if upper == "HIGH" || upper == "LOW" {
hasRangeLabels = true
}
rangeLabel = upper
}
details = append(details, UniformityDetailItem{
Id: id,
Weight: row.Weight,
Range: rangeLabel,
})
}
total := float64(len(weights))
if total == 0 {
return UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found")
}
var sum float64
for _, w := range weights {
sum += w
}
mean := sum / total
meanUpThreshold := roundToPrecision(mean*1.10, 3)
meanDownThreshold := roundToPrecision(mean*0.90, 3)
var uniformCount float64
for i := range details {
if hasRangeLabels {
if details[i].Range == "HIGH" || details[i].Range == "LOW" {
details[i].Range = "Outside"
continue
}
details[i].Range = "Ideal"
uniformCount++
continue
}
w := details[i].Weight
if w > meanUpThreshold || w < meanDownThreshold {
details[i].Range = "Outside"
continue
}
details[i].Range = "Ideal"
uniformCount++
}
outsideCount := total - uniformCount
var cv float64
if mean > 0 && total > 1 {
stddevWeights := weights
if len(stddevWeights) > 100 {
stddevWeights = stddevWeights[:100]
}
stddevCount := float64(len(stddevWeights))
if stddevCount > 1 {
var stddevSum float64
for _, w := range stddevWeights {
stddevSum += w
}
stddevMean := stddevSum / stddevCount
var sumSquares float64
for _, w := range stddevWeights {
diff := w - stddevMean
sumSquares += diff * diff
}
stddev := math.Sqrt(sumSquares / (stddevCount - 1))
cv = (stddev / mean) * 100
}
}
uniformity := (uniformCount / total) * 100
return UniformityCalculation{
ChickQtyOfWeight: total,
MeanWeight: roundToPrecision(mean, 0),
MeanDown: roundToPrecision(mean*0.90, 0),
MeanUp: roundToPrecision(mean*1.10, 0),
UniformQty: uniformCount,
OutsideQty: outsideCount,
Uniformity: roundToPrecision(uniformity, 0),
Cv: roundToPrecision(cv, 1),
Details: details,
}, nil
}
func roundToPrecision(value float64, precision int) float64 {
if precision < 0 {
return value
}
scale := math.Pow10(precision)
scaled := value * scale
fraction := scaled - math.Floor(scaled)
if fraction >= 0.5 {
return math.Ceil(scaled) / scale
}
return math.Floor(scaled) / scale
}