package controller import ( "fmt" "strconv" "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" ) const purchaseExportSheetName = "Purchases" func isAllPurchaseExcelExportRequest(c *fiber.Ctx) bool { return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") && strings.EqualFold(strings.TrimSpace(c.Query("type")), "all") } func exportPurchaseListExcel(c *fiber.Ctx, purchases []entity.Purchase) error { content, err := buildPurchaseExportWorkbook(purchases) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") } filename := fmt.Sprintf("purchases_all_%s.xlsx", time.Now().Format("20060102_150405")) 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 buildPurchaseExportWorkbook(purchases []entity.Purchase) ([]byte, error) { file := excelize.NewFile() defer file.Close() defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) if defaultSheet != purchaseExportSheetName { if err := file.SetSheetName(defaultSheet, purchaseExportSheetName); err != nil { return nil, err } } if err := setPurchaseExportColumns(file, purchaseExportSheetName); err != nil { return nil, err } if err := setPurchaseExportHeaders(file, purchaseExportSheetName); err != nil { return nil, err } if err := setPurchaseExportRows(file, purchaseExportSheetName, purchases); err != nil { return nil, err } if err := file.SetPanes(purchaseExportSheetName, &excelize.Panes{ Freeze: true, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft", }); err != nil { return nil, err } buffer, err := file.WriteToBuffer() if err != nil { return nil, err } return buffer.Bytes(), nil } func setPurchaseExportColumns(file *excelize.File, sheet string) error { columnWidths := map[string]float64{ "A": 16, "B": 16, "C": 14, "D": 14, "E": 22, "F": 22, "G": 22, "H": 32, "I": 10, "J": 12, "K": 16, "L": 16, "M": 22, "N": 12, "O": 16, "P": 16, "Q": 18, "R": 18, "S": 24, } for col, width := range columnWidths { 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 setPurchaseExportHeaders(file *excelize.File, sheet string) error { headers := []string{ "PR Number", // A "PO Number", // B "Tanggal PO", // C "Tanggal Terima", // D "Supplier", // E "Lokasi", // F "Gudang", // G "Product", // H "Qty", // I "Satuan", // J "Price", // K "Total Produk", // L "Vendor Ekspedisi",// M "Qty Ekspedisi", // N "Price Ekspedisi", // O "Total Ekspedisi", // P "Grand Total All", // Q "Status", // R "Notes", // S } for i, header := range headers { colName, err := excelize.ColumnNumberToName(i + 1) if err != nil { return err } if err := file.SetCellValue(sheet, colName+"1", header); err != nil { return err } } headerStyle, err := file.NewStyle(&excelize.Style{ Font: &excelize.Font{Bold: true, Color: "1F2937"}, Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"DCEBFA"}}, Alignment: &excelize.Alignment{Horizontal: "center", 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}, }, }) if err != nil { return err } return file.SetCellStyle(sheet, "A1", "S1", headerStyle) } func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase) error { if len(purchases) == 0 { return nil } var sumL, sumP, sumQ float64 rowIdx := 2 for p := range purchases { purchase := &purchases[p] if len(purchase.Items) == 0 { if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, &sumL, &sumP, &sumQ); err != nil { return err } rowIdx++ continue } for it := range purchase.Items { if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], &sumL, &sumP, &sumQ); err != nil { return err } rowIdx++ } } lastDataRow := rowIdx - 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}, }, }) if err != nil { return err } if err := file.SetCellStyle(sheet, "A2", "S"+strconv.Itoa(lastDataRow), 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}, }, }) if err != nil { return err } if err := file.SetCellStyle(sheet, "K2", "Q"+strconv.Itoa(lastDataRow), moneyStyle); err != nil { return err } return addPurchaseExportSumRow(file, sheet, rowIdx, sumL, sumP, sumQ) } func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, sumL, sumP, sumQ *float64) error { row := strconv.Itoa(rowIdx) // Purchase-level columns (repeat for every item row of the same purchase) if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(purchase.PrNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "B"+row, safePurchaseExportPointerText(purchase.PoNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(purchase.PoDate)); err != nil { return err } if err := file.SetCellValue(sheet, "E"+row, safePurchaseExportEntitySupplierName(purchase)); err != nil { return err } if err := file.SetCellValue(sheet, "R"+row, formatPurchaseExportEntityStatus(purchase)); err != nil { return err } if err := file.SetCellValue(sheet, "S"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil { return err } if item == nil { for _, col := range []string{"D", "F", "G", "H", "J", "M"} { if err := file.SetCellValue(sheet, col+row, "-"); err != nil { return err } } for _, col := range []string{"I", "K", "L", "N", "O", "P", "Q"} { if err := file.SetCellValue(sheet, col+row, 0); err != nil { return err } } return nil } // Item-level columns var expeditionQty, expeditionPrice, expeditionTotal float64 if item.ExpenseNonstock != nil { expeditionQty = item.ExpenseNonstock.Qty expeditionPrice = item.ExpenseNonstock.Price expeditionTotal = expeditionQty * expeditionPrice } itemGrandTotal := item.TotalPrice + expeditionTotal *sumL += item.TotalPrice *sumP += expeditionTotal *sumQ += itemGrandTotal if err := file.SetCellValue(sheet, "D"+row, formatPurchaseExportDate(item.ReceivedDate)); err != nil { return err } if err := file.SetCellValue(sheet, "F"+row, safePurchaseItemLocationName(item)); err != nil { return err } if err := file.SetCellValue(sheet, "G"+row, safePurchaseWarehouseName(item)); err != nil { return err } if err := file.SetCellValue(sheet, "H"+row, safePurchaseItemProductName(item)); err != nil { return err } if err := file.SetCellValue(sheet, "I"+row, item.TotalQty); err != nil { return err } if err := file.SetCellValue(sheet, "J"+row, safePurchaseItemUomName(item)); err != nil { return err } if err := file.SetCellValue(sheet, "K"+row, item.Price); err != nil { return err } if err := file.SetCellValue(sheet, "L"+row, item.TotalPrice); err != nil { return err } if err := file.SetCellValue(sheet, "M"+row, safePurchaseItemExpeditionVendorName(item)); err != nil { return err } if err := file.SetCellValue(sheet, "N"+row, expeditionQty); err != nil { return err } if err := file.SetCellValue(sheet, "O"+row, expeditionPrice); err != nil { return err } if err := file.SetCellValue(sheet, "P"+row, expeditionTotal); err != nil { return err } if err := file.SetCellValue(sheet, "Q"+row, itemGrandTotal); err != nil { return err } return nil } func addPurchaseExportSumRow(file *excelize.File, sheet string, rowIdx int, sumL, sumP, sumQ float64) error { row := strconv.Itoa(rowIdx) sumStyle, err := file.NewStyle(&excelize.Style{ Font: &excelize.Font{Bold: true, Color: "1F2937"}, Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FEF3C7"}}, Alignment: &excelize.Alignment{ Horizontal: "left", Vertical: "center", }, Border: []excelize.Border{ {Type: "left", Color: "D1D5DB", Style: 1}, {Type: "top", Color: "D1D5DB", Style: 2}, {Type: "bottom", Color: "D1D5DB", Style: 1}, {Type: "right", Color: "D1D5DB", Style: 1}, }, }) if err != nil { return err } sumMoneyStyle, err := file.NewStyle(&excelize.Style{ Font: &excelize.Font{Bold: true, Color: "1F2937"}, Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"FEF3C7"}}, Alignment: &excelize.Alignment{ Horizontal: "right", Vertical: "center", }, Border: []excelize.Border{ {Type: "left", Color: "D1D5DB", Style: 1}, {Type: "top", Color: "D1D5DB", Style: 2}, {Type: "bottom", Color: "D1D5DB", Style: 1}, {Type: "right", Color: "D1D5DB", Style: 1}, }, }) if err != nil { return err } if err := file.SetCellStyle(sheet, "A"+row, "S"+row, sumStyle); err != nil { return err } if err := file.SetCellStyle(sheet, "L"+row, "L"+row, sumMoneyStyle); err != nil { return err } if err := file.SetCellStyle(sheet, "P"+row, "Q"+row, sumMoneyStyle); err != nil { return err } if err := file.SetCellValue(sheet, "A"+row, "TOTAL"); err != nil { return err } if err := file.SetCellValue(sheet, "L"+row, sumL); err != nil { return err } if err := file.SetCellValue(sheet, "P"+row, sumP); err != nil { return err } return file.SetCellValue(sheet, "Q"+row, sumQ) } func safePurchaseExportEntitySupplierName(purchase *entity.Purchase) string { if purchase.Supplier.Id == 0 { return "-" } return safePurchaseExportText(purchase.Supplier.Name) } func safePurchaseWarehouseName(item *entity.PurchaseItem) string { if item.Warehouse == nil { return "-" } return safePurchaseExportText(item.Warehouse.Name) } func safePurchaseItemLocationName(item *entity.PurchaseItem) string { if item.Warehouse == nil || item.Warehouse.Location == nil { return "-" } return safePurchaseExportText(item.Warehouse.Location.Name) } func safePurchaseItemProductName(item *entity.PurchaseItem) string { if item.Product == nil { return "-" } return safePurchaseExportText(item.Product.Name) } func safePurchaseItemUomName(item *entity.PurchaseItem) string { if item.Product == nil || item.Product.Uom.Id == 0 { return "-" } return safePurchaseExportText(item.Product.Uom.Name) } func safePurchaseItemExpeditionVendorName(item *entity.PurchaseItem) string { if item.ExpenseNonstock == nil || item.ExpenseNonstock.Expense == nil { return "-" } exp := item.ExpenseNonstock.Expense if exp.Supplier == nil || exp.Supplier.Id == 0 { return "-" } return safePurchaseExportText(exp.Supplier.Name) } func formatPurchaseExportEntityStatus(purchase *entity.Purchase) string { if purchase.LatestApproval == nil { return "-" } if purchase.LatestApproval.Action != nil && strings.EqualFold(strings.TrimSpace(string(*purchase.LatestApproval.Action)), string(entity.ApprovalActionRejected)) { return "Ditolak" } return safePurchaseExportText(purchase.LatestApproval.StepName) } func formatPurchaseExportDate(value *time.Time) string { if value == nil || value.IsZero() { return "-" } t := *value location, err := time.LoadLocation("Asia/Jakarta") if err == nil { t = t.In(location) } return t.Format("02-01-2006") } func safePurchaseExportPointerText(value *string) string { if value == nil { return "-" } return safePurchaseExportText(*value) } func safePurchaseExportText(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "-" } return trimmed }