feat[BE-378]:Create API Get All HPP Harian Kandang

This commit is contained in:
MacBook Air M1
2025-12-28 18:41:46 +07:00
parent 824eb5905f
commit 10f42ed9c4
7 changed files with 977 additions and 1 deletions
@@ -164,3 +164,26 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error {
Data: result,
})
}
func (c *RepportController) GetHppPerKandang(ctx *fiber.Ctx) error {
data, meta, err := c.RepportService.GetHppPerKandang(ctx)
if err != nil {
return err
}
resp := struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Meta dto.HppPerKandangMetaDTO `json:"meta"`
Data dto.HppPerKandangResponseData `json:"data"`
}{
Code: fiber.StatusOK,
Status: "success",
Message: "Get HPP harian kandang layer successfully",
Meta: *meta,
Data: *data,
}
return ctx.Status(fiber.StatusOK).JSON(resp)
}
@@ -0,0 +1,123 @@
package dto
type HppPerKandangFiltersDTO struct {
AreaID string `json:"area_id"`
LocationID string `json:"location_id"`
KandangID string `json:"kandang_id"`
WeightMin string `json:"weight_min"`
WeightMax string `json:"weight_max"`
Period string `json:"period"`
ShowUnrecorded string `json:"show_unrecorded"`
}
type HppPerKandangMetaDTO struct {
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int64 `json:"total_pages"`
TotalResults int64 `json:"total_results"`
Filters HppPerKandangFiltersDTO `json:"filters"`
}
type HppPerKandangResponseData struct {
Period string `json:"period"`
Rows []HppPerKandangRowDTO `json:"rows"`
Summary HppPerKandangSummaryDTO `json:"summary"`
}
type HppPerKandangRowDTO struct {
ID int `json:"id"`
Kandang HppPerKandangRowKandangDTO `json:"kandang"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"`
// FeedCostRp float64 `json:"feed_cost_rp"`
// OvkCostRp float64 `json:"ovk_cost_rp"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
EggValueRp int64 `json:"egg_value_rp"`
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp int64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
}
type HppPerKandangRowKandangDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Location HppPerKandangLocationDTO `json:"location"`
Pic HppPerKandangPICDTO `json:"pic"`
}
type HppPerKandangLocationDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type HppPerKandangPICDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type HppPerKandangWeightRangeDTO struct {
WeightMin float64 `json:"weight_min"`
WeightMax float64 `json:"weight_max"`
}
type HppPerKandangSupplierDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
Alias string `json:"alias"`
Category string `json:"category"`
}
type HppPerKandangSummaryDTO struct {
PerWeightRange []HppPerKandangSummaryWeightRangeDTO `json:"per_weight_range"`
Total HppPerKandangSummaryTotalDTO `json:"total"`
}
type HppPerKandangSummaryWeightRangeDTO struct {
ID int `json:"id"`
WeightRange HppPerKandangWeightRangeDTO `json:"weight_range"`
Label string `json:"label"`
RemainingChickenBirds int64 `json:"remaining_chicken_birds"`
RemainingChickenWeightKg float64 `json:"remaining_chicken_weight_kg"`
AvgWeightKg float64 `json:"avg_weight_kg"`
EggProductionPieces int64 `json:"egg_production_pieces"`
EggProductionKg float64 `json:"egg_production_kg"`
EggHppRpPerKg float64 `json:"egg_hpp_rp_per_kg"`
EggValueRp int64 `json:"egg_value_rp"`
FeedSuppliers []HppPerKandangSupplierDTO `json:"feed_suppliers"`
DocSuppliers []HppPerKandangSupplierDTO `json:"doc_suppliers"`
AverageDocPriceRp float64 `json:"average_doc_price_rp"`
HppRp float64 `json:"hpp_rp"`
RemainingValueRp int64 `json:"remaining_value_rp"`
}
type HppPerKandangSummaryTotalDTO struct {
TotalRemainingChickenBirds int64 `json:"total_remaining_chicken_birds"`
TotalRemainingChickenWeightKg float64 `json:"total_remaining_chicken_weight_kg"`
AverageWeightKg float64 `json:"average_weight_kg"`
TotalRemainingValueRp int64 `json:"total_remaining_value_rp"`
TotalEggProductionPieces int64 `json:"total_egg_production_pieces"`
TotalEggProductionKg float64 `json:"total_egg_production_kg"`
AverageEggHppRpPerKg float64 `json:"average_egg_hpp_rp_per_kg"`
TotalEggValueRp int64 `json:"total_egg_value_rp"`
TotalHppRp float64 `json:"total_hpp_rp"`
TotalAverageDocPriceRp float64 `json:"total_average_doc_price_rp"`
}
func NewHppPerKandangFiltersDTO(area, location, kandang, weightMin, weightMax, period, showUnrecorded string) HppPerKandangFiltersDTO {
return HppPerKandangFiltersDTO{
AreaID: area,
LocationID: location,
KandangID: kandang,
WeightMin: weightMin,
WeightMax: weightMax,
Period: period,
ShowUnrecorded: showUnrecorded,
}
}
+2 -1
View File
@@ -31,10 +31,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
recordingRepository := recordingRepo.NewRecordingRepository(db)
approvalRepository := commonRepo.NewApprovalRepository(db)
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
userRepository := rUser.NewUserRepository(db)
approvalSvc := approvalService.NewApprovalService(approvalRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository)
repportService := sRepport.NewRepportService(validate, expenseRealizationRepository, marketingDeliveryProductRepository, purchaseRepository, chickinRepository, recordingRepository, approvalSvc, purchaseSupplierRepository, hppPerKandangRepository)
userService := sUser.NewUserService(userRepository, validate)
RepportRoutes(router, userService, repportService)
@@ -0,0 +1,361 @@
package repositories
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
)
type HppPerKandangRow struct {
KandangID uint
KandangName string
KandangStatus string
LocationID uint
LocationName string
PicID uint
PicName string
RemainingChickenBirds float64
RemainingChickenWeight float64
EggProductionWeightKg float64
EggProductionPieces float64
}
type HppPerKandangCostRow struct {
KandangID uint
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
}
type HppPerKandangSupplierRow struct {
KandangID uint
SupplierID uint
SupplierName string
SupplierAlias string
Category string
}
type HppPerKandangRepository interface {
GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error)
GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error)
}
type hppPerKandangRepository struct {
db *gorm.DB
}
func NewHppPerKandangRepository(db *gorm.DB) HppPerKandangRepository {
return &hppPerKandangRepository{db: db}
}
func (r *hppPerKandangRepository) GetRowsByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangRow, error) {
var rows []HppPerKandangRow
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
k.id AS kandang_id,
k.name AS kandang_name,
k.status AS kandang_status,
loc.id AS location_id,
loc.name AS location_name,
pic.id AS pic_id,
pic.name AS pic_name,
COALESCE(MAX(r.total_chick_qty), 0) AS remaining_chicken_birds,
COALESCE(SUM(rbw.total_weight), 0) AS remaining_chicken_weight,
COALESCE(SUM(re.weight), 0) AS egg_production_weight_kg,
COALESCE(SUM(re.qty), 0) AS egg_production_pieces`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN users AS pic ON pic.id = k.pic_id").
Joins("LEFT JOIN recording_bws AS rbw ON rbw.recording_id = r.id").
Joins("LEFT JOIN recording_eggs AS re ON re.recording_id = r.id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
query = query.Group("k.id, k.name, k.status, loc.id, loc.name, pic.id, pic.name").
Order("k.id ASC")
if err := query.Scan(&rows).Error; err != nil {
return nil, err
}
return rows, nil
}
func (r *hppPerKandangRepository) GetFeedOvkDocCostByPeriod(ctx context.Context, start, end time.Time, areaIDs, locationIDs, kandangIDs []int64) ([]HppPerKandangCostRow, []HppPerKandangSupplierRow, error) {
var rows []HppPerKandangCostRow
recordingPfk := r.db.WithContext(ctx).
Table("recordings AS r").
Select("DISTINCT pfk.id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
recordingPfk = applyLocationFilters(recordingPfk, areaIDs, locationIDs, kandangIDs)
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS").String()
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_DETAILS").String()
query := r.db.WithContext(ctx).
Table("recordings AS r").
Select(`
k.id AS kandang_id,
COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0
END), 0) AS feed_cost,
COALESCE(SUM(CASE
WHEN f.name = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? AND tf.name = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0)
ELSE 0
END), 0) AS ovk_cost`,
utils.FlagPakan, transferStockableKey, utils.FlagPakan,
utils.FlagOVK, transferStockableKey, utils.FlagOVK).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
Joins("LEFT JOIN stock_transfer_details AS std ON std.id = sa.stockable_id AND sa.stockable_type = ?", transferStockableKey).
Joins("LEFT JOIN stock_transfers AS st ON st.id = std.stock_transfer_id").
Joins("LEFT JOIN purchase_items AS tpi ON tpi.product_id = std.product_id AND tpi.warehouse_id = st.from_warehouse_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Joins("LEFT JOIN flags AS tf ON tf.flagable_id = std.product_id AND tf.flagable_type = ?", entity.FlagableTypeProduct).
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
query = applyLocationFilters(query, areaIDs, locationIDs, kandangIDs)
query = query.Group("k.id").Order("k.id ASC")
if err := query.Scan(&rows).Error; err != nil {
return nil, nil, err
}
docRows := make([]struct {
KandangID uint
DocCost float64
DocQty float64
SupplierID *uint
SupplierName *string
SupplierAlias *string
}, 0)
docQuery := r.db.WithContext(ctx).
Table("project_chickins AS pc").
Select(`
pfk.kandang_id AS kandang_id,
COALESCE(SUM(pc.usage_qty * COALESCE(pi.price, 0)), 0) AS doc_cost,
COALESCE(SUM(pc.usage_qty), 0) AS doc_qty,
s.id AS supplier_id,
s.name AS supplier_name,
s.alias AS supplier_alias`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("LEFT JOIN purchase_items AS pi ON pi.product_warehouse_id = pc.product_warehouse_id").
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
Where("pc.project_flock_kandang_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("pfk.kandang_id, s.id, s.name, s.alias")
docQuery = applyLocationFilters(docQuery, areaIDs, locationIDs, kandangIDs)
if err := docQuery.Scan(&docRows).Error; err != nil {
return nil, nil, err
}
costMap := make(map[uint]*HppPerKandangCostRow, len(rows))
for i := range rows {
row := rows[i]
costMap[row.KandangID] = &rows[i]
}
docSuppliers := make([]HppPerKandangSupplierRow, 0)
docSeen := make(map[uint]map[uint]bool)
for _, doc := range docRows {
entry, ok := costMap[doc.KandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
KandangID: doc.KandangID,
})
entry = &rows[len(rows)-1]
costMap[doc.KandangID] = entry
}
entry.DocCost += doc.DocCost
entry.DocQty += doc.DocQty
if doc.SupplierID != nil {
if docSeen[doc.KandangID] == nil {
docSeen[doc.KandangID] = make(map[uint]bool)
}
if !docSeen[doc.KandangID][*doc.SupplierID] {
docSeen[doc.KandangID][*doc.SupplierID] = true
supplierName := ""
if doc.SupplierName != nil {
supplierName = *doc.SupplierName
}
supplierAlias := ""
if doc.SupplierAlias != nil {
supplierAlias = *doc.SupplierAlias
}
docSuppliers = append(docSuppliers, HppPerKandangSupplierRow{
KandangID: doc.KandangID,
SupplierID: *doc.SupplierID,
SupplierName: supplierName,
SupplierAlias: supplierAlias,
Category: "DOC",
})
}
}
}
budgetRows := make([]struct {
KandangID uint
BudgetCost float64
}, 0)
pfkUsageSub := r.db.
Table("project_chickins AS pc").
Select(`
pc.project_flock_kandang_id,
SUM(pc.usage_qty) AS kandang_usage_qty`).
Group("pc.project_flock_kandang_id")
projectUsageSub := r.db.
Table("project_chickins AS pc").
Select(`
pfk.project_flock_id,
SUM(pc.usage_qty) AS project_usage_qty`).
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = pc.project_flock_kandang_id").
Group("pfk.project_flock_id")
budgetQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
k.id AS kandang_id,
COALESCE(SUM((pb.qty * pb.price) * COALESCE(k_usage.kandang_usage_qty, 0) / NULLIF(p_usage.project_usage_qty, 0)), 0) AS budget_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN project_budgets AS pb ON pb.project_flock_id = pfk.project_flock_id").
Joins("LEFT JOIN (?) AS k_usage ON k_usage.project_flock_kandang_id = pfk.id", pfkUsageSub).
Joins("LEFT JOIN (?) AS p_usage ON p_usage.project_flock_id = pfk.project_flock_id", projectUsageSub).
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("k.id")
budgetQuery = applyLocationFilters(budgetQuery, areaIDs, locationIDs, kandangIDs)
if err := budgetQuery.Scan(&budgetRows).Error; err != nil {
return nil, nil, err
}
for _, budget := range budgetRows {
entry, ok := costMap[budget.KandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
KandangID: budget.KandangID,
})
entry = &rows[len(rows)-1]
costMap[budget.KandangID] = entry
}
entry.BudgetCost += budget.BudgetCost
}
expenseRows := make([]struct {
KandangID uint
ExpenseCost float64
}, 0)
expenseQuery := r.db.WithContext(ctx).
Table("project_flock_kandangs AS pfk").
Select(`
k.id AS kandang_id,
COALESCE(SUM(er.qty * er.price), 0) AS expense_cost`).
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("JOIN expense_nonstocks AS en ON en.project_flock_kandang_id = pfk.id").
Joins("JOIN expense_realizations AS er ON er.expense_nonstock_id = en.id").
Where("pfk.id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Group("k.id")
expenseQuery = applyLocationFilters(expenseQuery, areaIDs, locationIDs, kandangIDs)
if err := expenseQuery.Scan(&expenseRows).Error; err != nil {
return nil, nil, err
}
for _, exp := range expenseRows {
entry, ok := costMap[exp.KandangID]
if !ok {
rows = append(rows, HppPerKandangCostRow{
KandangID: exp.KandangID,
})
entry = &rows[len(rows)-1]
costMap[exp.KandangID] = entry
}
entry.ExpenseCost += exp.ExpenseCost
}
feedSuppliers := make([]HppPerKandangSupplierRow, 0)
feedQuery := r.db.WithContext(ctx).
Table("recordings AS r").
Select("DISTINCT k.id AS kandang_id, s.id AS supplier_id, s.name AS supplier_name, s.alias AS supplier_alias").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
Joins("JOIN locations AS loc ON loc.id = k.location_id").
Joins("LEFT JOIN recording_stocks AS rs ON rs.recording_id = r.id").
Joins("LEFT JOIN stock_allocations AS sa ON sa.usable_type = ? AND sa.usable_id = rs.id AND sa.status = ?", fifo.UsableKeyRecordingStock.String(), entity.StockAllocationStatusActive).
Joins("LEFT JOIN purchase_items AS pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", purchaseStockableKey).
Joins("LEFT JOIN purchases AS pur ON pur.id = pi.purchase_id").
Joins("LEFT JOIN suppliers AS s ON s.id = pur.supplier_id").
Joins("LEFT JOIN flags AS f ON f.flagable_id = pi.product_id AND f.flagable_type = ?", entity.FlagableTypeProduct).
Where("f.name IN ?", []utils.FlagType{utils.FlagPakan, utils.FlagOVK}).
Where("r.project_flock_kandangs_id IN (?)", recordingPfk.Session(&gorm.Session{NewDB: true})).
Where("r.record_datetime >= ? AND r.record_datetime < ?", start, end).
Where("r.deleted_at IS NULL")
feedQuery = applyLocationFilters(feedQuery, areaIDs, locationIDs, kandangIDs)
if err := feedQuery.Scan(&feedSuppliers).Error; err != nil {
return nil, nil, err
}
for i := range feedSuppliers {
if _, exists := costMap[feedSuppliers[i].KandangID]; !exists {
rows = append(rows, HppPerKandangCostRow{
KandangID: feedSuppliers[i].KandangID,
})
costMap[feedSuppliers[i].KandangID] = &rows[len(rows)-1]
}
feedSuppliers[i].Category = "FEED"
}
supplierRows := append(docSuppliers, feedSuppliers...)
return rows, supplierRows, nil
}
func applyLocationFilters(query *gorm.DB, areaIDs, locationIDs, kandangIDs []int64) *gorm.DB {
if len(areaIDs) > 0 {
query = query.Where("loc.area_id IN ?", areaIDs)
}
if len(locationIDs) > 0 {
query = query.Where("k.location_id IN ?", locationIDs)
}
if len(kandangIDs) > 0 {
query = query.Where("k.id IN ?", kandangIDs)
}
return query
}
+2
View File
@@ -18,4 +18,6 @@ func RepportRoutes(v1 fiber.Router, u user.UserService, s repport.RepportService
route.Get("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
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)
}
@@ -2,6 +2,12 @@ package service
import (
"context"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
@@ -28,6 +34,7 @@ type RepportService interface {
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
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)
}
type repportService struct {
@@ -40,6 +47,16 @@ type repportService struct {
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
}
type HppCostAggregate struct {
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
}
func NewRepportService(
@@ -51,6 +68,7 @@ func NewRepportService(
recordingRepo recordingRepo.RecordingRepository,
approvalSvc approvalService.ApprovalService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository,
) RepportService {
return &repportService{
Log: utils.Log,
@@ -62,6 +80,7 @@ func NewRepportService(
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
}
}
@@ -265,3 +284,438 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
return result, totalSuppliers, nil
}
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
params, filters, err := s.parseHppPerKandangQuery(ctx)
if err != nil {
return nil, nil, err
}
if err := s.Validate.Struct(params); err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
}
periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location)
if err != nil {
return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD")
}
startOfDay := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, location)
endOfDay := startOfDay.Add(24 * time.Hour)
repoRows, err := s.HppPerKandangRepo.GetRowsByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs)
if err != nil {
return nil, nil, err
}
costRows, supplierRows, err := s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs)
if err != nil {
return nil, nil, err
}
costMap := make(map[uint]HppCostAggregate, len(costRows))
for _, row := range costRows {
costMap[row.KandangID] = HppCostAggregate{
FeedCost: row.FeedCost,
OvkCost: row.OvkCost,
DocCost: row.DocCost,
DocQty: row.DocQty,
BudgetCost: row.BudgetCost,
ExpenseCost: row.ExpenseCost,
}
}
docSupplierMap := make(map[uint][]dto.HppPerKandangSupplierDTO)
feedSupplierMap := make(map[uint][]dto.HppPerKandangSupplierDTO)
docSeen := make(map[uint]map[uint]bool)
feedSeen := make(map[uint]map[uint]bool)
for _, sup := range supplierRows {
if sup.SupplierID == 0 {
continue
}
targetMap := feedSupplierMap
seen := feedSeen
category := "FEED"
if strings.EqualFold(sup.Category, "DOC") {
targetMap = docSupplierMap
seen = docSeen
category = "DOC"
}
if seen[sup.KandangID] == nil {
seen[sup.KandangID] = make(map[uint]bool)
}
if seen[sup.KandangID][sup.SupplierID] {
continue
}
seen[sup.KandangID][sup.SupplierID] = true
targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{
ID: int64(sup.SupplierID),
Name: sup.SupplierName,
Alias: sup.SupplierAlias,
Category: category,
})
}
type weightRangeKey struct {
Min float64
Max float64
}
type weightRangeAggregate struct {
Summary *dto.HppPerKandangSummaryWeightRangeDTO
EggHppSum float64
EggHppCount int
}
dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows))
perRangeMap := make(map[weightRangeKey]*weightRangeAggregate)
var totalBirds int64
var totalWeight float64
var totalEggPieces int64
var totalEggKg float64
var totalRemainingValueRp int64
var totalEggValueRp int64
var totalHppSum float64
var totalHppCount int
var totalDocPriceSum float64
var totalDocPriceCount int
var totalEggHppSum float64
var totalEggHppCount int
for _, row := range repoRows {
birdsFloat := row.RemainingChickenBirds
if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) {
birdsFloat = 0
}
weightFloat := row.RemainingChickenWeight
if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) {
weightFloat = 0
}
eggPiecesFloat := row.EggProductionPieces
if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) {
eggPiecesFloat = 0
}
eggWeightFloat := row.EggProductionWeightKg
if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) {
eggWeightFloat = 0
}
avgWeight := 0.0
if birdsFloat > 0 {
avgWeight = weightFloat / birdsFloat
}
weightMin := math.Floor(avgWeight*10) / 10
if weightMin < 0 {
weightMin = 0
}
weightMax := weightMin + 0.09
rangeKey := weightRangeKey{Min: weightMin, Max: weightMax}
rowBirds := int64(math.Round(birdsFloat))
costEntry := costMap[row.KandangID]
totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost
hppRp := 0.0
if weightFloat > 0 {
hppRp = totalCost / weightFloat
}
eggHpp := 0.0
if eggWeightFloat > 0 {
eggHpp = totalCost / eggWeightFloat
}
rowEggPieces := int64(math.Round(eggPiecesFloat))
rowEggValue := int64(eggHpp * eggWeightFloat)
rowRemainingValue := int64(hppRp * weightFloat)
avgDocPrice := int64(0)
if costEntry.DocQty > 0 {
avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty))
}
dataRows = append(dataRows, dto.HppPerKandangRowDTO{
ID: int(row.KandangID),
Kandang: dto.HppPerKandangRowKandangDTO{
ID: int64(row.KandangID),
Name: row.KandangName,
Status: row.KandangStatus,
Location: dto.HppPerKandangLocationDTO{
ID: int64(row.LocationID),
Name: row.LocationName,
},
Pic: dto.HppPerKandangPICDTO{
ID: int64(row.PicID),
Name: row.PicName,
},
},
WeightRange: dto.HppPerKandangWeightRangeDTO{
WeightMin: weightMin,
WeightMax: weightMax,
},
RemainingChickenBirds: rowBirds,
RemainingChickenWeightKg: weightFloat,
AvgWeightKg: avgWeight,
// FeedCostRp: costEntry.FeedCost,
// OvkCostRp: costEntry.OvkCost,
DocSuppliers: docSupplierMap[row.KandangID],
FeedSuppliers: feedSupplierMap[row.KandangID],
EggProductionPieces: rowEggPieces,
EggProductionKg: eggWeightFloat,
AverageDocPriceRp: avgDocPrice,
HppRp: hppRp,
EggHppRpPerKg: eggHpp,
RemainingValueRp: rowRemainingValue,
EggValueRp: rowEggValue,
})
totalBirds += rowBirds
totalWeight += weightFloat
totalEggPieces += rowEggPieces
totalEggKg += eggWeightFloat
totalRemainingValueRp += rowRemainingValue
totalEggValueRp += rowEggValue
if weightFloat > 0 {
totalHppSum += hppRp
totalHppCount++
}
if avgDocPrice > 0 {
totalDocPriceSum += float64(avgDocPrice)
totalDocPriceCount++
}
if eggWeightFloat > 0 {
totalEggHppSum += eggHpp
totalEggHppCount++
}
rangeAgg, exists := perRangeMap[rangeKey]
if !exists {
rangeAgg = &weightRangeAggregate{
Summary: &dto.HppPerKandangSummaryWeightRangeDTO{
WeightRange: dto.HppPerKandangWeightRangeDTO{
WeightMin: weightMin,
WeightMax: weightMax,
},
Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax),
},
}
perRangeMap[rangeKey] = rangeAgg
}
rangeSummary := rangeAgg.Summary
rangeSummary.RemainingChickenBirds += rowBirds
rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight
rangeSummary.EggProductionPieces += rowEggPieces
rangeSummary.EggProductionKg += eggWeightFloat
rangeSummary.RemainingValueRp += rowRemainingValue
rangeSummary.EggValueRp += rowEggValue
if eggWeightFloat > 0 {
rangeAgg.EggHppSum += eggHpp
rangeAgg.EggHppCount++
}
}
rangeKeys := make([]weightRangeKey, 0, len(perRangeMap))
for key := range perRangeMap {
rangeKeys = append(rangeKeys, key)
}
sort.Slice(rangeKeys, func(i, j int) bool {
if rangeKeys[i].Min == rangeKeys[j].Min {
return rangeKeys[i].Max < rangeKeys[j].Max
}
return rangeKeys[i].Min < rangeKeys[j].Min
})
perRangeSummary := make([]dto.HppPerKandangSummaryWeightRangeDTO, 0, len(rangeKeys))
for idx, key := range rangeKeys {
agg := perRangeMap[key]
entry := agg.Summary
entry.ID = idx + 1
if entry.RemainingChickenBirds > 0 {
entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds)
}
if agg.EggHppCount > 0 {
entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount)
}
perRangeSummary = append(perRangeSummary, *entry)
}
totalSummary := dto.HppPerKandangSummaryTotalDTO{
TotalRemainingChickenBirds: totalBirds,
TotalRemainingChickenWeightKg: totalWeight,
TotalEggProductionPieces: totalEggPieces,
TotalEggProductionKg: totalEggKg,
TotalRemainingValueRp: totalRemainingValueRp,
TotalEggValueRp: totalEggValueRp,
}
if totalBirds > 0 {
totalSummary.AverageWeightKg = totalWeight / float64(totalBirds)
}
if totalEggHppCount > 0 {
totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount)
}
if totalHppCount > 0 {
totalSummary.TotalHppRp = totalHppSum / float64(totalHppCount)
}
if totalDocPriceCount > 0 {
totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount)
}
limit := params.Limit
if limit <= 0 {
limit = 10
}
totalCount := len(dataRows)
offset := (params.Page - 1) * limit
if offset < 0 {
offset = 0
}
if offset > totalCount {
offset = totalCount
}
end := offset + limit
if end > totalCount {
end = totalCount
}
pagedRows := dataRows[offset:end]
data := dto.HppPerKandangResponseData{
Period: params.Period,
Rows: pagedRows,
Summary: dto.HppPerKandangSummaryDTO{
PerWeightRange: perRangeSummary,
Total: totalSummary,
},
}
totalResults := int64(totalCount)
totalPages := int64(0)
if totalResults > 0 {
totalPages = int64(math.Ceil(float64(totalResults) / float64(limit)))
}
if totalPages == 0 {
totalPages = 1
}
meta := &dto.HppPerKandangMetaDTO{
Page: params.Page,
Limit: limit,
TotalPages: totalPages,
TotalResults: totalResults,
Filters: filters,
}
return &data, meta, nil
}
func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.HppPerKandangQuery, dto.HppPerKandangFiltersDTO, error) {
page := ctx.QueryInt("page", 1)
if page < 1 {
page = 1
}
limit := ctx.QueryInt("limit", 10)
if limit < 1 {
limit = 10
}
rawArea := ctx.Query("area_id", "")
rawLocation := ctx.Query("location_id", "")
rawKandang := ctx.Query("kandang_id", "")
rawWeightMin := ctx.Query("weight_min", "")
rawWeightMax := ctx.Query("weight_max", "")
period := ctx.Query("period", "")
showUnrecorded := ctx.QueryBool("show_unrecorded", false)
areaIDs, err := parseCommaSeparatedInt64s(rawArea)
if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
locationIDs, err := parseCommaSeparatedInt64s(rawLocation)
if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
kandangIDs, err := parseCommaSeparatedInt64s(rawKandang)
if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
weightMin, err := parseOptionalFloat64(rawWeightMin)
if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
weightMax, err := parseOptionalFloat64(rawWeightMax)
if err != nil {
return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error())
}
params := &validation.HppPerKandangQuery{
Page: page,
Limit: limit,
Period: period,
ShowUnrecorded: showUnrecorded,
AreaIDs: areaIDs,
LocationIDs: locationIDs,
KandangIDs: kandangIDs,
WeightMin: weightMin,
WeightMax: weightMax,
}
showUnrecordedFilter := ""
if showUnrecorded {
showUnrecordedFilter = "true"
}
filters := dto.NewHppPerKandangFiltersDTO(
rawArea,
rawLocation,
rawKandang,
rawWeightMin,
rawWeightMax,
period,
showUnrecordedFilter,
)
return params, filters, nil
}
func parseCommaSeparatedInt64s(raw string) ([]int64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
parts := strings.Split(raw, ",")
result := make([]int64, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
id, err := strconv.ParseInt(part, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid integer value '%s'", part)
}
result = append(result, id)
}
return result, nil
}
func parseOptionalFloat64(raw string) (*float64, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return nil, nil
}
value, err := strconv.ParseFloat(raw, 64)
if err != nil {
return nil, fmt.Errorf("invalid float value '%s'", raw)
}
return &value, nil
}
@@ -42,3 +42,15 @@ type PurchaseSupplierQuery struct {
SortBy string `query:"sort_by" validate:"omitempty"`
FilterBy string `query:"filter_by" validate:"omitempty"`
}
type HppPerKandangQuery struct {
Page int `query:"page" validate:"omitempty,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"`
Period string `query:"period" validate:"required"`
ShowUnrecorded bool `query:"show_unrecorded"`
AreaIDs []int64 `query:"-"`
LocationIDs []int64 `query:"-"`
KandangIDs []int64 `query:"-"`
WeightMin *float64 `query:"-"`
WeightMax *float64 `query:"-"`
}