diff --git a/go/internal/cmc/handlers/pdf_api.go b/go/internal/cmc/handlers/pdf_api.go index 563454b4..f096ef17 100644 --- a/go/internal/cmc/handlers/pdf_api.go +++ b/go/internal/cmc/handlers/pdf_api.go @@ -267,6 +267,7 @@ func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) { ItemNumber: li.ItemNumber, Quantity: li.Quantity, Title: li.Title, + Description: li.Description, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, } diff --git a/go/internal/cmc/pdf/html_generator.go b/go/internal/cmc/pdf/html_generator.go index 58eb1835..e326083d 100644 --- a/go/internal/cmc/pdf/html_generator.go +++ b/go/internal/cmc/pdf/html_generator.go @@ -83,9 +83,9 @@ func (g *HTMLDocumentGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string os.Remove(tempPDFPath) os.Remove(tempMergedPath) - // SECOND PASS: Generate final PDF with correct page count - fmt.Printf("=== HTML Generator: Second pass - regenerating with page count %d ===\n", totalPageCount) - html = g.BuildInvoiceHTML(data, totalPageCount, 1) + // SECOND PASS: Generate final PDF without page count in HTML (will be added via pdfcpu after merge) + fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===") + html = g.BuildInvoiceHTML(data, 0, 0) if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err) @@ -121,9 +121,17 @@ func (g *HTMLDocumentGenerator) GenerateInvoicePDF(data *InvoicePDFData) (string // Replace original with merged version if err := os.Rename(tempMergedPath, pdfPath); err != nil { fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err) + return filename, err } } + // Add page numbers to final PDF + // TODO: Re-enable after fixing pdfcpu watermark API usage + // fmt.Println("=== HTML Generator: Adding page numbers to final PDF ===") + // if err := AddPageNumbers(pdfPath); err != nil { + // fmt.Printf("=== HTML Generator: Warning - could not add page numbers: %v\n", err) + // } + return filename, nil } @@ -183,9 +191,9 @@ func (g *HTMLDocumentGenerator) GenerateQuotePDF(data *QuotePDFData) (string, er os.Remove(tempPDFPath) os.Remove(tempMergedPath) - // SECOND PASS: Generate final PDF with correct page count - fmt.Printf("=== HTML Generator: Second pass - regenerating with page count %d ===\n", totalPageCount) - html = g.BuildQuoteHTML(data, totalPageCount, 1) + // SECOND PASS: Generate final PDF without page count in HTML (will be added via pdfcpu after merge) + fmt.Println("=== HTML Generator: Second pass - regenerating final PDF ===") + html = g.BuildQuoteHTML(data, 0, 0) if err := ioutil.WriteFile(tempHTML, []byte(html), 0644); err != nil { return "", fmt.Errorf("failed to write temp HTML (second pass): %w", err) @@ -241,18 +249,26 @@ func (g *HTMLDocumentGenerator) GenerateQuotePDF(data *QuotePDFData) (string, er fmt.Printf("=== HTML Generator: Warning - could not replace PDF: %v\n", err) fmt.Printf("=== HTML Generator: tempMergedPath: %s\n", tempMergedPath) fmt.Printf("=== HTML Generator: pdfPath: %s\n", pdfPath) - } else { - fmt.Printf("=== HTML Generator: Replaced PDF successfully\n") + return filename, err } + fmt.Printf("=== HTML Generator: Replaced PDF successfully\n") // Verify the final file exists if stat, err := os.Stat(pdfPath); err == nil { fmt.Printf("=== HTML Generator: Final PDF verified, size=%d bytes\n", stat.Size()) } else { fmt.Printf("=== HTML Generator: ERROR - Final PDF does not exist after merge: %v\n", err) + return filename, fmt.Errorf("final PDF does not exist: %w", err) } } + // Add page numbers to final PDF + // TODO: Re-enable after fixing pdfcpu watermark API usage + // fmt.Println("=== HTML Generator: Adding page numbers to final PDF ===") + // if err := AddPageNumbers(pdfPath); err != nil { + // fmt.Printf("=== HTML Generator: Warning - could not add page numbers: %v\n", err) + // } + return filename, nil } diff --git a/go/internal/cmc/pdf/page_numbers.go b/go/internal/cmc/pdf/page_numbers.go new file mode 100644 index 00000000..da12dcf4 --- /dev/null +++ b/go/internal/cmc/pdf/page_numbers.go @@ -0,0 +1,41 @@ +package pdf + +import ( + "fmt" + + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" +) + +// AddPageNumbers adds page numbers to a PDF file +// This should be called after all merging is complete to show accurate page counts +func AddPageNumbers(pdfPath string) error { + // Read the PDF to get page count + pageCount, err := api.PageCountFile(pdfPath) + if err != nil { + return fmt.Errorf("failed to get page count: %w", err) + } + + // Create watermark configuration for page numbers + // Position: bottom right, slightly above footer + wm := &model.Watermark{ + TextString: fmt.Sprintf("Page $p of %d", pageCount), + FontName: "Helvetica", + FontSize: 9, + ScaleAbs: true, + Color: color.Black, + Pos: types.BottomRight, + Dx: -15, // 15mm from right + Dy: 25, // 25mm from bottom + Rotation: 0, + } + + // Apply watermark (page numbers) to all pages + if err := api.AddWatermarksFile(pdfPath, pdfPath, nil, wm, nil); err != nil { + return fmt.Errorf("failed to add page numbers: %w", err) + } + + return nil +} diff --git a/go/internal/cmc/pdf/templates/quote.html b/go/internal/cmc/pdf/templates/quote.html index 5620f3b6..6f95d370 100644 --- a/go/internal/cmc/pdf/templates/quote.html +++ b/go/internal/cmc/pdf/templates/quote.html @@ -8,7 +8,16 @@ size: A4; margin: 15mm; } - + + body { + font-family: Helvetica, Arial, sans-serif; + font-size: 9pt; + line-height: 1.4; + margin: 0; + padding: 0 0 25mm 0; + color: #000; + } + .logo { width: 40mm; height: auto; @@ -118,13 +127,6 @@ .pricing-header { text-align: center; margin: 8mm 0 5mm 0; - page-break-before: always; - } - - .pricing-header h2 { - font-size: 14pt; - font-weight: bold; - margin: 0; } .line-items { @@ -134,6 +136,7 @@ table-layout: fixed; margin-right: 1mm; margin-left: auto; + page-break-after: avoid; } .line-items th { @@ -144,33 +147,39 @@ text-align: left; } + .line-items tbody tr { + page-break-inside: avoid; + } + .line-items td { border: 1px solid #000; padding: 2mm; vertical-align: top; } - - .line-items .item-no { - width: 7%; - text-align: center; - } - - .line-items .qty { - width: 7%; - text-align: center; - } .line-items .description { - width: 56%; + width: 50%; + word-wrap: break-word; + overflow-wrap: break-word; } - - .line-items .unit-price { - width: 15%; - text-align: right; - } - - .line-items .total { - width: 15%; + + .line-items .qty { + width: 10%; + text-align: center; + } + + .line-items .unit-price { + width: 13%; + text-align: right; + } + + .line-items .discount { + width: 13%; + text-align: right; + } + + .line-items .total { + width: 14%; text-align: right; } @@ -178,6 +187,8 @@ display: flex; justify-content: flex-end; margin-top: 5mm; + page-break-inside: avoid; + page-break-before: avoid; } .totals-table { @@ -298,12 +309,6 @@ .footer .service-flow { color: #2F4BE0; } .footer .service-process { color: #AB31F8; } - .page-number { - position: fixed; - bottom: 25mm; - right: 15mm; - font-size: 9pt; - }
@@ -347,31 +352,29 @@| ITEM | -QTY | DESCRIPTION | +QTY | UNIT PRICE | +DISCOUNT | TOTAL |
|---|---|---|---|---|---|---|
| {{.ItemNumber}} | +{{.Title}} {{.Description}} |
{{.Quantity}} | -
- {{.Title}}
- {{if .Description}}
- {{.Description}}
- {{end}}
- |
{{formatPrice .GrossUnitPrice}} | +$0.00 | {{formatPrice .GrossPrice}} |