#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 invoice data from JSON file #let data = json("data.json") // Helper function to format hours as HH:MM #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 format currency with thousands separator and cents #let format-currency(amount) = { let dollars = calc.floor(amount) let cents = calc.round((amount - dollars) * 100) // Convert to string and add thousands separators let dollar-str = str(dollars) let len = dollar-str.len() let formatted = "" for i in range(len) { let digit = dollar-str.at(i) formatted += digit let remaining = len - i - 1 if remaining > 0 and calc.rem(remaining, 3) == 0 { formatted += "," } } "$" + formatted + "." + if cents < 10 { "0" + str(cents) } else { str(cents) } } // 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) // Invoice title and number grid( columns: (1fr, auto), align(left)[ #text(size: 28pt, weight: "bold")[Invoice] ], align(right)[ #text(size: 11pt)[ #text(weight: "bold")[Invoice \##data.invoice_number] \ #data.generated_date ] ] ) v(2.5em) } #let client-info-section() = { grid( columns: (1fr, 1fr), gutter: 3em, // Bill To section [ #text(size: 9pt, fill: gray)[BILL TO] #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] ] ], // Invoice details align(right)[ #text(size: 9pt, fill: gray)[INVOICE PERIOD] #v(0.5em) #text(size: 10pt)[#data.date_range_start to #data.date_range_end] ] ) v(2.5em) } #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), align: (left, center, center, right), column-gutter: 12pt, // 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]], // Line items ..data.line_items.map(item => ( text(size: 10pt)[#item.description], text(size: 10pt)[#format-hours(item.hours)], text(size: 10pt)[#format-currency(item.rate)], text(size: 10pt, weight: "medium")[#format-currency(item.amount)] )).flatten() ) } #let invoice-summary() = { v(1.5em) // Subtotal and total section align(right, table( columns: (auto, auto), stroke: none, inset: (x: 12pt, y: 6pt), align: (right, right), [#text(size: 10pt)[Total Hours:]], [#text(size: 10pt)[#format-hours(data.total_hours)]], table.hline(stroke: 0.5pt), [#text(size: 12pt, weight: "bold")[Total Amount:]], [#text(size: 12pt, weight: "bold")[#format-currency(data.total_amount)]] ) ) } #let payment-terms() = { v(3em) [ #text(size: 9pt, fill: gray)[ *Payment Terms:* Net 30 days. Please remit payment within 30 days of invoice date. ] ] } // Main invoice layout #professional-header() #client-info-section() #professional-table() #invoice-summary() //#payment-terms()