package controller import ( "fmt" "sort" "strconv" "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" ) const expenseReportExportSheetName = "Expense Reports" var expenseReportTemplateSheetOrder = []string{ "UANG MAKAN", "UPAH", "EKSPEDISI ADE", "GALON", "GAS", "KEBUTUHAN", "EKSPEDISI LTI", "KONTRIBUSI", "PRODUKSI", "KOMPENSASI", "LAIN-LAIN", "PERBAIKAN", "LISTRIK", "PAJAK", "SOLAR", } var expenseReportSheetAliasMap = map[string]string{ "TRANSPORT 2": "EKSPEDISI ADE", "TRANSPORT": "EKSPEDISI LTI", "GAS BROODING": "GAS", } func isAllExpenseExcelExportRequest(c *fiber.Ctx) bool { return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") && strings.EqualFold(strings.TrimSpace(c.Query("type")), "all") } func exportExpenseReportListExcel(c *fiber.Ctx, items []dto.RepportExpenseListDTO) error { content, err := buildExpenseReportExportWorkbook(items) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") } filename := fmt.Sprintf("reports_expense_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 buildExpenseReportExportWorkbook(items []dto.RepportExpenseListDTO) ([]byte, error) { file := excelize.NewFile() defer file.Close() defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) groups := groupExpenseReportRowsBySheet(items) orderedSheetNames := orderExpenseReportSheetNames(groups) if len(orderedSheetNames) == 0 { if defaultSheet != expenseReportExportSheetName { if err := file.SetSheetName(defaultSheet, expenseReportExportSheetName); err != nil { return nil, err } } if err := writeExpenseReportSheet(file, expenseReportExportSheetName, []dto.RepportExpenseListDTO{}); err != nil { return nil, err } } else { for idx, sheetName := range orderedSheetNames { 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 := writeExpenseReportSheet(file, sheetName, groups[sheetName]); err != nil { return nil, err } } } buffer, err := file.WriteToBuffer() if err != nil { return nil, err } return buffer.Bytes(), nil } func groupExpenseReportRowsBySheet(items []dto.RepportExpenseListDTO) map[string][]dto.RepportExpenseListDTO { groups := make(map[string][]dto.RepportExpenseListDTO) for _, item := range items { product := resolveExpenseReportProduct(item) sheetName := resolveExpenseReportSheetName(product) groups[sheetName] = append(groups[sheetName], item) } return groups } func orderExpenseReportSheetNames(groups map[string][]dto.RepportExpenseListDTO) []string { if len(groups) == 0 { return nil } templateSet := make(map[string]struct{}, len(expenseReportTemplateSheetOrder)) ordered := make([]string, 0, len(groups)) for _, sheet := range expenseReportTemplateSheetOrder { templateSet[sheet] = struct{}{} if _, ok := groups[sheet]; ok { ordered = append(ordered, sheet) } } extras := make([]string, 0) for sheet := range groups { if _, ok := templateSet[sheet]; !ok { extras = append(extras, sheet) } } sort.Slice(extras, func(i, j int) bool { return strings.ToUpper(extras[i]) < strings.ToUpper(extras[j]) }) ordered = append(ordered, extras...) return ordered } func resolveExpenseReportSheetName(product string) string { normalizedProduct := strings.ToUpper(strings.TrimSpace(product)) if alias, exists := expenseReportSheetAliasMap[normalizedProduct]; exists { return alias } if normalizedProduct == "" { normalizedProduct = "-" } return sanitizeExpenseReportSheetName(normalizedProduct) } func sanitizeExpenseReportSheetName(name string) string { replacer := strings.NewReplacer( ":", " ", "\\", " ", "/", " ", "?", " ", "*", " ", "[", " ", "]", " ", ) sanitized := strings.TrimSpace(replacer.Replace(name)) if sanitized == "" { sanitized = "Sheet" } runes := []rune(sanitized) if len(runes) > 31 { sanitized = string(runes[:31]) } return sanitized } func writeExpenseReportSheet(file *excelize.File, sheet string, items []dto.RepportExpenseListDTO) error { if err := setExpenseReportTemplateColumns(file, sheet); err != nil { return err } if err := setExpenseReportTemplateHeaders(file, sheet); err != nil { return err } return setExpenseReportTemplateRows(file, sheet, items) } func setExpenseReportTemplateColumns(file *excelize.File, sheet string) error { columnWidths := map[string]float64{ "A": 5.83203125, "B": 20.83203125, "C": 20.83203125, "D": 15.83203125, "E": 15.83203125, "F": 15.83203125, "G": 30.83203125, "H": 20.83203125, "I": 15.83203125, "J": 15.83203125, "K": 15.83203125, "L": 20.83203125, "M": 15.83203125, "N": 20.83203125, } for col, width := range columnWidths { if err := file.SetColWidth(sheet, col, col, width); err != nil { return err } } return nil } func setExpenseReportTemplateHeaders(file *excelize.File, sheet string) error { headers := []string{ "No", "No. PO", "No. Referensi", "Tanggal Realisasi", "Tanggal Transaksi", "Kategori", "Produk", "Lokasi", "Kandang", "Qty Pengajuan", "Harga Pengajuan", "Total Pengajuan", "Qty Realisasi", "Harga Realisasi", "Total Realisasi", "Status Pencairan", } for i, header := range headers { columnName, err := excelize.ColumnNumberToName(i + 1) if err != nil { return err } if err := file.SetCellValue(sheet, columnName+"1", header); err != nil { return err } } return nil } func setExpenseReportTemplateRows(file *excelize.File, sheet string, items []dto.RepportExpenseListDTO) error { totalQtyPengajuan := 0.0 totalPengajuan := 0.0 totalQtyRealisasi := 0.0 totalRealisasi := 0.0 for idx, item := range items { row := idx + 2 rowString := strconv.Itoa(row) produk := resolveExpenseReportProduct(item) status := formatExpenseReportStatus(item) lokasi := resolveExpenseReportLocation(item) kandang := resolveExpenseReportKandang(item) if err := file.SetCellValue(sheet, "A"+rowString, idx+1); err != nil { return err } if err := file.SetCellValue(sheet, "B"+rowString, safeExpenseReportExportText(item.PoNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "C"+rowString, safeExpenseReportExportText(item.ReferenceNumber)); err != nil { return err } if err := file.SetCellValue(sheet, "D"+rowString, formatExpenseReportOptionalDate(item.RealizationDate)); err != nil { return err } if err := file.SetCellValue(sheet, "E"+rowString, formatExpenseReportDate(item.TransactionDate)); err != nil { return err } if err := file.SetCellValue(sheet, "F"+rowString, safeExpenseReportExportText(item.Category)); err != nil { return err } if err := file.SetCellValue(sheet, "G"+rowString, produk); err != nil { return err } if err := file.SetCellValue(sheet, "H"+rowString, lokasi); err != nil { return err } if err := file.SetCellValue(sheet, "I"+rowString, kandang); err != nil { return err } if err := file.SetCellValue(sheet, "J"+rowString, item.Pengajuan.Qty); err != nil { return err } if err := file.SetCellValue(sheet, "K"+rowString, item.Pengajuan.Price); err != nil { return err } if err := file.SetCellValue(sheet, "L"+rowString, item.TotalPengajuan); err != nil { return err } if err := file.SetCellValue(sheet, "M"+rowString, item.Realisasi.Qty); err != nil { return err } if err := file.SetCellValue(sheet, "N"+rowString, item.Realisasi.Price); err != nil { return err } if err := file.SetCellValue(sheet, "O"+rowString, item.TotalRealisasi); err != nil { return err } if err := file.SetCellValue(sheet, "P"+rowString, status); err != nil { return err } totalQtyPengajuan += item.Pengajuan.Qty totalPengajuan += item.TotalPengajuan totalQtyRealisasi += item.Realisasi.Qty totalRealisasi += item.TotalRealisasi } totalRow := strconv.Itoa(len(items) + 2) if err := file.SetCellValue(sheet, "A"+totalRow, "Total"); err != nil { return err } if err := file.SetCellValue(sheet, "J"+totalRow, totalQtyPengajuan); err != nil { return err } if err := file.SetCellValue(sheet, "K"+totalRow, 0); err != nil { return err } if err := file.SetCellValue(sheet, "L"+totalRow, totalPengajuan); err != nil { return err } if err := file.SetCellValue(sheet, "M"+totalRow, totalQtyRealisasi); err != nil { return err } if err := file.SetCellValue(sheet, "N"+totalRow, 0); err != nil { return err } if err := file.SetCellValue(sheet, "O"+totalRow, totalRealisasi); err != nil { return err } return nil } func resolveExpenseReportProduct(item dto.RepportExpenseListDTO) string { if item.Realisasi.Nonstock != nil { name := strings.TrimSpace(item.Realisasi.Nonstock.Name) if name != "" { return name } } if item.Pengajuan.Nonstock != nil { name := strings.TrimSpace(item.Pengajuan.Nonstock.Name) if name != "" { return name } } return "-" } func resolveExpenseReportLocation(item dto.RepportExpenseListDTO) string { if item.Kandang != nil && item.Kandang.Location != nil { name := strings.TrimSpace(item.Kandang.Location.Name) if name != "" { return name } } return "-" } func resolveExpenseReportKandang(item dto.RepportExpenseListDTO) string { if item.Kandang != nil { name := strings.TrimSpace(item.Kandang.Name) if name != "" { return name } } return "-" } func formatExpenseReportStatus(item dto.RepportExpenseListDTO) string { if item.LatestApproval == nil { return "-" } if item.LatestApproval.Action != nil && strings.EqualFold(strings.TrimSpace(*item.LatestApproval.Action), string(entity.ApprovalActionRejected)) { return "Ditolak" } stepName := strings.TrimSpace(item.LatestApproval.StepName) if stepName == "" { return "-" } return stepName } func formatExpenseReportDate(value time.Time) string { if value.IsZero() { return "-" } location, err := time.LoadLocation("Asia/Jakarta") if err == nil { value = value.In(location) } return value.Format("02 Jan 2006") } func formatExpenseReportOptionalDate(value *time.Time) string { if value == nil || value.IsZero() { return "-" } return formatExpenseReportDate(*value) } func safeExpenseReportExportText(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "-" } return trimmed }