Files
lti-api/internal/modules/expenses/controllers/expense.export.go
T
2026-04-22 23:29:05 +07:00

296 lines
7.7 KiB
Go

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
}