diff --git a/internal/entities/recording_egg.go b/internal/entities/recording_egg.go index 775d15dc..90546448 100644 --- a/internal/entities/recording_egg.go +++ b/internal/entities/recording_egg.go @@ -12,6 +12,7 @@ type RecordingEgg struct { CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + ProductFlagName *string `gorm:"column:product_flag_name" json:"-"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` } diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 82229a45..39136e85 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -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, + }) +} diff --git a/internal/modules/repports/dto/repportProductionResult.dto.go b/internal/modules/repports/dto/repportProductionResult.dto.go new file mode 100644 index 00000000..ab2b3e0c --- /dev/null +++ b/internal/modules/repports/dto/repportProductionResult.dto.go @@ -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"` +} diff --git a/internal/modules/repports/module.go b/internal/modules/repports/module.go index 105d9ad5..40a3c0f3 100644 --- a/internal/modules/repports/module.go +++ b/internal/modules/repports/module.go @@ -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) diff --git a/internal/modules/repports/repositories/production_result.repository.go b/internal/modules/repports/repositories/production_result.repository.go new file mode 100644 index 00000000..f2decedf --- /dev/null +++ b/internal/modules/repports/repositories/production_result.repository.go @@ -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 +} diff --git a/internal/modules/repports/route.go b/internal/modules/repports/route.go index 707ef878..0da9adb2 100644 --- a/internal/modules/repports/route.go +++ b/internal/modules/repports/route.go @@ -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) } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index e2232a02..d2a5c982 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -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 diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 47a711cc..b909d77c 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -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"` +}