fix: landscape PDF reports, full text, wider columns, include CSV+PDF in monthly ZIP
This commit is contained in:
parent
d65c0cd5fa
commit
64844baa83
2 changed files with 89 additions and 91 deletions
|
|
@ -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: <event name>"
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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, `<div id="report-package" style="background: #064e3b; border: 1px solid #065f46; border-radius: 0.5rem; padding: 1rem; margin-top: 1rem;">
|
||||
<div style="font-weight: 600; color: #6ee7b7; margin-bottom: 0.5rem;">Monthly Report Ready</div>
|
||||
<p style="font-size: 0.8rem; color: var(--color-text-muted); margin-bottom: 0.75rem;">%s & %d events, %d receipt images packaged.</p>
|
||||
<p style="font-size: 0.8rem; color: var(--color-text-muted); margin-bottom: 0.75rem;">CSV + PDF & %d events, %d receipt images packaged.</p>
|
||||
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.75rem;">
|
||||
<a href="/dl/%s/%s" class="btn btn-primary" style="flex:1; text-align:center; text-decoration:none; font-size:0.85rem;" download>⬇ Download Now</a>
|
||||
</div>
|
||||
|
|
@ -470,7 +466,7 @@ func (h *MonthHandler) GenerateMonthlyReport(w http.ResponseWriter, r *http.Requ
|
|||
<div id="send-link-result"></div>
|
||||
</div>
|
||||
</div>`,
|
||||
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, "")
|
||||
|
|
|
|||
Loading…
Reference in a new issue