From 64844baa83c909d9ae727dbe82d1c3861d67ee14 Mon Sep 17 00:00:00 2001 From: cclohmar Date: Tue, 14 Jul 2026 10:06:31 +0000 Subject: [PATCH] fix: landscape PDF reports, full text, wider columns, include CSV+PDF in monthly ZIP --- internal/handlers/file.go | 85 +++++++++++++++++---------------- internal/handlers/months.go | 95 ++++++++++++++++++------------------- 2 files changed, 89 insertions(+), 91 deletions(-) diff --git a/internal/handlers/file.go b/internal/handlers/file.go index 42eaacd..8bbffb4 100644 --- a/internal/handlers/file.go +++ b/internal/handlers/file.go @@ -653,15 +653,15 @@ func generateCSV(eventName string, expenses []database.Expense, userName, userDe // If the expenses use a different currency than the base currency, both // original and converted amounts are included. func generatePDF(eventName string, expenses []database.Expense, userName, userDept string) (*email.Attachment, error) { - pdf := gofpdf.New("P", "mm", "A4", "") + pdf := gofpdf.New("L", "mm", "A4", "") pdf.AddPage() - // Title: "Expense Report: " - pdf.SetFont("Helvetica", "B", 16) + // Title. + pdf.SetFont("Helvetica", "B", 14) pdf.Cell(0, 10, "Expense Report: "+eventName) pdf.Ln(8) - // User info block. + // User info. if userName != "" { pdf.SetFont("Helvetica", "", 9) infoLine := fmt.Sprintf("Prepared by: %s", userName) @@ -681,69 +681,68 @@ func generatePDF(eventName string, expenses []database.Expense, userName, userDe } } - // Table header row with item number. - pdf.SetFont("Helvetica", "B", 10) + // Landscape A4: 297mm wide, 10mm margins → 277mm usable. + pdf.SetFont("Helvetica", "B", 8) var headers []string var colWidths []float64 if hasConversion { - headers = []string{"#", "Date", "Merchant", "Amount", "Curr.", "Converted", "Claim", "Category"} - colWidths = []float64{8, 22, 38, 18, 12, 20, 14, 30} + headers = []string{"#", "Date", "Merchant", "Amount", "Curr.", "Converted", "Claim", "Category", "Description"} + colWidths = []float64{7, 24, 44, 20, 14, 20, 14, 40, 94} } else { - headers = []string{"#", "Date", "Merchant", "Amount", "Currency", "Category"} - colWidths = []float64{10, 28, 48, 22, 18, 40} + headers = []string{"#", "Date", "Merchant", "Amount", "Currency", "Category", "Description"} + colWidths = []float64{8, 28, 58, 24, 18, 48, 93} } for i, h := range headers { - pdf.Cell(colWidths[i], 8, h) + pdf.Cell(colWidths[i], 7, h) } - pdf.Ln(8) + pdf.Ln(7) - // Table data rows with item numbers and automatic page breaks. - pdf.SetFont("Helvetica", "", 9) - marginBottom := 20.0 // mm margin from bottom before page break + // Table data rows. + pdf.SetFont("Helvetica", "", 8) + marginBottom := 18.0 var totalOrig, totalConv float64 for i, exp := range expenses { - // Check if we need a page break (A4 = 297mm height). - if pdf.GetY() > 297-marginBottom { + if pdf.GetY() > 210-marginBottom { pdf.AddPage() - // Re-draw header row on new page. - pdf.SetFont("Helvetica", "B", 9) + pdf.SetFont("Helvetica", "B", 8) for j, h := range headers { - pdf.Cell(colWidths[j], 8, h) + pdf.Cell(colWidths[j], 7, h) } - pdf.Ln(8) - pdf.SetFont("Helvetica", "", 9) + pdf.Ln(7) + pdf.SetFont("Helvetica", "", 8) } itemNum := i + 1 if hasConversion { - pdf.Cell(colWidths[0], 8, fmt.Sprintf("%d", itemNum)) - pdf.Cell(colWidths[1], 8, exp.Date) - pdf.Cell(colWidths[2], 8, truncateString(exp.Merchant, 18)) - pdf.Cell(colWidths[3], 8, fmt.Sprintf("%.2f", exp.Amount)) - pdf.Cell(colWidths[4], 8, exp.Currency) - pdf.Cell(colWidths[5], 8, fmt.Sprintf("%.2f", exp.ConvertedAmount)) - pdf.Cell(colWidths[6], 8, exp.BaseCurrency) - pdf.Cell(colWidths[7], 8, truncateString(exp.Category, 12)) + pdf.Cell(colWidths[0], 6, fmt.Sprintf("%d", itemNum)) + pdf.Cell(colWidths[1], 6, exp.Date) + pdf.Cell(colWidths[2], 6, exp.Merchant) + pdf.Cell(colWidths[3], 6, fmt.Sprintf("%.2f", exp.Amount)) + pdf.Cell(colWidths[4], 6, exp.Currency) + pdf.Cell(colWidths[5], 6, fmt.Sprintf("%.2f", exp.ConvertedAmount)) + pdf.Cell(colWidths[6], 6, exp.BaseCurrency) + pdf.Cell(colWidths[7], 6, exp.Category) + pdf.Cell(colWidths[8], 6, exp.Description) totalOrig += exp.Amount totalConv += exp.ConvertedAmount } else { - pdf.Cell(colWidths[0], 8, fmt.Sprintf("%d", itemNum)) - pdf.Cell(colWidths[1], 8, exp.Date) - pdf.Cell(colWidths[2], 8, truncateString(exp.Merchant, 20)) - pdf.Cell(colWidths[3], 8, fmt.Sprintf("%.2f", exp.Amount)) - pdf.Cell(colWidths[4], 8, exp.Currency) - pdf.Cell(colWidths[5], 8, truncateString(exp.Category, 20)) + pdf.Cell(colWidths[0], 6, fmt.Sprintf("%d", itemNum)) + pdf.Cell(colWidths[1], 6, exp.Date) + pdf.Cell(colWidths[2], 6, exp.Merchant) + pdf.Cell(colWidths[3], 6, fmt.Sprintf("%.2f", exp.Amount)) + pdf.Cell(colWidths[4], 6, exp.Currency) + pdf.Cell(colWidths[5], 6, exp.Category) + pdf.Cell(colWidths[6], 6, exp.Description) totalOrig += exp.Amount } - pdf.Ln(8) + pdf.Ln(6) } - // Totals row with a separator line. - pdf.SetDrawColor(71, 85, 105) // #475569 border colour - pdf.Line(10, pdf.GetY()+1, 200, pdf.GetY()+1) + // Totals row. + pdf.SetDrawColor(71, 85, 105) + pdf.Line(10, pdf.GetY()+1, 287, pdf.GetY()+1) pdf.Ln(3) pdf.SetFont("Helvetica", "B", 9) if hasConversion { - // Determine the base/claim currency from the first expense that has one. baseCur := "" for _, exp := range expenses { if exp.BaseCurrency != "" { @@ -759,6 +758,7 @@ func generatePDF(eventName string, expenses []database.Expense, userName, userDe pdf.Cell(colWidths[5], 8, fmt.Sprintf("%.2f", totalConv)) pdf.Cell(colWidths[6], 8, baseCur) pdf.Cell(colWidths[7], 8, "") + pdf.Cell(colWidths[8], 8, "") } else { pdf.Cell(colWidths[0], 8, "") pdf.Cell(colWidths[1], 8, "") @@ -766,9 +766,10 @@ func generatePDF(eventName string, expenses []database.Expense, userName, userDe pdf.Cell(colWidths[3], 8, fmt.Sprintf("%.2f", totalOrig)) pdf.Cell(colWidths[4], 8, "") pdf.Cell(colWidths[5], 8, "") + pdf.Cell(colWidths[6], 8, "") } - // Write the PDF document to a memory buffer. + // Write to buffer. var buf bytes.Buffer if err := pdf.Output(&buf); err != nil { return nil, fmt.Errorf("PDF output: %w", err) diff --git a/internal/handlers/months.go b/internal/handlers/months.go index d675ff8..1fd50ca 100644 --- a/internal/handlers/months.go +++ b/internal/handlers/months.go @@ -310,11 +310,6 @@ func (h *MonthHandler) GenerateMonthlyReport(w http.ResponseWriter, r *http.Requ return } - format := strings.ToLower(strings.TrimSpace(r.FormValue("format"))) - if format != "csv" && format != "pdf" { - format = "pdf" - } - userID := getUserID(r) if userID == "" { renderFileError(w, "Session expired. Please log in again.") @@ -365,16 +360,18 @@ func (h *MonthHandler) GenerateMonthlyReport(w http.ResponseWriter, r *http.Requ uDept = repUser.Department } - // Generate the report. - var reportAtt *email.Attachment - if format == "csv" { - reportAtt, err = generateMonthlyCSV(month.Name, events, allExpenses, uName, uDept) - } else { - reportAtt, err = generateMonthlyPDF(month.Name, events, allExpenses, uName, uDept) - } + // Generate both CSV and PDF reports. + csvAtt, err := generateMonthlyCSV(month.Name, events, allExpenses, uName, uDept) if err != nil { - log.Printf("ERROR [%s] handlers: GenerateMonthlyReport: generate %s: %v", - time.Now().Format(time.RFC3339), format, err) + log.Printf("ERROR [%s] handlers: GenerateMonthlyReport: generate CSV: %v", + time.Now().Format(time.RFC3339), err) + renderFileError(w, "Failed to generate report.") + return + } + pdfAtt, err := generateMonthlyPDF(month.Name, events, allExpenses, uName, uDept) + if err != nil { + log.Printf("ERROR [%s] handlers: GenerateMonthlyReport: generate PDF: %v", + time.Now().Format(time.RFC3339), err) renderFileError(w, "Failed to generate report.") return } @@ -383,7 +380,8 @@ func (h *MonthHandler) GenerateMonthlyReport(w http.ResponseWriter, r *http.Requ var pkgBuf bytes.Buffer pkg := zip.NewWriter(&pkgBuf) - addToZip(pkg, reportAtt.Filename, reportAtt.Content) + addToZip(pkg, csvAtt.Filename, csvAtt.Content) + addToZip(pkg, pdfAtt.Filename, pdfAtt.Content) // Add all receipt images from all events. imgIdx := 0 @@ -450,12 +448,10 @@ func (h *MonthHandler) GenerateMonthlyReport(w http.ResponseWriter, r *http.Requ log.Printf("INFO [%s] handlers: GenerateMonthlyReport: package %s created for month %s", time.Now().Format(time.RFC3339), pkgFilename, monthID) - ext := format - reportName := fmt.Sprintf("%s-report.%s", sanitiseFilename(month.Name), ext) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, `
Monthly Report Ready
-

%s & %d events, %d receipt images packaged.

+

CSV + PDF & %d events, %d receipt images packaged.

⬇ Download Now
@@ -470,7 +466,7 @@ func (h *MonthHandler) GenerateMonthlyReport(w http.ResponseWriter, r *http.Requ
`, - template.HTMLEscapeString(reportName), len(events), imgIdx, + len(events), imgIdx, template.HTMLEscapeString(token), template.HTMLEscapeString(dlName), template.HTMLEscapeString(monthID), template.HTMLEscapeString(token)) } @@ -621,14 +617,14 @@ func generateMonthlyCSV(monthName string, events []database.Event, expenses []da }, nil } -// generateMonthlyPDF creates a PDF attachment aggregating expenses across -// all events in a month, with a section per event. +// generateMonthlyPDF creates a landscape PDF attachment aggregating expenses +// across all events in a month, with a section per event. Full text, no truncation. func generateMonthlyPDF(monthName string, events []database.Event, expenses []database.Expense, userName, userDept string) (*email.Attachment, error) { - pdf := gofpdf.New("P", "mm", "A4", "") + pdf := gofpdf.New("L", "mm", "A4", "") pdf.AddPage() // Title. - pdf.SetFont("Helvetica", "B", 16) + pdf.SetFont("Helvetica", "B", 14) pdf.Cell(0, 10, "Monthly Expense Report: "+monthName) pdf.Ln(8) @@ -654,9 +650,10 @@ func generateMonthlyPDF(monthName string, events []database.Event, expenses []da } itemNum := 1 - colWidths := []float64{8, 22, 38, 18, 14, 24, 30} - headers := []string{"#", "Date", "Merchant", "Amount", "Curr.", "Category", "Desc."} - marginBottom := 20.0 + // Landscape A4: 297mm wide, 10mm margins → 277mm usable. + colWidths := []float64{8, 28, 55, 22, 16, 50, 98} + headers := []string{"#", "Date", "Merchant", "Amount", "Curr.", "Category", "Description"} + marginBottom := 18.0 var grandTotal float64 for _, evt := range events { @@ -665,48 +662,48 @@ func generateMonthlyPDF(monthName string, events []database.Event, expenses []da continue } - // Event section header with page break check. - if pdf.GetY() > 260 { + // Event section header. + if pdf.GetY() > 180 { pdf.AddPage() } - pdf.SetFont("Helvetica", "B", 11) + pdf.SetFont("Helvetica", "B", 10) pdf.Cell(0, 8, eventNames[evt.ID]) - pdf.Ln(10) + pdf.Ln(9) // Column headers. - pdf.SetFont("Helvetica", "B", 9) + pdf.SetFont("Helvetica", "B", 8) for j, h := range headers { - pdf.Cell(colWidths[j], 8, h) + pdf.Cell(colWidths[j], 7, h) } - pdf.Ln(8) + pdf.Ln(7) var evtTotal float64 - pdf.SetFont("Helvetica", "", 9) + pdf.SetFont("Helvetica", "", 8) for _, exp := range evtExpenses { - if pdf.GetY() > 297-marginBottom { + if pdf.GetY() > 210-marginBottom { pdf.AddPage() - pdf.SetFont("Helvetica", "B", 9) + pdf.SetFont("Helvetica", "B", 8) for j, h := range headers { - pdf.Cell(colWidths[j], 8, h) + pdf.Cell(colWidths[j], 7, h) } - pdf.Ln(8) - pdf.SetFont("Helvetica", "", 9) + pdf.Ln(7) + pdf.SetFont("Helvetica", "", 8) } - pdf.Cell(colWidths[0], 7, fmt.Sprintf("%d", itemNum)) - pdf.Cell(colWidths[1], 7, exp.Date) - pdf.Cell(colWidths[2], 7, truncateString(exp.Merchant, 18)) - pdf.Cell(colWidths[3], 7, fmt.Sprintf("%.2f", exp.Amount)) - pdf.Cell(colWidths[4], 7, exp.Currency) - pdf.Cell(colWidths[5], 7, truncateString(exp.Category, 12)) - pdf.Cell(colWidths[6], 7, truncateString(exp.Description, 18)) - pdf.Ln(7) + pdf.Cell(colWidths[0], 6, fmt.Sprintf("%d", itemNum)) + pdf.Cell(colWidths[1], 6, exp.Date) + pdf.Cell(colWidths[2], 6, exp.Merchant) + pdf.Cell(colWidths[3], 6, fmt.Sprintf("%.2f", exp.Amount)) + pdf.Cell(colWidths[4], 6, exp.Currency) + pdf.Cell(colWidths[5], 6, exp.Category) + pdf.Cell(colWidths[6], 6, exp.Description) + pdf.Ln(6) evtTotal += exp.Amount itemNum++ } - // Event subtotal with separator. + // Event subtotal. pdf.SetDrawColor(71, 85, 105) - pdf.Line(10, pdf.GetY()+1, 200, pdf.GetY()+1) + pdf.Line(10, pdf.GetY()+1, 287, pdf.GetY()+1) pdf.Ln(3) pdf.SetFont("Helvetica", "B", 9) pdf.Cell(colWidths[0], 8, "")