package controller import ( "fmt" "strconv" "strings" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" "github.com/gofiber/fiber/v2" "github.com/xuri/excelize/v2" ) const marketingExportSheetName = "Marketings" func isAllExcelExportRequest(c *fiber.Ctx) bool { return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel") && strings.EqualFold(strings.TrimSpace(c.Query("type")), "all") } func exportMarketingListExcel(c *fiber.Ctx, items []dto.MarketingListDTO) error { content, err := buildMarketingExportWorkbook(items) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file") } filename := fmt.Sprintf("marketings_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 buildMarketingExportWorkbook(items []dto.MarketingListDTO) ([]byte, error) { file := excelize.NewFile() defer file.Close() defaultSheet := file.GetSheetName(file.GetActiveSheetIndex()) if defaultSheet != marketingExportSheetName { if err := file.SetSheetName(defaultSheet, marketingExportSheetName); err != nil { return nil, err } } if err := setMarketingExportColumns(file, marketingExportSheetName); err != nil { return nil, err } if err := setMarketingExportHeaders(file, marketingExportSheetName); err != nil { return nil, err } if err := setMarketingExportRows(file, marketingExportSheetName, items); err != nil { return nil, err } if err := file.SetPanes(marketingExportSheetName, &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 setMarketingExportColumns(file *excelize.File, sheet string) error { // A–Q = 17 columns // E = Sales (new), H = Gudang (new), Satuan (old I) removed columnWidths := map[string]float64{ "A": 16, // No. Order "B": 14, // Tanggal "C": 18, // Status "D": 20, // Customer "E": 20, // Sales (new) "F": 14, // Tipe "G": 40, // Nama Produk "H": 20, // Gudang (new) "I": 10, // Week "J": 12, // Jumlah "K": 12, // Qty Peti "L": 16, // Berat Rata-rata (kg) "M": 16, // Total Berat (kg) "N": 18, // Harga Satuan "O": 18, // Total Harga "P": 18, // Grand Total "Q": 24, // Catatan } 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 setMarketingExportHeaders(file *excelize.File, sheet string) error { headers := []string{ "No. Order", // A "Tanggal", // B "Status", // C "Customer", // D "Sales", // E (new) "Tipe", // F "Nama Produk", // G "Gudang", // H (new) "Week", // I "Jumlah Butir", // J "Qty Peti", // K "Berat Rata-rata (kg)", // L "Total Berat (kg)", // M "Harga Satuan", // N "Total Harga", // O "Grand Total", // P "Catatan", // Q } for i, header := range headers { colName, err := excelize.ColumnNumberToName(i + 1) if err != nil { return err } cell := colName + "1" if err := file.SetCellValue(sheet, cell, 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", "Q1", headerStyle) } func setMarketingExportRows(file *excelize.File, sheet string, items []dto.MarketingListDTO) error { if len(items) == 0 { return nil } row := 1 for _, item := range items { soNumber := safeMarketingExportText(item.SoNumber) soDate := formatMarketingExportDate(item.SoDate) status := formatMarketingExportStatus(item) customer := safeMarketingExportText(item.Customer.Name) notes := safeMarketingExportText(item.Notes) salesPerson := safeMarketingExportText(item.SalesPerson.Name) isDeliveryOrder := strings.EqualFold(strings.TrimSpace(status), "delivery order") // ── Delivery Order branch ────────────────────────────────────────────── if isDeliveryOrder { grandTotal := sumDeliveryGrandTotal(item.DeliveryOrder) if len(item.DeliveryOrder) == 0 { row++ r := strconv.Itoa(row) vals := map[string]interface{}{ "A": soNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson, "F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "K": "-", "L": "-", "M": "-", "N": "-", "O": "-", "P": grandTotal, "Q": notes, } for col, val := range vals { if err := file.SetCellValue(sheet, col+r, val); err != nil { return err } } continue } // Build lookup map: MarketingProductId → SO product (for Week & MarketingType) soProductMap := make(map[uint]*dto.DeliveryMarketingProductDTO, len(item.SalesOrder)) for i := range item.SalesOrder { soProductMap[item.SalesOrder[i].Id] = &item.SalesOrder[i] } for _, group := range item.DeliveryOrder { doNumber := safeMarketingExportText(group.DoNumber) gudang := "-" if group.Warehouse != nil { gudang = safeMarketingExportText(group.Warehouse.Name) } if len(group.Deliveries) == 0 { row++ r := strconv.Itoa(row) vals := map[string]interface{}{ "A": doNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson, "F": "-", "G": "-", "H": gudang, "I": "-", "J": "-", "K": "-", "L": "-", "M": "-", "N": "-", "O": "-", "P": grandTotal, "Q": notes, } for col, val := range vals { if err := file.SetCellValue(sheet, col+r, val); err != nil { return err } } continue } for _, delivery := range group.Deliveries { row++ r := strconv.Itoa(row) productName := "-" if delivery.ProductWarehouse != nil && delivery.ProductWarehouse.Product != nil { if n := strings.TrimSpace(delivery.ProductWarehouse.Product.Name); n != "" { productName = n } } week := "-" marketingType := "-" if soProduct, ok := soProductMap[delivery.MarketingProductId]; ok { if soProduct.Week != nil { week = strconv.Itoa(*soProduct.Week) } marketingType = safeMarketingExportText(soProduct.MarketingType) } if err := file.SetCellValue(sheet, "A"+r, doNumber); err != nil { return err } if err := file.SetCellValue(sheet, "B"+r, soDate); err != nil { return err } if err := file.SetCellValue(sheet, "C"+r, status); err != nil { return err } if err := file.SetCellValue(sheet, "D"+r, customer); err != nil { return err } if err := file.SetCellValue(sheet, "E"+r, salesPerson); err != nil { return err } if err := file.SetCellValue(sheet, "F"+r, marketingType); err != nil { return err } if err := file.SetCellValue(sheet, "G"+r, productName); err != nil { return err } if err := file.SetCellValue(sheet, "H"+r, gudang); err != nil { return err } if err := file.SetCellValue(sheet, "I"+r, week); err != nil { return err } if err := file.SetCellValue(sheet, "J"+r, delivery.Qty); err != nil { return err } if delivery.TotalPeti != nil { if err := file.SetCellValue(sheet, "K"+r, *delivery.TotalPeti); err != nil { return err } } else { if err := file.SetCellValue(sheet, "K"+r, "-"); err != nil { return err } } if err := file.SetCellValue(sheet, "L"+r, delivery.AvgWeight); err != nil { return err } if err := file.SetCellValue(sheet, "M"+r, delivery.TotalWeight); err != nil { return err } if err := file.SetCellValue(sheet, "N"+r, delivery.UnitPrice); err != nil { return err } if err := file.SetCellValue(sheet, "O"+r, delivery.TotalPrice); err != nil { return err } if err := file.SetCellValue(sheet, "P"+r, grandTotal); err != nil { return err } if err := file.SetCellValue(sheet, "Q"+r, notes); err != nil { return err } } } continue } // ── Sales Order branch (all other statuses) ─────────────────────────── grandTotal := sumMarketingGrandTotal(item.SalesOrder) if len(item.SalesOrder) == 0 { row++ r := strconv.Itoa(row) vals := map[string]interface{}{ "A": soNumber, "B": soDate, "C": status, "D": customer, "E": salesPerson, "F": "-", "G": "-", "H": "-", "I": "-", "J": "-", "K": "-", "L": "-", "M": "-", "N": "-", "O": "-", "P": grandTotal, "Q": notes, } for col, val := range vals { if err := file.SetCellValue(sheet, col+r, val); err != nil { return err } } continue } for _, prod := range item.SalesOrder { row++ r := strconv.Itoa(row) productName := "-" if prod.ProductWarehouse != nil && prod.ProductWarehouse.Product != nil { if n := strings.TrimSpace(prod.ProductWarehouse.Product.Name); n != "" { productName = n } } week := "-" if prod.Week != nil { week = strconv.Itoa(*prod.Week) } gudang := "-" if prod.ProductWarehouse != nil && prod.ProductWarehouse.Warehouse != nil { gudang = safeMarketingExportText(prod.ProductWarehouse.Warehouse.Name) } if err := file.SetCellValue(sheet, "A"+r, soNumber); err != nil { return err } if err := file.SetCellValue(sheet, "B"+r, soDate); err != nil { return err } if err := file.SetCellValue(sheet, "C"+r, status); err != nil { return err } if err := file.SetCellValue(sheet, "D"+r, customer); err != nil { return err } if err := file.SetCellValue(sheet, "E"+r, salesPerson); err != nil { return err } if err := file.SetCellValue(sheet, "F"+r, safeMarketingExportText(prod.MarketingType)); err != nil { return err } if err := file.SetCellValue(sheet, "G"+r, productName); err != nil { return err } if err := file.SetCellValue(sheet, "H"+r, gudang); err != nil { return err } if err := file.SetCellValue(sheet, "I"+r, week); err != nil { return err } if err := file.SetCellValue(sheet, "J"+r, prod.Qty); err != nil { return err } if prod.TotalPeti != nil { if err := file.SetCellValue(sheet, "K"+r, *prod.TotalPeti); err != nil { return err } } else { if err := file.SetCellValue(sheet, "K"+r, "-"); err != nil { return err } } if err := file.SetCellValue(sheet, "L"+r, prod.AvgWeight); err != nil { return err } if err := file.SetCellValue(sheet, "M"+r, prod.TotalWeight); err != nil { return err } if err := file.SetCellValue(sheet, "N"+r, prod.UnitPrice); err != nil { return err } if err := file.SetCellValue(sheet, "O"+r, prod.TotalPrice); err != nil { return err } if err := file.SetCellValue(sheet, "P"+r, grandTotal); err != nil { return err } if err := file.SetCellValue(sheet, "Q"+r, notes); err != nil { return err } } } lastRow := row lastRowStr := strconv.Itoa(lastRow) 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}, } dataStyle, err := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true}, Border: border, }) if err != nil { return err } if err := file.SetCellStyle(sheet, "A2", "Q"+lastRowStr, dataStyle); err != nil { return err } numberStyle, err := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{Horizontal: "right", Vertical: "center"}, Border: border, }) if err != nil { return err } if err := file.SetCellStyle(sheet, "L2", "P"+lastRowStr, numberStyle); err != nil { return err } centerStyle, err := file.NewStyle(&excelize.Style{ Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"}, Border: border, }) if err != nil { return err } for _, col := range []string{"I", "J", "K"} { if err := file.SetCellStyle(sheet, col+"2", col+lastRowStr, centerStyle); err != nil { return err } } return nil } func formatMarketingExportDate(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-01-2006") } func formatMarketingExportStatus(item dto.MarketingListDTO) string { if item.LatestApproval.Action != nil && strings.EqualFold(strings.TrimSpace(*item.LatestApproval.Action), string(entity.ApprovalActionRejected)) { return "Ditolak" } return safeMarketingExportText(item.LatestApproval.StepName) } func sumMarketingGrandTotal(items []dto.DeliveryMarketingProductDTO) float64 { total := 0.0 for _, item := range items { total += item.TotalPrice } return total } func sumDeliveryGrandTotal(groups []dto.DeliveryGroupDTO) float64 { total := 0.0 for _, g := range groups { for _, d := range g.Deliveries { total += d.TotalPrice } } return total } func safeMarketingExportText(value string) string { trimmed := strings.TrimSpace(value) if trimmed == "" { return "-" } return trimmed }