diff options
author | T <t@tjp.lol> | 2025-08-04 15:34:23 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-04 19:49:08 -0600 |
commit | dc895cec9d8a84af89ce2501db234dff33c757e2 (patch) | |
tree | 8c961466f0769616b3a82da91f4cde4d3a881b73 /internal/reports/pdf.go | |
parent | 56e0af3b41742876b471332aeb943a5a2ca8dfbf (diff) |
timesheet and unified reports
Diffstat (limited to 'internal/reports/pdf.go')
-rw-r--r-- | internal/reports/pdf.go | 203 |
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) +} |