package controller import ( "fmt" "math" "strconv" "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" ) const expenseExportSheetName = "Expenses" func isAllExpenseExcelExportRequest(c *fiber.Ctx) bool { return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") && strings.EqualFold(strings.TrimSpace(c.Query("type")), "all") } func exportExpenseListExcel(c *fiber.Ctx, items []dto.ExpenseListDTO) error { content, err := buildExpenseExportWorkbook(items) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") } filename := fmt.Sprintf("expenses_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 buildExpenseExportWorkbook(items []dto.ExpenseListDTO) ([]byte, error) { file := excelize.NewFile() defer file.Close() defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) if defaultSheet != expenseExportSheetName { if err := file.SetSheetName(defaultSheet, expenseExportSheetName); err != nil { return nil, err } } if err := setExpenseExportColumns(file, expenseExportSheetName); err != nil { return nil, err } if err := setExpenseExportHeaders(file, expenseExportSheetName); err != nil { return nil, err } if err := setExpenseExportRows(file, expenseExportSheetName, items); err != nil { return nil, err } if err := file.SetPanes(expenseExportSheetName, &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 setExpenseExportColumns(file *excelize.File, sheet string) error { columnWidths := map[string]float64{ "A": 8, "B": 16, "C": 20, "D": 18, "E": 18, "F": 16, "G": 24, "H": 22, "I": 16, "J": 24, } for col, width := range columnWidths { if err := file.SetColWidth(sheet, col, col, width); err != nil { return err } } return file.SetRowHeight(sheet, 1, 24) } func setExpenseExportHeaders(file *excelize.File, sheet string) error { headers := []string{ "No", "No. PO", "No. Referensi", "Tanggal Realisasi", "Tanggal Transaksi", "Kategori", "Produk", "Lokasi", "Grand Total", "Status", } 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", "J1", headerStyle) } func setExpenseExportRows(file *excelize.File, sheet string, items []dto.ExpenseListDTO) error { if len(items) == 0 { return nil } for i, item := range items { row := strconv.Itoa(i + 2) if err := file.SetCellValue(sheet, "A"+row, i+1); err != nil { return err } if err := file.SetCellValue(sheet, "B"+row, safeExpenseExportText(item.PoNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "C"+row, safeExpenseExportText(item.ReferenceNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "D"+row, formatExpenseExportDate(item.RealizationDate)); err != nil { return err } if err := file.SetCellValue(sheet, "E"+row, formatExpenseExportDate(&item.TransactionDate)); err != nil { return err } if err := file.SetCellValue(sheet, "F"+row, safeExpenseExportText(item.Category)); err != nil { return err } if err := file.SetCellValue(sheet, "G"+row, safeExpenseSupplierName(item.Supplier)); err != nil { return err } if err := file.SetCellValue(sheet, "H"+row, safeExpenseLocationName(item.Location)); err != nil { return err } if err := file.SetCellValue(sheet, "I"+row, safeExpenseExportNumber(item.GrandTotal)); err != nil { return err } if err := file.SetCellValue(sheet, "J"+row, formatExpenseExportStatus(item.LatestApproval)); err != nil { return err } } lastRow := len(items) + 1 dataStyle, err := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{ Horizontal: "left", Vertical: "center", WrapText: false, }, 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", "J"+strconv.Itoa(lastRow), dataStyle); err != nil { return err } ordinalStyle, err := file.NewStyle(&excelize.Style{ 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 } if err := file.SetCellStyle(sheet, "A2", "A"+strconv.Itoa(lastRow), ordinalStyle); err != nil { return err } numberFormat := "#,##0.##" numberStyle, err := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{ Horizontal: "right", Vertical: "center", }, CustomNumFmt: &numberFormat, 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, "I2", "I"+strconv.Itoa(lastRow), numberStyle) } func formatExpenseExportDate(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 formatExpenseExportStatus(latestApproval *approvalDTO.ApprovalRelationDTO) string { if latestApproval == nil { return "-" } if latestApproval.Action != nil && strings.EqualFold(strings.TrimSpace(*latestApproval.Action), string(entity.ApprovalActionRejected)) { return "Ditolak" } return safeExpenseExportText(latestApproval.StepName) } func safeExpenseSupplierName(value *supplierDTO.SupplierRelationDTO) string { if value == nil { return "-" } return safeExpenseExportText(value.Name) } func safeExpenseLocationName(value *locationDTO.LocationRelationDTO) string { if value == nil { return "-" } return safeExpenseExportText(value.Name) } func safeExpenseExportNumber(value float64) float64 { if math.IsNaN(value) || math.IsInf(value, 0) { return 0 } return value } func safeExpenseExportText(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "-" } return trimmed }