package dto import ( "sort" "strings" "time" ) type SapronakDetailDTO struct { ProductID uint `json:"product_id"` ProductName string `json:"product_name"` Flag string `json:"flag"` Tanggal *time.Time `json:"tanggal,omitempty"` NoReferensi string `json:"no_referensi,omitempty"` JenisTransaksi string `json:"jenis_transaksi,omitempty"` QtyMasuk float64 `json:"qty_masuk"` QtyKeluar float64 `json:"qty_keluar"` Harga float64 `json:"harga"` Nilai float64 `json:"nilai"` } type SapronakGroupDTO struct { Flag string `json:"flag"` Items []SapronakDetailDTO `json:"items"` TotalMasuk float64 `json:"total_masuk"` TotalKeluar float64 `json:"total_keluar"` SaldoAkhir float64 `json:"saldo_akhir"` TotalNilai float64 `json:"total_nilai"` } type SapronakItemDTO struct { ProductID uint `json:"product_id"` ProductName string `json:"product_name"` Flag string `json:"flag"` IncomingQty float64 `json:"incoming_qty"` IncomingValue float64 `json:"incoming_value"` UsageQty float64 `json:"usage_qty"` UsageValue float64 `json:"usage_value"` RemainingQty float64 `json:"remaining_qty"` AveragePrice float64 `json:"average_price"` } type SapronakReportDTO struct { ProjectFlockKandangID uint `json:"project_flock_kandang_id"` ProjectFlockID uint `json:"project_flock_id"` ProjectName string `json:"project_name"` KandangID uint `json:"kandang_id"` KandangName string `json:"kandang_name"` Period int `json:"period"` Status string `json:"status"` StartDate *time.Time `json:"start_date,omitempty"` EndDate *time.Time `json:"end_date,omitempty"` TotalIncomingValue float64 `json:"total_incoming_value"` TotalUsageValue float64 `json:"total_usage_value"` Items []SapronakItemDTO `json:"items"` Groups []SapronakGroupDTO `json:"groups,omitempty"` } // Simplified view for project-level sapronak response type SapronakCategoryRowDTO struct { ID int `json:"id"` Date string `json:"date"` ReferenceNumber string `json:"reference_number"` QtyIn float64 `json:"qty_in"` QtyOut float64 `json:"qty_out"` QtyUsed float64 `json:"qty_used"` Description string `json:"description"` ProductCategory []string `json:"product_category"` UnitPrice float64 `json:"unit_price"` TotalAmount float64 `json:"total_amount"` Notes string `json:"notes"` } type SapronakCategoryTotalDTO struct { Label string `json:"label"` QtyIn float64 `json:"qty_in"` QtyOut float64 `json:"qty_out"` QtyUsed float64 `json:"qty_used"` AvgUnitPrice float64 `json:"avg_unit_price"` TotalAmount float64 `json:"total_amount"` } type SapronakCategoryDTO struct { Rows []SapronakCategoryRowDTO `json:"rows"` Total SapronakCategoryTotalDTO `json:"total"` } type SapronakProjectAggregatedDTO struct { Doc *SapronakCategoryDTO `json:"doc,omitempty"` Ovk *SapronakCategoryDTO `json:"ovk,omitempty"` Pakan *SapronakCategoryDTO `json:"pakan,omitempty"` Pullet *SapronakCategoryDTO `json:"pullet,omitempty"` } type ClosingSapronakItemDTO struct { Id uint64 `json:"id"` Date string `json:"date"` ReferenceNumber string `json:"reference_number"` TransactionType string `json:"transaction_type"` ProductName string `json:"product_name"` ProductCategory string `json:"product_category"` ProductSubCategory string `json:"product_sub_category"` SourceWarehouse string `json:"source_warehouse"` DestinationWarehouse string `json:"destination_warehouse,omitempty"` // Destination string `json:"destination,omitempty"` Quantity float64 `json:"quantity"` Unit string `json:"unit"` FormattedQuantity string `json:"formatted_quantity"` Notes string `json:"notes"` SortDate time.Time `json:"-"` } type ClosingSapronakDTO struct { IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"` OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"` } type ClosingSapronakSummaryItemDTO struct { Category string `json:"category"` TotalQty int64 `json:"total_qty"` Uom UomSummaryDTO `json:"uom"` } type UomSummaryDTO struct { ID uint `json:"id"` Name string `json:"name"` } // === Mapper Functions for Aggregated Sapronak Response === func ToSapronakProjectAggregatedFromReports(reports []SapronakReportDTO, flag string, productFlags map[uint][]string) SapronakProjectAggregatedDTO { result := SapronakProjectAggregatedDTO{} if len(reports) == 0 { return result } rep := reports[0] return ToSapronakProjectAggregatedFromReport(&rep, flag, productFlags) } func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag string, productFlags map[uint][]string) SapronakProjectAggregatedDTO { result := SapronakProjectAggregatedDTO{} if report == nil { report = &SapronakReportDTO{} } normalizeFlag := func(raw string) string { normalized := strings.ToUpper(strings.TrimSpace(raw)) if normalized == "PULLET" { return "DOC" } return normalized } filter := normalizeFlag(flag) byFlag := map[string]**SapronakCategoryDTO{} if filter == "" || filter == "DOC" { result.Doc = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)} byFlag["DOC"] = &result.Doc } if filter == "" || filter == "OVK" { result.Ovk = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)} byFlag["OVK"] = &result.Ovk } if filter == "" || filter == "PAKAN" { result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)} byFlag["PAKAN"] = &result.Pakan } formatDate := func(t *time.Time) string { if t == nil { return "" } return t.Format("02-Jan-2006") } flagOrder := map[string]int{ "DOC": 0, "PAKAN": 0, "OVK": 0, "PULLET": 0, } buildFlagList := func(productID uint, fallback string) []string { rawFlags := productFlags[productID] if len(rawFlags) == 0 { if fallback == "" { return []string{} } return []string{fallback} } seen := make(map[string]struct{}, len(rawFlags)) ordered := make([]string, 0, len(rawFlags)) for _, f := range rawFlags { flagName := strings.ToUpper(strings.TrimSpace(f)) if flagName == "" { continue } if _, ok := seen[flagName]; ok { continue } seen[flagName] = struct{}{} ordered = append(ordered, flagName) } sort.SliceStable(ordered, func(i, j int) bool { li := ordered[i] lj := ordered[j] ri, iok := flagOrder[li] rj, jok := flagOrder[lj] if iok != jok { if iok { return true } return false } if iok && jok && ri != rj { return ri < rj } return li < lj }) return ordered } for _, group := range report.Groups { flagKey := normalizeFlag(group.Flag) ptr := byFlag[flagKey] if ptr == nil || *ptr == nil { continue } target := *ptr rowIndexByProduct := make(map[string]int) getOrCreateRow := func(productKey string, base SapronakCategoryRowDTO) *SapronakCategoryRowDTO { if idx, ok := rowIndexByProduct[productKey]; ok { return &target.Rows[idx] } target.Rows = append(target.Rows, base) idx := len(target.Rows) - 1 rowIndexByProduct[productKey] = idx return &target.Rows[idx] } for idx, item := range group.Items { refKey := strings.TrimSpace(item.NoReferensi) productKey := strings.ToUpper(flagKey + "|" + item.ProductName + "|" + refKey) if refKey == "" { productKey = strings.ToUpper(flagKey + "|" + item.ProductName + "|" + formatDate(item.Tanggal)) } baseRow := SapronakCategoryRowDTO{ ID: idx + 1, Date: formatDate(item.Tanggal), ReferenceNumber: item.NoReferensi, Description: item.ProductName, ProductCategory: buildFlagList(item.ProductID, flagKey), UnitPrice: item.Harga, Notes: "-", } row := getOrCreateRow(productKey, baseRow) switch strings.ToLower(item.JenisTransaksi) { case "pembelian", "adjustment masuk", "mutasi masuk": row.QtyIn += item.QtyMasuk if item.Tanggal != nil { row.Date = formatDate(item.Tanggal) } if row.UnitPrice == 0 { if item.QtyMasuk > 0 && item.Nilai > 0 { row.UnitPrice = item.Nilai / item.QtyMasuk } else if item.Harga > 0 { row.UnitPrice = item.Harga } } if strings.ToLower(item.JenisTransaksi) == "mutasi masuk" { ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi)) if strings.HasPrefix(ref, "TL-") { row.Notes = "TRANSFER LAYING" } else if strings.HasPrefix(ref, "ST-") { row.Notes = "TRANSFER STOCK" } } case "pemakaian": price := row.UnitPrice if price == 0 { price = item.Harga } row.QtyUsed += item.QtyKeluar row.TotalAmount += item.QtyKeluar * price case "adjustment keluar", "mutasi keluar", "penjualan": price := row.UnitPrice if price == 0 { price = item.Harga } row.QtyOut += item.QtyKeluar if strings.ToLower(item.JenisTransaksi) == "mutasi keluar" { ref := strings.ToUpper(strings.TrimSpace(item.NoReferensi)) if strings.HasPrefix(ref, "TL-") { row.Notes = "TRANSFER LAYING" } else if strings.HasPrefix(ref, "ST-") { row.Notes = "TRANSFER STOCK" } } default: row.QtyIn += item.QtyMasuk row.TotalAmount += item.Nilai if row.QtyIn > 0 { row.UnitPrice = row.TotalAmount / row.QtyIn } } } for i := range target.Rows { target.Rows[i].ID = i + 1 } } buildTotals := func(cat *SapronakCategoryDTO, label string) { if cat == nil { return } var qtyIn, qtyOut, qtyUsed, total float64 for _, r := range cat.Rows { qtyIn += r.QtyIn qtyOut += r.QtyOut qtyUsed += r.QtyUsed total += r.TotalAmount } avg := 0.0 if qtyUsed > 0 { avg = total / qtyUsed } cat.Total = SapronakCategoryTotalDTO{ Label: label, QtyIn: qtyIn, QtyOut: qtyOut, QtyUsed: qtyUsed, AvgUnitPrice: avg, TotalAmount: total, } } buildTotals(result.Doc, "TOTAL DOC") buildTotals(result.Ovk, "TOTAL OVK") buildTotals(result.Pakan, "TOTAL PAKAN") return result }