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() }