feat(shifts): /shift→/shifts, clickable activity log (shared event-detail), booth-style full-height layout

Three changes to the shift hub, addressing the report:

1. Route rename /shift → /shifts (matches the plural "Turnet" label and the
   section). /shift and /setup/shifts both redirect to /shifts; the header link
   and the operator-landing fallback point at /shifts.

2. The activity-log rows are now CLICKABLE and open the same read-only
   event-detail modal the booth live feed uses (full signed payload + entry/exit
   snapshots + chain provenance) — previously they were static rows. Extracted
   EVENT_STYLE, the feed row, the detail modal, and their helpers out of
   BoothScreen into a shared apps/web/src/ui/event-detail.tsx imported by both the
   booth and the shift log, so the two render and behave identically and can't
   drift.

3. Reworked the /shifts layout to fill the viewport like /booth: a fixed
   title + filters, then a two-pane area (shift list | activity log) where each
   pane scrolls independently (min-h-0/flex-1 + overflow-y-auto) instead of the
   whole page growing. ShiftActivityLog is now a flex column with a fixed header
   and a scrollable list.

Verified at runtime (Playwright): /shift redirects to /shifts, an activity row
opens the detail modal, the layout fills height, and the booth still works (0
console errors after the extraction). build+lint 14/14.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-21 14:58:50 +02:00
parent 2a9e6846a1
commit 35c10a7310
6 changed files with 327 additions and 336 deletions
+8 -7
View File
@@ -368,7 +368,7 @@ function RootLayout() {
<span className="text-sm font-bold uppercase tracking-widest text-term-amber">▮ Parking</span>
<nav className="flex items-center gap-1">
<NavLink to="/booth" label={t("nav.booth")} />
<NavLink to="/shift" label={t("nav.shifts")} />
<NavLink to="/shifts" label={t("nav.shifts")} />
{/* Subscriptions — a standalone section (Abonimet / Planet / Lab tarife).
Shown if the user can reach ANY of its tabs. */}
{(show("subscription:read") || show("subscription:plan") || show("tariff:read")) && (
@@ -437,10 +437,11 @@ const legacyRedirects = (
["/site", "/setup/site"],
["/users", "/setup/users"],
["/roles", "/setup/roles"],
["/shift", "/shifts"],
["/setup/subscriptions", "/subscriptions"],
["/setup/plans", "/subscriptions/plans"],
["/setup/tariff-lab", "/subscriptions/tariff-lab"],
["/setup/shifts", "/shift"],
["/setup/shifts", "/shifts"],
] as const
).map(([from, to]) =>
createRoute({
@@ -454,7 +455,7 @@ const legacyRedirects = (
const shiftRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/shift",
path: "/shifts",
component: function ShiftRoute() {
const { user } = rootRoute.useRouteContext();
// The shift hub: list (current/open shift on top + history) + per-shift activity log.
@@ -483,14 +484,14 @@ function requirePerm(perm: Permission) {
// The Setup tabs in display order, each with the permission its screen needs. Used
// to land a user on the FIRST tab they may see when they open /setup without
// `site:update` (e.g. an operator who only has shift:read → goes to the standalone
// /shift hub, which is no longer a Setup tab).
// /shifts hub, which is no longer a Setup tab).
const SETUP_TABS: { to: string; perm: Permission }[] = [
{ to: "/setup", perm: "site:update" },
{ to: "/setup/tariff", perm: "tariff:read" },
{ to: "/setup/site", perm: "site:read" },
{ to: "/setup/users", perm: "user:read" },
{ to: "/setup/roles", perm: "role:read" },
{ to: "/shift", perm: "shift:read" },
{ to: "/shifts", perm: "shift:read" },
{ to: "/setup/logs", perm: "log:read" },
];
@@ -584,8 +585,8 @@ const rolesRoute = createRoute({
return <RolesManager user={user} />;
},
});
// (Shift history lives at the standalone /shift route — see shiftRoute. It was
// removed as a Setup tab; /setup/shifts redirects there for old bookmarks.)
// (Shift history lives at the standalone /shifts route — see shiftRoute. It was
// removed as a Setup tab; /setup/shifts and the old /shift both redirect there.)
// Diagnostic logs. Gated by log:read (an admin/diagnostic permission).
const logsRoute = createRoute({