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 +}