From 65a1282312cc2aa4475a3a487dec62d61cbaddce Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 25 May 2026 11:25:53 +0700 Subject: [PATCH 1/3] add excel export for purchase supplier report Co-Authored-By: Claude Sonnet 4.6 --- .../controllers/repport.controller.go | 7 + .../repport.purchase_supplier.export.go | 415 ++++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 internal/modules/repports/controllers/repport.purchase_supplier.export.go diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 7a66f247..255b7c3f 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -324,6 +324,13 @@ func (c *RepportController) GetPurchaseSupplier(ctx *fiber.Ctx) error { return err } + if isPurchaseSupplierExcelExportRequest(ctx) { + return exportPurchaseSupplierExcel(ctx, result) + } + if isPurchaseSupplierExcelAllExportRequest(ctx) { + return exportPurchaseSupplierExcelAll(ctx, result) + } + filters := map[string]interface{}{ "area_id": query.AreaIDs, "supplier_id": query.SupplierIDs, diff --git a/internal/modules/repports/controllers/repport.purchase_supplier.export.go b/internal/modules/repports/controllers/repport.purchase_supplier.export.go new file mode 100644 index 00000000..d82a4e9e --- /dev/null +++ b/internal/modules/repports/controllers/repport.purchase_supplier.export.go @@ -0,0 +1,415 @@ +package controller + +import ( + "fmt" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/xuri/excelize/v2" + "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" +) + +func isPurchaseSupplierExcelExportRequest(c *fiber.Ctx) bool { + return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") +} + +func isPurchaseSupplierExcelAllExportRequest(c *fiber.Ctx) bool { + return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel-all") +} + +func exportPurchaseSupplierExcel(c *fiber.Ctx, items []dto.PurchaseSupplierDTO) error { + content, err := buildPurchaseSupplierWorkbook(items) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") + } + + filename := fmt.Sprintf("laporan-pembelian-supplier-%s.xlsx", time.Now().Format("2006-01-02-1504")) + c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + return c.Status(fiber.StatusOK).Send(content) +} + +func exportPurchaseSupplierExcelAll(c *fiber.Ctx, items []dto.PurchaseSupplierDTO) error { + content, err := buildPurchaseSupplierAllWorkbook(items) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") + } + + filename := fmt.Sprintf("laporan-pembelian-supplier-all-%s.xlsx", time.Now().Format("2006-01-02-1504")) + c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + return c.Status(fiber.StatusOK).Send(content) +} + +// buildPurchaseSupplierWorkbook creates a workbook with one sheet per supplier. +func buildPurchaseSupplierWorkbook(items []dto.PurchaseSupplierDTO) ([]byte, error) { + file := excelize.NewFile() + defer file.Close() + + defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) + + if len(items) == 0 { + if err := writePurchaseSupplierSheet(file, defaultSheet, dto.PurchaseSupplierDTO{}); err != nil { + return nil, err + } + buf, err := file.WriteToBuffer() + if err != nil { + return nil, err + } + return buf.Bytes(), nil + } + + for idx, item := range items { + sheetName := sanitizePurchaseSupplierSheetName(purchaseSupplierName(item)) + if sheetName == "" { + sheetName = fmt.Sprintf("Supplier %d", idx+1) + } + + if idx == 0 { + if defaultSheet != sheetName { + if err := file.SetSheetName(defaultSheet, sheetName); err != nil { + return nil, err + } + } + } else { + if _, err := file.NewSheet(sheetName); err != nil { + return nil, err + } + } + + if err := writePurchaseSupplierSheet(file, sheetName, item); err != nil { + return nil, err + } + } + + buf, err := file.WriteToBuffer() + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// buildPurchaseSupplierAllWorkbook creates a single-sheet workbook with all suppliers. +func buildPurchaseSupplierAllWorkbook(items []dto.PurchaseSupplierDTO) ([]byte, error) { + file := excelize.NewFile() + defer file.Close() + + const sheet = "Rekap Pembelian Supplier" + defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) + if defaultSheet != sheet { + if err := file.SetSheetName(defaultSheet, sheet); err != nil { + return nil, err + } + } + + if err := setPurchaseSupplierAllColumns(file, sheet); err != nil { + return nil, err + } + if err := setPurchaseSupplierAllHeaders(file, sheet); err != nil { + return nil, err + } + if err := writePurchaseSupplierAllRows(file, sheet, items); err != nil { + return nil, err + } + if err := file.SetPanes(sheet, &excelize.Panes{ + Freeze: true, + YSplit: 1, + TopLeftCell: "A2", + ActivePane: "bottomLeft", + }); err != nil { + return nil, err + } + + buf, err := file.WriteToBuffer() + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +var purchaseSupplierSheetHeaders = []string{ + "No", + "Tanggal Terima", + "Tanggal PO", + "No. Referensi", + "Nama Produk", + "Tujuan", + "QTY", + "Harga Beli (Rp)", + "Value Harga Beli (Rp)", + "Transport (Rp)", + "Value Transport (Rp)", + "Jumlah (Rp)", + "Ekspedisi", + "Surat Jalan", +} + +var purchaseSupplierAllSheetHeaders = append([]string{"Supplier"}, purchaseSupplierSheetHeaders...) + +var purchaseSupplierSheetColumnWidths = map[string]float64{ + "A": 5, + "B": 14, + "C": 12, + "D": 16, + "E": 20, + "F": 20, + "G": 10, + "H": 20, + "I": 20, + "J": 22, + "K": 22, + "L": 16, + "M": 20, + "N": 20, +} + +var purchaseSupplierAllSheetColumnWidths = map[string]float64{ + "A": 24, + "B": 6, + "C": 14, + "D": 12, + "E": 16, + "F": 20, + "G": 20, + "H": 10, + "I": 20, + "J": 20, + "K": 22, + "L": 22, + "M": 16, + "N": 20, + "O": 20, +} + +func writePurchaseSupplierSheet(file *excelize.File, sheet string, item dto.PurchaseSupplierDTO) error { + for col, width := range purchaseSupplierSheetColumnWidths { + if err := file.SetColWidth(sheet, col, col, width); err != nil { + return err + } + } + + for i, h := range purchaseSupplierSheetHeaders { + col, _ := excelize.ColumnNumberToName(i + 1) + if err := file.SetCellValue(sheet, col+"1", h); err != nil { + return err + } + } + + for i, row := range item.Rows { + rowNum := i + 2 + rowStr := fmt.Sprintf("%d", rowNum) + + values := purchaseSupplierRowCells(row, i+1) + for colIdx, val := range values { + col, _ := excelize.ColumnNumberToName(colIdx + 1) + if err := file.SetCellValue(sheet, col+rowStr, val); err != nil { + return err + } + } + } + + // Summary row + totalRowNum := len(item.Rows) + 2 + totalRowStr := fmt.Sprintf("%d", totalRowNum) + totalCells := map[string]interface{}{ + "A": "Total", + "G": item.Summary.TotalQty, + "H": item.Summary.TotalUnitPrice, + "I": item.Summary.TotalPurchaseValue, + "J": item.Summary.TotalTransportUnitPrice, + "K": item.Summary.TotalTransportValue, + "L": item.Summary.TotalAmount, + } + for col, val := range totalCells { + if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil { + return err + } + } + + return nil +} + +func setPurchaseSupplierAllColumns(file *excelize.File, sheet string) error { + for col, width := range purchaseSupplierAllSheetColumnWidths { + if err := file.SetColWidth(sheet, col, col, width); err != nil { + return err + } + } + if err := file.SetRowHeight(sheet, 1, 24); err != nil { + return err + } + return nil +} + +func setPurchaseSupplierAllHeaders(file *excelize.File, sheet string) error { + headerStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF", Family: "Arial", Size: 10}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + WrapText: true, + }, + Border: []excelize.Border{ + {Type: "left", Color: "000000", Style: 1}, + {Type: "top", Color: "000000", Style: 1}, + {Type: "bottom", Color: "000000", Style: 1}, + {Type: "right", Color: "000000", Style: 1}, + }, + }) + if err != nil { + return err + } + + for i, h := range purchaseSupplierAllSheetHeaders { + col, _ := excelize.ColumnNumberToName(i + 1) + if err := file.SetCellValue(sheet, col+"1", h); err != nil { + return err + } + } + + lastCol, _ := excelize.ColumnNumberToName(len(purchaseSupplierAllSheetHeaders)) + return file.SetCellStyle(sheet, "A1", lastCol+"1", headerStyle) +} + +func writePurchaseSupplierAllRows(file *excelize.File, sheet string, items []dto.PurchaseSupplierDTO) error { + borderStyle := []excelize.Border{ + {Type: "left", Color: "000000", Style: 1}, + {Type: "top", Color: "000000", Style: 1}, + {Type: "bottom", Color: "000000", Style: 1}, + {Type: "right", Color: "000000", Style: 1}, + } + + dataStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Color: "000000", Family: "Arial", Size: 10}, + Alignment: &excelize.Alignment{Vertical: "center", WrapText: true}, + Border: borderStyle, + }) + if err != nil { + return err + } + + totalStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "000000", Family: "Arial", Size: 10}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}}, + Alignment: &excelize.Alignment{Vertical: "center", WrapText: true}, + Border: borderStyle, + }) + if err != nil { + return err + } + + lastHeaderCol, _ := excelize.ColumnNumberToName(len(purchaseSupplierAllSheetHeaders)) + + currentRow := 2 + for _, item := range items { + supplierName := purchaseSupplierName(item) + + // Data rows + for seq, row := range item.Rows { + rowStr := fmt.Sprintf("%d", currentRow) + if err := file.SetCellValue(sheet, "A"+rowStr, supplierName); err != nil { + return err + } + values := purchaseSupplierRowCells(row, seq+1) + for colIdx, val := range values { + col, _ := excelize.ColumnNumberToName(colIdx + 2) + if err := file.SetCellValue(sheet, col+rowStr, val); err != nil { + return err + } + } + if err := file.SetCellStyle(sheet, "A"+rowStr, lastHeaderCol+rowStr, dataStyle); err != nil { + return err + } + currentRow++ + } + + // Summary row + totalRowStr := fmt.Sprintf("%d", currentRow) + totalCells := map[string]interface{}{ + "A": supplierName, + "B": "Total", + "H": item.Summary.TotalQty, + "I": item.Summary.TotalUnitPrice, + "J": item.Summary.TotalPurchaseValue, + "K": item.Summary.TotalTransportUnitPrice, + "L": item.Summary.TotalTransportValue, + "M": item.Summary.TotalAmount, + } + for col, val := range totalCells { + if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil { + return err + } + } + if err := file.SetCellStyle(sheet, "A"+totalRowStr, lastHeaderCol+totalRowStr, totalStyle); err != nil { + return err + } + currentRow++ + + // Empty separator row + currentRow++ + } + + return nil +} + +// purchaseSupplierRowCells returns cell values for one data row. +func purchaseSupplierRowCells(row dto.PurchaseSupplierRowDTO, seq int) []interface{} { + productName := "-" + if row.Product != nil && strings.TrimSpace(row.Product.Name) != "" { + productName = row.Product.Name + } + warehouseName := "-" + if row.Warehouse != nil && strings.TrimSpace(row.Warehouse.Name) != "" { + warehouseName = row.Warehouse.Name + } + + return []interface{}{ + seq, + safePurchaseSupplierText(row.ReceiveDate), + safePurchaseSupplierText(row.PoDate), + safePurchaseSupplierText(row.PoNumber), + productName, + warehouseName, + row.Qty, + row.UnitPrice, + row.PurchaseValue, + row.TransportUnitPrice, + row.TransportValue, + row.TotalAmount, + safePurchaseSupplierText(row.Expedition), + safePurchaseSupplierText(row.DeliveryNumber), + } +} + +func purchaseSupplierName(item dto.PurchaseSupplierDTO) string { + if item.Supplier != nil && strings.TrimSpace(item.Supplier.Name) != "" { + return item.Supplier.Name + } + return "Supplier" +} + +func sanitizePurchaseSupplierSheetName(name string) string { + replacer := strings.NewReplacer( + ":", " ", "\\", " ", "/", " ", + "?", " ", "*", " ", "[", " ", "]", " ", + ) + sanitized := strings.TrimSpace(replacer.Replace(name)) + if sanitized == "" { + return "Sheet" + } + runes := []rune(sanitized) + if len(runes) > 31 { + return string(runes[:31]) + } + return sanitized +} + +func safePurchaseSupplierText(s string) string { + t := strings.TrimSpace(s) + if t == "" { + return "-" + } + return t +} From c107f0f6832557057fbcbd06656b2d5b1ad46677 Mon Sep 17 00:00:00 2001 From: ValdiANS Date: Mon, 25 May 2026 14:18:47 +0700 Subject: [PATCH 2/3] feat(reports): add Excel export to balance monitoring endpoint Add ?export=excel support to GetBalanceMonitoring. Creates a new repport.balance_monitoring.export.go with a 2-row merged header layout matching the UI (Penjualan Ayam and Penjualan Telur grouped columns), a totals row, red styling for negative Saldo Akhir, and frozen panes below the header rows. Exported data reflects all active query filters. Co-Authored-By: Claude Sonnet 4.6 --- .../repport.balance_monitoring.export.go | 286 ++++++++++++++++++ .../controllers/repport.controller.go | 4 + 2 files changed, 290 insertions(+) create mode 100644 internal/modules/repports/controllers/repport.balance_monitoring.export.go diff --git a/internal/modules/repports/controllers/repport.balance_monitoring.export.go b/internal/modules/repports/controllers/repport.balance_monitoring.export.go new file mode 100644 index 00000000..5802743c --- /dev/null +++ b/internal/modules/repports/controllers/repport.balance_monitoring.export.go @@ -0,0 +1,286 @@ +package controller + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/gofiber/fiber/v2" + "github.com/xuri/excelize/v2" + "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" +) + +func isBalanceMonitoringExcelExportRequest(c *fiber.Ctx) bool { + return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") +} + +func exportBalanceMonitoringExcel(c *fiber.Ctx, items []dto.BalanceMonitoringRowDTO, totals dto.BalanceMonitoringTotalsDTO) error { + content, err := buildBalanceMonitoringWorkbook(items, totals) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") + } + + filename := fmt.Sprintf("laporan-balance-monitoring-%s.xlsx", time.Now().Format("2006-01-02-1504")) + c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename)) + return c.Status(fiber.StatusOK).Send(content) +} + +func buildBalanceMonitoringWorkbook(items []dto.BalanceMonitoringRowDTO, totals dto.BalanceMonitoringTotalsDTO) ([]byte, error) { + file := excelize.NewFile() + defer file.Close() + + const sheet = "Balance Monitoring" + defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) + if defaultSheet != sheet { + if err := file.SetSheetName(defaultSheet, sheet); err != nil { + return nil, err + } + } + + if err := setBalanceMonitoringColumns(file, sheet); err != nil { + return nil, err + } + if err := setBalanceMonitoringHeaders(file, sheet); err != nil { + return nil, err + } + if err := writeBalanceMonitoringRows(file, sheet, items, totals); err != nil { + return nil, err + } + if err := file.SetPanes(sheet, &excelize.Panes{ + Freeze: true, + YSplit: 2, + TopLeftCell: "A3", + ActivePane: "bottomLeft", + }); err != nil { + return nil, err + } + + buf, err := file.WriteToBuffer() + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +var bmColumnWidths = map[string]float64{ + "A": 5, + "B": 28, + "C": 18, + "D": 12, + "E": 12, + "F": 20, + "G": 12, + "H": 12, + "I": 20, + "J": 20, + "K": 18, + "L": 12, + "M": 16, + "N": 20, +} + +func setBalanceMonitoringColumns(file *excelize.File, sheet string) error { + for col, width := range bmColumnWidths { + if err := file.SetColWidth(sheet, col, col, width); err != nil { + return err + } + } + if err := file.SetRowHeight(sheet, 1, 24); err != nil { + return err + } + return file.SetRowHeight(sheet, 2, 24) +} + +func setBalanceMonitoringHeaders(file *excelize.File, sheet string) error { + borderStyle := []excelize.Border{ + {Type: "left", Color: "000000", Style: 1}, + {Type: "top", Color: "000000", Style: 1}, + {Type: "bottom", Color: "000000", Style: 1}, + {Type: "right", Color: "000000", Style: 1}, + } + headerStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FFFFFF", Family: "Arial", Size: 10}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"4472C4"}}, + Alignment: &excelize.Alignment{ + Horizontal: "center", + Vertical: "center", + WrapText: true, + }, + Border: borderStyle, + }) + if err != nil { + return err + } + + // Single-column headers: merge rows 1 and 2 vertically + singleColHeaders := map[string]string{ + "A": "No", + "B": "Customer", + "C": "Saldo Awal", + "J": "Penjualan Trading", + "K": "Pembayaran", + "L": "Aging", + "M": "Aging Rata-Rata", + "N": "Saldo Akhir", + } + for col, header := range singleColHeaders { + if err := file.SetCellValue(sheet, col+"1", header); err != nil { + return err + } + if err := file.MergeCell(sheet, col+"1", col+"2"); err != nil { + return err + } + } + + // Group headers: merge columns horizontally in row 1 + if err := file.SetCellValue(sheet, "D1", "Penjualan Ayam"); err != nil { + return err + } + if err := file.MergeCell(sheet, "D1", "F1"); err != nil { + return err + } + if err := file.SetCellValue(sheet, "G1", "Penjualan Telur"); err != nil { + return err + } + if err := file.MergeCell(sheet, "G1", "I1"); err != nil { + return err + } + + // Sub-column headers in row 2 + subHeaders := map[string]string{ + "D": "Ekor", + "E": "Kg", + "F": "Nominal", + "G": "Butir", + "H": "Kg", + "I": "Nominal", + } + for col, header := range subHeaders { + if err := file.SetCellValue(sheet, col+"2", header); err != nil { + return err + } + } + + return file.SetCellStyle(sheet, "A1", "N2", headerStyle) +} + +func writeBalanceMonitoringRows(file *excelize.File, sheet string, items []dto.BalanceMonitoringRowDTO, totals dto.BalanceMonitoringTotalsDTO) error { + borderStyle := []excelize.Border{ + {Type: "left", Color: "000000", Style: 1}, + {Type: "top", Color: "000000", Style: 1}, + {Type: "bottom", Color: "000000", Style: 1}, + {Type: "right", Color: "000000", Style: 1}, + } + + dataStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Color: "000000", Family: "Arial", Size: 10}, + Alignment: &excelize.Alignment{Vertical: "center", WrapText: true}, + Border: borderStyle, + }) + if err != nil { + return err + } + + totalStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "000000", Family: "Arial", Size: 10}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}}, + Alignment: &excelize.Alignment{Vertical: "center", WrapText: true}, + Border: borderStyle, + }) + if err != nil { + return err + } + + redDataStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Color: "FF0000", Family: "Arial", Size: 10}, + Alignment: &excelize.Alignment{Vertical: "center", WrapText: true}, + Border: borderStyle, + }) + if err != nil { + return err + } + + redTotalStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "FF0000", Family: "Arial", Size: 10}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"E2EFDA"}}, + Alignment: &excelize.Alignment{Vertical: "center", WrapText: true}, + Border: borderStyle, + }) + if err != nil { + return err + } + + for i, row := range items { + rowNum := i + 3 + rowStr := strconv.Itoa(rowNum) + + cells := map[string]interface{}{ + "A": i + 1, + "B": row.Customer.Name, + "C": row.SaldoAwal, + "D": row.PenjualanAyam.Ekor, + "E": row.PenjualanAyam.Kg, + "F": row.PenjualanAyam.Nominal, + "G": row.PenjualanTelur.Butir, + "H": row.PenjualanTelur.Kg, + "I": row.PenjualanTelur.Nominal, + "J": row.PenjualanTrading.Nominal, + "K": row.Pembayaran, + "L": fmt.Sprintf("%d hari", row.Aging), + "M": formatBMAging(row.AgingRataRata), + "N": row.SaldoAkhir, + } + for col, val := range cells { + if err := file.SetCellValue(sheet, col+rowStr, val); err != nil { + return err + } + } + if err := file.SetCellStyle(sheet, "A"+rowStr, "N"+rowStr, dataStyle); err != nil { + return err + } + if row.SaldoAkhir < 0 { + if err := file.SetCellStyle(sheet, "N"+rowStr, "N"+rowStr, redDataStyle); err != nil { + return err + } + } + } + + // Totals row + totalRowStr := strconv.Itoa(len(items) + 3) + totalCells := map[string]interface{}{ + "A": "Total", + "C": totals.SaldoAwal, + "D": totals.PenjualanAyam.Ekor, + "E": totals.PenjualanAyam.Kg, + "F": totals.PenjualanAyam.Nominal, + "G": totals.PenjualanTelur.Butir, + "H": totals.PenjualanTelur.Kg, + "I": totals.PenjualanTelur.Nominal, + "J": totals.PenjualanTrading.Nominal, + "K": totals.Pembayaran, + "N": totals.SaldoAkhir, + } + for col, val := range totalCells { + if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil { + return err + } + } + if err := file.SetCellStyle(sheet, "A"+totalRowStr, "N"+totalRowStr, totalStyle); err != nil { + return err + } + if totals.SaldoAkhir < 0 { + if err := file.SetCellStyle(sheet, "N"+totalRowStr, "N"+totalRowStr, redTotalStyle); err != nil { + return err + } + } + + return nil +} + +func formatBMAging(v float64) string { + s := strconv.FormatFloat(v, 'f', 2, 64) + s = strings.ReplaceAll(s, ".", ",") + return s + " hari" +} diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 7a66f247..38fe7052 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -555,6 +555,10 @@ func (c *RepportController) GetBalanceMonitoring(ctx *fiber.Ctx) error { return err } + if isBalanceMonitoringExcelExportRequest(ctx) { + return exportBalanceMonitoringExcel(ctx, result, totals) + } + limit := query.Limit if limit < 1 { limit = 10 From ef985b5da59353b84d19bfec48661f6f270c32fc Mon Sep 17 00:00:00 2001 From: giovanni Date: Mon, 25 May 2026 14:50:01 +0700 Subject: [PATCH 3/3] fix list penjualan and export penjualan dengan qty --- .../controllers/deliveryorder.export.go | 248 +++++++++++------- .../marketing/dto/deliveryorder.dto.go | 19 ++ 2 files changed, 179 insertions(+), 88 deletions(-) diff --git a/internal/modules/marketing/controllers/deliveryorder.export.go b/internal/modules/marketing/controllers/deliveryorder.export.go index 7d26191a..06b3ab28 100644 --- a/internal/modules/marketing/controllers/deliveryorder.export.go +++ b/internal/modules/marketing/controllers/deliveryorder.export.go @@ -75,9 +75,18 @@ func setMarketingExportColumns(file *excelize.File, sheet string) error { "B": 14, "C": 18, "D": 20, - "E": 18, - "F": 60, - "G": 24, + "E": 14, + "F": 40, + "G": 10, + "H": 12, + "I": 12, + "J": 12, + "K": 16, + "L": 16, + "M": 18, + "N": 18, + "O": 18, + "P": 24, } for col, width := range columnWidths { @@ -95,13 +104,22 @@ func setMarketingExportColumns(file *excelize.File, sheet string) error { func setMarketingExportHeaders(file *excelize.File, sheet string) error { headers := []string{ - "No. Order", - "Tanggal", - "Status", - "Customer", - "Grand Total", - "Products", - "Notes", + "No. Order", // A + "Tanggal", // B + "Status", // C + "Customer", // D + "Tipe", // E + "Nama Produk", // F + "Week", // G + "Jumlah", // H + "Satuan", // I + "Qty Peti", // J + "Berat Rata-rata (kg)", // K + "Total Berat (kg)", // L + "Harga Satuan", // M + "Total Harga", // N + "Grand Total", // O + "Catatan", // P } for i, header := range headers { @@ -130,7 +148,7 @@ func setMarketingExportHeaders(file *excelize.File, sheet string) error { return err } - return file.SetCellStyle(sheet, "A1", "G1", headerStyle) + return file.SetCellStyle(sheet, "A1", "P1", headerStyle) } func setMarketingExportRows(file *excelize.File, sheet string, items []dto.MarketingListDTO) error { @@ -138,70 +156,154 @@ func setMarketingExportRows(file *excelize.File, sheet string, items []dto.Marke return nil } - for i, item := range items { - rowNumber := i + 2 - if err := file.SetCellValue(sheet, "A"+strconv.Itoa(rowNumber), safeMarketingExportText(item.SoNumber)); err != nil { - return err + row := 1 + for _, item := range items { + soNumber := safeMarketingExportText(item.SoNumber) + soDate := formatMarketingExportDate(item.SoDate) + status := formatMarketingExportStatus(item) + customer := safeMarketingExportText(item.Customer.Name) + grandTotal := sumMarketingGrandTotal(item.SalesOrder) + notes := safeMarketingExportText(item.Notes) + + if len(item.SalesOrder) == 0 { + row++ + r := strconv.Itoa(row) + vals := map[string]interface{}{ + "A": soNumber, "B": soDate, "C": status, "D": customer, + "E": "-", "F": "-", "G": "-", "H": "-", "I": "-", "J": "-", + "K": "-", "L": "-", "M": "-", "N": "-", + "O": grandTotal, "P": notes, + } + for col, val := range vals { + if err := file.SetCellValue(sheet, col+r, val); err != nil { + return err + } + } + continue } - if err := file.SetCellValue(sheet, "B"+strconv.Itoa(rowNumber), formatMarketingExportDate(item.SoDate)); err != nil { - return err - } - if err := file.SetCellValue(sheet, "C"+strconv.Itoa(rowNumber), formatMarketingExportStatus(item)); err != nil { - return err - } - if err := file.SetCellValue(sheet, "D"+strconv.Itoa(rowNumber), safeMarketingExportText(item.Customer.Name)); err != nil { - return err - } - if err := file.SetCellValue(sheet, "E"+strconv.Itoa(rowNumber), sumMarketingGrandTotal(item.SalesOrder)); err != nil { - return err - } - if err := file.SetCellValue(sheet, "F"+strconv.Itoa(rowNumber), formatMarketingProducts(item.SalesOrder)); err != nil { - return err - } - if err := file.SetCellValue(sheet, "G"+strconv.Itoa(rowNumber), safeMarketingExportText(item.Notes)); err != nil { - return err + + for _, prod := range item.SalesOrder { + row++ + r := strconv.Itoa(row) + + productName := "-" + if prod.ProductWarehouse != nil && prod.ProductWarehouse.Product != nil { + if n := strings.TrimSpace(prod.ProductWarehouse.Product.Name); n != "" { + productName = n + } + } + + week := "-" + if prod.Week != nil { + week = strconv.Itoa(*prod.Week) + } + + satuan := "-" + if prod.ConvertionUnit != nil && strings.TrimSpace(*prod.ConvertionUnit) != "" { + satuan = *prod.ConvertionUnit + } + + if err := file.SetCellValue(sheet, "A"+r, soNumber); err != nil { + return err + } + if err := file.SetCellValue(sheet, "B"+r, soDate); err != nil { + return err + } + if err := file.SetCellValue(sheet, "C"+r, status); err != nil { + return err + } + if err := file.SetCellValue(sheet, "D"+r, customer); err != nil { + return err + } + if err := file.SetCellValue(sheet, "E"+r, safeMarketingExportText(prod.MarketingType)); err != nil { + return err + } + if err := file.SetCellValue(sheet, "F"+r, productName); err != nil { + return err + } + if err := file.SetCellValue(sheet, "G"+r, week); err != nil { + return err + } + if err := file.SetCellValue(sheet, "H"+r, prod.Qty); err != nil { + return err + } + if err := file.SetCellValue(sheet, "I"+r, satuan); err != nil { + return err + } + if prod.TotalPeti != nil { + if err := file.SetCellValue(sheet, "J"+r, *prod.TotalPeti); err != nil { + return err + } + } else { + if err := file.SetCellValue(sheet, "J"+r, "-"); err != nil { + return err + } + } + if err := file.SetCellValue(sheet, "K"+r, prod.AvgWeight); err != nil { + return err + } + if err := file.SetCellValue(sheet, "L"+r, prod.TotalWeight); err != nil { + return err + } + if err := file.SetCellValue(sheet, "M"+r, prod.UnitPrice); err != nil { + return err + } + if err := file.SetCellValue(sheet, "N"+r, prod.TotalPrice); err != nil { + return err + } + if err := file.SetCellValue(sheet, "O"+r, grandTotal); err != nil { + return err + } + if err := file.SetCellValue(sheet, "P"+r, notes); err != nil { + return err + } } } - lastRow := len(items) + 1 + lastRow := row + lastRowStr := strconv.Itoa(lastRow) + border := []excelize.Border{ + {Type: "left", Color: "D1D5DB", Style: 1}, + {Type: "top", Color: "D1D5DB", Style: 1}, + {Type: "bottom", Color: "D1D5DB", Style: 1}, + {Type: "right", Color: "D1D5DB", Style: 1}, + } + dataStyle, err := file.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "left", - Vertical: "center", - WrapText: true, - }, - Border: []excelize.Border{ - {Type: "left", Color: "D1D5DB", Style: 1}, - {Type: "top", Color: "D1D5DB", Style: 1}, - {Type: "bottom", Color: "D1D5DB", Style: 1}, - {Type: "right", Color: "D1D5DB", Style: 1}, - }, + Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true}, + Border: border, }) if err != nil { return err } - - if err := file.SetCellStyle(sheet, "A2", "G"+strconv.Itoa(lastRow), dataStyle); err != nil { + if err := file.SetCellStyle(sheet, "A2", "P"+lastRowStr, dataStyle); err != nil { return err } - moneyStyle, err := file.NewStyle(&excelize.Style{ - Alignment: &excelize.Alignment{ - Horizontal: "right", - Vertical: "center", - }, - Border: []excelize.Border{ - {Type: "left", Color: "D1D5DB", Style: 1}, - {Type: "top", Color: "D1D5DB", Style: 1}, - {Type: "bottom", Color: "D1D5DB", Style: 1}, - {Type: "right", Color: "D1D5DB", Style: 1}, - }, + numberStyle, err := file.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{Horizontal: "right", Vertical: "center"}, + Border: border, }) if err != nil { return err } + if err := file.SetCellStyle(sheet, "K2", "O"+lastRowStr, numberStyle); err != nil { + return err + } - return file.SetCellStyle(sheet, "E2", "E"+strconv.Itoa(lastRow), moneyStyle) + centerStyle, err := file.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, + Border: border, + }) + if err != nil { + return err + } + for _, col := range []string{"G", "H", "J"} { + if err := file.SetCellStyle(sheet, col+"2", col+lastRowStr, centerStyle); err != nil { + return err + } + } + return nil } func formatMarketingExportDate(value time.Time) string { @@ -225,36 +327,6 @@ func formatMarketingExportStatus(item dto.MarketingListDTO) string { return safeMarketingExportText(item.LatestApproval.StepName) } -func formatMarketingProducts(items []dto.DeliveryMarketingProductDTO) string { - if len(items) == 0 { - return "-" - } - - seen := make(map[string]struct{}) - names := make([]string, 0, len(items)) - for _, item := range items { - if item.ProductWarehouse == nil || item.ProductWarehouse.Product == nil { - continue - } - - name := strings.TrimSpace(item.ProductWarehouse.Product.Name) - if name == "" { - continue - } - - if _, exists := seen[name]; exists { - continue - } - seen[name] = struct{}{} - names = append(names, name) - } - - if len(names) == 0 { - return "-" - } - - return strings.Join(names, ", ") -} func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 { total := 0.0 diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index 0e21d534..915a183b 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -29,6 +29,7 @@ type MarketingListDTO struct { SalesPerson userDTO.UserRelationDTO `json:"sales_person"` SoDocs string `json:"so_docs"` SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"` + DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"` CreatedUser userDTO.UserRelationDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -203,6 +204,7 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M SalesPerson: salesPerson, SoDocs: marketing.SoDocs, SalesOrder: salesOrderProducts, + DeliveryOrder: extractDeliveryGroupsFromProducts(marketing), CreatedUser: createdUser, CreatedAt: marketing.CreatedAt, UpdatedAt: marketing.UpdatedAt, @@ -376,6 +378,23 @@ func GenerateDeliveryOrderNumber(soNumber string, deliveryDate *time.Time, wareh return numberPrefix } +func extractDeliveryGroupsFromProducts(marketing *entity.Marketing) []DeliveryGroupDTO { + var dps []MarketingDeliveryProductDTO + for _, product := range marketing.Products { + if product.DeliveryProduct == nil || product.DeliveryProduct.DeliveryDate == nil { + continue + } + dp := ToMarketingDeliveryProductDTO(*product.DeliveryProduct) + if product.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(product.ProductWarehouse) + dp.ProductWarehouse = &mapped + } + dp.ConvertionUnit = product.ConvertionUnit + dps = append(dps, dp) + } + return groupDeliveryProducts(dps, marketing.SoNumber) +} + func collectDoNumbers(marketing *entity.Marketing) []string { if marketing == nil || len(marketing.Products) == 0 { return nil