From f9b23f5c12eda13c6014f6291bdeecb77bb40141 Mon Sep 17 00:00:00 2001 From: T Date: Mon, 23 Feb 2026 11:27:12 -0700 Subject: PDF design tweaks --- internal/reports/invoice.go | 8 +-- internal/reports/testdata/invoice_test_data.json | 28 +++++---- templates/invoice.typ | 18 +++--- templates/timesheet.typ | 74 ++++++++++-------------- 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() ) } -- cgit v1.2.3