first commit api production-result

This commit is contained in:
MacBook Air M1
2026-01-02 11:24:26 +07:00
parent 10f42ed9c4
commit 39909d1c2e
8 changed files with 523 additions and 3 deletions
@@ -2,6 +2,7 @@ package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
@@ -95,8 +96,6 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
if err != nil {
return err
}
total := dto.ToSummaryFromDTOItems(result)
return ctx.Status(fiber.StatusOK).
@@ -187,3 +186,44 @@ func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
return ctx.Status(fiber.StatusOK).JSON(resp)
}
func (c *RepportController) GetProductionResult(ctx *fiber.Ctx) error {
idParam := ctx.Params("idProjectFlockKandang")
if idParam == "" {
return fiber.NewError(fiber.StatusBadRequest, "idProjectFlockKandang is required")
}
projectFlockKandangID, err := strconv.ParseUint(idParam, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid idProjectFlockKandang")
}
query := &validation.ProductionResultQuery{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
ProjectFlockKandangID: uint(projectFlockKandangID),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
data, totalResults, err := c.RepportService.GetProductionResult(ctx, query)
if err != nil {
return err
}
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductionResultDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get Laporan Hasil Produksi successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: data,
})
}
@@ -0,0 +1,43 @@
package dto
import "time"
type ProductionResultDTO struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Woa float64 `json:"woa"`
Bw float64 `json:"bw"`
StdBw float64 `json:"std_bw"`
Uniformity float64 `json:"uniformity"`
StdUniformity string `json:"std_uniformity"`
DepKum float64 `json:"dep_kum"`
DepStd float64 `json:"dep_std"`
ButiranUtuh int64 `json:"butiran_utuh"`
ButiranPutih int64 `json:"butiran_putih"`
ButiranRetak int64 `json:"butiran_retak"`
ButiranPecah int64 `json:"butiran_pecah"`
ButiranJumlah int64 `json:"butiran_jumlah"`
TotalButir int64 `json:"total_butir"`
KgUtuh float64 `json:"kg_utuh"`
KgPutih float64 `json:"kg_putih"`
KgRetak float64 `json:"kg_retak"`
KgPecah float64 `json:"kg_pecah"`
KgJumlah float64 `json:"kg_jumlah"`
TotalKg float64 `json:"total_kg"`
PersenUtuh float64 `json:"persen_utuh"`
PersenPutih float64 `json:"persen_putih"`
PersenRetak float64 `json:"persen_retak"`
PersenPecah float64 `json:"persen_pecah"`
Hd float64 `json:"hd"`
HdStd float64 `json:"hd_std"`
Fi float64 `json:"fi"`
FiStd float64 `json:"fi_std"`
Em float64 `json:"em"`
EmStd float64 `json:"em_std"`
Ew float64 `json:"ew"`
EwStd float64 `json:"ew_std"`
Fcr float64 `json:"fcr"`
FcrStd float64 `json:"fcr_std"`
Hh float64 `json:"hh"`
HhStd float64 `json:"hh_std"`
}
+2 -1
View File
@@ -32,10 +32,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
approvalRepository := commonRepo.NewApprovalRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
productionResultRepository := repportRepo.NewProductionResultRepository(db)
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository, productionResultRepository)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)
@@ -0,0 +1,79 @@
package repositories
import (
"context"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
)
type ProductionResultRepository interface {
GetRecordingsByProjectFlockKandang(ctx context.Context, projectFlockKandangID uint, offset, limit int) ([]entity.Recording, int64, error)
}
type productionResultRepositoryImpl struct {
db *gorm.DB
}
func NewProductionResultRepository(db *gorm.DB) ProductionResultRepository {
return &productionResultRepositoryImpl{db: db}
}
func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
ctx context.Context,
projectFlockKandangID uint,
offset, limit int,
) ([]entity.Recording, int64, error) {
if projectFlockKandangID == 0 {
return []entity.Recording{}, 0, nil
}
countQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID)
var total int64
if err := countQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
if total == 0 {
return []entity.Recording{}, 0, nil
}
if limit <= 0 {
limit = 10
}
if offset < 0 {
offset = 0
}
flagNames := []string{
string(utils.FlagTelurUtuh),
string(utils.FlagTelurPutih),
string(utils.FlagTelurRetak),
string(utils.FlagTelurPecah),
}
dataQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Preload("BodyWeights").
Preload("Eggs", func(db *gorm.DB) *gorm.DB {
return db.Select("recording_eggs.*, f.name AS product_flag_name").
Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").
Joins("LEFT JOIN flags f ON f.flagable_id = pw.product_id AND f.flagable_type = ? AND f.name IN ?", entity.FlagableTypeProduct, flagNames)
}).
Preload("Eggs.ProductWarehouse").
Order("record_datetime ASC").
Offset(offset).
Limit(limit)
var recordings []entity.Recording
if err := dataQuery.Find(&recordings).Error; err != nil {
return nil, 0, err
}
return recordings, total, nil
}
+1
View File
@@ -19,5 +19,6 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
route.Get("/hpp-per-kandang", ctrl.GetHppPerKandang)
route.Get("/production-result/:idProjectFlockKandang", ctrl.GetProductionResult)
}
@@ -35,6 +35,7 @@ type RepportService interface {
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error)
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
}
type repportService struct {
@@ -48,6 +49,7 @@ type repportService struct {
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
}
type HppCostAggregate struct {
@@ -69,6 +71,7 @@ func NewRepportService(
approvalSvc approvalService.ApprovalService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
) RepportService {
return &repportService{
Log: utils.Log,
@@ -81,6 +84,7 @@ func NewRepportService(
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
}
}
@@ -230,6 +234,351 @@ func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID
return cost
}
func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
const (
recordsPerWeek = 7
defaultStartWoa = 18
defaultStdBw = 1951
defaultBw = 0
defaultUniformText = "90% up"
)
if params.Limit <= 0 {
params.Limit = 10
}
if params.Page <= 0 {
params.Page = 1
}
weeksPerPage := params.Limit
recordLimit := weeksPerPage * recordsPerWeek
if recordLimit <= 0 {
recordLimit = recordsPerWeek
}
recordOffset := (params.Page - 1) * recordLimit
if recordOffset < 0 {
recordOffset = 0
}
recordings, totalRecordings, err := s.ProductionResultRepo.GetRecordingsByProjectFlockKandang(ctx.Context(), params.ProjectFlockKandangID, recordOffset, recordLimit)
if err != nil {
return nil, 0, err
}
dailyResults := make([]dto.ProductionResultDTO, len(recordings))
for i := range recordings {
dailyResults[i] = mapRecordingToProductionResultDTO(recordings[i])
if dailyResults[i].StdUniformity == "" {
dailyResults[i].StdUniformity = defaultUniformText
}
}
weeklyResults := summarizeProductionResults(dailyResults, recordsPerWeek)
var cumulativeButir int64
var cumulativeKg float64
for i := range weeklyResults {
weeklyResults[i].Woa = float64(defaultStartWoa + i)
weeklyResults[i].StdBw = defaultStdBw
weeklyResults[i].Bw = defaultBw
if weeklyResults[i].StdUniformity == "" {
weeklyResults[i].StdUniformity = defaultUniformText
}
cumulativeButir += weeklyResults[i].ButiranJumlah
weeklyResults[i].TotalButir = cumulativeButir
cumulativeKg += weeklyResults[i].KgJumlah
weeklyResults[i].TotalKg = cumulativeKg
}
totalWeeks := int64(math.Ceil(float64(totalRecordings) / float64(recordsPerWeek)))
return weeklyResults, totalWeeks, nil
}
func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO {
result := dto.ProductionResultDTO{
CreatedAt: record.CreatedAt,
UpdatedAt: record.UpdatedAt,
StdUniformity: "90% up",
DepKum: valueOrZero(record.CumDepletionRate),
DepStd: valueOrZero(record.TotalDepletionQty),
Fcr: valueOrZero(record.FcrValue),
Hh: valueOrZero(record.TotalChickQty),
}
if record.Day != nil {
result.Woa = float64(*record.Day)
}
if record.CumIntake != nil {
result.Fi = float64(*record.CumIntake)
}
avgWeight := calculateAverageBodyWeight(record.BodyWeights)
if avgWeight > 0 {
result.Bw = avgWeight
}
eggSummary := summarizeEggs(record.Eggs)
result.ButiranUtuh = eggSummary.Utuh
result.ButiranPutih = eggSummary.Putih
result.ButiranRetak = eggSummary.Retak
result.ButiranPecah = eggSummary.Pecah
result.ButiranJumlah = eggSummary.TotalQty
result.TotalButir = eggSummary.TotalQty
result.KgUtuh = eggSummary.KgUtuh
result.KgPutih = eggSummary.KgPutih
result.KgRetak = eggSummary.KgRetak
result.KgPecah = eggSummary.KgPecah
result.KgJumlah = eggSummary.TotalKg
result.TotalKg = eggSummary.TotalKg
if eggSummary.TotalQty > 0 {
total := float64(eggSummary.TotalQty)
result.PersenUtuh = roundFloat((float64(result.ButiranUtuh)/total)*100, 2)
result.PersenPutih = roundFloat((float64(result.ButiranPutih)/total)*100, 2)
result.PersenRetak = roundFloat((float64(result.ButiranRetak)/total)*100, 2)
result.PersenPecah = roundFloat((float64(result.ButiranPecah)/total)*100, 2)
result.Ew = (eggSummary.TotalKg * 1000) / total
result.Em = eggSummary.TotalKg
}
return result
}
func calculateAverageBodyWeight(bodyWeights []entity.RecordingBW) float64 {
var totalQty float64
var totalWeight float64
for _, bw := range bodyWeights {
totalQty += bw.Qty
if bw.TotalWeight > 0 {
totalWeight += bw.TotalWeight
} else {
totalWeight += bw.AvgWeight * bw.Qty
}
}
if totalQty == 0 {
return 0
}
return totalWeight / totalQty
}
type eggSummary struct {
TotalQty int64
TotalKg float64
Utuh int64
Putih int64
Retak int64
Pecah int64
KgUtuh float64
KgPutih float64
KgRetak float64
KgPecah float64
}
func summarizeEggs(eggs []entity.RecordingEgg) eggSummary {
var summary eggSummary
for _, egg := range eggs {
qty := int64(egg.Qty)
weightKg := valueOrZero(egg.Weight)
summary.TotalQty += qty
summary.TotalKg += weightKg
if flagType, ok := getEggFlagType(egg); ok {
switch flagType {
case utils.FlagTelurUtuh:
summary.Utuh += qty
summary.KgUtuh += weightKg
case utils.FlagTelurPutih:
summary.Putih += qty
summary.KgPutih += weightKg
case utils.FlagTelurRetak:
summary.Retak += qty
summary.KgRetak += weightKg
case utils.FlagTelurPecah:
summary.Pecah += qty
summary.KgPecah += weightKg
}
}
}
return summary
}
func valueOrZero(value *float64) float64 {
if value == nil {
return 0
}
return *value
}
func roundFloat(val float64, precision int) float64 {
if precision < 0 {
return val
}
factor := math.Pow(10, float64(precision))
return math.Round(val*factor) / factor
}
func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) {
if egg.ProductFlagName == nil || *egg.ProductFlagName == "" {
return "", false
}
flagType := utils.FlagType(*egg.ProductFlagName)
switch flagType {
case utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah:
return flagType, true
}
return "", false
}
func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO {
if groupSize <= 0 || len(daily) == 0 {
return daily
}
result := make([]dto.ProductionResultDTO, 0, (len(daily)+groupSize-1)/groupSize)
for i := 0; i < len(daily); i += groupSize {
end := i + groupSize
if end > len(daily) {
end = len(daily)
}
result = append(result, aggregateProductionResultGroup(daily[i:end]))
}
return result
}
func aggregateProductionResultGroup(group []dto.ProductionResultDTO) dto.ProductionResultDTO {
count := len(group)
if count == 0 {
return dto.ProductionResultDTO{}
}
agg := dto.ProductionResultDTO{
CreatedAt: group[0].CreatedAt,
UpdatedAt: group[0].UpdatedAt,
StdUniformity: group[0].StdUniformity,
}
var sumBw, sumStdBw, sumUniformity float64
var sumDepStd float64
var sumKgUtuh, sumKgPutih, sumKgRetak, sumKgPecah float64
var sumKgJumlah, sumTotalKg float64
var sumPersenUtuh, sumPersenPutih, sumPersenRetak, sumPersenPecah float64
var percentSamples int
var sumHd, sumHdStd float64
var sumFi, sumFiStd float64
var sumEm, sumEmStd float64
var sumEw, sumEwStd float64
var sumFcr, sumFcrStd float64
var sumHh, sumHhStd float64
var sumButiranUtuh, sumButiranPutih int64
var sumButiranRetak, sumButiranPecah int64
var sumButiranJumlah, sumTotalButir int64
for _, item := range group {
sumBw += item.Bw
sumStdBw += item.StdBw
sumUniformity += item.Uniformity
sumDepStd += item.DepStd
sumKgUtuh += item.KgUtuh
sumKgPutih += item.KgPutih
sumKgRetak += item.KgRetak
sumKgPecah += item.KgPecah
sumKgJumlah += item.KgJumlah
sumTotalKg += item.TotalKg
if item.ButiranJumlah > 0 {
sumPersenUtuh += item.PersenUtuh
sumPersenPutih += item.PersenPutih
sumPersenRetak += item.PersenRetak
sumPersenPecah += item.PersenPecah
percentSamples++
}
sumHd += item.Hd
sumHdStd += item.HdStd
sumFi += item.Fi
sumFiStd += item.FiStd
sumEm += item.Em
sumEmStd += item.EmStd
sumEw += item.Ew
sumEwStd += item.EwStd
sumFcr += item.Fcr
sumFcrStd += item.FcrStd
sumHh += item.Hh
sumHhStd += item.HhStd
sumButiranUtuh += item.ButiranUtuh
sumButiranPutih += item.ButiranPutih
sumButiranRetak += item.ButiranRetak
sumButiranPecah += item.ButiranPecah
sumButiranJumlah += item.ButiranJumlah
sumTotalButir += item.TotalButir
}
divider := float64(count)
if divider == 0 {
divider = 1
}
agg.Bw = sumBw / divider
agg.StdBw = sumStdBw / divider
agg.Uniformity = sumUniformity / divider
agg.DepKum = group[count-1].DepKum
agg.DepStd = sumDepStd / divider
agg.KgUtuh = sumKgUtuh
agg.KgPutih = sumKgPutih
agg.KgRetak = sumKgRetak
agg.KgPecah = sumKgPecah
agg.KgJumlah = sumKgJumlah
agg.TotalKg = sumTotalKg
agg.ButiranUtuh = sumButiranUtuh
agg.ButiranPutih = sumButiranPutih
agg.ButiranRetak = sumButiranRetak
agg.ButiranPecah = sumButiranPecah
agg.ButiranJumlah = sumButiranJumlah
agg.TotalButir = sumTotalButir
if percentSamples > 0 {
percentDivider := float64(percentSamples)
agg.PersenUtuh = roundFloat(sumPersenUtuh/percentDivider, 2)
agg.PersenPutih = roundFloat(sumPersenPutih/percentDivider, 2)
agg.PersenRetak = roundFloat(sumPersenRetak/percentDivider, 2)
agg.PersenPecah = roundFloat(sumPersenPecah/percentDivider, 2)
}
agg.Hd = sumHd / divider
agg.HdStd = sumHdStd / divider
agg.Fi = sumFi / divider
agg.FiStd = sumFiStd / divider
agg.Em = sumEm / divider
agg.EmStd = sumEmStd / divider
agg.Ew = sumEw / divider
agg.EwStd = sumEwStd / divider
agg.Fcr = sumFcr / divider
agg.FcrStd = sumFcrStd / divider
agg.Hh = sumHh / divider
agg.HhStd = sumHhStd / divider
return agg
}
func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
@@ -54,3 +54,9 @@ type HppPerKandangQuery struct {
WeightMin *float64 `query:"-"`
WeightMax *float64 `query:"-"`
}
type ProductionResultQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
ProjectFlockKandangID uint `query:"-" validate:"required,gt=0"`
}