Rebuild /hr/attendance as a payroll console

Featurekamo-internal
Shipped
August 19, 2026 at 6:33 AM UTC
Author
kamo
Commit
893d2b8

Eight flat tabs over eight panels that each drew their own heading, table, skeleton, error line and Snackbar — and had drifted. The overhaul is three things at the page level and a rebuild of each panel on top of them. **One summary read** backs a new command bar and every rail badge. It answers the question the screen could not: whether anything stands between here and payroll. That used to take three clicks — an admin opened the roster with no way to know that twelve blocking exceptions would refuse every approval they were about to make on the next tab. Blocking and awaiting chips deep-link to the panel that owns them. Polling is gated on page visibility, so a tab left open overnight stops asking. **One toast host**, hoisted above the panel switch. A message raised by an action that navigates — "clear the blocking exception first", which sends the reader to another sub-tab — used to unmount with its own panel before anyone read it. **One tone map** (attendanceConsole.ts, unit-tested) replaces three that already disagreed, and carries the rule this module turns on: an unknown is never a zero. A withheld approval count, an absent net-minutes figure and a period nobody has generated all reach the eye as "we were not told", because rendering any of them as 0 tells an HR admin a queue they cannot see is clear. `railBadgeFor` therefore never emits a zero badge, and `consoleVerdict` is `unknown` before the first read rather than `clear`. Two gaps closed that were not cosmetic: - **A person is a person.** Five panels printed a raw 19-digit `unique_rowid()` on rows deciding whether to pay somebody. `PersonCell` shows the face and name, with the id on the tooltip; ids stay STRINGS end to end and nothing parses one. `useMemberDisplayNames` now caches the whole directory row rather than the name alone, so the same walk feeds avatars and job titles instead of needing a second one. - **A drill-in that never existed.** The member Timecard tab under /settings is self-only by construction — its endpoints take no member id — so the only route from "this person's hours are wrong" to "here is the punch" was the exception queue, which lists the day and not the shift around it. `MemberTimecardDrawer` opens the existing period editor on another member; those endpoints take a `subjectMemberId` and SecurityService attributes the adjustment to the HR actor, which is what makes the reuse correct. Per panel: the roster gains search, sort, a card and a table view, a current-period range and CSV (every value the server's own string — a file payroll reconciles against must not carry a number this app invented). Approvals becomes keyboard triage with clear cards separated from blocked ones and an "approve everything ready" that still walks the state machine one card at a time and names every refusal. Exceptions gains a type histogram that doubles as a filter and, more importantly, a **Fix it** action: the screen shipped offering only Re-detect and Waive, neither of which puts a missing punch back, so the path of least resistance from "a day is missing its clock-out" was to waive it — precisely the decision the queue exists to make somebody think about. Pay periods gains a timeline, because a gap or an overlap in a calendar is visible on a track and invisible in a list. Export computes the variance against the prior period instead of advising the operator to remember to compare. Close becomes a numbered stepper, since the order is real: recomputing after approving changes figures already agreed to. One `ReasonDialog` replaces six hand-rolled ones that had drifted — some trimmed after checking, so three spaces enabled the button and submitted an empty reason the server took as present. Under Anderson v. Mt. Clemens Pottery that reason is the record, so the check is on the trimmed value now. `TIMECARD_SUB_TAB_IDS` is untouched: it stays the append-only URL contract and `ATTENDANCE_RAIL` reorders only the pills, so every bookmark still lands. A test asserts the rail covers every id exactly once.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing