Escape

Timesheet template for JavaScript

A timesheet is where the hours start. Payroll pays from it, and the client invoice bills from it. The person who approves it wants a week at a time, with a total under each week. Finance wants the billable hours apart from the rest. Most of the time, they want it as a spreadsheet they can sum again. This is a ready-made quario timesheet — copy it, point it at your time entries, and render to Excel, PDF, HTML, or Word.

The template

The whole timesheet is a definition — data you can version and review. The employee and employer blocks, the weeks, the totals and the two signature lines are all in here. This is the exact file the preview below renders:

timesheet.report.json
{
  "data": "$.timesheet.entries[*]",
  "sort": [{ "by": "[email protected]" }],
  "aggregates": {
    "hours": "sum:[email protected]",
    "billable": "sum:[email protected] ? @.hours : 0",
    "days": "countDistinct:[email protected]"
  },
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "style": { "spaceAfter": 18 },
      "slots": [
        { "type": "image", "source": "=logo()", "fit": "width", "alt": "{{ $.input.timesheet.employer.name }} logo", "width": 28 },
        { "type": "text", "value": "", "style": { "size": 8 } }
      ]
    },
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Timesheet", "width": 50,
          "style": { "family": "serif", "size": 19, "bold": true, "color": "#1e1f22" } },
        { "type": "text", "value": [{ "value": "{{ $.input.timesheet.from }}" }, { "value": " to " }, { "value": "{{ $.input.timesheet.to }}" }],
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "split",
      "style": { "spaceAfter": 4 },
      "slots": [
        { "type": "text", "value": "No. {{ $.input.timesheet.number }}", "width": 50,
          "style": { "size": 8, "bold": false, "color": "#989a97" } },
        { "type": "text", "value": [{ "value": "Submitted " }, { "value": "{{ $.input.timesheet.submitted }}" }],
          "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 50 }, {}],
      "rows": [
        {
          "style": { "family": "mono", "size": 7, "bold": false, "uppercase": true, "color": "#989a97", "paddingTop": 22 },
          "slots": [{ "type": "text", "value": "Employee" }, { "type": "text", "value": "Employer" }]
        },
        {
          "style": { "size": 10, "bold": true, "color": "#1e1f22" },
          "slots": [
            { "type": "text", "value": "{{ $.input.timesheet.employee.name }}" },
            { "type": "text", "value": "{{ $.input.timesheet.employer.name }}" }
          ]
        },
        {
          "style": { "size": 10, "bold": false, "color": "#5e6062", "paddingBottom": 14 },
          "slots": [
            { "type": "text", "value": "{{ $.input.timesheet.employee.role }}\nNo. {{ $.input.timesheet.employee.number }}" },
            { "type": "text", "value": "{{ $.input.timesheet.employer.street }}\n{{ $.input.timesheet.employer.city }}" }
          ]
        }
      ]
    }
  ],
  "groups": [
    {
      "name": "week",
      "by": "[email protected]",
      "aggregates": { "hours": "sum:[email protected]", "from": "first:[email protected]", "to": "last:[email protected]" },
      "header": [
        {
          "type": "split",
          "style": { "spaceBefore": 10, "spaceAfter": 2 },
          "slots": [
            { "type": "text", "value": "Week {{ week.key }}", "width": 50,
              "style": { "size": 11, "bold": true, "color": "#1e1f22" } },
            { "type": "text", "value": [{ "value": "{{ week.from }}" }, { "value": " to " }, { "value": "{{ week.to }}" }],
              "style": { "size": 8, "bold": false, "color": "#989a97", "align": "right", "format": "date" } }
          ]
        }
      ],
      "footer": [
        {
          "type": "split",
          "style": { "paddingTop": 5, "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#dcdcdc" },
          "slots": [
            { "type": "text", "value": "Week {{ week.key }} total", "width": 84,
              "style": { "size": 8, "color": "#989a97", "align": "right" } },
            { "type": "text", "value": "{{ week.hours }}",
              "style": { "color": "#1e1f22", "align": "right", "format": { "kind": "number", "digits": 2 } } }
          ]
        }
      ]
    }
  ],
  "detail": {
    "header": {
      "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97",
        "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" }
    },
    "row": { "style": { "color": "#5e6062" } },
    "columns": [
      { "header": "Date", "value": "{{ @.date }}", "width": 16, "style": { "format": "date" } },
      { "header": "Project", "value": "{{ @.project }}", "width": 28, "style": { "color": "#1e1f22" } },
      { "header": "Task", "value": "{{ @.task }}", "width": 30 },
      { "header": "Billable", "value": "{{ @.billable ? 'Yes' : 'No' }}", "width": 10, "style": { "color": "#989a97" } },
      { "header": { "value": "Hours", "style": { "align": "right" } }, "value": "{{ @.hours }}", "width": 16,
        "style": { "align": "right", "color": "#1e1f22", "format": { "kind": "number", "digits": 2 } } }
    ]
  },
  "footer": [
    {
      "type": "grid",
      "columns": [{ "width": 84, "style": { "align": "right" } }, { "style": { "align": "right", "format": { "kind": "number", "digits": 2 } } }],
      "rows": [
        {
          "style": { "color": "#5e6062", "paddingTop": 12 },
          "slots": [{ "type": "text", "value": "Billable" }, { "type": "text", "value": "{{ $.billable }}" }]
        },
        {
          "style": { "color": "#5e6062" },
          "slots": [{ "type": "text", "value": "Non-billable" }, { "type": "text", "value": "{{ $.hours - $.billable }}" }]
        },
        {
          "style": { "size": 11, "bold": true, "color": "#1e1f22", "paddingTop": 6, "paddingBottom": 4,
            "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "slots": [{ "type": "text", "value": "Total hours, {{ $.days }} days" }, { "type": "text", "value": "{{ $.hours }}" }]
        }
      ]
    },
    {
      "type": "grid",
      "columns": [{ "width": 45 }, { "width": 10 }, {}],
      "rows": [
        {
          "style": { "paddingTop": 34 },
          "slots": [
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } },
            { "type": "text", "value": "" },
            { "type": "text", "value": "", "style": { "borderBottomWidth": 0.5, "borderBottomStyle": "solid", "borderBottomColor": "#1e1f22" } }
          ]
        },
        {
          "style": { "family": "mono", "size": 7, "uppercase": true, "color": "#989a97", "paddingTop": 4 },
          "slots": [
            { "type": "text", "value": "Employee · {{ $.input.timesheet.employee.name }}" },
            { "type": "text", "value": "" },
            { "type": "text", "value": "Approved · {{ $.input.timesheet.approver }}" }
          ]
        }
      ]
    }
  ],
  "page": {
    "footer": [
      {
        "type": "split",
        "style": { "paddingTop": 10, "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
        "slots": [
          { "type": "text", "value": "Hours as recorded by the employee. Approval confirms them for payroll and client billing.", "width": 80,
            "style": { "size": 8, "color": "#989a97" } },
          { "type": "text", "value": "Page {{ page.number }} of {{ page.total }}",
            "style": { "size": 8, "color": "#989a97", "align": "right" } }
        ]
      }
    ]
  }
}

The weeks are a group on [email protected]. Each one carries its own aggregates: week.hours sums that week’s entries, and week.from and week.to are its first and last date. The group header reads them as 17 Aug 2026 to 21 Aug 2026, and the group footer prints the week’s total. The report sort on the date is what keeps both the weeks and the days in order.

Billable hours are one aggregate, sum:[email protected] ? @.hours : 0. Non-billable hours are the total less that, in the footer. Nothing about the split lives in your code, so a different rule is a different expression in the file.

Hours present through { "kind": "number", "digits": 2 }, so 7.25 reads 7.25 and 6 reads 6.00. The cell still stores the bare number. That matters in the workbook, where a total row has to sum.

The data

A definition renders nothing on its own — you hand it a document. This is the sample the preview below uses. $.timesheet.entries[*] in the definition selects its rows, and $.input reads the period, the people and the approver:

timesheet.data.json
{
  "timesheet": {
    "number": "TS-2026-34",
    "from": "2026-08-17",
    "to": "2026-08-30",
    "submitted": "2026-08-31",
    "employer": {
      "name": "Quire Press Ltd.",
      "street": "Binnenkant 42",
      "city": "1011 BM Amsterdam"
    },
    "employee": {
      "name": "S. Vink",
      "number": "10417",
      "role": "Compositor"
    },
    "approver": "M. de Graaf",
    "entries": [
      {"week": 34, "date": "2026-08-17", "project": "Folio & Sons catalogue", "task": "Typesetting, sections 1–3", "hours": 6, "billable": true},
      {"week": 34, "date": "2026-08-17", "project": "Internal", "task": "Press maintenance", "hours": 1.5, "billable": false},
      {"week": 34, "date": "2026-08-18", "project": "Folio & Sons catalogue", "task": "Typesetting, sections 4–6", "hours": 7.5, "billable": true},
      {"week": 34, "date": "2026-08-19", "project": "Folio & Sons catalogue", "task": "Proof corrections", "hours": 4, "billable": true},
      {"week": 34, "date": "2026-08-19", "project": "Type specimen reprint", "task": "Plate check", "hours": 3.5, "billable": true},
      {"week": 34, "date": "2026-08-20", "project": "Type specimen reprint", "task": "Make-ready and press run", "hours": 6.5, "billable": true},
      {"week": 34, "date": "2026-08-20", "project": "Internal", "task": "Team meeting", "hours": 1, "billable": false},
      {"week": 34, "date": "2026-08-21", "project": "Folio & Sons catalogue", "task": "Index and cross-references", "hours": 6.5, "billable": true},
      {"week": 35, "date": "2026-08-24", "project": "Folio & Sons catalogue", "task": "Second proof", "hours": 7.5, "billable": true},
      {"week": 35, "date": "2026-08-25", "project": "Type specimen reprint", "task": "Press run", "hours": 7.25, "billable": true},
      {"week": 35, "date": "2026-08-26", "project": "Type specimen reprint", "task": "Binding check", "hours": 3, "billable": true},
      {"week": 35, "date": "2026-08-26", "project": "Internal", "task": "Training, new guillotine", "hours": 4.5, "billable": false},
      {"week": 35, "date": "2026-08-27", "project": "Folio & Sons catalogue", "task": "Final files to print", "hours": 6, "billable": true},
      {"week": 35, "date": "2026-08-27", "project": "Internal", "task": "Press maintenance", "hours": 1.5, "billable": false},
      {"week": 35, "date": "2026-08-28", "project": "Folio & Sons catalogue", "task": "Press check", "hours": 5.75, "billable": true}
    ]
  }
}

The entries are flat, one line per task per day, with the week number on the row. That’s the shape a time-tracking table already has, and the group does the nesting on the page.

What it renders

This isn’t a screenshot. quario compiles the definition and data above, and @quario/viewer renders them in your browser, right here. The PDF button hands you the same timesheet as real @quario/pdf bytes, one A4 page with its page footer on the baseline. The DOCX button beside it hands the same definition to @quario/docx, where Word paginates and resolves the faces itself:

Rendering live with @quario/viewerA4

Use it

The workbook is the render most timesheets end up as, so this one uses @quario/xlsx:

shell
npm install quario @quario/xlsx
timesheet.ts
import { readFileSync, writeFileSync } from 'node:fs'
import { quario } from 'quario'
import { xlsx } from '@quario/xlsx'
import timesheet from './timesheet.report.json' with { type: 'json' }

const logo = () => readFileSync('logo.png')          // your logo, as PNG bytes
const report = quario({ locale: 'en-IE' }).report(timesheet, { logo })

const data = await getTimesheet('10417', '2026-08-17')   // your time entries
const bytes = await report.render(xlsx(), data)

writeFileSync('timesheet.xlsx', bytes)

Each hours cell lands in the sheet as a number with a two-digit number format. So a payroll clerk can select a week and read its sum in the status bar. The dates land as typed dates that present through the instance’s locale. Swap xlsx() for pdf() from @quario/pdf and the same definition prints the signed copy. See JSON to Excel for the workbook target in full.

Make it yours

Because the timesheet is declarative, changing it means changing data. Overtime is one more aggregate, the hours over the contract, and one more row in the footer. A column per day instead of a row per entry is a different shape of data with the same totals. A month per sheet is "break": "before" on a month group. A rate per project turns the billable hours into an amount in the footer, under format: "currency". It renders without eval, so a task description typed by an employee never becomes code. Security →

FAQ

Can I export the timesheet to Excel?

Yes, and that’s the render the sample above uses. @quario/xlsx writes each week as its own section with its header row and its total, and every hours cell is a real number. A finance team can sum, filter, and pivot it without retyping anything. Full example →

Can I generate it as a PDF to sign?

Yes. @quario/pdf renders this definition straight to bytes, with the two signature lines at the foot of the last page and no browser anywhere. A Word render through @quario/docx gives a copy the employee can still correct before it’s signed. Full example →

Can I use this timesheet template commercially?

The definition is yours — the report definitions, data, and output you create with quario belong to you. quario is free to evaluate with no time limit. Running payroll or billing from the timesheet is past evaluation, so that needs a license for each production deployment. Its users, agents, and renders are never counted. Pricing →

Where are the other templates?

Browse the template gallery → — payslips, work orders, summary reports, and more.