summaryrefslogtreecommitdiff
path: root/internal/reports/pdf.go
blob: 96630cf0f4fe430fc31aa122a759c153e5ea2295 (plain)
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)
}