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 isDebtSupplierExcelExportRequest(c *fiber.Ctx) bool { return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") } func isDebtSupplierExcelAllExportRequest(c *fiber.Ctx) bool { return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel-all") } func exportDebtSupplierExcel(c *fiber.Ctx, items []dto.DebtSupplierDTO) error { content, err := buildDebtSupplierWorkbook(items) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") } filename := fmt.Sprintf("laporan-hutang-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 exportDebtSupplierExcelAll(c *fiber.Ctx, items []dto.DebtSupplierDTO) error { content, err := buildDebtSupplierAllWorkbook(items) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") } filename := fmt.Sprintf("laporan-hutang-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) } // buildDebtSupplierWorkbook creates a workbook with one sheet per supplier. func buildDebtSupplierWorkbook(items []dto.DebtSupplierDTO) ([]byte, error) { file := excelize.NewFile() defer file.Close() defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) if len(items) == 0 { if err := writeDebtSupplierSheet(file, defaultSheet, dto.DebtSupplierDTO{}); 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 := sanitizeDebtSupplierSheetName(debtSupplierName(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 := writeDebtSupplierSheet(file, sheetName, item); err != nil { return nil, err } } buf, err := file.WriteToBuffer() if err != nil { return nil, err } return buf.Bytes(), nil } // buildDebtSupplierAllWorkbook creates a single-sheet workbook with purchase-supplier styling. func buildDebtSupplierAllWorkbook(items []dto.DebtSupplierDTO) ([]byte, error) { file := excelize.NewFile() defer file.Close() const sheet = "Rekap Hutang Supplier" defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) if defaultSheet != sheet { if err := file.SetSheetName(defaultSheet, sheet); err != nil { return nil, err } } if err := setDebtSupplierAllColumns(file, sheet); err != nil { return nil, err } if err := setDebtSupplierAllHeaders(file, sheet); err != nil { return nil, err } if err := writeDebtSupplierAllRows(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 debtSupplierSheetHeaders = []string{ "No", "Nomor PR", "Nomor PO", "Tanggal Terima/Bayar", "Tanggal PO", "Aging (Hari)", "Area", "Gudang", "Jatuh Tempo", "Status Jatuh Tempo", "Nominal Pembelian (Rp)", "Pembayaran (Rp)", "Sisa Saldo Hutang (Rp)", "Status", "Nomor Perjalanan", } var debtSupplierAllSheetHeaders = append([]string{"Supplier"}, debtSupplierSheetHeaders...) var debtSupplierSheetColumnWidths = map[string]float64{ "A": 5, "B": 14, "C": 12, "D": 20, "E": 10, "F": 12, "G": 15, "H": 20, "I": 12, "J": 20, "K": 20, "L": 15, "M": 20, "N": 12, "O": 15, } var debtSupplierAllSheetColumnWidths = map[string]float64{ "A": 24, "B": 6, "C": 14, "D": 14, "E": 20, "F": 12, "G": 10, "H": 16, "I": 22, "J": 12, "K": 22, "L": 20, "M": 18, "N": 22, "O": 14, "P": 18, } func writeDebtSupplierSheet(file *excelize.File, sheet string, item dto.DebtSupplierDTO) error { for col, width := range debtSupplierSheetColumnWidths { if err := file.SetColWidth(sheet, col, col, width); err != nil { return err } } // Row 1: headers for i, h := range debtSupplierSheetHeaders { col, _ := excelize.ColumnNumberToName(i + 1) if err := file.SetCellValue(sheet, col+"1", h); err != nil { return err } } // Row 2: saldo awal if err := file.SetCellValue(sheet, "M2", item.InitialBalance); err != nil { return err } // Rows 3+: data redStyle, err := file.NewStyle(&excelize.Style{ Font: &excelize.Font{Color: "FF0000"}, }) if err != nil { return err } for i, row := range item.Rows { rowNum := i + 3 rowStr := fmt.Sprintf("%d", rowNum) values := debtSupplierRowCells(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 } } if row.DebtPrice < 0 { if err := file.SetCellStyle(sheet, "M"+rowStr, "M"+rowStr, redStyle); err != nil { return err } } } // Total row totalRowNum := len(item.Rows) + 3 totalRowStr := fmt.Sprintf("%d", totalRowNum) totalCells := map[string]interface{}{ "A": "Total", "F": item.Total.Aging, "K": item.Total.TotalPrice, "L": item.Total.PaymentPrice, "M": item.Total.DebtPrice, } for col, val := range totalCells { if err := file.SetCellValue(sheet, col+totalRowStr, val); err != nil { return err } } if item.Total.DebtPrice < 0 { if err := file.SetCellStyle(sheet, "M"+totalRowStr, "M"+totalRowStr, redStyle); err != nil { return err } } return nil } func setDebtSupplierAllColumns(file *excelize.File, sheet string) error { for col, width := range debtSupplierAllSheetColumnWidths { 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 setDebtSupplierAllHeaders(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 debtSupplierAllSheetHeaders { col, _ := excelize.ColumnNumberToName(i + 1) if err := file.SetCellValue(sheet, col+"1", h); err != nil { return err } } lastCol, _ := excelize.ColumnNumberToName(len(debtSupplierAllSheetHeaders)) return file.SetCellStyle(sheet, "A1", lastCol+"1", headerStyle) } func writeDebtSupplierAllRows(file *excelize.File, sheet string, items []dto.DebtSupplierDTO) 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(debtSupplierAllSheetHeaders)) currentRow := 2 for _, item := range items { supplierName := debtSupplierName(item) // Saldo awal row saldoRowStr := fmt.Sprintf("%d", currentRow) if err := file.SetCellValue(sheet, "A"+saldoRowStr, supplierName); err != nil { return err } if err := file.SetCellValue(sheet, "N"+saldoRowStr, item.InitialBalance); err != nil { return err } if err := file.SetCellStyle(sheet, "A"+saldoRowStr, lastHeaderCol+saldoRowStr, dataStyle); err != nil { return err } currentRow++ // 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 := debtSupplierRowCells(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++ } // Total row totalRowStr := fmt.Sprintf("%d", currentRow) totalCells := map[string]interface{}{ "A": supplierName, "B": "Total", "L": item.Total.TotalPrice, "M": item.Total.PaymentPrice, "N": item.Total.DebtPrice, } 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 } // debtSupplierRowCells returns cell values for one data row (columns: No, PR, PO, ReceivedDate, PoDate, Aging, Area, Warehouse, DueDate, DueStatus, TotalPrice, PaymentPrice, DebtPrice, Status, TravelNumber). func debtSupplierRowCells(row dto.DebtSupplierRowDTO, seq int) []interface{} { areaName := "-" if row.Area != nil && strings.TrimSpace(row.Area.Name) != "" { areaName = row.Area.Name } warehouseName := "-" if row.Warehouse != nil && strings.TrimSpace(row.Warehouse.Name) != "" { warehouseName = row.Warehouse.Name } return []interface{}{ seq, safeDebtSupplierText(row.PrNumber), safeDebtSupplierText(row.PoNumber), safeDebtSupplierText(row.ReceivedDate), safeDebtSupplierText(row.PoDate), row.Aging, areaName, warehouseName, safeDebtSupplierText(row.DueDate), safeDebtSupplierText(row.DueStatus), row.TotalPrice, row.PaymentPrice, row.DebtPrice, safeDebtSupplierText(row.Status), safeDebtSupplierText(row.TravelNumber), } } func debtSupplierName(item dto.DebtSupplierDTO) string { if item.Supplier != nil && strings.TrimSpace(item.Supplier.Name) != "" { return item.Supplier.Name } return "Supplier" } func sanitizeDebtSupplierSheetName(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 safeDebtSupplierText(s string) string { t := strings.TrimSpace(s) if t == "" { return "-" } return t }