Files
lti-api/internal/modules/purchases/controllers/purchase.export.go
T
2026-05-05 06:41:10 +07:00

375 lines
9.3 KiB
Go

package controller
import (
"fmt"
"math"
"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
}
}
grandTotals := buildPurchaseGrandTotalMap(purchases)
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, grandTotals); 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": 18,
"J": 18,
"K": 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",
"PO Number",
"Tanggal PO",
"Tanggal Terima",
"Supplier",
"Lokasi",
"Gudang",
"Product",
"Status",
"Grand Total",
"Notes",
}
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", "K1", headerStyle)
}
func setPurchaseExportRows(file *excelize.File, sheet string, purchases []entity.Purchase, grandTotals map[uint]float64) error {
if len(purchases) == 0 {
return nil
}
rowIdx := 2
for p := range purchases {
purchase := &purchases[p]
total := grandTotals[purchase.Id]
if len(purchase.Items) == 0 {
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, nil, total); err != nil {
return err
}
rowIdx++
continue
}
for it := range purchase.Items {
if err := writePurchaseExportRow(file, sheet, rowIdx, purchase, &purchase.Items[it], total); err != nil {
return err
}
rowIdx++
}
}
lastRow := 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", "K"+strconv.Itoa(lastRow), 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
}
return file.SetCellStyle(sheet, "J2", "J"+strconv.Itoa(lastRow), moneyStyle)
}
func writePurchaseExportRow(file *excelize.File, sheet string, rowIdx int, purchase *entity.Purchase, item *entity.PurchaseItem, grandTotal float64) error {
row := strconv.Itoa(rowIdx)
// Purchase-level columns (repeat across rows 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, "I"+row, formatPurchaseExportEntityStatus(purchase)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "J"+row, formatPurchaseRupiah(grandTotal)); err != nil {
return err
}
if err := file.SetCellValue(sheet, "K"+row, safePurchaseExportPointerText(purchase.Notes)); err != nil {
return err
}
// Item-level columns
if item == nil {
for _, col := range []string{"D", "F", "G", "H"} {
if err := file.SetCellValue(sheet, col+row, "-"); err != nil {
return err
}
}
return nil
}
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
}
return nil
}
func buildPurchaseGrandTotalMap(items []entity.Purchase) map[uint]float64 {
result := make(map[uint]float64, len(items))
for i := range items {
total := 0.0
for j := range items[i].Items {
total += items[i].Items[j].TotalPrice
}
result[items[i].Id] = total
}
return result
}
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 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
}
func formatPurchaseRupiah(value float64) string {
if math.IsNaN(value) || math.IsInf(value, 0) {
return "Rp 0"
}
rounded := int64(math.Round(value))
sign := ""
if rounded < 0 {
sign = "-"
rounded = -rounded
}
raw := strconv.FormatInt(rounded, 10)
if raw == "" {
raw = "0"
}
var grouped strings.Builder
rem := len(raw) % 3
if rem > 0 {
grouped.WriteString(raw[:rem])
if len(raw) > rem {
grouped.WriteString(".")
}
}
for i := rem; i < len(raw); i += 3 {
grouped.WriteString(raw[i : i+3])
if i+3 < len(raw) {
grouped.WriteString(".")
}
}
return "Rp " + sign + grouped.String()
}