L Ledger Timesheets UI mockup
EV · single user
Reference

States, dialogs & parts

Everything the screens are built from, in one place: rejections as the user meets them, the four dialogs that guard the model, the day editor in both policies, the Quick Fill arithmetic, and the raw material (type, colour, badges) the other pages are made of.

Error codesDialogs Day editorQuick Fill Empty & loadingKeyboard Day cell anatomyTokens

Status, state and permission badges

Draft Exported 🔒 Locked Current artifact Superseded Archived client Invoice reference missing Write refused Future month No timesheet
weekendpublic holiday · Kerstmis future date✓ confirmed for this date overtime inherits workspace default client override version 14
No badge, column or copy anywhere says final, sent, delivered, invoiced or closed month. Locked protects from editing and says nothing else.

Controls

D standard day · H half day · Enter open · Esc cancel · Del clear · ↑↓←→ move
409 invoice_reference_taken — already used on February 2026 for Hazelnoot Logistics NV.
Amend to edit. A snapshot refresh keeps this value.

Every rejection, as the user meets it

The four dialogs that guard the model

Restricted date

One confirmation, no reason field, stored per (timesheet, date). Three reasons share one dialog.

Amendment reason

Same component for Exported→Draft and Locked→Draft; one payload shape, so one path cannot skip it.

Export & its warnings

Reference, month and totals echoed back; a zero-entry month warns but is allowed.

Consequences elsewhere

Rejections that arrive mid-edit — the ones that would otherwise look like the app ignoring you.

Day editor — both policies, one record

Client without Clock Details

duration entered directly

Client with Clock Details

duration derived, never typed

Quick Fill — the arithmetic behind D and H

Server-side derivation from the snapshot's working terms

the SPA never computes these
Snapshot working termsKeyStoredDisplayedWhat this case is for
Quick Fill replaces only the regular duration or clock values. Any Work Summary and the whole Overtime Record survive untouched; a second press on the same date records no history row, because it is a field edit.

Empty, loading and transitional

No entries yet

March is empty

Press D on a working day to record a standard day, or open a date to enter a duration, clock times, a summary or overtime.

Loading

preferences load once at boot

Nothing is recomputed client-side, so a slow response shows a skeleton rather than a plausible-looking wrong total.

Banners in place of errors

A calendar cell, annotated

Anatomy

Mon
Tue
Wed
Thu
Fri
Sat
Sun
  1. Day number — the focus ring is the only selection affordance.
  2. Recorded duration for the regular side, always the persisted value, never a local guess.
  3. Day equivalent against this timesheet's standard day.
  4. Overtime as its own line with its own tag: never added into the number above.
  5. Summary preview, two lines max — the full text lives in the day editor.
  6. ✓ corner: a restricted-date confirmation is stored for this date; the flag underneath says why it was restricted.

Keyboard map

Phase 6 lives in one hook
KeyOn a focused date (Draft)Otherwise
DSave one Standard Day, keep summary and overtime, advance focusInline “return to draft to edit”; no request sent
HSave half a Standard Day (integer minutes: 450 → 225)Same refusal
EnterOpen the day editor prefilledOpen read-only view
↑ ↓ ← →Move focus within the month — filler cells are never targets
DelClear the entry (history keeps the record)Refused with the state name
EscClose the day editor; nothing written
Focus advances row-major through real dates only, and stays put on the last date of the month. Pressing D on a restricted date intercepts the key, opens the confirmation, and on Cancel the focus does not move.

Raw material

Colour and type

one accent, warm paper neutrals, three status hues
Page title 21/650 Card 16/650 Body 14 — reads dense without being small Muted 12 for metadata 1 234,56 tabular figures mono for codes and references

Record work on Saturday 7 March 2026?

This date is a weekend. Recording a positive duration needs your confirmation once; it is remembered for this date.

weekend
No written reason is asked for or stored. Confirming unlocks 7 March in this timesheet only — 14 March will ask again.

Record work on Tuesday 21 July 2026?

This date is a public holiday in the Belgium calendar (Nationale Feestdag) and a weekday.

public holiday · Nationale Feestdag
The calendar is the workspace preference be. Changing it later never rewrites this entry — the confirmation is only demanded when a duration is written.

Record work on Friday 20 March 2026?

This date is in the future — “future” is evaluated in Europe/Brussels, the time zone stored in preferences, not your browser's.

future date
Today itself is never restricted merely for being today.

Edit this timesheet

Artifact #2 is current. Returning to Draft marks it superseded — its bytes, timestamp and invoice reference never change.

Stored verbatim in the history, visible in the panel beside this dialog.

Edit this timesheet

422 amendment_reason_required — status stays Exported, artifact #2 stays current, nothing is written.

Export this timesheet

Client · month
De Kelder Meubelatelier BV · March 2026
Invoice reference
TS-2026-014
Totals
71.25 u (68.75 u regular + 2.5 u overtime) · 10.61 day equivalent
Artifact
will be #1 for this timesheet

Permanent and immutable: no edit, no replace, no delete. A correction costs one Amendment Reason and creates artifact #2.

Export this timesheet

Client · month
De Kelder Meubelatelier BV · January 2026
Totals
0 u · 0 day equivalent · 0 entries
This month has no entries. Exporting is allowed and the PDF will show zero totals — for instance when a retainer must still be invoiced. Confirm you meant it.

Export this timesheet

Invoice reference
missing
422 export_requires_invoice_reference — no artifact row, no history row, the timesheet stays Draft. The reference is the only link to the invoice in your Peppol application, so it cannot be guessed.

Someone else saved first

Your edit carried version 14; the stored timesheet is now version 15, so the write was refused instead of silently overwriting the other tab's work.

409 version_conflict · the screen reloads the winning value and you can re-apply your change.

That client already has a timesheet for this month

Hazelnoot Logistics NV already has an Exported timesheet for March 2026 (id t2). A client can have at most one per month, and a timesheet cannot be deleted.

409 duplicate_timesheet · existingId: t2 · one row exists in the database, not two.

Those clock times cannot be right

Finish (16:00) is before start (17:00), so the derived duration would be negative. The entry keeps its previous values.

422 clock_invalid_range · overnight shifts are not supported yet: a night shift must be recorded as clock times that fit inside one date.

This client is archived

Ostend Ferry Terminals NV cannot get a new timesheet while archived. Its three existing timesheets and one artifact stay exactly as they are and stay downloadable.

409 client_archived · restore the client first, then create.