1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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)
}
|