diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 6d3ca4f4..47b18ace 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -130,18 +130,19 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error { func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error { param := c.Params("project_flock_id") + flag := c.Query("flag", "") projectID, err := strconv.Atoi(param) if err != nil || projectID <= 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") } - result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID)) + result, err := u.SapronakService.GetSapronakByProject(c, uint(projectID), flag) if err != nil { return err } - payload := u.SapronakFormatter.ProjectPayload(result) + payload := u.SapronakFormatter.ProjectPayload(result, flag) return c.Status(fiber.StatusOK). JSON(response.Success{ @@ -155,6 +156,7 @@ func (u *ClosingController) GetSapronakByProject(c *fiber.Ctx) error { func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { projectParam := c.Params("project_flock_id") kandangParam := c.Params("project_flock_kandang_id") + flag := c.Query("flag", "") projectID, err := strconv.Atoi(projectParam) if err != nil || projectID <= 0 { @@ -165,12 +167,12 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") } - result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID)) + result, err := u.SapronakService.GetSapronakByKandang(c, uint(projectID), uint(pfkID), flag) if err != nil { return err } - payload := u.SapronakFormatter.KandangPayload(result) + payload := u.SapronakFormatter.KandangPayload(result, flag) return c.Status(fiber.StatusOK). JSON(response.Success{ diff --git a/internal/modules/closings/dto/sapronak.dto.go b/internal/modules/closings/dto/sapronak.dto.go index fdf2559a..edb6bc88 100644 --- a/internal/modules/closings/dto/sapronak.dto.go +++ b/internal/modules/closings/dto/sapronak.dto.go @@ -82,7 +82,7 @@ type SapronakCategoryDTO struct { } type SapronakProjectAggregatedDTO struct { - Doc SapronakCategoryDTO `json:"doc"` - Ovk SapronakCategoryDTO `json:"ovk"` - Pakan SapronakCategoryDTO `json:"pakan"` + Doc *SapronakCategoryDTO `json:"doc,omitempty"` + Ovk *SapronakCategoryDTO `json:"ovk,omitempty"` + Pakan *SapronakCategoryDTO `json:"pakan,omitempty"` } diff --git a/internal/modules/closings/services/sapronak.service.go b/internal/modules/closings/services/sapronak.service.go index dca4c373..31952479 100644 --- a/internal/modules/closings/services/sapronak.service.go +++ b/internal/modules/closings/services/sapronak.service.go @@ -17,8 +17,8 @@ import ( ) type SapronakService interface { - GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint) ([]dto.SapronakReportDTO, error) - GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint) (*dto.SapronakReportDTO, error) + GetSapronakByProject(ctx *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) + GetSapronakByKandang(ctx *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) GetSapronakReport(ctx *fiber.Ctx, params *validation.SapronakQuery) ([]dto.SapronakReportDTO, error) } @@ -43,13 +43,14 @@ func (s sapronakService) GetSapronakReport(c *fiber.Ctx, params *validation.Sapr return s.computeSapronakReports(c.Context(), params) } -func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint) ([]dto.SapronakReportDTO, error) { +func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint, flag string) ([]dto.SapronakReportDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id is required") } reports, err := s.computeSapronakReports(c.Context(), &validation.SapronakQuery{ ProjectFlockID: projectFlockID, Status: "all", + Flag: flag, }) if err != nil { return nil, err @@ -62,7 +63,7 @@ func (s sapronakService) GetSapronakByProject(c *fiber.Ctx, projectFlockID uint) return []dto.SapronakReportDTO{combined}, nil } -func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint) (*dto.SapronakReportDTO, error) { +func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, pfkID uint, flag string) (*dto.SapronakReportDTO, error) { if projectFlockID == 0 || pfkID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_id and project_flock_kandang_id are required") } @@ -71,6 +72,7 @@ func (s sapronakService) GetSapronakByKandang(c *fiber.Ctx, projectFlockID uint, ProjectFlockID: projectFlockID, ProjectFlockKandangID: pfkID, Status: "all", + Flag: flag, }) if err != nil { return nil, err @@ -130,7 +132,7 @@ func (s sapronakService) computeSapronakReports(ctx context.Context, params *val endPtr = &endCopy } - items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, startPtr, endPtr) + items, groups, totalIncoming, totalUsage, err := s.buildSapronakItems(ctx, pfk, startPtr, endPtr, params.Flag) if err != nil { s.Log.Errorf("Failed to build sapronak items for pfk %d: %+v", pfk.Id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to calculate sapronak report") @@ -310,7 +312,75 @@ func (s sapronakService) computeStatusAndNextStart(pfks []entity.ProjectFlockKan return statusMap, nextStartMap } -func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { +func mapIncomingUsage(incomingRows []repository.SapronakIncomingRow, usageRows []repository.SapronakUsageRow) (map[uint]repository.SapronakIncomingRow, map[uint]repository.SapronakUsageRow) { + incoming := make(map[uint]repository.SapronakIncomingRow, len(incomingRows)) + for _, row := range incomingRows { + incoming[row.ProductID] = row + } + usage := make(map[uint]repository.SapronakUsageRow, len(usageRows)) + for _, row := range usageRows { + usage[row.ProductID] = row + } + return incoming, usage +} + +type sapronakDetailMaps struct { + Incoming map[uint][]dto.SapronakDetailDTO + Usage map[uint][]dto.SapronakDetailDTO + AdjIncoming map[uint][]dto.SapronakDetailDTO + AdjOutgoing map[uint][]dto.SapronakDetailDTO + TransferIn map[uint][]dto.SapronakDetailDTO +} + +func buildSapronakDetails( + incomingRows map[uint][]repository.SapronakDetailRow, + usageRows map[uint][]repository.SapronakDetailRow, + adjIncomingRows map[uint][]repository.SapronakDetailRow, + adjOutgoingRows map[uint][]repository.SapronakDetailRow, + transferInRows map[uint][]repository.SapronakDetailRow, +) sapronakDetailMaps { + result := sapronakDetailMaps{ + Incoming: make(map[uint][]dto.SapronakDetailDTO), + Usage: make(map[uint][]dto.SapronakDetailDTO), + AdjIncoming: make(map[uint][]dto.SapronakDetailDTO), + AdjOutgoing: make(map[uint][]dto.SapronakDetailDTO), + TransferIn: make(map[uint][]dto.SapronakDetailDTO), + } + + addRows := func(target map[uint][]dto.SapronakDetailDTO, src map[uint][]repository.SapronakDetailRow, jenis string, masuk bool) { + for pid, rows := range src { + for _, r := range rows { + d := dto.SapronakDetailDTO{ + ProductID: r.ProductID, + ProductName: r.ProductName, + Flag: r.Flag, + Tanggal: r.Date, + NoReferensi: r.Reference, + JenisTransaksi: jenis, + Harga: r.Price, + } + if masuk { + d.QtyMasuk = r.QtyIn + d.Nilai = r.QtyIn * r.Price + } else { + d.QtyKeluar = r.QtyOut + d.Nilai = r.QtyOut * r.Price + } + target[pid] = append(target[pid], d) + } + } + } + + addRows(result.Incoming, incomingRows, "Pembelian", true) + addRows(result.Usage, usageRows, "Pemakaian", false) + addRows(result.AdjIncoming, adjIncomingRows, "Adjustment Masuk", true) + addRows(result.AdjOutgoing, adjOutgoingRows, "Adjustment Keluar", false) + addRows(result.TransferIn, transferInRows, "Mutasi Masuk", true) + + return result +} + +func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.ProjectFlockKandang, start, end *time.Time, flagFilter string) ([]dto.SapronakItemDTO, []dto.SapronakGroupDTO, float64, float64, error) { incomingRows, err := s.Repository.FetchSapronakIncoming(ctx, pfk.KandangId, start, end) if err != nil { return nil, nil, 0, 0, err @@ -336,10 +406,24 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj return nil, nil, 0, 0, err } + filterFlag := strings.ToUpper(strings.TrimSpace(flagFilter)) + matchesFlag := func(f string) bool { + if filterFlag == "" { + return true + } + return strings.ToUpper(f) == filterFlag + } + incoming, usage := mapIncomingUsage(incomingRows, usageRows) itemMap := make(map[uint]dto.SapronakItemDTO, len(incoming)+len(usage)) groupMap := make(map[string]*dto.SapronakGroupDTO) - details := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows) + + detailMaps := buildSapronakDetails(incomingDetailsRows, usageDetailsRows, adjIncomingRows, adjOutgoingRows, transIncomingRows) + incomingDetails := detailMaps.Incoming + usageDetails := detailMaps.Usage + adjIncoming := detailMaps.AdjIncoming + adjOutgoing := detailMaps.AdjOutgoing + transIncoming := detailMaps.TransferIn ensureGroup := func(flag string) *dto.SapronakGroupDTO { if g, ok := groupMap[flag]; ok { @@ -350,6 +434,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for _, row := range incoming { + if !matchesFlag(row.Flag) { + continue + } avgPrice := row.DefaultPrice if row.Qty > 0 && row.Value > 0 { avgPrice = row.Value / row.Qty @@ -367,6 +454,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj } for _, row := range usage { + if !matchesFlag(row.Flag) { + continue + } existing := itemMap[row.ProductID] price := existing.AveragePrice if price == 0 { @@ -396,6 +486,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj for productID, details := range adjIncoming { for _, d := range details { + if !matchesFlag(d.Flag) { + continue + } existing := itemMap[productID] if existing.Flag == "" { existing.Flag = d.Flag @@ -419,6 +512,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj for productID, details := range adjOutgoing { for _, d := range details { + if !matchesFlag(d.Flag) { + continue + } existing := itemMap[productID] if existing.Flag == "" { existing.Flag = d.Flag @@ -439,6 +535,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj for productID, details := range transIncoming { for _, d := range details { + if !matchesFlag(d.Flag) { + continue + } existing := itemMap[productID] if existing.Flag == "" { existing.Flag = d.Flag @@ -475,6 +574,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj flag = item.Flag name = item.ProductName } + if !matchesFlag(flag) { + continue + } group := ensureGroup(flag) for _, d := range details { d.Flag = flag @@ -493,6 +595,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj flag = item.Flag name = item.ProductName } + if !matchesFlag(flag) { + continue + } group := ensureGroup(flag) for _, d := range details { d.Flag = flag @@ -511,6 +616,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj flag = item.Flag name = item.ProductName } + if !matchesFlag(flag) { + continue + } group := ensureGroup(flag) for _, d := range details { d.Flag = flag @@ -528,6 +636,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj flag = item.Flag name = item.ProductName } + if !matchesFlag(flag) { + continue + } group := ensureGroup(flag) for _, d := range details { d.Flag = flag @@ -545,6 +656,9 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj flag = item.Flag name = item.ProductName } + if !matchesFlag(flag) { + continue + } group := ensureGroup(flag) for _, d := range details { d.Flag = flag diff --git a/internal/modules/closings/services/sapronak_formatter.go b/internal/modules/closings/services/sapronak_formatter.go index ce4b5ca2..880d2149 100644 --- a/internal/modules/closings/services/sapronak_formatter.go +++ b/internal/modules/closings/services/sapronak_formatter.go @@ -8,8 +8,8 @@ import ( ) type SapronakFormatter interface { - ProjectPayload(reports []dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO - KandangPayload(report *dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO + ProjectPayload(reports []dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO + KandangPayload(report *dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO } type sapronakFormatter struct{} @@ -18,40 +18,42 @@ func NewSapronakFormatter() SapronakFormatter { return &sapronakFormatter{} } -func (f *sapronakFormatter) ProjectPayload(reports []dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO { - result := dto.SapronakProjectAggregatedDTO{ - Doc: dto.SapronakCategoryDTO{}, - Ovk: dto.SapronakCategoryDTO{}, - Pakan: dto.SapronakCategoryDTO{}, - } +func (f *sapronakFormatter) ProjectPayload(reports []dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO { + result := dto.SapronakProjectAggregatedDTO{} if len(reports) == 0 { return result } rep := reports[0] - return f.mapFromReport(&rep) + return f.mapFromReport(&rep, flag) } -func (f *sapronakFormatter) KandangPayload(report *dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO { - return f.mapFromReport(report) +func (f *sapronakFormatter) KandangPayload(report *dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO { + return f.mapFromReport(report, flag) } -func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO) dto.SapronakProjectAggregatedDTO { - result := dto.SapronakProjectAggregatedDTO{ - Doc: dto.SapronakCategoryDTO{}, - Ovk: dto.SapronakCategoryDTO{}, - Pakan: dto.SapronakCategoryDTO{}, - } +func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO, flag string) dto.SapronakProjectAggregatedDTO { + result := dto.SapronakProjectAggregatedDTO{} if report == nil { return result } - byFlag := map[string]*dto.SapronakCategoryDTO{ - "DOC": &result.Doc, - "OVK": &result.Ovk, - "PAKAN": &result.Pakan, + filter := strings.ToUpper(strings.TrimSpace(flag)) + + byFlag := map[string]**dto.SapronakCategoryDTO{} + if filter == "" || filter == "DOC" { + result.Doc = &dto.SapronakCategoryDTO{} + byFlag["DOC"] = &result.Doc + } + if filter == "" || filter == "OVK" { + result.Ovk = &dto.SapronakCategoryDTO{} + byFlag["OVK"] = &result.Ovk + } + if filter == "" || filter == "PAKAN" { + result.Pakan = &dto.SapronakCategoryDTO{} + byFlag["PAKAN"] = &result.Pakan } formatDate := func(t *time.Time) string { @@ -63,10 +65,11 @@ func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO) dto.Sap for _, group := range report.Groups { flag := strings.ToUpper(group.Flag) - target := byFlag[flag] - if target == nil { + ptr := byFlag[flag] + if ptr == nil || *ptr == nil { continue } + target := *ptr for idx, item := range group.Items { qtyUsed := item.QtyKeluar if qtyUsed == 0 { @@ -90,6 +93,9 @@ func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO) dto.Sap } buildTotals := func(cat *dto.SapronakCategoryDTO, label string) { + if cat == nil { + return + } var qtyIn, qtyOut, qtyUsed, total float64 for _, r := range cat.Rows { qtyIn += r.QtyIn @@ -111,9 +117,9 @@ func (f *sapronakFormatter) mapFromReport(report *dto.SapronakReportDTO) dto.Sap } } - buildTotals(&result.Doc, "TOTAL DOC") - buildTotals(&result.Ovk, "TOTAL OVK") - buildTotals(&result.Pakan, "TOTAL PAKAN") + buildTotals(result.Doc, "TOTAL DOC") + buildTotals(result.Ovk, "TOTAL OVK") + buildTotals(result.Pakan, "TOTAL PAKAN") return result } diff --git a/internal/modules/closings/validations/sapronak.validation.go b/internal/modules/closings/validations/sapronak.validation.go index 3656e854..1f2ca54f 100644 --- a/internal/modules/closings/validations/sapronak.validation.go +++ b/internal/modules/closings/validations/sapronak.validation.go @@ -5,5 +5,5 @@ type SapronakQuery struct { KandangID uint `query:"kandang_id" validate:"omitempty,gt=0"` ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` Status string `query:"status" validate:"omitempty,oneof=active closing all"` - Debug bool `query:"debug"` + Flag string `query:"flag" validate:"omitempty,oneof=DOC OVK PAKAN doc ovk pakan"` }