package controller import ( "fmt" "math" "sort" "strconv" "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto" "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 } } listItems := dto.ToPurchaseListDTOs(purchases) 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, listItems, 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": 22, "E": 18, "F": 18, "G": 52, "H": 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", "Supplier", "Status", "Grand Total", "Products", "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", "H1", headerStyle) } func setPurchaseExportRows(file *excelize.File, sheet string, items []dto.PurchaseListDTO, grandTotals map[uint]float64) error { if len(items) == 0 { return nil } for i, item := range items { row := strconv.Itoa(i + 2) if err := file.SetCellValue(sheet, "A"+row, safePurchaseExportText(item.PrNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "B"+row, safePurchaseExportPointerText(item.PoNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "C"+row, formatPurchaseExportDate(item.PoDate)); err != nil { return err } if err := file.SetCellValue(sheet, "D"+row, safePurchaseSupplierName(item)); err != nil { return err } if err := file.SetCellValue(sheet, "E"+row, formatPurchaseExportStatus(item)); err != nil { return err } if err := file.SetCellValue(sheet, "F"+row, formatPurchaseRupiah(grandTotals[item.Id])); err != nil { return err } if err := file.SetCellValue(sheet, "G"+row, formatPurchaseProducts(item)); err != nil { return err } if err := file.SetCellValue(sheet, "H"+row, safePurchaseExportPointerText(item.Notes)); err != nil { return err } } lastRow := len(items) + 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", "H"+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, "F2", "F"+strconv.Itoa(lastRow), moneyStyle) } 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 safePurchaseSupplierName(item dto.PurchaseListDTO) string { if item.Supplier == nil { return "-" } return safePurchaseExportText(item.Supplier.Name) } func formatPurchaseExportStatus(item dto.PurchaseListDTO) string { if item.LatestApproval == nil { return "-" } if item.LatestApproval.Action != nil && strings.EqualFold(strings.TrimSpace(*item.LatestApproval.Action), string(entity.ApprovalActionRejected)) { return "Ditolak" } return safePurchaseExportText(item.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 formatPurchaseProducts(item dto.PurchaseListDTO) string { if len(item.Products) == 0 { return "-" } seen := make(map[string]struct{}) names := make([]string, 0, len(item.Products)) for i := range item.Products { name := strings.TrimSpace(item.Products[i].Name) if name == "" { continue } if _, exists := seen[name]; exists { continue } seen[name] = struct{}{} names = append(names, name) } if len(names) == 0 { return "-" } sort.Strings(names) return strings.Join(names, ", ") } 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() }