mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-21 22:05:44 +00:00
feat(BE-281): fixing recording error, fixing limit upload uniformity and purchase, add filter and statistic uniformity
This commit is contained in:
@@ -1,200 +0,0 @@
|
||||
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")
|
||||
}
|
||||
|
||||
sheetName := sheets[0]
|
||||
if len(sheets) > 1 {
|
||||
sheetName = sheets[1]
|
||||
}
|
||||
|
||||
rows, err := xlsx.GetRows(sheetName, 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,393 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
|
||||
|
||||
"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")
|
||||
}
|
||||
|
||||
sheetName := sheets[0]
|
||||
if len(sheets) > 1 {
|
||||
sheetName = sheets[1]
|
||||
}
|
||||
|
||||
rows, err := xlsx.GetRows(sheetName, 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
|
||||
}
|
||||
|
||||
func computeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error) {
|
||||
weights := make([]float64, 0, len(rows))
|
||||
details := make([]utypes.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, utypes.UniformityDetailItem{
|
||||
Id: id,
|
||||
Weight: row.Weight,
|
||||
Range: rangeLabel,
|
||||
})
|
||||
}
|
||||
|
||||
total := float64(len(weights))
|
||||
if total == 0 {
|
||||
return utypes.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
|
||||
}
|
||||
|
||||
if details[i].Weight > meanUpThreshold || details[i].Weight < meanDownThreshold {
|
||||
details[i].Range = "Outside"
|
||||
continue
|
||||
}
|
||||
details[i].Range = "Ideal"
|
||||
uniformCount++
|
||||
}
|
||||
|
||||
var deviationSum float64
|
||||
for _, w := range weights {
|
||||
deviation := w - mean
|
||||
deviationSum += deviation * deviation
|
||||
}
|
||||
stdDev := math.Sqrt(deviationSum / total)
|
||||
|
||||
cv := 0.0
|
||||
if mean != 0 {
|
||||
cv = (stdDev / mean) * 100
|
||||
}
|
||||
|
||||
outsideCount := total - uniformCount
|
||||
uniformity := 0.0
|
||||
if total > 0 {
|
||||
uniformity = (uniformCount / total) * 100
|
||||
}
|
||||
|
||||
return utypes.UniformityCalculation{
|
||||
ChickQtyOfWeight: total,
|
||||
MeanWeight: roundToPrecision(mean, 0),
|
||||
MeanDown: roundToPrecision(meanDownThreshold, 0),
|
||||
MeanUp: roundToPrecision(meanUpThreshold, 0),
|
||||
UniformQty: uniformCount,
|
||||
OutsideQty: outsideCount,
|
||||
Uniformity: roundToPrecision(uniformity, 0),
|
||||
Cv: roundToPrecision(cv, 1),
|
||||
Details: details,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func extractWeights(rows []BodyWeightExcelRow) []float64 {
|
||||
weights := make([]float64, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.Weight <= 0 {
|
||||
continue
|
||||
}
|
||||
weights = append(weights, row.Weight)
|
||||
}
|
||||
return weights
|
||||
}
|
||||
|
||||
func buildChartWeekSummary(weights []float64) utypes.UniformityChartWeek {
|
||||
if len(weights) == 0 {
|
||||
return utypes.UniformityChartWeek{
|
||||
HasData: false,
|
||||
WeightDistribution: []utypes.UniformityChartRange{},
|
||||
}
|
||||
}
|
||||
|
||||
minWeight := weights[0]
|
||||
maxWeight := weights[0]
|
||||
var sum float64
|
||||
for _, w := range weights {
|
||||
sum += w
|
||||
if w < minWeight {
|
||||
minWeight = w
|
||||
}
|
||||
if w > maxWeight {
|
||||
maxWeight = w
|
||||
}
|
||||
}
|
||||
mean := sum / float64(len(weights))
|
||||
|
||||
idealMin := roundToPrecision(mean*0.90, 0)
|
||||
idealMax := roundToPrecision(mean*1.10, 0)
|
||||
idealCount := 0.0
|
||||
for _, w := range weights {
|
||||
if w >= idealMin && w <= idealMax {
|
||||
idealCount++
|
||||
}
|
||||
}
|
||||
|
||||
const bucketSize = 5.0
|
||||
start := math.Floor(minWeight/bucketSize) * bucketSize
|
||||
end := math.Floor(maxWeight/bucketSize) * bucketSize
|
||||
|
||||
distribution := make([]utypes.UniformityChartRange, 0)
|
||||
for bucket := start; bucket <= end; bucket += bucketSize {
|
||||
minBucket := bucket
|
||||
maxBucket := bucket + bucketSize - 1
|
||||
count := 0.0
|
||||
for _, w := range weights {
|
||||
if w >= minBucket && w < minBucket+bucketSize {
|
||||
count++
|
||||
}
|
||||
}
|
||||
distribution = append(distribution, utypes.UniformityChartRange{
|
||||
Range: fmt.Sprintf("%d-%d", int(minBucket), int(maxBucket)),
|
||||
MinWeight: minBucket,
|
||||
MaxWeight: maxBucket,
|
||||
BirdCount: count,
|
||||
IsIdealRange: minBucket >= idealMin && maxBucket <= idealMax,
|
||||
})
|
||||
}
|
||||
|
||||
statistics := &utypes.UniformityChartStatistics{
|
||||
MinWeight: roundToPrecision(minWeight, 0),
|
||||
MaxWeight: roundToPrecision(maxWeight, 0),
|
||||
AverageWeight: roundToPrecision(mean, 1),
|
||||
TotalBirdsMeasured: float64(len(weights)),
|
||||
}
|
||||
|
||||
return utypes.UniformityChartWeek{
|
||||
HasData: true,
|
||||
WeightDistribution: distribution,
|
||||
IdealRange: &utypes.UniformityChartIdealRange{
|
||||
MinWeight: idealMin,
|
||||
MaxWeight: idealMax,
|
||||
TotalIdealBirds: idealCount,
|
||||
},
|
||||
Statistics: statistics,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
|
||||
utypes "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/types"
|
||||
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"
|
||||
@@ -31,17 +33,18 @@ 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)
|
||||
GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error)
|
||||
MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error)
|
||||
GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error)
|
||||
MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityStandard, error)
|
||||
MapCharts(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityChartData, error)
|
||||
MapDocuments(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, 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)
|
||||
ComputeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error)
|
||||
GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
|
||||
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
|
||||
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (utypes.UniformityCalculation, *entity.Document, string, error)
|
||||
}
|
||||
|
||||
type uniformityService struct {
|
||||
@@ -79,29 +82,13 @@ func NewUniformityService(
|
||||
}
|
||||
}
|
||||
|
||||
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("id DESC")
|
||||
})
|
||||
uniformitys, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get uniformitys: %+v", err)
|
||||
@@ -114,7 +101,7 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
|
||||
}
|
||||
|
||||
func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) {
|
||||
uniformity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
uniformity, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found")
|
||||
}
|
||||
@@ -132,14 +119,14 @@ func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlo
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) {
|
||||
func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error) {
|
||||
if uniformity == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return s.resolveUniformityStandard(c.Context(), *uniformity)
|
||||
}
|
||||
|
||||
func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error) {
|
||||
func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityStandard, error) {
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -149,7 +136,7 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
|
||||
|
||||
categoryStandard := make(map[string]*entity.ProductionStandard)
|
||||
detailCache := make(map[uint]map[int]entity.StandardGrowthDetail)
|
||||
result := make(map[uint]UniformityStandard, len(items))
|
||||
result := make(map[uint]utypes.UniformityStandard, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
if item.Id == 0 {
|
||||
@@ -180,7 +167,7 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
standardDTO := UniformityStandard{
|
||||
standardDTO := utypes.UniformityStandard{
|
||||
MeanWeight: cloneFloat64(detail.TargetMeanBw),
|
||||
Uniformity: float64Ptr(detail.MinUniformity),
|
||||
}
|
||||
@@ -190,6 +177,109 @@ func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFloc
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s uniformityService) MapCharts(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]utypes.UniformityChartData, error) {
|
||||
if len(items) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
grouped := make(map[uint][]entity.ProjectFlockKandangUniformity)
|
||||
for _, item := range items {
|
||||
if item.ProjectFlockKandangId == 0 {
|
||||
continue
|
||||
}
|
||||
grouped[item.ProjectFlockKandangId] = append(grouped[item.ProjectFlockKandangId], item)
|
||||
}
|
||||
|
||||
if len(grouped) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
result := make(map[uint]utypes.UniformityChartData, len(items))
|
||||
|
||||
for _, group := range grouped {
|
||||
allWeeks := make(map[int]utypes.UniformityChartWeek)
|
||||
weekOrder := make([]int, 0, len(group))
|
||||
weekSeen := make(map[int]struct{}, len(group))
|
||||
weeksWithData := 0
|
||||
gaugeWeeks := make([]utypes.UniformityChartGaugeWeek, 0, len(group))
|
||||
|
||||
for _, item := range group {
|
||||
if item.Week == 0 {
|
||||
continue
|
||||
}
|
||||
var weekSummary utypes.UniformityChartWeek
|
||||
if len(item.ChartData) > 0 {
|
||||
if err := json.Unmarshal(item.ChartData, &weekSummary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if weekSummary.WeightDistribution == nil {
|
||||
weekSummary.WeightDistribution = []utypes.UniformityChartRange{}
|
||||
}
|
||||
if !weekSummary.HasData && item.ChickQtyOfWeight > 0 {
|
||||
weekSummary.HasData = true
|
||||
}
|
||||
if weekSummary.HasData {
|
||||
weeksWithData++
|
||||
}
|
||||
allWeeks[item.Week] = weekSummary
|
||||
|
||||
if _, ok := weekSeen[item.Week]; !ok {
|
||||
weekSeen[item.Week] = struct{}{}
|
||||
weekOrder = append(weekOrder, item.Week)
|
||||
}
|
||||
|
||||
hasData := item.ChickQtyOfWeight > 0
|
||||
gaugeWeeks = append(gaugeWeeks, utypes.UniformityChartGaugeWeek{
|
||||
Week: item.Week,
|
||||
UniformityPercent: item.Uniformity,
|
||||
IdealCount: item.UniformQty,
|
||||
OutsideIdealCount: item.NotUniformQty,
|
||||
TotalCount: item.ChickQtyOfWeight,
|
||||
HasData: hasData,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Ints(weekOrder)
|
||||
sort.Slice(gaugeWeeks, func(i, j int) bool {
|
||||
return gaugeWeeks[i].Week < gaugeWeeks[j].Week
|
||||
})
|
||||
|
||||
weekIndex := make(map[int]int, len(weekOrder))
|
||||
for idx, week := range weekOrder {
|
||||
weekIndex[week] = idx
|
||||
}
|
||||
|
||||
totalWeeks := len(weekOrder)
|
||||
for _, item := range group {
|
||||
if item.Id == 0 || item.Week == 0 {
|
||||
continue
|
||||
}
|
||||
currentIndex := weekIndex[item.Week]
|
||||
chart := utypes.UniformityChartData{
|
||||
BarChart: utypes.UniformityChartBar{
|
||||
CurrentWeek: item.Week,
|
||||
AllWeeks: allWeeks,
|
||||
},
|
||||
GaugeChart: utypes.UniformityChartGauge{
|
||||
CurrentWeek: item.Week,
|
||||
AvailableWeeks: gaugeWeeks,
|
||||
WeekInfo: utypes.UniformityChartWeekInfo{
|
||||
TotalWeeks: totalWeeks,
|
||||
WeeksWithData: weeksWithData,
|
||||
CurrentWeekIndex: currentIndex,
|
||||
HasPrevWeek: currentIndex > 0,
|
||||
HasNextWeek: currentIndex < totalWeeks-1,
|
||||
},
|
||||
},
|
||||
}
|
||||
result[item.Id] = chart
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s uniformityService) MapDocuments(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]string, error) {
|
||||
if s.DocumentSvc == nil || len(items) == 0 {
|
||||
return map[uint]string{}, nil
|
||||
@@ -252,6 +342,11 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartSummary := buildChartWeekSummary(extractWeights(rows))
|
||||
chartJSON, err := json.Marshal(chartSummary)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to build chart data")
|
||||
}
|
||||
actorID, err := m.ActorIDFromContext(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -267,6 +362,7 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||
UniformQty: calculation.UniformQty,
|
||||
NotUniformQty: calculation.OutsideQty,
|
||||
ChartData: chartJSON,
|
||||
UniformDate: &uniformDate,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
@@ -307,7 +403,12 @@ func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
s.rollbackUniformityCreate(c.Context(), createBody.Id)
|
||||
if errDelete := s.ApprovalRepo.DeleteByTarget(c.Context(), utils.ApprovalWorkflowUniformity.String(), createBody.Id); errDelete != nil {
|
||||
s.Log.WithError(errDelete).Warnf("Failed to rollback uniformity approvals for %d", createBody.Id)
|
||||
}
|
||||
if errDelete := s.Repository.DeleteOne(c.Context(), createBody.Id); errDelete != nil {
|
||||
s.Log.WithError(errDelete).Warnf("Failed to rollback uniformity %d", createBody.Id)
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document")
|
||||
}
|
||||
}
|
||||
@@ -391,6 +492,11 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chartSummary := buildChartWeekSummary(extractWeights(rows))
|
||||
chartJSON, err := json.Marshal(chartSummary)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "failed to build chart data")
|
||||
}
|
||||
|
||||
updateBody["uniformity"] = calculation.Uniformity
|
||||
updateBody["cv"] = calculation.Cv
|
||||
@@ -399,6 +505,7 @@ func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id ui
|
||||
updateBody["mean_down"] = calculation.MeanDown
|
||||
updateBody["uniform_qty"] = calculation.UniformQty
|
||||
updateBody["not_uniform_qty"] = calculation.OutsideQty
|
||||
updateBody["chart_data"] = chartJSON
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
@@ -590,30 +697,7 @@ func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]en
|
||||
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
|
||||
}
|
||||
|
||||
type UniformityStandard struct {
|
||||
MeanWeight *float64
|
||||
Uniformity *float64
|
||||
}
|
||||
|
||||
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) {
|
||||
func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (utypes.UniformityCalculation, error) {
|
||||
return computeUniformity(rows)
|
||||
}
|
||||
|
||||
@@ -621,37 +705,37 @@ func (s uniformityService) GetDocumentInfo(c *fiber.Ctx, uniformityID uint) (*en
|
||||
return s.fetchUniformityDocument(c.Context(), uniformityID, true)
|
||||
}
|
||||
|
||||
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
|
||||
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (utypes.UniformityCalculation, *entity.Document, string, error) {
|
||||
document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
return utypes.UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
if document == nil || url == "" {
|
||||
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||
return utypes.UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
return utypes.UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
return utypes.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")
|
||||
return utypes.UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document")
|
||||
}
|
||||
|
||||
rows, err := parseBodyWeightExcelReader(resp.Body)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
return utypes.UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
|
||||
calculation, err := computeUniformity(rows)
|
||||
if err != nil {
|
||||
return UniformityCalculation{}, nil, "", err
|
||||
return utypes.UniformityCalculation{}, nil, "", err
|
||||
}
|
||||
|
||||
return calculation, document, url, nil
|
||||
@@ -783,7 +867,7 @@ func (s *uniformityService) attachLatestApproval(ctx context.Context, item *enti
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) {
|
||||
func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*utypes.UniformityStandard, error) {
|
||||
if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -801,7 +885,7 @@ func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UniformityStandard{
|
||||
return &utypes.UniformityStandard{
|
||||
MeanWeight: cloneFloat64(detail.TargetMeanBw),
|
||||
Uniformity: float64Ptr(detail.MinUniformity),
|
||||
}, nil
|
||||
@@ -858,22 +942,6 @@ func float64Ptr(value float64) *float64 {
|
||||
return ©
|
||||
}
|
||||
|
||||
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
|
||||
@@ -893,114 +961,3 @@ func uniqueUintSlice(values []uint) []uint {
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user