rentease/internal/driver/pdf/html.go
Ruidy 24f289b767
feat(pdf): redesign invoice PDF layout and structure
Refactored the invoice PDF generation to use a modular, visually
improved
layout. Extracted header, details, items, payments, summary, and notes
sections into dedicated functions for clarity and maintainability.
Introduced color constants and improved table formatting for better
readability. Updated labels and formatting to enhance the overall
appearance and user experience of generated invoices.
2026-03-27 14:35:52 +01:00

305 lines
10 KiB
Go

package pdf
import (
"bytes"
"fmt"
"log"
"os"
"text/template"
"github.com/jung-kurt/gofpdf/v2"
"github.com/rjNemo/rentease/assets"
"github.com/rjNemo/rentease/internal/service/booking"
)
type HTMLPdfClient struct{}
const (
invoiceMarginX = 10.0
invoiceContentWidth = 190.0
invoiceHeaderHeight = 28.0
invoiceTableRowH = 8.5
invoiceSummaryLineH = 10.0
invoiceNotesPadding = 3.0
)
type rgbColor struct {
r int
g int
b int
}
var (
invoiceTeal = rgbColor{0, 123, 143}
invoiceLightGray = rgbColor{244, 244, 244}
invoiceMidGray = rgbColor{221, 221, 221}
invoiceTextGray = rgbColor{85, 85, 85}
invoiceNoteBg = rgbColor{237, 247, 249}
)
func NewPdfClient() (*HTMLPdfClient, error) {
return &HTMLPdfClient{}, nil
}
func (pc *HTMLPdfClient) BuildInvoice(data booking.Invoice) (*booking.GeneratedFile, error) {
pdf := gofpdf.New("P", "mm", "A4", "")
pdf.SetMargins(invoiceMarginX, 12, invoiceMarginX)
pdf.SetAutoPageBreak(true, 14)
pdf.AddPage()
tr := pdf.UnicodeTranslatorFromDescriptor("")
drawInvoiceHeader(pdf, tr, data)
drawInvoiceDetails(pdf, tr, data)
drawInvoiceItemsTable(pdf, tr, data)
drawInvoicePayments(pdf, tr, data)
drawInvoiceSummary(pdf, tr, data)
drawInvoiceNotes(pdf, tr, data)
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, fmt.Errorf("error writing PDF file: %v", err)
}
return &booking.GeneratedFile{
Name: fmt.Sprintf("%s.pdf", data.ID),
ContentType: "application/pdf",
Data: buf.Bytes(),
}, nil
}
func (pc *HTMLPdfClient) BuildReport(report booking.ReportData, period string, month int, year int) (string, error) {
tmpl, err := template.ParseFS(assets.Static, "assets/html/report.html")
if err != nil {
return "", fmt.Errorf("error parsing template: %v", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, report); err != nil {
log.Printf("err: %+v\n", err)
return "", fmt.Errorf("error executing template: %v", err)
}
outputPath := fmt.Sprintf("report-%s-%d-%d.html", period, month, year)
if err := os.WriteFile(outputPath, buf.Bytes(), 0o644); err != nil {
log.Printf("err: %+v\n", err)
return "", fmt.Errorf("error writing HTML file: %v", err)
}
return outputPath, nil
}
func drawInvoiceHeader(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
pdf.SetFillColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
pdf.Rect(invoiceMarginX, 12, invoiceContentWidth, invoiceHeaderHeight, "F")
if logo, err := assets.Static.ReadFile("assets/img/logo.png"); err == nil {
info := pdf.RegisterImageOptionsReader(
"invoice-logo",
gofpdf.ImageOptions{ImageType: "PNG"},
bytes.NewReader(logo),
)
if info != nil {
pdf.ImageOptions("invoice-logo", 14, 15.5, 17, 17, false, gofpdf.ImageOptions{ImageType: "PNG"}, 0, "")
}
}
pdf.SetTextColor(255, 255, 255)
pdf.SetXY(110, 14.5)
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(86, 4.8, tr(data.Host.Name), "", 1, "R", false, 0, "")
pdf.SetFont("Helvetica", "", 8.8)
hostLines := []string{
data.Host.Address,
fmt.Sprintf("%s %s", data.Host.ZipCode, data.Host.City),
fmt.Sprintf("Tel : %s", data.Host.PhoneNumber),
fmt.Sprintf("Mail : %s", data.Host.Email),
}
for _, line := range hostLines {
pdf.SetX(110)
pdf.CellFormat(86, 4.0, tr(line), "", 1, "R", false, 0, "")
}
pdf.SetTextColor(0, 0, 0)
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
pdf.Line(invoiceMarginX, 45, invoiceMarginX+invoiceContentWidth, 45)
pdf.SetY(49)
}
func drawInvoiceDetails(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
leftX := invoiceMarginX + 4
rightX := 103.0
labelColor := invoiceTextGray
writeDetail := func(x float64, label, value string, boldValue bool) {
pdf.SetX(x)
pdf.SetFont("Helvetica", "B", 10.5)
pdf.SetTextColor(labelColor.r, labelColor.g, labelColor.b)
pdf.CellFormat(38, 7, tr(label), "", 0, "", false, 0, "")
pdf.SetTextColor(0, 0, 0)
style := ""
if boldValue {
style = "B"
}
pdf.SetFont("Helvetica", style, 10.5)
pdf.CellFormat(48, 7, tr(value), "", 1, "", false, 0, "")
}
pdf.SetFont("Helvetica", "B", 12)
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
pdf.SetX(leftX)
pdf.CellFormat(86, 7, tr(data.Name), "", 0, "", false, 0, "")
pdf.SetX(rightX)
pdf.CellFormat(86, 7, tr("N° de facture :"), "", 0, "", false, 0, "")
pdf.SetTextColor(0, 0, 0)
pdf.SetFont("Helvetica", "", 11)
pdf.CellFormat(20, 7, tr(data.ID), "", 1, "R", false, 0, "")
writeDetail(leftX, "Tel :", emptyFallback(data.PhoneNumber), false)
writeDetail(rightX, "Du :", data.From, false)
writeDetail(leftX, "Nombre de clients :", fmt.Sprintf("%d", data.CustomersNumber), false)
writeDetail(rightX, "Au :", data.To, false)
writeDetail(leftX, "Plateforme :", data.Platform, false)
writeDetail(rightX, "Montant Total :", euroAmount(data.Total), false)
pdf.Ln(4)
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
pdf.Line(invoiceMarginX, pdf.GetY(), invoiceMarginX+invoiceContentWidth, pdf.GetY())
pdf.Ln(5)
}
func drawInvoiceItemsTable(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
widths := []float64{70, 28, 39, 53}
headers := []string{"Objet", "Quantité", "Prix (€)", "Total (€)"}
writeTableHeader(pdf, tr, widths, headers)
pdf.SetFont("Helvetica", "", 10.5)
pdf.SetTextColor(0, 0, 0)
for i, line := range data.Lines {
fill := i%2 == 0
if fill {
pdf.SetFillColor(invoiceLightGray.r, invoiceLightGray.g, invoiceLightGray.b)
}
pdf.CellFormat(widths[0], invoiceTableRowH, tr(line.Name), "1", 0, "", fill, 0, "")
pdf.CellFormat(widths[1], invoiceTableRowH, fmt.Sprintf("%d", line.Quantity), "1", 0, "C", fill, 0, "")
pdf.CellFormat(widths[2], invoiceTableRowH, line.Price, "1", 0, "R", fill, 0, "")
pdf.CellFormat(widths[3], invoiceTableRowH, line.Total, "1", 1, "R", fill, 0, "")
}
pdf.Ln(6)
}
func drawInvoicePayments(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
pdf.SetFont("Helvetica", "B", 13)
pdf.SetTextColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
pdf.CellFormat(0, 7, tr("Historique des Paiements"), "", 1, "", false, 0, "")
pdf.Ln(1)
widths := []float64{54, 84, 52}
headers := []string{"Date", "Mode de Paiement", "Montant (€)"}
writeTableHeader(pdf, tr, widths, headers)
pdf.SetFont("Helvetica", "", 10.5)
pdf.SetTextColor(0, 0, 0)
if len(data.Payments) == 0 {
pdf.SetFillColor(invoiceLightGray.r, invoiceLightGray.g, invoiceLightGray.b)
pdf.CellFormat(widths[0], invoiceTableRowH, tr("-"), "1", 0, "", true, 0, "")
pdf.CellFormat(widths[1], invoiceTableRowH, tr("Aucun paiement enregistré"), "1", 0, "", true, 0, "")
pdf.CellFormat(widths[2], invoiceTableRowH, tr("0.00"), "1", 1, "R", true, 0, "")
} else {
for i, payment := range data.Payments {
fill := i%2 == 0
if fill {
pdf.SetFillColor(invoiceLightGray.r, invoiceLightGray.g, invoiceLightGray.b)
}
pdf.CellFormat(widths[0], invoiceTableRowH, tr(payment.Date), "1", 0, "", fill, 0, "")
pdf.CellFormat(widths[1], invoiceTableRowH, tr(payment.Method), "1", 0, "", fill, 0, "")
pdf.CellFormat(widths[2], invoiceTableRowH, payment.Amount, "1", 1, "R", fill, 0, "")
}
}
pdf.Ln(6)
}
func drawInvoiceSummary(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
labels := []string{"Montant Total :", "Montant Payé :", "Solde Restant :"}
values := []string{data.Total, data.AmountPaid, data.BalanceDue}
for i := range labels {
pdf.SetX(invoiceMarginX + 3)
pdf.SetFont("Helvetica", "B", 11)
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
pdf.CellFormat(60, invoiceSummaryLineH, tr(labels[i]), "", 0, "", false, 0, "")
pdf.SetFont("Helvetica", "", 11)
pdf.SetTextColor(0, 0, 0)
pdf.CellFormat(95, invoiceSummaryLineH, "", "", 0, "", false, 0, "")
pdf.CellFormat(32, invoiceSummaryLineH, values[i], "", 1, "R", false, 0, "")
}
pdf.Ln(2)
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
pdf.Line(invoiceMarginX, pdf.GetY(), invoiceMarginX+invoiceContentWidth, pdf.GetY())
pdf.Ln(6)
}
func drawInvoiceNotes(pdf *gofpdf.Fpdf, tr func(string) string, data booking.Invoice) {
notes := "Notes\nTVA non applicable, art. 293 B du CGI\nDispensé d'immatriculation au registre du commerce et des sociétés (RCS) et au répertoire des métiers.\nConditions de paiement : paiement à réception de facture. Aucun escompte consenti pour règlement anticipé ou désistement. Tout incident de paiement est passible d'intérêts de retard. Le montant des pénalités résulte de l'application aux sommes restant dues d'un taux d'intérêt légal en vigueur au moment de l'incident.\nIl sera demandé 15€ par jour par personne supplémentaire."
pdf.SetFont("Helvetica", "", 7.5)
lines := pdf.SplitLines([]byte(tr(notes)), invoiceContentWidth-(invoiceNotesPadding*2))
noteBoxHeight := float64(len(lines))*3.2 + invoiceNotesPadding*2
totalBlockHeight := noteBoxHeight + 18
if pdf.GetY()+totalBlockHeight > 282 {
pdf.AddPage()
}
y := pdf.GetY()
pdf.SetFillColor(invoiceNoteBg.r, invoiceNoteBg.g, invoiceNoteBg.b)
pdf.Rect(invoiceMarginX, y, invoiceContentWidth, noteBoxHeight, "F")
pdf.SetXY(invoiceMarginX+invoiceNotesPadding, y+invoiceNotesPadding)
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
pdf.MultiCell(invoiceContentWidth-(invoiceNotesPadding*2), 3.2, tr(notes), "", "L", false)
pdf.Ln(4)
pdf.SetDrawColor(invoiceMidGray.r, invoiceMidGray.g, invoiceMidGray.b)
pdf.Line(invoiceMarginX, pdf.GetY(), invoiceMarginX+invoiceContentWidth, pdf.GetY())
pdf.Ln(4)
pdf.SetTextColor(invoiceTextGray.r, invoiceTextGray.g, invoiceTextGray.b)
pdf.SetFont("Helvetica", "B", 11)
pdf.CellFormat(0, 5, tr("Total"), "", 1, "R", false, 0, "")
pdf.SetTextColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
pdf.SetFont("Helvetica", "B", 15)
pdf.CellFormat(0, 7, tr(euroAmount(data.Total)), "", 0, "R", false, 0, "")
}
func writeTableHeader(pdf *gofpdf.Fpdf, tr func(string) string, widths []float64, headers []string) {
pdf.SetFillColor(invoiceTeal.r, invoiceTeal.g, invoiceTeal.b)
pdf.SetTextColor(255, 255, 255)
pdf.SetFont("Helvetica", "B", 10)
alignments := []string{"L", "C", "R", "R"}
if len(widths) == 3 {
alignments = []string{"L", "L", "R"}
}
for i, header := range headers {
end := 0
if i == len(headers)-1 {
end = 1
}
pdf.CellFormat(widths[i], 8, tr(header), "1", end, alignments[i], true, 0, "")
}
}
func euroAmount(value string) string {
return fmt.Sprintf("%s €", value)
}
func emptyFallback(value string) string {
if value == "" {
return "-"
}
return value
}