Files
lti-api/internal/modules/repports/services/repport.service.go
T

1633 lines
47 KiB
Go

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"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
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)
GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error)
GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error)
GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error)
GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error)
}
type repportService struct {
Log *logrus.Logger
Validate *validator.Validate
DB *gorm.DB
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository
PurchaseRepo purchaseRepo.PurchaseRepository
ChickinRepo chickinRepo.ProjectChickinRepository
RecordingRepo recordingRepo.RecordingRepository
ApprovalSvc approvalService.ApprovalService
PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository
DebtSupplierRepo repportRepo.DebtSupplierRepository
HppPerKandangRepo repportRepo.HppPerKandangRepository
ProductionResultRepo repportRepo.ProductionResultRepository
CustomerPaymentRepo repportRepo.CustomerPaymentRepository
}
type HppCostAggregate struct {
FeedCost float64
OvkCost float64
DocCost float64
DocQty float64
BudgetCost float64
ExpenseCost float64
}
func NewRepportService(
db *gorm.DB,
validate *validator.Validate,
expenseRealizationRepo expenseRepo.ExpenseRealizationRepository,
marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository,
purchaseRepo purchaseRepo.PurchaseRepository,
chickinRepo chickinRepo.ProjectChickinRepository,
recordingRepo recordingRepo.RecordingRepository,
approvalSvc approvalService.ApprovalService,
purchaseSupplierRepo repportRepo.PurchaseSupplierRepository,
debtSupplierRepo repportRepo.DebtSupplierRepository,
hppPerKandangRepo repportRepo.HppPerKandangRepository,
productionResultRepo repportRepo.ProductionResultRepository,
customerPaymentRepo repportRepo.CustomerPaymentRepository,
) RepportService {
return &repportService{
Log: utils.Log,
Validate: validate,
DB: db,
ExpenseRealizationRepo: expenseRealizationRepo,
MarketingDeliveryRepo: marketingDeliveryRepo,
PurchaseRepo: purchaseRepo,
ChickinRepo: chickinRepo,
RecordingRepo: recordingRepo,
ApprovalSvc: approvalSvc,
PurchaseSupplierRepo: purchaseSupplierRepo,
DebtSupplierRepo: debtSupplierRepo,
HppPerKandangRepo: hppPerKandangRepo,
ProductionResultRepo: productionResultRepo,
CustomerPaymentRepo: customerPaymentRepo,
}
}
func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
realizations, total, err := s.ExpenseRealizationRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
if err != nil {
s.Log.Errorf("GetAllWithFilters error: %v", err)
return nil, 0, err
}
result := dto.ToRepportExpenseListDTOs(realizations)
expenseIDs := make([]uint, 0, len(result))
for i := range result {
expenseIDs = append(expenseIDs, uint(result[i].Id))
}
approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, expenseIDs, func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
})
if err != nil {
s.Log.Warnf("LatestByTargets error: %v", err)
}
for i := range result {
expenseIDAsUint := uint(result[i].Id)
if approval, exists := approvals[expenseIDAsUint]; exists && approval != nil {
mapped := approvalDTO.ToApprovalDTO(*approval)
result[i].LatestApproval = &mapped
}
}
return result, total, nil
}
func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
deliveryProducts, total, err := s.MarketingDeliveryRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params)
if err != nil {
return nil, 0, err
}
projectFlockIDMap := make(map[uint]bool)
hppMap := make(map[uint]float64)
for _, dp := range deliveryProducts {
if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil {
projectFlockID := projectFlockKandang.ProjectFlockId
if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] {
projectFlockIDMap[projectFlockID] = true
category := projectFlockKandang.ProjectFlock.Category
hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category)
hppMap[projectFlockID] = hppPerKg
}
}
}
items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap)
return items, total, nil
}
func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 {
totalCost := s.getTotalProjectCost(ctx, projectFlockID)
if totalCost == 0 {
return 0
}
chickinQty, err := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("HPP calculation: Failed to get chickin qty for project flock ID %d: %v", projectFlockID, err)
}
depletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("HPP calculation: Failed to get depletion for project flock ID %d: %v", projectFlockID, err)
}
avgWeight, err := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("HPP calculation: Failed to get avg weight for project flock ID %d: %v", projectFlockID, err)
}
var totalWeight float64
if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing {
totalWeight = (chickinQty - depletion) * avgWeight
} else {
eggWeight, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("HPP calculation: Failed to get egg weight for project flock ID %d: %v", projectFlockID, err)
}
totalWeight = (chickinQty-depletion)*avgWeight + eggWeight
}
if totalWeight == 0 {
return 0
}
hppPricePerKg := totalCost / totalWeight
return hppPricePerKg
}
func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 {
if projectFlockID == 0 {
return 0
}
purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Errorf("getTotalProjectCost: GetItemsByProjectFlockID error for project flock ID %d: %v", projectFlockID, err)
return 0
}
cost := float64(0)
purchaseCost := float64(0)
for _, p := range purchases {
purchaseCost += p.TotalPrice
}
cost += purchaseCost
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID)
if err != nil {
s.Log.Warnf("getTotalProjectCost: GetByProjectFlockID error for project flock ID %d: %v", projectFlockID, err)
}
bopCost := float64(0)
for _, r := range realizations {
if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil &&
r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) {
bopCost += r.Price * r.Qty
}
}
cost += bopCost
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 (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
// Determine customer IDs to process
var customerIDs []uint
var totalCustomers int64
if len(params.CustomerIDs) > 0 {
// Specific customer IDs mode (no pagination)
customerIDs = params.CustomerIDs
totalCustomers = int64(len(customerIDs))
if len(customerIDs) == 0 {
return []dto.CustomerPaymentReportItem{}, 0, nil
}
} else {
// Multiple customers mode with pagination
page := params.Page
limit := params.Limit
if page < 1 {
page = 1
}
if limit < 1 {
limit = 10
}
offset := (page - 1) * limit
var err error
customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset)
if err != nil {
return nil, 0, err
}
if len(customerIDs) == 0 {
return []dto.CustomerPaymentReportItem{}, 0, nil
}
}
// Process each customer
var result []dto.CustomerPaymentReportItem
for _, customerID := range customerIDs {
item, err := s.processCustomerPayment(ctx.Context(), customerID, params)
if err != nil {
return nil, 0, err
}
result = append(result, item)
}
return result, totalCustomers, nil
}
func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) {
customer := entity.Customer{}
if err := s.DB.WithContext(ctx).
Where("id = ?", customerID).
First(&customer).Error; err != nil {
return dto.CustomerPaymentReportItem{}, err
}
initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
cid := customerID
transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions))
runningBalance := initialBalance
for i, tx := range transactions {
previousBalance := runningBalance
row := dto.ToCustomerPaymentReportRow(tx)
if tx.TransactionType == "SALES" {
runningBalance -= tx.TotalPrice
status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance)
row.Status = status
if status == "LUNAS" {
if previousBalance >= tx.TotalPrice {
days := 0
row.AgingDay = &days
} else if paymentDate != nil {
// Aging = payment_date - trans_date (SO date)
days := int(paymentDate.Sub(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
} else {
days := 0
row.AgingDay = &days
}
} else {
// Aging = current_date - trans_date (SO date)
days := int(time.Since(tx.TransDate).Hours() / 24)
if days < 0 {
days = 0
}
row.AgingDay = &days
}
} else if tx.TransactionType == "PAYMENT" {
runningBalance += tx.PaymentAmount
row.Status = ""
row.AgingDay = nil
}
row.AccountsReceivable = runningBalance
rows = append(rows, row)
}
if params.StartDate != "" || params.EndDate != "" {
filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows))
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
var startDate, endDate *time.Time
if params.StartDate != "" {
parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
startDate = &parsed
}
if params.EndDate != "" {
parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location)
if err != nil {
return dto.CustomerPaymentReportItem{}, err
}
// End date should be inclusive, so set to end of day
endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location)
endDate = &endOfDay
}
for _, row := range rows {
transDate := row.TransDate.In(location)
// Check if transaction date is within range
if startDate != nil && transDate.Before(*startDate) {
continue
}
if endDate != nil && transDate.After(*endDate) {
continue
}
filteredRows = append(filteredRows, row)
}
rows = filteredRows
}
summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance)
return dto.CustomerPaymentReportItem{
Customer: customerDTO.ToCustomerRelationDTO(customer),
InitialBalance: initialBalance,
Rows: rows,
Summary: summary,
}, nil
}
func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) {
currentSales := transactions[currentIndex]
// Status Logic:
// 1. LUNAS: previousBalance >= salesAmount (paid from deposit)
// 2. LUNAS: future payments make AR >= 0 (eventually paid)
// 3. DIBAYAR SEBAGIAN: has payment but not enough
// 4. BELUM LUNAS: no payment at all
if previousBalance >= currentSales.TotalPrice {
return "LUNAS", nil
}
hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice
futureBalance := currentBalance
hasPayment := false
var paymentDateThatMadeItLunas *time.Time
for i := currentIndex + 1; i < len(transactions); i++ {
if transactions[i].TransactionType == "PAYMENT" {
futureBalance += transactions[i].PaymentAmount
hasPayment = true
if futureBalance >= 0 {
paymentDateThatMadeItLunas = &transactions[i].TransDate
return "LUNAS", paymentDateThatMadeItLunas
}
} else if transactions[i].TransactionType == "SALES" {
futureBalance -= transactions[i].TotalPrice
}
}
if hasPayment || hasPartialPaymentFromBalance {
return "DIBAYAR SEBAGIAN", nil
}
return "BELUM LUNAS", 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)
avgWeight := 1.0
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
}
offset := (params.Page - 1) * params.Limit
if offset < 0 {
offset = 0
}
suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
if err != nil {
return nil, 0, err
}
if totalSuppliers == 0 || len(suppliers) == 0 {
return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil
}
supplierMap := make(map[uint]entity.Supplier, len(suppliers))
supplierIDs := make([]uint, 0, len(suppliers))
for _, supplier := range suppliers {
supplierMap[supplier.Id] = supplier
supplierIDs = append(supplierIDs, supplier.Id)
}
items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
itemsBySupplier := make(map[uint][]entity.PurchaseItem)
for _, item := range items {
if item.Purchase == nil {
continue
}
supplierID := item.Purchase.SupplierId
itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item)
}
result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs))
for _, supplierID := range supplierIDs {
supplier, exists := supplierMap[supplierID]
if !exists {
continue
}
supplierItems := itemsBySupplier[supplierID]
dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems)
result = append(result, dtoItem)
}
return result, totalSuppliers, nil
}
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
params.FilterBy = "received_date"
}
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
if offset < 0 {
offset = 0
}
suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params)
if err != nil {
return nil, 0, err
}
if totalSuppliers == 0 || len(suppliers) == 0 {
return []dto.DebtSupplierDTO{}, totalSuppliers, nil
}
supplierMap := make(map[uint]entity.Supplier, len(suppliers))
supplierIDs := make([]uint, 0, len(suppliers))
for _, supplier := range suppliers {
supplierMap[supplier.Id] = supplier
supplierIDs = append(supplierIDs, supplier.Id)
}
purchases, err := s.DebtSupplierRepo.GetPurchasesBySuppliers(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
references := make([]string, 0)
seenRefs := make(map[string]struct{})
for _, purchase := range purchases {
supplierID := purchase.SupplierId
purchasesBySupplier[supplierID] = append(purchasesBySupplier[supplierID], purchase)
reference := purchase.PrNumber
if purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" {
reference = *purchase.PoNumber
}
if _, exists := seenRefs[reference]; !exists {
seenRefs[reference] = struct{}{}
references = append(references, reference)
}
}
paymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsByReferences(c.Context(), supplierIDs, references)
if err != nil {
return nil, 0, err
}
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
for _, payment := range payments {
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
}
initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params)
if err != nil {
return nil, 0, err
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
}
now := time.Now().In(location)
result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs))
for _, supplierID := range supplierIDs {
supplier, exists := supplierMap[supplierID]
if !exists {
continue
}
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
items := purchasesBySupplier[supplierID]
paymentItems := paymentsBySupplier[supplierID]
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
total := dto.DebtSupplierTotalDTO{}
type debtSupplierRowItem struct {
Row dto.DebtSupplierRowDTO
SortTime time.Time
Order int
DeltaBalance float64
CountTotals bool
}
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
for _, purchase := range items {
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
SortTime: sortTime,
Order: 0,
DeltaBalance: -row.TotalPrice,
CountTotals: true,
})
}
for _, payment := range paymentItems {
row := buildDebtSupplierPaymentRow(payment, location)
sortTime := payment.PaymentDate.In(location)
combinedRows = append(combinedRows, debtSupplierRowItem{
Row: row,
SortTime: sortTime,
Order: 1,
DeltaBalance: payment.Nominal,
CountTotals: false,
})
}
sort.SliceStable(combinedRows, func(i, j int) bool {
if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) {
return combinedRows[i].Order < combinedRows[j].Order
}
return combinedRows[i].SortTime.Before(combinedRows[j].SortTime)
})
balance := initialBalance
for i := range combinedRows {
balance += combinedRows[i].DeltaBalance
combinedRows[i].Row.Balance = balance
if combinedRows[i].CountTotals {
row := combinedRows[i].Row
if row.Aging > total.Aging {
total.Aging = row.Aging
}
total.TotalPrice += row.TotalPrice
total.PaymentPrice += row.PaymentPrice
total.DebtPrice += row.DebtPrice
} else {
combinedRows[i].Row.DebtPrice = balance
}
}
sortDesc := strings.EqualFold(params.SortOrder, "desc")
if sortDesc {
for i := len(combinedRows) - 1; i >= 0; i-- {
rows = append(rows, combinedRows[i].Row)
}
} else {
for i := range combinedRows {
rows = append(rows, combinedRows[i].Row)
}
}
var supplierDTORef *supplierDTO.SupplierRelationDTO
if supplier.Id != 0 {
mapped := supplierDTO.ToSupplierRelationDTO(supplier)
supplierDTORef = &mapped
}
result = append(result, dto.DebtSupplierDTO{
Supplier: supplierDTORef,
InitialBalance: initialBalance,
Rows: rows,
Total: total,
})
}
return result, totalSuppliers, nil
}
func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]float64, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO {
prNumber := purchase.PrNumber
poNumber := ""
if purchase.PoNumber != nil {
poNumber = *purchase.PoNumber
}
reference := prNumber
if strings.TrimSpace(poNumber) != "" {
reference = poNumber
}
prDate := purchase.CreatedAt.In(loc)
startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc)
endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
aging := int(endDate.Sub(startDate).Hours() / 24)
totalPrice := 0.0
travelNumber := "-"
receivedDate := ""
var area *areaDTO.AreaRelationDTO
var warehouse *warehouseDTO.WarehouseRelationDTO
if len(purchase.Items) > 0 {
firstItem := purchase.Items[0]
if firstItem.TravelNumber != nil && strings.TrimSpace(*firstItem.TravelNumber) != "" {
travelNumber = *firstItem.TravelNumber
}
if firstItem.Warehouse != nil && firstItem.Warehouse.Id != 0 {
mappedWarehouse := warehouseDTO.ToWarehouseRelationDTO(*firstItem.Warehouse)
warehouse = &mappedWarehouse
if firstItem.Warehouse.Area.Id != 0 {
mappedArea := areaDTO.ToAreaRelationDTO(firstItem.Warehouse.Area)
area = &mappedArea
}
}
earliestReceived := time.Time{}
for _, item := range purchase.Items {
totalPrice += item.TotalPrice
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
received := item.ReceivedDate.In(loc)
if earliestReceived.IsZero() || received.Before(earliestReceived) {
earliestReceived = received
}
}
if !earliestReceived.IsZero() {
receivedDate = earliestReceived.Format("2006-01-02")
}
}
paymentPrice := paymentTotals[reference]
debtPrice := paymentPrice - totalPrice
dueDate := ""
dueStatus := "-"
if purchase.DueDate != nil && !purchase.DueDate.IsZero() {
due := purchase.DueDate.In(loc)
dueDate = due.Format("2006-01-02")
if now.After(due) {
dueStatus = "Sudah Jatuh Tempo"
} else {
dueStatus = "Mendekati Jatuh Tempo"
}
}
status := "Belum Lunas"
if debtPrice >= 0 {
status = "Lunas"
}
poDate := ""
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
poDate = purchase.PoDate.In(loc).Format("2006-01-02")
}
return dto.DebtSupplierRowDTO{
PrNumber: prNumber,
PoNumber: poNumber,
PoDate: poDate,
ReceivedDate: receivedDate,
Aging: aging,
Area: area,
Warehouse: warehouse,
DueDate: dueDate,
DueStatus: dueStatus,
TotalPrice: totalPrice,
PaymentPrice: paymentPrice,
DebtPrice: debtPrice,
Status: status,
TravelNumber: travelNumber,
}
}
func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO {
referenceNumber := ""
if payment.ReferenceNumber != nil {
referenceNumber = *payment.ReferenceNumber
}
prNumber := payment.PaymentCode
if strings.TrimSpace(prNumber) == "" {
prNumber = referenceNumber
}
return dto.DebtSupplierRowDTO{
PrNumber: prNumber,
PoNumber: referenceNumber,
PoDate: "-",
ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"),
Aging: 0,
Area: nil,
Warehouse: nil,
DueDate: "-",
DueStatus: "-",
TotalPrice: 0,
PaymentPrice: payment.Nominal,
DebtPrice: 0,
Status: "Pembayaran",
TravelNumber: "-",
}
}
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
switch strings.ToLower(strings.TrimSpace(filterBy)) {
case "po_date":
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
return purchase.PoDate.In(loc)
}
case "pr_date":
return purchase.CreatedAt.In(loc)
default:
earliest := time.Time{}
for _, item := range purchase.Items {
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
continue
}
received := item.ReceivedDate.In(loc)
if earliest.IsZero() || received.Before(earliest) {
earliest = received
}
}
if !earliest.IsZero() {
return earliest
}
}
return purchase.CreatedAt.In(loc)
}
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
}