apskel-pos-backend/internal/service/report_pdf_util.go
Aditya Siregar 5a42523f0f Update
2025-08-10 20:41:34 +07:00

134 lines
3.4 KiB
Go

package service
import (
"bytes"
"fmt"
"html/template"
"os"
"os/exec"
"path/filepath"
)
func renderTemplateToPDF(templatePath string, data interface{}) ([]byte, error) {
// Create template with custom functions
funcMap := template.FuncMap{
"add": func(a, b int) int {
return a + b
},
}
// Parse and execute HTML template
tmpl, err := template.New(filepath.Base(templatePath)).Funcs(funcMap).ParseFiles(templatePath)
if err != nil {
return nil, fmt.Errorf("parse template: %w", err)
}
var htmlBuf bytes.Buffer
if err := tmpl.Execute(&htmlBuf, data); err != nil {
return nil, fmt.Errorf("execute template: %w", err)
}
// Write HTML to a temp file
tmpDir, err := os.MkdirTemp("", "daily_report_")
if err != nil {
return nil, fmt.Errorf("tmp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
htmlPath := filepath.Join(tmpDir, "report.html")
pdfPath := filepath.Join(tmpDir, "report.pdf")
if err := os.WriteFile(htmlPath, htmlBuf.Bytes(), 0644); err != nil {
return nil, fmt.Errorf("write html: %w", err)
}
// Use Chrome headless for better CSS rendering
chromeArgs := []string{
"--headless",
"--disable-gpu",
"--no-sandbox",
"--disable-dev-shm-usage",
"--print-to-pdf=" + pdfPath,
"--print-to-pdf-no-header",
"--print-to-pdf-no-footer",
"--print-to-pdf-margin-top=0",
"--print-to-pdf-margin-bottom=0",
"--print-to-pdf-margin-left=0",
"--print-to-pdf-margin-right=0",
"--print-to-pdf-paper-width=210mm",
"--print-to-pdf-paper-height=297mm",
htmlPath,
}
// Try Google Chrome first, then Chromium, then wkhtmltopdf as fallback
chromeCmd := exec.Command("google-chrome", chromeArgs...)
if out, err := chromeCmd.CombinedOutput(); err != nil {
// Fallback to Chromium
chromiumCmd := exec.Command("chromium", chromeArgs...)
if chromiumOut, err := chromiumCmd.CombinedOutput(); err != nil {
// Final fallback to wkhtmltopdf
wkhtmlArgs := []string{
"--enable-local-file-access",
"--dpi", "300",
"--page-size", "A4",
"--orientation", "Portrait",
"--margin-top", "0",
"--margin-bottom", "0",
"--margin-left", "0",
"--margin-right", "0",
htmlPath,
pdfPath,
}
wkhtmlCmd := exec.Command("wkhtmltopdf", wkhtmlArgs...)
if wkhtmlOut, err := wkhtmlCmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("all PDF generators failed. Chrome error: %v, Chromium error: %v, wkhtmltopdf error: %v, chrome output: %s, chromium output: %s, wkhtmltopdf output: %s", chromeCmd.Err, chromiumCmd.Err, err, string(out), string(chromiumOut), string(wkhtmlOut))
}
}
}
// Read PDF bytes
pdfBytes, err := os.ReadFile(pdfPath)
if err != nil {
return nil, fmt.Errorf("read pdf: %w", err)
}
return pdfBytes, nil
}
func formatCurrency(amount float64) string {
// Simple currency formatting: Rp with thousands separators
// Note: For exact locale formatting, integrate a locale library later
s := fmt.Sprintf("%.0f", amount) // Remove decimal places for cleaner display
intPart := addThousandsSep(s)
return "Rp " + intPart
}
func splitAmount(s string) (string, string) {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
return s[:i], s[i+1:]
}
}
return s, "00"
}
func addThousandsSep(s string) string {
n := len(s)
if n <= 3 {
return s
}
var out bytes.Buffer
pre := n % 3
if pre > 0 {
out.WriteString(s[:pre])
if n > pre {
out.WriteByte('.')
}
}
for i := pre; i < n; i += 3 {
out.WriteString(s[i : i+3])
if i+3 < n {
out.WriteByte('.')
}
}
return out.String()
}