summaryrefslogtreecommitdiff
path: root/templates/timesheet.typ
diff options
context:
space:
mode:
authorT <t@tjp.lol>2025-08-04 15:34:23 -0600
committerT <t@tjp.lol>2025-08-04 19:49:08 -0600
commitdc895cec9d8a84af89ce2501db234dff33c757e2 (patch)
tree8c961466f0769616b3a82da91f4cde4d3a881b73 /templates/timesheet.typ
parent56e0af3b41742876b471332aeb943a5a2ca8dfbf (diff)
timesheet and unified reports
Diffstat (limited to 'templates/timesheet.typ')
-rw-r--r--templates/timesheet.typ163
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()