v3.0.0

<bmx-timesheet>

A timesheet for professional services, agencies, consultancies, IT departments and anyone who bills or costs time: one person's lines of project and task across the days of a week (or any period up to 31 days).

12 properties · 4 events · 25 methods · 7 parts

Example

A consultant's week at a fictional consultancy, Northgate Partners, part filled in. Type 7:30, 7.5, 45m or a span such as 9-17:30 into any day; start a timer on a line; add a line below the grid; then Submit. Wednesday's support line still needs a note, so the sheet will not submit until it has one.

Show markup
<bmx-timesheet id="ex-ts" label="Timesheet, week 42" today="2026-10-15" locale="en-GB" file-name="northgate-timesheet"></bmx-timesheet>
<p class="note" id="ex-ts-out" role="status">
  A consultant's week at a fictional consultancy, Northgate Partners, part filled in. Type 7:30, 7.5, 45m or a span such as 9-17:30 into any day; start a timer on a line; add a
  line below the grid; then Submit. Wednesday's support line still needs a note, so the sheet will not submit until it has one.
</p>

<div class="row" style="gap: 0.75rem; flex-wrap: wrap; align-items: center; margin-block: 0.5rem 1.5rem">
  <label><input type="checkbox" id="ex-ts-approver" /> View as the approver</label>
  <button type="button" id="ex-ts-csv">Show the week as CSV</button>
</div>
<pre id="ex-ts-csv-out" style="max-block-size: 14rem; overflow: auto" hidden></pre>

<script type="module">
  await customElements.whenDefined('bmx-timesheet');
  const sheet = document.getElementById('ex-ts');
  const out = document.getElementById('ex-ts-out');

  sheet.sheet = {
    person: { name: 'Amara Osei', role: 'Senior consultant' },
    start: '2026-10-12',
    rules: { expected: [0, 7.5, 7.5, 7.5, 7.5, 7, 0], dayMaxHours: 12, increment: 15, rounding: 'nearest', lockedBefore: '2026-10-12' },
    holidays: [],
    projects: [
      {
        id: 'HRB',
        code: 'HRB-104',
        name: 'Harbour Bank onboarding',
        client: 'Harbour Bank',
        color: '#2563eb',
        requireTask: true,
        tasks: ['Discovery', 'Process design', 'Workshops', { name: 'Travel', billable: false }],
      },
      { id: 'MER', code: 'MER-022', name: 'Meridian ERP rollout', client: 'Meridian Foods', color: '#0d9488', tasks: ['Data migration', 'Testing', 'Cut-over'] },
      { id: 'SUP', code: 'SUP', name: 'Client support', client: 'Several', color: '#d97706', requireNote: true, tasks: ['Tickets', 'Calls'] },
      { id: 'BD', name: 'Proposals and sales', kind: 'internal', color: '#7c3aed', tasks: ['Bid writing', 'Pitches'] },
      { id: 'TRN', name: 'Training', kind: 'internal', color: '#64748b' },
      { id: 'AL', name: 'Annual leave', kind: 'leave', color: '#16a34a' },
      { id: 'SICK', name: 'Sickness', kind: 'leave', color: '#dc2626' },
      { id: 'OLD', name: 'Atlas retail (closed)', client: 'Atlas', closed: true },
    ],
    rows: [
      { id: 'r1', project: 'HRB', task: 'Workshops' },
      { id: 'r2', project: 'HRB', task: 'Process design' },
      { id: 'r3', project: 'MER', task: 'Testing' },
      { id: 'r4', project: 'SUP', task: 'Tickets' },
      { id: 'r5', project: 'BD', task: 'Bid writing' },
      { id: 'r6', project: 'AL' },
    ],
    entries: [
      { row: 'r1', date: '2026-10-12', start: '09:00', end: '13:00' },
      { row: 'r2', date: '2026-10-12', minutes: 150, note: 'Current-state maps for payments' },
      { row: 'r4', date: '2026-10-12', minutes: 45, note: 'Two password resets, one report fix' },
      { row: 'r1', date: '2026-10-13', minutes: 210 },
      { row: 'r3', date: '2026-10-13', minutes: 240, note: 'Regression pack, week 2' },
      { row: 'r3', date: '2026-10-14', minutes: 300 },
      { row: 'r4', date: '2026-10-14', minutes: 60 },
      { row: 'r5', date: '2026-10-14', minutes: 90 },
      { row: 'r2', date: '2026-10-15', minutes: 120 },
      { row: 'r6', date: '2026-10-16', minutes: 450, note: 'Booked in August' },
    ],
  };

  sheet.addEventListener('bmxChange', e => {
    if (e.detail.reason === 'status') out.textContent = `The sheet is now ${e.detail.sheet.status}.`;
  });
  sheet.addEventListener('bmxStatusChange', e => {
    // A real page would save here, and cancel the event if the server refused.
    out.textContent = `${e.detail.from} → ${e.detail.to}${e.detail.comment ? `: “${e.detail.comment}”` : ''}. ${e.detail.problems.length} warning${e.detail.problems.length === 1 ? '' : 's'} left.`;
  });
  sheet.addEventListener('bmxTimer', e => {
    out.textContent =
      e.detail.state === 'stop' ? `Timer stopped: ${e.detail.minutes} minutes booked to ${e.detail.date}.` : `Timer ${e.detail.state === 'start' ? 'started' : 'discarded'}.`;
  });
  document.getElementById('ex-ts-approver').addEventListener('change', e => {
    sheet.mode = e.target.checked ? 'approve' : 'entry';
    out.textContent = e.target.checked ? 'Approver view: read only, with Approve and Reject once the sheet is submitted.' : 'Back to the consultant’s view.';
  });
  document.getElementById('ex-ts-csv').addEventListener('click', async () => {
    const pre = document.getElementById('ex-ts-csv-out');
    pre.textContent = await sheet.exportCsv();
    pre.hidden = false;
  });
</script>

WHAT IT DOES

  • Time is typed straight into the grid the way people say it: 7:30, 7.5, 45m, 7h 30m, a span of the day (9-17:30, 9am-5:30pm, past midnight too), or +30m to add to what is there; rounded to the organisation's increment. Arrow keys and Enter move like a spreadsheet.
  • A timer on any line books the time it ran, rounded, to the day it started.
  • Totals for every line and day, against the expected hours of each day (public holidays included), with overtime, time still to book, billable share and leave; a breakdown by project.
  • Rules: hours a day and a period, notes and tasks a project needs, closed projects and tasks, locked days, time booked ahead of the day, the same line twice, spans that overlap. Errors stop a submission.
  • Draft, submitted, approved and rejected, with the trail of who did what; an approver mode with Approve and Reject (with a reason).
  • Copy last period's lines (and time), copy a day across the week; undo; CSV and JSON out. On a phone, one day at a time.

The sheet is plain JSON on the element (sheet, bmxChange); nothing is sent anywhere - the page saves it where it wants.

Properties

PropertyAttributeTypeDefaultDescription
actor actor string — The name recorded in the approval trail for changes made here. Unset: the sheet's person (entry) or nobody (approve).
compactBelow compact-below number 640 Below this width in pixels, one day is shown at a time. 0 never does.
fileName file-name string 'timesheet' The name exported files are saved under, without the extension.
format format BmxTimesheetFormat 'hm' How time is written: hm (7:30) or decimal (7.50).
label label string — The element's accessible name.
locale locale string — The locale for dates and numbers. Unset: the page's lang.
mode mode BmxTimesheetMode 'entry' entry for the person filling the sheet in; approve for someone approving it (read only, with Approve and Reject).
readonly readonly boolean false Look but do not change.
sheet sheet Partial<BmxTimesheetData> | string — The sheet: projects, lines, time, rules, holidays, status (an object, or its JSON). Kept up to date as it changes.
showSummary show-summary boolean true Show the summary, the breakdown by project and the approval trail.
strings strings Partial<BmxTimesheetStrings> | string — Replacements for any of the wording (an object, or its JSON).
today today string — Today, as YYYY-MM-DD. Unset: the computer's date.

Events

EventDetailDescription
bmxChange BmxTimesheetChangeDetail Fired when time, notes, lines, the period or the status change.
bmxPeriodChange BmxTimesheetPeriodDetail Fired before the sheet moves to another period. Cancelable: cancel it to load that period's sheet yourself.
bmxStatusChange BmxTimesheetStatusDetail Fired before the sheet is submitted, recalled, approved, rejected or reopened. Cancelable.
bmxTimer BmxTimesheetTimerDetail Fired when a timer starts, stops (with the time booked) or is discarded.

Methods

MethodSignatureDescription
addLine addLine(project: string, task?: string) => Promise<string | null> Adds a line for a project (and task) and returns its id; an existing line for the same pair is returned instead.
approve approve(comment?: string) => Promise<boolean> Approves a submitted sheet.
check check(options?: { forSubmit?: boolean; }) => Promise<BmxTimesheetProblem[]> What needs attention; forSubmit also counts short days after today.
copyFrom copyFrom(sheet: Partial<BmxTimesheetData> | string | null, options?: { withTime?: boolean; }) => Promise<void> Brings in the lines (and with withTime, the time) of another sheet - or, with null, of the period before this one in this sheet.
download download(format?: "csv" | "csv-grid" | "json") => Promise<void> Saves the sheet as a file.
exportCsv exportCsv(layout?: "long" | "grid") => Promise<string> The period as CSV: one line per entry (long) or per sheet line with a column per day (grid).
focusCell focusCell(row: string, date: string) => Promise<void> Moves the focus to a line's time on a day.
getSheet getSheet() => Promise<BmxTimesheetData> The sheet as it is now.
getTotals getTotals() => Promise<BmxTimesheetTotals> The period's figures.
goToPeriod goToPeriod(start: string) => Promise<boolean> Moves to the period starting on a date (YYYY-MM-DD).
nextPeriod nextPeriod() => Promise<boolean> Moves to the next period.
previousPeriod previousPeriod() => Promise<boolean> Moves to the previous period.
recall recall() => Promise<boolean> Takes a submitted sheet back to draft.
redo redo() => Promise<void> Puts back what undo took.
reject reject(comment: string) => Promise<boolean> Rejects a submitted sheet, with the reason.
removeLine removeLine(row: string) => Promise<void> Removes a line and its time.
reopen reopen(comment?: string) => Promise<boolean> Takes an approved sheet back to draft.
setFocus setFocus() => Promise<void> Moves the focus into the sheet: the chosen day, else the first.
setNote setNote(row: string, date: string, note: string) => Promise<boolean> Sets (or with an empty text clears) the note on a line on a day.
setSheet setSheet(sheet: Partial<BmxTimesheetData> | string, options?: { keepHistory?: boolean; }) => Promise<void> Replaces the sheet. Undo goes back to what was there, unless keepHistory is false.
setTime setTime(row: string, date: string, value: number | string) => Promise<boolean> Sets the time on a line on a day: minutes, or text as it would be typed (7:30, 7.5, 9-17:30, +30m). False when the text is not a time or the day cannot be changed.
startTimer startTimer(row: string) => Promise<void> Starts a timer on a line (a timer running elsewhere is stopped and booked first).
stopTimer stopTimer() => Promise<number> Stops the running timer and books its time; returns the minutes booked.
submit submit() => Promise<boolean> Submits the sheet. False when it cannot be (errors, or not a draft).
undo undo() => Promise<void> Takes back the last change.

CSS shadow parts

PartDescription
details The chosen day's time, span and note.
grid The table of lines and days.
header The period, the status and the tools.
history The approval trail.
problems What needs attention.
summary The figures and the breakdown by project.
timer The running timer's bar.

CSS custom properties

PropertyDescription
--bmx-timesheet-day-width Least width of a day's column.
--bmx-timesheet-line-width Width of the project-and-task column.
--bmx-timesheet-line-width-narrow Its width when the sheet is narrow (one day at a time).
--bmx-timesheet-radius Corner radius of the frame.
--bmx-timesheet-today Accent of today's column.
--bmx-timesheet-weekend Background of days with no expected hours.