#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(0.5em) line(length: 100%, stroke: 0.4pt + rgb("#d1d5db")) v(2em) // 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( 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 rows = () for (i, entry) in entries.enumerate() { 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], )) } // Thin separator between days rows.push(( table.cell(colspan: 4, inset: (x: 0pt, y: 0pt))[ #line(length: 100%, stroke: 0.4pt + rgb("#e5e7eb")) ], )) 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()