mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-06-09 15:07:49 +00:00
981fb98248
In the Excel export, Delivery Order rows were writing `group.DeliveryDate`
(the actual delivery date) to column B ("Tanggal"), while the web UI always
shows `so_date` for every row. This caused a visible mismatch — e.g. DO-01954
displayed "31 Mei 2026" on the web but "01-06-2026" in the exported file.
Changes:
- Remove the `doDate` variable from the DO branch; both the empty-deliveries
fallback row and each per-delivery row now write `soDate` to column B,
consistent with what the web shows
- Fix a pre-existing nil pointer dereference: `prod.ProductWarehouse.Warehouse`
was accessed without a nil guard in the SO branch
- Update the export test to match the current 17-column layout (headers and
row assertions were stale), and add a regression case that explicitly
asserts a DO row with soDate=2026-05-31 / deliveryDate=2026-06-01 produces
"31-05-2026" in column B
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
501 lines
14 KiB
Go
501 lines
14 KiB
Go
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
|
||
}
|