summaryrefslogtreecommitdiff
path: root/internal/reports/pdf.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/reports/pdf.go')
-rw-r--r--internal/reports/pdf.go131
1 files changed, 131 insertions, 0 deletions
diff --git a/internal/reports/pdf.go b/internal/reports/pdf.go
new file mode 100644
index 0000000..96630cf
--- /dev/null
+++ b/internal/reports/pdf.go
@@ -0,0 +1,131 @@
+package reports
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "time"
+
+ "punchcard/internal/queries"
+ "punchcard/templates"
+)
+
+// RecordInvoice records the invoice in the database after successful generation
+func RecordInvoice(q *queries.Queries, year, month, number, clientID, totalAmountCents int64) error {
+ _, err := q.CreateInvoice(context.Background(), queries.CreateInvoiceParams{
+ Year: year,
+ Month: month,
+ Number: number,
+ ClientID: clientID,
+ TotalAmount: totalAmountCents,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to record invoice in database: %w", err)
+ }
+ return nil
+}
+
+// InvoiceJSONData represents the data structure for the JSON file that Typst will consume
+type InvoiceJSONData struct {
+ ClientName string `json:"client_name"`
+ ProjectName string `json:"project_name"`
+ DateRangeStart string `json:"date_range_start"`
+ DateRangeEnd string `json:"date_range_end"`
+ GeneratedDate string `json:"generated_date"`
+ InvoiceNumber string `json:"invoice_number"`
+ ContractorName string `json:"contractor_name"`
+ ContractorLabel string `json:"contractor_label"`
+ ContractorEmail string `json:"contractor_email"`
+ LineItems []LineItem `json:"line_items"`
+ TotalHours float64 `json:"total_hours"`
+ TotalAmount float64 `json:"total_amount"`
+}
+
+func GenerateInvoicePDF(invoiceData *InvoiceData, outputPath string) error {
+ // Check if Typst is installed
+ if err := checkTypstInstalled(); err != nil {
+ return err
+ }
+
+ // Create temporary directory for template and data files
+ tempDir, err := os.MkdirTemp("", "punchcard-invoice")
+ if err != nil {
+ return fmt.Errorf("failed to create temp directory: %w", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ // Create JSON data for Typst template
+ jsonData := InvoiceJSONData{
+ ClientName: invoiceData.ClientName,
+ ProjectName: invoiceData.ProjectName,
+ DateRangeStart: invoiceData.DateRange.Start.Format("2006-01-02"),
+ DateRangeEnd: invoiceData.DateRange.End.Format("2006-01-02"),
+ GeneratedDate: invoiceData.GeneratedDate.Format("2006-01-02"),
+ InvoiceNumber: fmt.Sprintf("%04d-%02d-%03d",
+ invoiceData.DateRange.Start.Year(),
+ invoiceData.DateRange.Start.Month(),
+ invoiceData.InvoiceNumber,
+ ),
+ ContractorName: invoiceData.ContractorName,
+ ContractorLabel: invoiceData.ContractorLabel,
+ ContractorEmail: invoiceData.ContractorEmail,
+ LineItems: invoiceData.LineItems,
+ TotalHours: invoiceData.TotalHours,
+ TotalAmount: invoiceData.TotalAmount,
+ }
+
+ // Write JSON data file
+ dataFile := filepath.Join(tempDir, "data.json")
+ jsonBytes, err := json.MarshalIndent(jsonData, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal JSON data: %w", err)
+ }
+
+ if err := os.WriteFile(dataFile, jsonBytes, 0o644); err != nil {
+ return fmt.Errorf("failed to write JSON data file: %w", err)
+ }
+
+ // Write Typst template file
+ typstFile := filepath.Join(tempDir, "invoice.typ")
+ if err := os.WriteFile(typstFile, []byte(templates.InvoiceTemplate), 0o644); err != nil {
+ return fmt.Errorf("failed to write Typst template file: %w", err)
+ }
+
+ // Generate PDF using Typst
+ cmd := exec.Command("typst", "compile", typstFile, outputPath)
+ cmd.Dir = tempDir // Set working directory so Typst can find data.json
+
+ if output, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("failed to generate PDF: %w\nTypst output: %s", err, string(output))
+ }
+
+ return nil
+}
+
+func checkTypstInstalled() error {
+ _, err := exec.LookPath("typst")
+ if err != nil {
+ return fmt.Errorf("typst is not installed or not in PATH. Please install Typst from https://typst.org/")
+ }
+ return nil
+}
+
+func GenerateDefaultInvoiceFilename(clientName, projectName string, dateRange DateRange) string {
+ var name string
+ if projectName != "" {
+ name = fmt.Sprintf("%s_%s", clientName, projectName)
+ } else {
+ name = clientName
+ }
+
+ // Replace spaces and special characters
+ name = filepath.Base(name)
+
+ dateStr := dateRange.Start.Format("2006-01")
+ timestamp := time.Now().Format("20060102_150405")
+
+ return fmt.Sprintf("invoice_%s_%s_%s.pdf", name, dateStr, timestamp)
+}