mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
feat[BE-378]:Create API Get All HPP Harian Kandang
This commit is contained in:
@@ -164,3 +164,26 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error {
|
|||||||
Data: result,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,10 +31,11 @@ func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
recordingRepository := recordingRepo.NewRecordingRepository(db)
|
recordingRepository := recordingRepo.NewRecordingRepository(db)
|
||||||
approvalRepository := commonRepo.NewApprovalRepository(db)
|
approvalRepository := commonRepo.NewApprovalRepository(db)
|
||||||
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
|
purchaseSupplierRepository := repportRepo.NewPurchaseSupplierRepository(db)
|
||||||
|
hppPerKandangRepository := repportRepo.NewHppPerKandangRepository(db)
|
||||||
userRepository := rUser.NewUserRepository(db)
|
userRepository := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
approvalSvc := approvalService.NewApprovalService(approvalRepository)
|
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)
|
userService := sUser.NewUserService(userRepository, validate)
|
||||||
|
|
||||||
RepportRoutes(router, userService, repportService)
|
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
|
||||||
|
}
|
||||||
@@ -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("/expense", m.RequirePermissions(m.P_ReportExpenseGetAll), ctrl.GetExpense)
|
||||||
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
|
route.Get("/marketing", m.RequirePermissions(m.P_ReportDeliveryGetAll), ctrl.GetMarketing)
|
||||||
route.Get("/purchase-supplier", m.RequirePermissions(m.P_ReportPurchaseSupplierGetAll), ctrl.GetPurchaseSupplier)
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||||
repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories"
|
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)
|
GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error)
|
||||||
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
|
GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error)
|
||||||
GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, 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 {
|
type repportService struct {
|
||||||
@@ -40,6 +47,16 @@ type repportService struct {
|
|||||||
RecordingRepo recordingRepo.RecordingRepository
|
RecordingRepo recordingRepo.RecordingRepository
|
||||||
ApprovalSvc approvalService.ApprovalService
|
ApprovalSvc approvalService.ApprovalService
|
||||||
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
|
||||||
|
HppPerKandangRepo repportRepo.HppPerKandangRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
type HppCostAggregate struct {
|
||||||
|
FeedCost float64
|
||||||
|
OvkCost float64
|
||||||
|
DocCost float64
|
||||||
|
DocQty float64
|
||||||
|
BudgetCost float64
|
||||||
|
ExpenseCost float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepportService(
|
func NewRepportService(
|
||||||
@@ -51,6 +68,7 @@ func NewRepportService(
|
|||||||
recordingRepo recordingRepo.RecordingRepository,
|
recordingRepo recordingRepo.RecordingRepository,
|
||||||
approvalSvc approvalService.ApprovalService,
|
approvalSvc approvalService.ApprovalService,
|
||||||
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
|
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
|
||||||
|
hppPerKandangRepo repportRepo.HppPerKandangRepository,
|
||||||
) RepportService {
|
) RepportService {
|
||||||
return &repportService{
|
return &repportService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
@@ -62,6 +80,7 @@ func NewRepportService(
|
|||||||
RecordingRepo: recordingRepo,
|
RecordingRepo: recordingRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
PurchaseSupplierRepo: purchaseSupplierRepo,
|
PurchaseSupplierRepo: purchaseSupplierRepo,
|
||||||
|
HppPerKandangRepo: hppPerKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,3 +284,438 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
|
|||||||
|
|
||||||
return result, totalSuppliers, nil
|
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"`
|
SortBy string `query:"sort_by" validate:"omitempty"`
|
||||||
FilterBy string `query:"filter_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:"-"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user