diff options
Diffstat (limited to 'internal/reports/pdf.go')
-rw-r--r-- | internal/reports/pdf.go | 131 |
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) +} |