mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'feat/export-penjualan' into 'development'
[FEAT][BE]: add export excel and pdf report penjualan See merge request mbugroup/lti-api!492
This commit is contained in:
@@ -52,6 +52,7 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-pdf/fpdf v0.9.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
|
||||
@@ -77,6 +77,8 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
||||
@@ -246,6 +246,16 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isMarketingExcelExportRequest(ctx) {
|
||||
return exportMarketingReportExcel(ctx, result)
|
||||
}
|
||||
|
||||
if isMarketingPdfExportRequest(ctx) {
|
||||
meta := buildMarketingPdfMeta(query.StartDate, query.EndDate)
|
||||
return exportMarketingReportPdf(ctx, result, meta)
|
||||
}
|
||||
|
||||
total := dto.ToSummaryFromDTOItems(result)
|
||||
|
||||
return ctx.Status(fiber.StatusOK).
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/xuri/excelize/v2"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||
)
|
||||
|
||||
const marketingReportExportSheetName = "Laporan Marketing Harian"
|
||||
|
||||
func isMarketingExcelExportRequest(c *fiber.Ctx) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "excel")
|
||||
}
|
||||
|
||||
func exportMarketingReportExcel(c *fiber.Ctx, items []dto.RepportMarketingItemDTO) error {
|
||||
content, err := buildMarketingReportWorkbook(items)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate excel file")
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("laporan_marketing_harian_%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 buildMarketingReportWorkbook(items []dto.RepportMarketingItemDTO) ([]byte, error) {
|
||||
file := excelize.NewFile()
|
||||
defer file.Close()
|
||||
|
||||
defaultSheet := file.GetSheetName(file.GetActiveSheetIndex())
|
||||
if defaultSheet != marketingReportExportSheetName {
|
||||
if err := file.SetSheetName(defaultSheet, marketingReportExportSheetName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := setMarketingReportColumns(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setMarketingReportHeaders(file); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setMarketingReportRows(file, items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer, err := file.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func setMarketingReportColumns(file *excelize.File) error {
|
||||
columnWidths := map[string]float64{
|
||||
"A": 10,
|
||||
"B": 15,
|
||||
"C": 18,
|
||||
"D": 10,
|
||||
"E": 25,
|
||||
"F": 25,
|
||||
"G": 15,
|
||||
"H": 20,
|
||||
"I": 15,
|
||||
"J": 15,
|
||||
"K": 20,
|
||||
"L": 12,
|
||||
"M": 20,
|
||||
"N": 18,
|
||||
"O": 18,
|
||||
"P": 15,
|
||||
"Q": 20,
|
||||
"R": 20,
|
||||
}
|
||||
|
||||
sheet := marketingReportExportSheetName
|
||||
for col, width := range columnWidths {
|
||||
if err := file.SetColWidth(sheet, col, col, width); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setMarketingReportHeaders(file *excelize.File) error {
|
||||
sheet := marketingReportExportSheetName
|
||||
headers := []string{
|
||||
"No",
|
||||
"Tanggal Jual",
|
||||
"Tanggal Realisasi",
|
||||
"Aging",
|
||||
"Gudang Fisik",
|
||||
"Pelanggan",
|
||||
"No. DO",
|
||||
"Sales/Marketing",
|
||||
"No. Polisi",
|
||||
"Marketing Type",
|
||||
"Produk",
|
||||
"Kuantitas",
|
||||
"Bobot Rata-Rata (Kg)",
|
||||
"Bobot Total (Kg)",
|
||||
"Harga Jual (Rp)",
|
||||
"HPP (Rp)",
|
||||
"HPP Amount (Rp)",
|
||||
"Total (Rp)",
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setMarketingReportRows(file *excelize.File, items []dto.RepportMarketingItemDTO) error {
|
||||
sheet := marketingReportExportSheetName
|
||||
summary := dto.ToSummaryFromDTOItems(items)
|
||||
|
||||
for idx, item := range items {
|
||||
row := strconv.Itoa(idx + 2)
|
||||
|
||||
warehouseName := "-"
|
||||
if item.Warehouse != nil {
|
||||
warehouseName = safeMarketingExportText(item.Warehouse.Name)
|
||||
}
|
||||
|
||||
customerName := "-"
|
||||
if item.Customer != nil {
|
||||
customerName = safeMarketingExportText(item.Customer.Name)
|
||||
}
|
||||
|
||||
salesName := "-"
|
||||
if item.Sales != nil {
|
||||
salesName = safeMarketingExportText(item.Sales.Name)
|
||||
}
|
||||
|
||||
productName := "-"
|
||||
if item.Product != nil {
|
||||
productName = safeMarketingExportText(item.Product.Name)
|
||||
}
|
||||
|
||||
agingText := fmt.Sprintf("%d hari", item.AgingDays)
|
||||
|
||||
values := []interface{}{
|
||||
idx + 1,
|
||||
formatMarketingDate(item.SoDate),
|
||||
formatMarketingDate(item.RealizationDate),
|
||||
agingText,
|
||||
warehouseName,
|
||||
customerName,
|
||||
safeMarketingExportText(item.DoNumber),
|
||||
salesName,
|
||||
safeMarketingExportText(item.VehicleNumber),
|
||||
safeMarketingExportText(item.MarketingType),
|
||||
productName,
|
||||
item.Qty,
|
||||
item.AverageWeightKg,
|
||||
item.TotalWeightKg,
|
||||
formatMarketingRupiah(item.SalesPricePerKg),
|
||||
formatMarketingRupiah(item.HppPricePerKg),
|
||||
formatMarketingRupiah(item.HppAmount),
|
||||
formatMarketingRupiah(item.SalesAmount),
|
||||
}
|
||||
|
||||
for colIdx, val := range values {
|
||||
colName, err := excelize.ColumnNumberToName(colIdx + 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, colName+row, val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Baris TOTAL
|
||||
totalRow := strconv.Itoa(len(items) + 2)
|
||||
if err := file.SetCellValue(sheet, "A"+totalRow, "TOTAL"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if summary != nil {
|
||||
if err := file.SetCellValue(sheet, "L"+totalRow, summary.TotalQty); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "M"+totalRow, summary.AverageWeightKg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "N"+totalRow, summary.TotalWeightKg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := file.SetCellValue(sheet, "O"+totalRow, formatMarketingRupiah(summary.AverageSalesPrice)); err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatMarketingDate(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err == nil {
|
||||
t = t.In(location)
|
||||
}
|
||||
|
||||
return t.Format("02 Jan 2006")
|
||||
}
|
||||
|
||||
func safeMarketingExportText(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return "-"
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// formatMarketingRupiah formats a float64 as Indonesian Rupiah string.
|
||||
// e.g. 1000000 → "Rp 1.000.000"
|
||||
func formatMarketingRupiah(value float64) string {
|
||||
rounded := int64(math.Round(value))
|
||||
|
||||
negative := rounded < 0
|
||||
abs := rounded
|
||||
if negative {
|
||||
abs = -rounded
|
||||
}
|
||||
|
||||
numStr := strconv.FormatInt(abs, 10)
|
||||
n := len(numStr)
|
||||
|
||||
var b strings.Builder
|
||||
for i, c := range numStr {
|
||||
if i > 0 && (n-i)%3 == 0 {
|
||||
b.WriteByte('.')
|
||||
}
|
||||
b.WriteRune(c)
|
||||
}
|
||||
|
||||
if negative {
|
||||
return "Rp -" + b.String()
|
||||
}
|
||||
return "Rp " + b.String()
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-pdf/fpdf"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Trigger
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func isMarketingPdfExportRequest(c *fiber.Ctx) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(c.Query("export")), "pdf")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP handler
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func exportMarketingReportPdf(c *fiber.Ctx, items []dto.RepportMarketingItemDTO, query marketingPdfQueryMeta) error {
|
||||
content, err := buildMarketingReportPdf(items, query)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "failed to generate pdf file")
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("laporan_marketing_harian_%s.pdf", time.Now().Format("20060102_150405"))
|
||||
c.Set("Content-Type", "application/pdf")
|
||||
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||
return c.Status(fiber.StatusOK).Send(content)
|
||||
}
|
||||
|
||||
// marketingPdfQueryMeta holds display metadata for the PDF header.
|
||||
type marketingPdfQueryMeta struct {
|
||||
StartDate string // e.g. "01 April 2026"
|
||||
EndDate string // e.g. "29 April 2026"
|
||||
PrintedAt time.Time
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Column definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type pdfColumn struct {
|
||||
header string
|
||||
width float64 // mm
|
||||
align string // "L", "C", "R"
|
||||
}
|
||||
|
||||
var marketingPdfColumns = []pdfColumn{
|
||||
{"No", 6, "C"},
|
||||
{"Tanggal Sales Order", 16, "C"},
|
||||
{"Tanggal Delivery Order", 16, "C"},
|
||||
{"Aging\n(Hari)", 9, "C"},
|
||||
{"Gudang Fisik", 20, "L"},
|
||||
{"Pelanggan", 20, "L"},
|
||||
{"Sales", 18, "L"},
|
||||
{"Produk", 16, "L"},
|
||||
{"Nomor DO", 14, "C"},
|
||||
{"Nomor Polisi", 14, "C"},
|
||||
{"Tipe\nMarketing", 14, "C"},
|
||||
{"Quantity", 13, "R"},
|
||||
{"Rata-Rata\n(Kg)", 13, "R"},
|
||||
{"Total Berat\n(Kg)", 14, "R"},
|
||||
{"Harga Jual\n(Rp)", 17, "R"},
|
||||
{"HPP\n(Rp)", 17, "R"},
|
||||
{"Total Jual\n(Rp)", 18, "R"},
|
||||
{"Total HPP\n(Rp)", 18, "R"},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Colours
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
headerR, headerG, headerB = 30, 64, 120 // dark blue header bg
|
||||
headerTextR, headerTextG, headerTextB = 255, 255, 255 // white header text
|
||||
rowAltR, rowAltG, rowAltB = 245, 247, 250 // alternating row bg
|
||||
borderR, borderG, borderB = 200, 200, 200 // light border
|
||||
|
||||
badgeTelurR, badgeTelurG, badgeTelurB = 59, 130, 246 // blue
|
||||
badgeAyamR, badgeAyamG, badgeAyamB = 34, 197, 94 // green
|
||||
badgeTradingR, badgeTradingG, badgeTradingB = 249, 115, 22 // orange
|
||||
badgeDefaultR, badgeDefaultG, badgeDefaultB = 107, 114, 128 // gray
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workbook builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func buildMarketingReportPdf(items []dto.RepportMarketingItemDTO, meta marketingPdfQueryMeta) ([]byte, error) {
|
||||
pdf := fpdf.New("L", "mm", "A4", "")
|
||||
pdf.SetMargins(10, 12, 10)
|
||||
pdf.SetAutoPageBreak(true, 12)
|
||||
pdf.AddPage()
|
||||
|
||||
// ---- title ----
|
||||
pdf.SetFont("Helvetica", "B", 14)
|
||||
pdf.SetTextColor(30, 64, 120)
|
||||
pdf.CellFormat(0, 8, "Laporan > Penjualan Harian", "", 1, "L", false, 0, "")
|
||||
|
||||
// ---- subtitle ----
|
||||
pdf.SetFont("Helvetica", "", 8)
|
||||
pdf.SetTextColor(80, 80, 80)
|
||||
dateLabel := buildMarketingPdfDateLabel(meta)
|
||||
printedAt := formatMarketingPdfDateTime(meta.PrintedAt)
|
||||
pdf.CellFormat(0, 5, fmt.Sprintf("%s Dicetak: %s", dateLabel, printedAt), "", 1, "L", false, 0, "")
|
||||
pdf.Ln(3)
|
||||
|
||||
// ---- table ----
|
||||
writeMarketingPdfHeader(pdf)
|
||||
writeMarketingPdfRows(pdf, items)
|
||||
writeMarketingPdfTotal(pdf, items)
|
||||
|
||||
return marshalMarketingPdf(pdf)
|
||||
}
|
||||
|
||||
func marshalMarketingPdf(pdf *fpdf.Fpdf) ([]byte, error) {
|
||||
w := &pdfByteBuffer{}
|
||||
err := pdf.Output(w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
|
||||
// pdfByteBuffer implements io.Writer and accumulates bytes.
|
||||
type pdfByteBuffer struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (b *pdfByteBuffer) Write(p []byte) (n int, err error) {
|
||||
b.buf = append(b.buf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (b *pdfByteBuffer) Bytes() []byte { return b.buf }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func writeMarketingPdfHeader(pdf *fpdf.Fpdf) {
|
||||
pdf.SetFont("Helvetica", "B", 6.5)
|
||||
pdf.SetFillColor(headerR, headerG, headerB)
|
||||
pdf.SetTextColor(headerTextR, headerTextG, headerTextB)
|
||||
pdf.SetDrawColor(borderR, borderG, borderB)
|
||||
pdf.SetLineWidth(0.1)
|
||||
|
||||
headerH := 9.0 // height of header row (mm)
|
||||
|
||||
x0 := pdf.GetX()
|
||||
y0 := pdf.GetY()
|
||||
|
||||
for _, col := range marketingPdfColumns {
|
||||
lines := strings.Split(col.header, "\n")
|
||||
lineH := headerH / float64(len(lines))
|
||||
|
||||
x := pdf.GetX()
|
||||
for li, line := range lines {
|
||||
pdf.SetXY(x, y0+float64(li)*lineH)
|
||||
pdf.CellFormat(col.width, lineH, line, "", 0, "C", true, 0, "")
|
||||
}
|
||||
pdf.SetXY(x+col.width, y0)
|
||||
}
|
||||
|
||||
_ = x0
|
||||
pdf.SetXY(10, y0+headerH)
|
||||
pdf.Ln(0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data rows
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func writeMarketingPdfRows(pdf *fpdf.Fpdf, items []dto.RepportMarketingItemDTO) {
|
||||
pdf.SetFont("Helvetica", "", 6)
|
||||
pdf.SetDrawColor(borderR, borderG, borderB)
|
||||
pdf.SetLineWidth(0.1)
|
||||
|
||||
rowH := 6.0
|
||||
|
||||
for idx, item := range items {
|
||||
// page break check
|
||||
if pdf.GetY()+rowH > marketingPdfPageHeight(pdf)-12 {
|
||||
pdf.AddPage()
|
||||
writeMarketingPdfHeader(pdf)
|
||||
pdf.SetFont("Helvetica", "", 6)
|
||||
}
|
||||
|
||||
// alternating bg
|
||||
if idx%2 == 1 {
|
||||
pdf.SetFillColor(rowAltR, rowAltG, rowAltB)
|
||||
} else {
|
||||
pdf.SetFillColor(255, 255, 255)
|
||||
}
|
||||
pdf.SetTextColor(40, 40, 40)
|
||||
|
||||
y := pdf.GetY()
|
||||
writeMarketingPdfRow(pdf, idx+1, item, rowH, y)
|
||||
}
|
||||
}
|
||||
|
||||
func writeMarketingPdfRow(pdf *fpdf.Fpdf, no int, item dto.RepportMarketingItemDTO, h, y float64) {
|
||||
fill := true // use the fill colour already set
|
||||
|
||||
cols := marketingPdfColumns
|
||||
x := 10.0 // left margin
|
||||
|
||||
values := marketingPdfRowValues(no, item)
|
||||
|
||||
for i, col := range cols {
|
||||
pdf.SetXY(x, y)
|
||||
|
||||
if i == 10 { // Tipe Marketing → badge
|
||||
drawMarketingTypeBadge(pdf, x, y, col.width, h, item.MarketingType)
|
||||
} else {
|
||||
pdf.CellFormat(col.width, h, values[i], "1", 0, col.align, fill, 0, "")
|
||||
}
|
||||
|
||||
x += col.width
|
||||
}
|
||||
|
||||
pdf.SetXY(10, y+h)
|
||||
}
|
||||
|
||||
func marketingPdfRowValues(no int, item dto.RepportMarketingItemDTO) []string {
|
||||
warehouse := "-"
|
||||
if item.Warehouse != nil {
|
||||
warehouse = safeMarketingExportText(item.Warehouse.Name)
|
||||
}
|
||||
customer := "-"
|
||||
if item.Customer != nil {
|
||||
customer = safeMarketingExportText(item.Customer.Name)
|
||||
}
|
||||
sales := "-"
|
||||
if item.Sales != nil {
|
||||
sales = safeMarketingExportText(item.Sales.Name)
|
||||
}
|
||||
product := "-"
|
||||
if item.Product != nil {
|
||||
product = safeMarketingExportText(item.Product.Name)
|
||||
}
|
||||
|
||||
return []string{
|
||||
strconv.Itoa(no),
|
||||
formatMarketingDate(item.SoDate),
|
||||
formatMarketingDate(item.RealizationDate),
|
||||
strconv.Itoa(item.AgingDays),
|
||||
warehouse,
|
||||
customer,
|
||||
sales,
|
||||
product,
|
||||
safeMarketingExportText(item.DoNumber),
|
||||
safeMarketingExportText(item.VehicleNumber),
|
||||
safeMarketingExportText(item.MarketingType), // index 10, overridden by badge
|
||||
formatMarketingPdfNumber(item.Qty),
|
||||
formatMarketingPdfDecimal(item.AverageWeightKg),
|
||||
formatMarketingPdfDecimal(item.TotalWeightKg),
|
||||
formatMarketingPdfRupiah(item.SalesPricePerKg),
|
||||
formatMarketingPdfRupiah(item.HppPricePerKg),
|
||||
formatMarketingPdfRupiah(item.SalesAmount),
|
||||
formatMarketingPdfRupiah(item.HppAmount),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Total row
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func writeMarketingPdfTotal(pdf *fpdf.Fpdf, items []dto.RepportMarketingItemDTO) {
|
||||
summary := dto.ToSummaryFromDTOItems(items)
|
||||
if summary == nil {
|
||||
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
|
||||
|
||||
totals := []string{
|
||||
formatMarketingPdfNumber(float64(summary.TotalQty)),
|
||||
formatMarketingPdfDecimal(summary.AverageWeightKg),
|
||||
formatMarketingPdfDecimal(summary.TotalWeightKg),
|
||||
formatMarketingPdfRupiah(summary.AverageSalesPrice),
|
||||
formatMarketingPdfRupiah(summary.TotalHppPricePerKg),
|
||||
formatMarketingPdfRupiah(float64(summary.TotalSalesAmount)),
|
||||
formatMarketingPdfRupiah(float64(summary.TotalHppAmount)),
|
||||
}
|
||||
|
||||
for i, val := range totals {
|
||||
col := marketingPdfColumns[11+i]
|
||||
pdf.SetXY(x, y)
|
||||
pdf.CellFormat(col.width, rowH, val, "1", 0, "R", true, 0, "")
|
||||
x += col.width
|
||||
}
|
||||
|
||||
pdf.SetXY(10, y+rowH)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Badge drawer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func drawMarketingTypeBadge(pdf *fpdf.Fpdf, x, y, w, h float64, marketingType string) {
|
||||
lower := strings.ToLower(strings.TrimSpace(marketingType))
|
||||
|
||||
var r, g, b int
|
||||
switch lower {
|
||||
case "telur":
|
||||
r, g, b = badgeTelurR, badgeTelurG, badgeTelurB
|
||||
case "ayam":
|
||||
r, g, b = badgeAyamR, badgeAyamG, badgeAyamB
|
||||
case "trading":
|
||||
r, g, b = badgeTradingR, badgeTradingG, badgeTradingB
|
||||
default:
|
||||
r, g, b = badgeDefaultR, badgeDefaultG, badgeDefaultB
|
||||
}
|
||||
|
||||
// badge background (slightly inset from the cell border)
|
||||
padH := 1.2
|
||||
padV := 1.2
|
||||
bx := x + padH
|
||||
by := y + padV
|
||||
bw := w - padH*2
|
||||
bh := h - padV*2
|
||||
|
||||
pdf.SetFillColor(r, g, b)
|
||||
pdf.SetDrawColor(r, g, b)
|
||||
pdf.RoundedRect(bx, by, bw, bh, 1.5, "1234", "FD")
|
||||
|
||||
// border of the cell itself (transparent bg, just border)
|
||||
pdf.SetDrawColor(borderR, borderG, borderB)
|
||||
pdf.SetFillColor(255, 255, 255)
|
||||
pdf.SetXY(x, y)
|
||||
pdf.CellFormat(w, h, "", "1", 0, "C", false, 0, "")
|
||||
|
||||
// badge label
|
||||
pdf.SetFont("Helvetica", "B", 5.5)
|
||||
pdf.SetTextColor(255, 255, 255)
|
||||
label := strings.Title(strings.ToLower(marketingType))
|
||||
if label == "" {
|
||||
label = "-"
|
||||
}
|
||||
textW := pdf.GetStringWidth(label)
|
||||
pdf.SetXY(bx+(bw-textW)/2, by+(bh-3)/2)
|
||||
pdf.CellFormat(textW, 3, label, "", 0, "C", false, 0, "")
|
||||
|
||||
// reset
|
||||
pdf.SetFont("Helvetica", "", 6)
|
||||
pdf.SetTextColor(40, 40, 40)
|
||||
pdf.SetDrawColor(borderR, borderG, borderB)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers — date/time
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// buildMarketingPdfMeta converts raw query string dates into display meta.
|
||||
func buildMarketingPdfMeta(startDate, endDate string) marketingPdfQueryMeta {
|
||||
loc, _ := time.LoadLocation("Asia/Jakarta")
|
||||
|
||||
format := func(s string) string {
|
||||
t, err := time.ParseInLocation("2006-01-02", s, loc)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return t.Format("02 January 2006")
|
||||
}
|
||||
|
||||
return marketingPdfQueryMeta{
|
||||
StartDate: format(startDate),
|
||||
EndDate: format(endDate),
|
||||
PrintedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func buildMarketingPdfDateLabel(meta marketingPdfQueryMeta) string {
|
||||
if meta.StartDate != "" && meta.EndDate != "" {
|
||||
return fmt.Sprintf("Tanggal: %s - %s", meta.StartDate, meta.EndDate)
|
||||
}
|
||||
if meta.StartDate != "" {
|
||||
return fmt.Sprintf("Tanggal: %s", meta.StartDate)
|
||||
}
|
||||
if meta.EndDate != "" {
|
||||
return fmt.Sprintf("Tanggal: %s", meta.EndDate)
|
||||
}
|
||||
return fmt.Sprintf("Tanggal: %s", time.Now().Format("02 January 2006"))
|
||||
}
|
||||
|
||||
func formatMarketingPdfDateTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
t = time.Now()
|
||||
}
|
||||
loc, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err == nil {
|
||||
t = t.In(loc)
|
||||
}
|
||||
return t.Format("02 Jan 2006 15:04")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers — number formatting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// formatMarketingPdfNumber formats a float64 as an integer with period-thousands separator.
|
||||
// e.g. 7299 → "7.299"
|
||||
func formatMarketingPdfNumber(v float64) string {
|
||||
return formatMarketingPdfThousands(int64(math.Round(v)))
|
||||
}
|
||||
|
||||
// formatMarketingPdfDecimal formats a float64 with 2 decimal places (Indonesian locale).
|
||||
// e.g. 452.54 → "452,54"
|
||||
func formatMarketingPdfDecimal(v float64) string {
|
||||
rounded := math.Round(v*100) / 100
|
||||
intPart := int64(rounded)
|
||||
fracPart := int64(math.Round((rounded - float64(intPart)) * 100))
|
||||
if fracPart < 0 {
|
||||
fracPart = -fracPart
|
||||
}
|
||||
return fmt.Sprintf("%s,%02d", formatMarketingPdfThousands(intPart), fracPart)
|
||||
}
|
||||
|
||||
// formatMarketingPdfRupiah formats a float64 as Rupiah with Indonesian locale.
|
||||
// Drops trailing ",00" decimals for whole numbers.
|
||||
// e.g. 25500 → "Rp 25.500" | 240896.94 → "Rp 240.896,94"
|
||||
func formatMarketingPdfRupiah(v float64) string {
|
||||
rounded := math.Round(v*100) / 100
|
||||
intPart := int64(rounded)
|
||||
fracPart := int64(math.Round((rounded - float64(intPart)) * 100))
|
||||
if fracPart < 0 {
|
||||
fracPart = -fracPart
|
||||
}
|
||||
|
||||
negative := intPart < 0
|
||||
absInt := intPart
|
||||
if negative {
|
||||
absInt = -intPart
|
||||
}
|
||||
|
||||
s := formatMarketingPdfThousands(absInt)
|
||||
var result string
|
||||
if fracPart == 0 {
|
||||
result = "Rp " + s
|
||||
} else {
|
||||
result = fmt.Sprintf("Rp %s,%02d", s, fracPart)
|
||||
}
|
||||
if negative {
|
||||
return "Rp -" + s
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// marketingPdfPageHeight returns the page height in mm.
|
||||
func marketingPdfPageHeight(pdf *fpdf.Fpdf) float64 {
|
||||
_, h := pdf.GetPageSize()
|
||||
return h
|
||||
}
|
||||
|
||||
// formatMarketingPdfThousands inserts period every 3 digits.
|
||||
func formatMarketingPdfThousands(v int64) string {
|
||||
negative := v < 0
|
||||
abs := v
|
||||
if negative {
|
||||
abs = -v
|
||||
}
|
||||
|
||||
numStr := strconv.FormatInt(abs, 10)
|
||||
n := len(numStr)
|
||||
|
||||
var b strings.Builder
|
||||
for i, c := range numStr {
|
||||
if i > 0 && (n-i)%3 == 0 {
|
||||
b.WriteByte('.')
|
||||
}
|
||||
b.WriteRune(c)
|
||||
}
|
||||
|
||||
if negative {
|
||||
return "-" + b.String()
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user