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 /templates/timesheet.typ | |
parent | 56e0af3b41742876b471332aeb943a5a2ca8dfbf (diff) |
timesheet and unified reports
Diffstat (limited to 'templates/timesheet.typ')
-rw-r--r-- | templates/timesheet.typ | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/templates/timesheet.typ b/templates/timesheet.typ new file mode 100644 index 0000000..e888615 --- /dev/null +++ b/templates/timesheet.typ @@ -0,0 +1,163 @@ +#set page(margin: (top: 0.75in, bottom: 1in, left: 1in, right: 1in)) +#set text(font: ("EB Garamond", "Georgia"), size: 10pt) +#set par(leading: 0.65em) + +// Load timesheet data from JSON file +#let data = json("data.json") + +// Helper function to format hours as HH:MM with proper rounding +#let format-hours(hours) = { + let total-minutes = calc.round(hours * 60) + let h = calc.floor(total-minutes / 60) + let m = calc.rem(total-minutes, 60) + str(h) + ":" + if m < 10 { "0" + str(m) } else { str(m) } +} + +// Helper function to group entries by date +#let group-by-date(entries) = { + let groups = (:) + for entry in entries { + let date = entry.date + if date not in groups { + groups.insert(date, ()) + } + groups.at(date).push(entry) + } + groups +} + +// Professional header with company info +#let professional-header() = { + // Company header + align(left)[ + #text(size: 9pt, fill: gray)[ + #text(weight: "bold")[#data.contractor_name] • #data.contractor_label • #data.contractor_email + ] + ] + + v(3em) + + // Timesheet title + grid( + columns: (1fr, auto), + align(left)[ + #text(size: 28pt, weight: "bold")[Timesheet] + ], + align(right)[ + #text(size: 11pt)[ + #data.generated_date + ] + ] + ) + + v(2.5em) +} + +#let client-info-section() = { + grid( + columns: (1fr, 1fr), + gutter: 3em, + // Client section + [ + #text(size: 9pt, fill: gray)[CLIENT] + #v(0.5em) + #text(size: 12pt, weight: "bold")[#data.client_name] + #if data.project_name != "" [ + #v(0.3em) + #text(size: 10pt)[Project: #data.project_name] + ] + ], + // Period details + align(right)[ + #text(size: 9pt, fill: gray)[PERIOD] + #v(0.5em) + #text(size: 10pt)[#data.date_range_start to #data.date_range_end] + #v(0.3em) + #text(size: 9pt, fill: gray)[Times shown in: #data.timezone] + ] + ) + + v(2.5em) +} + +#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 + ..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 + 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], + text(size: 9pt)[#entry.project_name], + 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)[] + )) + + // Add separator line after each day + date-rows.push(( + table.cell(colspan: 6, stroke: (top: 0.5pt + gray))[#v(0.1em)], + )) + + date-rows + }.flatten() + ) +} + +#let timesheet-summary() = { + v(1.5em) + + // Total hours section + align(right, + table( + columns: (auto, auto), + stroke: none, + inset: (x: 12pt, y: 6pt), + align: (right, right), + + table.hline(stroke: 0.5pt), + [#text(size: 12pt, weight: "bold")[Total Hours:]], [#text(size: 12pt, weight: "bold")[#format-hours(data.total_hours)]] + ) + ) +} + +// Main timesheet layout +#professional-header() +#client-info-section() +#timesheet-table() +#timesheet-summary() |