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 }