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.go203
1 files changed, 202 insertions, 1 deletions
diff --git a/internal/reports/pdf.go b/internal/reports/pdf.go
index 96630cf..8434f07 100644
--- a/internal/reports/pdf.go
+++ b/internal/reports/pdf.go
@@ -55,7 +55,7 @@ func GenerateInvoicePDF(invoiceData *InvoiceData, outputPath string) error {
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
- defer os.RemoveAll(tempDir)
+ defer func() { _ = os.RemoveAll(tempDir) }()
// Create JSON data for Typst template
jsonData := InvoiceJSONData{
@@ -129,3 +129,204 @@ func GenerateDefaultInvoiceFilename(clientName, projectName string, dateRange Da
return fmt.Sprintf("invoice_%s_%s_%s.pdf", name, dateStr, timestamp)
}
+
+// TimesheetJSONData represents the data structure for the JSON file that Typst will consume
+type TimesheetJSONData 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"`
+ ContractorName string `json:"contractor_name"`
+ ContractorLabel string `json:"contractor_label"`
+ ContractorEmail string `json:"contractor_email"`
+ Entries []TimesheetEntry `json:"entries"`
+ TotalHours float64 `json:"total_hours"`
+ Timezone string `json:"timezone"`
+}
+
+func GenerateTimesheetPDF(timesheetData *TimesheetData, 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-timesheet")
+ if err != nil {
+ return fmt.Errorf("failed to create temp directory: %w", err)
+ }
+ defer func() { _ = os.RemoveAll(tempDir) }()
+
+ // Create JSON data for Typst template
+ jsonData := TimesheetJSONData{
+ ClientName: timesheetData.ClientName,
+ ProjectName: timesheetData.ProjectName,
+ DateRangeStart: timesheetData.DateRange.Start.Format("2006-01-02"),
+ DateRangeEnd: timesheetData.DateRange.End.Format("2006-01-02"),
+ GeneratedDate: timesheetData.GeneratedDate.Format("2006-01-02"),
+ ContractorName: timesheetData.ContractorName,
+ ContractorLabel: timesheetData.ContractorLabel,
+ ContractorEmail: timesheetData.ContractorEmail,
+ Entries: timesheetData.Entries,
+ TotalHours: timesheetData.TotalHours,
+ Timezone: timesheetData.Timezone,
+ }
+
+ // 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, "timesheet.typ")
+ if err := os.WriteFile(typstFile, []byte(templates.TimesheetTemplate), 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 GenerateDefaultTimesheetFilename(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("timesheet_%s_%s_%s.pdf", name, dateStr, timestamp)
+}
+
+// UnifiedJSONData represents the unified data structure containing all fields from both invoice and timesheet
+type UnifiedJSONData struct {
+ // Common fields
+ 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"`
+ ContractorName string `json:"contractor_name"`
+ ContractorLabel string `json:"contractor_label"`
+ ContractorEmail string `json:"contractor_email"`
+
+ // Invoice-specific fields
+ InvoiceNumber string `json:"invoice_number"`
+ LineItems []LineItem `json:"line_items"`
+ TotalAmount float64 `json:"total_amount"`
+
+ // Timesheet-specific fields
+ Entries []TimesheetEntry `json:"entries"`
+ Timezone string `json:"timezone"`
+
+ // Shared field with same value
+ TotalHours float64 `json:"total_hours"`
+}
+
+func GenerateUnifiedPDF(unifiedData *UnifiedReportData, 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-unified")
+ if err != nil {
+ return fmt.Errorf("failed to create temp directory: %w", err)
+ }
+ defer func() { _ = os.RemoveAll(tempDir) }()
+
+ // Create unified JSON data containing all fields needed by both templates
+ jsonData := UnifiedJSONData{
+ // Common fields (from invoice data, but both should be identical)
+ ClientName: unifiedData.InvoiceData.ClientName,
+ ProjectName: unifiedData.InvoiceData.ProjectName,
+ DateRangeStart: unifiedData.InvoiceData.DateRange.Start.Format("2006-01-02"),
+ DateRangeEnd: unifiedData.InvoiceData.DateRange.End.Format("2006-01-02"),
+ GeneratedDate: unifiedData.InvoiceData.GeneratedDate.Format("2006-01-02"),
+ ContractorName: unifiedData.InvoiceData.ContractorName,
+ ContractorLabel: unifiedData.InvoiceData.ContractorLabel,
+ ContractorEmail: unifiedData.InvoiceData.ContractorEmail,
+ TotalHours: unifiedData.InvoiceData.TotalHours, // Should match timesheet total
+
+ // Invoice-specific fields
+ InvoiceNumber: fmt.Sprintf("%04d-%02d-%03d",
+ unifiedData.InvoiceData.DateRange.Start.Year(),
+ unifiedData.InvoiceData.DateRange.Start.Month(),
+ unifiedData.InvoiceData.InvoiceNumber,
+ ),
+ LineItems: unifiedData.InvoiceData.LineItems,
+ TotalAmount: unifiedData.InvoiceData.TotalAmount,
+
+ // Timesheet-specific fields
+ Entries: unifiedData.TimesheetData.Entries,
+ Timezone: unifiedData.TimesheetData.Timezone,
+ }
+
+ // 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)
+ }
+
+ // Create unified template by combining invoice and timesheet templates
+ unifiedTemplate := templates.InvoiceTemplate + "\n\n#pagebreak()\n\n" + templates.TimesheetTemplate
+
+ // Write Typst template file
+ typstFile := filepath.Join(tempDir, "unified.typ")
+ if err := os.WriteFile(typstFile, []byte(unifiedTemplate), 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 GenerateDefaultUnifiedFilename(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("unified_%s_%s_%s.pdf", name, dateStr, timestamp)
+}