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