diff --git a/internal/modules/repports/controllers/repport.marketing.export.go b/internal/modules/repports/controllers/repport.marketing.export.go index 866590c4..ab11a921 100644 --- a/internal/modules/repports/controllers/repport.marketing.export.go +++ b/internal/modules/repports/controllers/repport.marketing.export.go @@ -50,6 +50,14 @@ func buildMarketingReportWorkbook(items []dto.RepportMarketingItemDTO) ([]byte, if err := setMarketingReportRows(file, items); err != nil { return nil, err } + if err := file.SetPanes(marketingReportExportSheetName, &excelize.Panes{ + Freeze: true, + YSplit: 1, + TopLeftCell: "A2", + ActivePane: "bottomLeft", + }); err != nil { + return nil, err + } buffer, err := file.WriteToBuffer() if err != nil { @@ -88,6 +96,10 @@ func setMarketingReportColumns(file *excelize.File) error { } } + if err := file.SetRowHeight(sheet, 1, 24); err != nil { + return err + } + return nil } @@ -110,7 +122,6 @@ func setMarketingReportHeaders(file *excelize.File) error { "Bobot Total (Kg)", "Harga Jual (Rp)", "HPP (Rp)", - "HPP Amount (Rp)", "Total (Rp)", } @@ -124,7 +135,22 @@ func setMarketingReportHeaders(file *excelize.File) error { } } - return nil + 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 setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingItemDTO) error { @@ -173,7 +199,6 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte item.TotalWeightKg, formatMarketingRupiah(item.SalesPricePerKg), formatMarketingRupiah(item.HppPricePerKg), - formatMarketingRupiah(item.HppAmount), formatMarketingRupiah(item.SalesAmount), } @@ -210,15 +235,81 @@ func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingIte if err := file.SetCellValue(sheet, "P"+totalRow, formatMarketingRupiah(summary.TotalHppPricePerKg)); err != nil { return err } - if err := file.SetCellValue(sheet, "Q"+totalRow, formatMarketingRupiah(float64(summary.TotalHppAmount))); err != nil { - return err - } - if err := file.SetCellValue(sheet, "R"+totalRow, formatMarketingRupiah(float64(summary.TotalSalesAmount))); err != nil { + if err := file.SetCellValue(sheet, "Q"+totalRow, formatMarketingRupiah(float64(summary.TotalSalesAmount))); err != nil { return err } } - return nil + if len(items) > 0 { + lastDataRow := strconv.Itoa(len(items) + 1) + + dataStyle, err := file.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{Horizontal: "left", Vertical: "center", WrapText: true}, + 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", "Q"+lastDataRow, dataStyle); err != nil { + return err + } + + numericStyle, err := file.NewStyle(&excelize.Style{ + Alignment: &excelize.Alignment{Horizontal: "right", 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, "L2", "Q"+lastDataRow, numericStyle); err != nil { + return err + } + } + + totalTextStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "1F2937"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"F3F4F6"}}, + Alignment: &excelize.Alignment{Horizontal: "left", 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, "A"+totalRow, "Q"+totalRow, totalTextStyle); err != nil { + return err + } + + totalNumericStyle, err := file.NewStyle(&excelize.Style{ + Font: &excelize.Font{Bold: true, Color: "1F2937"}, + Fill: excelize.Fill{Type: "pattern", Pattern: 1, Color: []string{"F3F4F6"}}, + Alignment: &excelize.Alignment{Horizontal: "right", 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, "L"+totalRow, "Q"+totalRow, totalNumericStyle) } func formatMarketingDate(t time.Time) string { diff --git a/internal/modules/repports/controllers/repport.marketing.pdf.go b/internal/modules/repports/controllers/repport.marketing.pdf.go index f5375a30..6e303891 100644 --- a/internal/modules/repports/controllers/repport.marketing.pdf.go +++ b/internal/modules/repports/controllers/repport.marketing.pdf.go @@ -56,22 +56,21 @@ type pdfColumn struct { var marketingPdfColumns = []pdfColumn{ {"No", 6, "C"}, {"Tanggal\nJual", 16, "C"}, - {"Tanggal\nRealisasi", 16, "C"}, + {"Tanggal\nRealisasi", 20, "C"}, {"Aging\n(Hari)", 9, "C"}, {"Gudang\nFisik", 20, "L"}, {"Pelanggan", 20, "L"}, {"No. DO", 18, "L"}, {"Sales", 18, "L"}, {"No. Polisi", 18, "L"}, - {"Tipe\nMarketing", 14, "C"}, + {"Tipe\nMarketing", 16, "C"}, {"Produk", 16, "L"}, {"Kuantitas", 13, "R"}, - {"Bobot Rata-Rata\n(Kg)", 13, "R"}, - {"Bobot Total Berat\n(Kg)", 14, "R"}, + {"Bobot\nRata-Rata (Kg)", 18, "R"}, + {"Bobot\nTotal Berat (Kg)", 18, "R"}, {"Harga Jual\n(Rp)", 17, "R"}, {"HPP\n(Rp)", 17, "R"}, - {"Total Jual\n(Rp)", 18, "R"}, - {"Total HPP\n(Rp)", 18, "R"}, + {"Total (Rp)", 18, "R"}, } // --------------------------------------------------------------------------- @@ -214,7 +213,7 @@ func calcMarketingRowHeight(pdf *fpdf.Fpdf, values []string, lineH float64) floa cols := marketingPdfColumns maxLines := 1 for i, col := range cols { - if i >= len(values) || i == 10 { + if i >= len(values) || i == 9 { continue } usableW := col.width - 2*margin @@ -238,7 +237,7 @@ func writeMarketingPdfRow(pdf *fpdf.Fpdf, item dto.RepportMarketingItemDTO, valu x := 10.0 for i, col := range cols { - if i == 10 { + if i == 9 { drawMarketingTypeBadge(pdf, x, y, col.width, rowH, item.MarketingType) pdf.SetDrawColor(borderR, borderG, borderB) pdf.SetTextColor(40, 40, 40) @@ -283,8 +282,8 @@ func marketingPdfRowValues(no int, item dto.RepportMarketingItemDTO) []string { customer, safeMarketingExportText(item.DoNumber), sales, - safeMarketingExportText(item.VehicleNumber), - safeMarketingExportText(item.MarketingType), // index 10, overridden by badge + safeMarketingExportText(formatMarketingVehicleNumber(item.VehicleNumber)), + safeMarketingExportText(item.MarketingType), // index 9, overridden by badge product, formatMarketingPdfNumber(item.Qty), formatMarketingPdfDecimal(item.AverageWeightKg), @@ -292,7 +291,6 @@ func marketingPdfRowValues(no int, item dto.RepportMarketingItemDTO) []string { formatMarketingPdfRupiah(item.SalesPricePerKg), formatMarketingPdfRupiah(item.HppPricePerKg), formatMarketingPdfRupiah(item.SalesAmount), - formatMarketingPdfRupiah(item.HppAmount), } } @@ -306,30 +304,9 @@ func writeMarketingPdfTotal(pdf *fpdf.Fpdf, items []dto.RepportMarketingItemDTO) return } - rowH := 6.5 - - if pdf.GetY()+rowH > marketingPdfPageHeight(pdf)-12 { - pdf.AddPage() - writeMarketingPdfHeader(pdf) - } - pdf.SetFont("Helvetica", "B", 6) - pdf.SetFillColor(220, 230, 245) - pdf.SetTextColor(30, 64, 120) - pdf.SetDrawColor(borderR, borderG, borderB) - pdf.SetLineWidth(0.1) - y := pdf.GetY() - x := 10.0 - - // merge first 11 cols (No … Tipe Marketing) into "TOTAL" label - mergedWidth := 0.0 - for i := 0; i < 11; i++ { - mergedWidth += marketingPdfColumns[i].width - } - pdf.SetXY(x, y) - pdf.CellFormat(mergedWidth, rowH, "TOTAL", "1", 0, "R", true, 0, "") - x += mergedWidth + lineH := 5.0 totals := []string{ formatMarketingPdfNumber(float64(summary.TotalQty)), @@ -338,13 +315,58 @@ func writeMarketingPdfTotal(pdf *fpdf.Fpdf, items []dto.RepportMarketingItemDTO) formatMarketingPdfRupiah(summary.AverageSalesPrice), formatMarketingPdfRupiah(summary.TotalHppPricePerKg), formatMarketingPdfRupiah(float64(summary.TotalSalesAmount)), - formatMarketingPdfRupiah(float64(summary.TotalHppAmount)), } + margin := pdf.GetCellMargin() + maxLines := 1 + for i, val := range totals { + col := marketingPdfColumns[11+i] + usableW := col.width - 2*margin + if usableW <= 0 { + continue + } + lines := pdf.SplitLines([]byte(val), usableW) + n := len(lines) + if n == 0 { + n = 1 + } + if n > maxLines { + maxLines = n + } + } + rowH := float64(maxLines) * lineH + + if pdf.GetY()+rowH > marketingPdfPageHeight(pdf)-12 { + pdf.AddPage() + writeMarketingPdfHeader(pdf) + pdf.SetFont("Helvetica", "B", 6) + } + + pdf.SetTextColor(30, 64, 120) + pdf.SetDrawColor(borderR, borderG, borderB) + pdf.SetLineWidth(0.1) + + y := pdf.GetY() + x := 10.0 + + const totalFillR, totalFillG, totalFillB = 220, 230, 245 + + mergedWidth := 0.0 + for i := range 11 { + mergedWidth += marketingPdfColumns[i].width + } + pdf.SetFillColor(totalFillR, totalFillG, totalFillB) + pdf.Rect(x, y, mergedWidth, rowH, "FD") + pdf.SetXY(x, y) + pdf.MultiCell(mergedWidth, lineH, "TOTAL", "", "R", false) + x += mergedWidth + for i, val := range totals { col := marketingPdfColumns[11+i] + pdf.SetFillColor(totalFillR, totalFillG, totalFillB) + pdf.Rect(x, y, col.width, rowH, "FD") pdf.SetXY(x, y) - pdf.CellFormat(col.width, rowH, val, "1", 0, "R", true, 0, "") + pdf.MultiCell(col.width, lineH, val, "", "R", false) x += col.width } @@ -510,6 +532,27 @@ func marketingPdfPageHeight(pdf *fpdf.Fpdf) float64 { return h } +// formatMarketingVehicleNumber spaces out Indonesian plate segments: D1234MBU → D 1234 MBU. +// Returns s unchanged if it doesn't match the [letters][digits][letters] pattern. +func formatMarketingVehicleNumber(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return s + } + i := 0 + for i < len(s) && (s[i] >= 'A' && s[i] <= 'Z' || s[i] >= 'a' && s[i] <= 'z') { + i++ + } + j := i + for j < len(s) && s[j] >= '0' && s[j] <= '9' { + j++ + } + if i == 0 || j == i || j == len(s) { + return s + } + return s[:i] + " " + s[i:j] + " " + s[j:] +} + // formatMarketingPdfThousands inserts period every 3 digits. func formatMarketingPdfThousands(v int64) string { negative := v < 0