summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorT <t@tjp.lol>2026-02-23 11:27:12 -0700
committerT <t@tjp.lol>2026-02-23 11:30:58 -0700
commitf9b23f5c12eda13c6014f6291bdeecb77bb40141 (patch)
tree85a8cd0b4a44830c0176dfe3b81240a81bb21c8f
parentb53c952fc8a054d935d90db2ccf3f0b897e6c771 (diff)
PDF design tweaksHEADmain
-rw-r--r--internal/reports/invoice.go8
-rw-r--r--internal/reports/testdata/invoice_test_data.json28
-rw-r--r--templates/invoice.typ18
-rw-r--r--templates/timesheet.typ74
4 files changed, 60 insertions, 68 deletions
diff --git a/internal/reports/invoice.go b/internal/reports/invoice.go
index 4ac5eb4..503851e 100644
--- a/internal/reports/invoice.go
+++ b/internal/reports/invoice.go
@@ -106,7 +106,7 @@ func GenerateInvoiceData(
return nil, fmt.Errorf("unsupported entry type")
}
- lineItems := groupTimeEntriesIntoLineItems(timeEntries)
+ lineItems := groupTimeEntriesIntoLineItems(timeEntries, contractor.Label)
totalHours := 0.0
totalAmount := 0.0
@@ -133,7 +133,7 @@ func GenerateInvoiceData(
return invoice, nil
}
-func groupTimeEntriesIntoLineItems(entries []timeEntryData) []LineItem {
+func groupTimeEntriesIntoLineItems(entries []timeEntryData, contractorLabel string) []LineItem {
var lineItems []LineItem
// Group 1: Entries with overridden rates
@@ -187,13 +187,13 @@ func groupTimeEntriesIntoLineItems(entries []timeEntryData) []LineItem {
// Process client rates
for _, entries := range clientRateGroups {
if len(entries) > 0 {
- clientName := entries[0].ClientName
rateCents := int64(0)
if entries[0].ClientBillableRate.Valid {
rateCents = entries[0].ClientBillableRate.Int64
}
- lineItem := createLineItem(entries, rateCents, fmt.Sprintf("General work - %s", clientName))
+ clientName := entries[0].ClientName
+ lineItem := createLineItem(entries, rateCents, fmt.Sprintf("%s - %s", contractorLabel, clientName))
lineItems = append(lineItems, lineItem)
}
}
diff --git a/internal/reports/testdata/invoice_test_data.json b/internal/reports/testdata/invoice_test_data.json
index a6adc28..499f104 100644
--- a/internal/reports/testdata/invoice_test_data.json
+++ b/internal/reports/testdata/invoice_test_data.json
@@ -1,26 +1,32 @@
{
- "client_name": "Test Client",
- "project_name": "Test Project",
+ "client_name": "Acme Corporation",
+ "project_name": "",
"date_range_start": "2025-07-01",
"date_range_end": "2025-07-31",
"generated_date": "2025-08-04",
"invoice_number": "2025-07-001",
"line_items": [
{
- "description": "Development work",
- "hours": 8.5,
+ "description": "Software Development - Acme Corporation",
+ "hours": 14.5,
"rate": 150.0,
- "amount": 1275.0
+ "amount": 2175.0
},
{
- "description": "Code review and testing",
- "hours": 2.25,
- "rate": 150.0,
- "amount": 337.5
+ "description": "Platform Modernization",
+ "hours": 8.0,
+ "rate": 175.0,
+ "amount": 1400.0
+ },
+ {
+ "description": "Custom rate work",
+ "hours": 3.0,
+ "rate": 225.0,
+ "amount": 675.0
}
],
- "total_hours": 10.75,
- "total_amount": 1612.5,
+ "total_hours": 25.5,
+ "total_amount": 4250.0,
"contractor_name": "Travis Parker",
"contractor_label": "Software Development",
"contractor_email": "travis.parker@gmail.com"
diff --git a/templates/invoice.typ b/templates/invoice.typ
index 7dacc59..b23f64b 100644
--- a/templates/invoice.typ
+++ b/templates/invoice.typ
@@ -44,7 +44,9 @@
]
]
- v(3em)
+ v(0.5em)
+ line(length: 100%, stroke: 0.4pt + rgb("#d1d5db"))
+ v(2em)
// Invoice title and number
grid(
@@ -91,16 +93,16 @@
#let professional-table() = {
table(
columns: (1fr, auto, auto, auto),
- stroke: (x, y) => if y == 0 or y == 1 { (bottom: 0.8pt + black) } else { none },
- inset: (x: 8pt, y: 12pt),
+ stroke: (x, y) => if y == 0 { (bottom: 0.8pt + black) } else if y > 1 { (top: 0.4pt + rgb("#e5e7eb")) } else { none },
+ inset: (x: 10pt, y: 12pt),
align: (left, center, center, right),
- column-gutter: 12pt,
+ fill: (x, y) => if y == 0 { rgb("#f8f9fa") } else { none },
// Header
- table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[DESCRIPTION]],
- table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[HOURS]],
- table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[RATE]],
- table.cell(fill: rgb("#f8f9fa"))[#text(weight: "bold", size: 9pt)[AMOUNT]],
+ text(weight: "bold", size: 9pt)[DESCRIPTION],
+ text(weight: "bold", size: 9pt)[HOURS],
+ text(weight: "bold", size: 9pt)[RATE],
+ text(weight: "bold", size: 9pt)[AMOUNT],
// Line items
..data.line_items.map(item => (
diff --git a/templates/timesheet.typ b/templates/timesheet.typ
index e888615..d008bbf 100644
--- a/templates/timesheet.typ
+++ b/templates/timesheet.typ
@@ -35,7 +35,9 @@
]
]
- v(3em)
+ v(0.5em)
+ line(length: 100%, stroke: 0.4pt + rgb("#d1d5db"))
+ v(2em)
// Timesheet title
grid(
@@ -83,58 +85,40 @@
#let timesheet-table() = {
let grouped = group-by-date(data.entries)
let sorted-dates = grouped.keys().sorted()
-
- // Table header
+
table(
- columns: (auto, auto, auto, auto, 1fr, 1fr),
- stroke: (x, y) => if y == 0 { (bottom: 0.8pt + black) } else { none },
- inset: (x: 8pt, y: 4pt),
- align: (center, center, center, center, left, left),
- column-gutter: 8pt,
-
- // Header row with extra vertical padding
- table.cell(fill: rgb("#f8f9fa"), inset: (x: 8pt, y: 12pt))[#text(weight: "bold", size: 9pt)[DATE]],
- table.cell(fill: rgb("#f8f9fa"), inset: (x: 8pt, y: 12pt))[#text(weight: "bold", size: 9pt)[START]],
- table.cell(fill: rgb("#f8f9fa"), inset: (x: 8pt, y: 12pt))[#text(weight: "bold", size: 9pt)[END]],
- table.cell(fill: rgb("#f8f9fa"), inset: (x: 8pt, y: 12pt))[#text(weight: "bold", size: 9pt)[DURATION]],
- table.cell(fill: rgb("#f8f9fa"), inset: (x: 8pt, y: 12pt))[#text(weight: "bold", size: 9pt)[PROJECT]],
- table.cell(fill: rgb("#f8f9fa"), inset: (x: 8pt, y: 12pt))[#text(weight: "bold", size: 9pt)[DESCRIPTION]],
-
- // Data rows grouped by date
+ columns: (auto, auto, 1fr, 2fr),
+ stroke: none,
+ inset: (x: 10pt, y: 4pt),
+ align: (center, left, left, left),
+ fill: (x, y) => if y == 0 { rgb("#f8f9fa") } else { none },
+
+ // Header row
+ table.cell(inset: (x: 10pt, y: 12pt))[#text(weight: "bold", size: 9pt)[DATE]],
+ table.cell(inset: (x: 10pt, y: 12pt))[#text(weight: "bold", size: 9pt)[DURATION]],
+ table.cell(inset: (x: 10pt, y: 12pt))[#text(weight: "bold", size: 9pt)[PROJECT]],
+ table.cell(inset: (x: 10pt, y: 12pt))[#text(weight: "bold", size: 9pt)[DESCRIPTION]],
+
+ table.hline(stroke: 0.8pt + black),
+
..for date in sorted-dates {
let entries = grouped.at(date)
- let daily-total = entries.map(entry => entry.hours).sum()
-
- // Create rows for this date
- let date-rows = ()
-
- // Add all entries for this date
+ let rows = ()
for (i, entry) in entries.enumerate() {
- let date-text = if i == 0 { date } else { "" }
-
- date-rows.push((
- text(size: 9pt, weight: "medium")[#date-text],
- text(size: 9pt)[#entry.start_time],
- text(size: 9pt)[#entry.end_time],
- text(size: 9pt)[#entry.duration],
+ rows.push((
+ text(size: 9pt, weight: "medium")[#if i == 0 { date }],
+ text(size: 9pt)[#entry.duration #text(fill: gray)[(#entry.start_time–#entry.end_time)]],
text(size: 9pt)[#entry.project_name],
- text(size: 9pt)[#entry.description]
+ text(size: 9pt)[#entry.description],
))
}
-
- // Add daily subtotal row
- date-rows.push((
- table.cell(colspan: 3, align: right)[#text(size: 9pt, weight: "medium", fill: gray)[Daily Total:]],
- text(size: 9pt, weight: "medium")[#format-hours(daily-total)],
- table.cell(colspan: 2)[]
+ // Thin separator between days
+ rows.push((
+ table.cell(colspan: 4, inset: (x: 0pt, y: 0pt))[
+ #line(length: 100%, stroke: 0.4pt + rgb("#e5e7eb"))
+ ],
))
-
- // Add separator line after each day
- date-rows.push((
- table.cell(colspan: 6, stroke: (top: 0.5pt + gray))[#v(0.1em)],
- ))
-
- date-rows
+ rows
}.flatten()
)
}