Stringer Dashboard / Order List¶
The home screen for a logged-in stringer. Owned by Mira. Cross-cuts: V2 scope M16 + M9, use cases — UC-1, UC-2, order lifecycle — hidden requirement #1, client identity & sharing — hidden requirement #1 inbox, data model — Order.
Source requirement¶
- M16 — reporting (year/month rollups, top clients, mean turnaround). Round 1 surfaces the counts only; the rollup screens are a follow-up round.
- M9 — Add Stringjob is the prominent CTA from this screen.
- Hidden requirement (order lifecycle): every order in lists shows its derived state badge (Draft / Ordered / Strung / Returned / Paid / Done).
- Hidden requirement (client identity & sharing): receiving stringers reach the "Shared with me" inbox via a link from the home screen.
Goal¶
Answer three questions at a glance, in this priority order:
- What do I need to string today? — the open queue.
- What did I just finish? — recent strung activity (so Stefan can verify a save landed).
- What does someone want me to look at? — shared-with-me inbox + (future) catalogue submissions / notifications.
The fourth question — "what's my CTA?" — is answered by a single, unambiguously-placed "Add Stringjob" button.
Viewports¶
sm 375 px (mobile-first baseline)¶
┌───────────────────────────────────────┐
│ ☰ racket-book 👤 Stefan ▾│ Header (sticky)
├───────────────────────────────────────┤
│ │
│ Today 5 open│ H2 + count
│ ┌───────────────────────────────────┐ │
│ │ 🟡 Ordered · 2026-05-02 │ │ Card (queue row)
│ │ Lukas M. · Pure Aero 98 │ │
│ │ Main 24 kg · Cross 23 kg · BYO │ │
│ │ Open → │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ 🟡 Ordered · 2026-05-01 │ │
│ │ Anna B. · Wilson Blade 98 v8 │ │
│ │ Main 25 kg · Cross same │ │
│ │ Open → │ │
│ └───────────────────────────────────┘ │
│ ⋯ │
│ │
│ Recent Last 10 │ H2
│ ┌───────────────────────────────────┐ │
│ │ ✅ Strung · 2026-04-30 │ │
│ │ Marc W. · RF 97 v14 │ │
│ │ Receipt #2026-0042 · CHF 48.00 │ │
│ └───────────────────────────────────┘ │
│ ⋯ │
│ │
│ Inbox │
│ 📬 Shared with me 3 new → │ Link (only if non-empty)
│ │
├───────────────────────────────────────┤
│ ┌─────────────────────────────────┐ │
│ │ + Add Stringjob │ │ Sticky-bottom CTA
│ └─────────────────────────────────┘ │
└───────────────────────────────────────┘
The Add Stringjob button is sticky-bottom on mobile, always one thumb away. Tapping a queue card opens the order detail (out of Round-1 scope).
md 768 px¶
Same single column at 720 px max width, centered, with more breathing room. The Add Stringjob CTA moves out of sticky-bottom and into the header bar (right-aligned next to the user menu) — the screen height makes a sticky bottom unnecessary.
lg 1280 px¶
Three zones:
┌────────────────────────────────────────────────────────────────────┐
│ racket-book [+ Add Stringjob] Stefan▾│
├──────────────────────────┬─────────────────────────────────────────┤
│ │ │
│ Today 5 open │ Recent Last 10 │
│ ┌──────────────────────┐ │ ┌─────────────────────────────────────┐ │
│ │ 🟡 Lukas M. · ... │ │ │ ✅ Marc W. · ... │ │
│ └──────────────────────┘ │ └─────────────────────────────────────┘ │
│ ┌──────────────────────┐ │ ⋯ │
│ │ 🟡 Anna B. · ... │ │ │
│ └──────────────────────┘ │ │
│ ⋯ │ │
│ ├─────────────────────────────────────────┤
│ │ Inbox │
│ │ 📬 Shared with me 3 new → │
│ │ 🔔 Notifications 0 new │
└──────────────────────────┴─────────────────────────────────────────┘
Two columns: Today's queue on the left (~50% width), Recent + Inbox on the right. The CTA moves into the top bar.
Component breakdown¶
Header¶
- Logo / brand (left): "racket-book" wordmark; click → home.
- Add Stringjob CTA (right,
md+ only): primary button, indigo-700, 48 px tall. Icon-leading (lucide:plus). Label EN "Add Stringjob"; DE catalog keydashboard.cta.add_stringjob. - User menu (right): avatar / initials + dropdown. Items: "Profile", "Sign out". Out-of-Round-1 items (locale switch, etc.) are stubs.
"Today" section¶
- H2 title "Today" + a small count chip "N open" (slate-200 background, slate-900 text).
- Sort: by
ordered_atascending (oldest first — the queue's natural FIFO). - Filter: orders where
strung_at IS NULL AND deleted_at IS NULL. The state machine implies these are in Draft or Ordered; the dashboard doesn't differentiate (both are "stuff I haven't strung yet"). The badge clarifies the state inside the row. - Empty state: "Nothing to string today. Enjoy the quiet." + reduced-emphasis illustration (Lucide
coffeeicon, 48 px, slate-300). The "Add Stringjob" CTA stays prominent.
Queue card (one per open order)¶
Card structure (top to bottom):
| Slot | Content | Source |
|---|---|---|
| State badge | 🟡 Ordered / ⚪ Draft (icon + label) |
Derived from lifecycle dates per order-lifecycle state machine. |
| Date | "2026-05-02" | ordered_at (or created_at for Draft). |
| Client name | "Lukas M." | order.client_profile.person.display_first_name + initial of last. |
| Racket | "Pure Aero 98" | order.racket.make + model + version (concise form). |
| String spec line | "Main 24 kg · Cross 23 kg · BYO" | Computed: tensions + BYO chip if any side is BYO. Cross collapses to "Cross same" when same-as-main per the cross-equals-main rule. |
| Affordance | "Open →" | Tap target = whole card (44 × 44 px minimum). |
Card is rounded-lg, white surface, shadow-card, border-slate-200 1 px on sm (drops on md+ where the shadow alone is enough).
Maximum cards visible without scrolling on 375 × 667 (iPhone SE) = 3 cards above the fold + the header — that's the design budget. If the queue is > 5, the list scrolls; "Show all" link appears when count > 5.
State badge legend (used everywhere in V2)¶
| Badge | Color (bg/fg) | When |
|---|---|---|
⚪ Draft |
slate-200 / slate-900 | No lifecycle dates set. |
🟡 Ordered |
amber-700 bg, white fg, OR amber-50 bg + amber-700 fg for low-emphasis contexts | ordered_at set, strung_at NULL. |
🟢 Strung |
green-700 bg, white fg | strung_at set, both returned_at + paid_at may be NULL. |
🔵 Returned |
sky-700 bg, white fg | returned_at set. |
💵 Paid |
indigo-700 bg, white fg | paid_at set. |
✅ Done |
green-700 bg, white fg, with check-mark icon | strung_at + returned_at + paid_at all set. |
The emoji glyphs in the wireframes are illustrative; production uses Lucide icons (circle-dashed, circle, check-circle, arrow-left-circle, banknote, circle-check) at 16 px, sized to match the badge text height.
"Recent" section¶
- H2 title "Recent" + a "Last 10" chip (or "Last N" where N is the actual count, capped at 10).
- Sort: by
strung_atdescending (most-recent strung first). - Filter:
strung_at IS NOT NULL AND deleted_at IS NULL, limit 10. - Card content: state badge,
strung_atdate, client name, racket, receipt number + total (e.g. "Receipt #2026-0042 · CHF 48.00"). The receipt number satisfies stringer-side searchability per receipt-content TR-1; total satisfies "did I save the right number?" verification. - Empty state: "No stringing activity yet." (only shown to fresh accounts; once any order is Strung this never re-appears for that stringer).
"Inbox" section¶
- H2 title "Inbox".
- "Shared with me" link — only rendered when the receiving stringer has at least one active Rule-1 grant pointing at them. Shows a count chip "N new" (count of unread shared jobs).
- "Notifications" link — placeholder for V2.x in-app notifications (per ADR-0005). Out of Round-1 scope; renders as a stub when the notification model lands.
- Tapping any inbox row → its detail view (out of Round-1 scope).
Add Stringjob CTA¶
- Always visible. Sticky-bottom on
sm; in the header bar onmd+. - Hit target: 48 × 48 px minimum; on
smthe button is full-width minus 16 px page padding (so it spans 343 px on a 375 px screen). - Icon + label:
lucide:plus+ "Add Stringjob". - Action: navigates to
/orders/new(the Add-Stringjob flow).
Interaction states¶
| State | What renders |
|---|---|
| Loading (initial page load) | Server-rendered HTML; no skeleton needed. The header + section titles + CTA are immediate; queue/recent rows render in the response. |
| Loading (HTMX swap, e.g. after marking an order Strung from another tab) | The affected card swaps with a 200 ms cross-fade (motion-default). |
| Empty (no open orders) | "Nothing to string today. Enjoy the quiet." + secondary illustration. CTA still prominent. |
| Empty (no recent activity) | "No stringing activity yet." Section header still rendered. |
| Empty (Shared-with-me empty) | Section row not rendered at all — link is conditionally hidden. |
| Error (server returned 500) | Full-page error template (out of Round-1 scope; reuses the FastAPI default error page styled with these tokens). |
| Stale data (offline / network drop) | Out of V2 scope (PWA / offline is C4, V3 candidate). |
Accessibility¶
- Keyboard navigation: Tab order = Header brand → User menu → CTA → Today's first card → Today's second card → ... → Recent first card → ... → Inbox links. Each card is a single focusable region with an internal
aria-label="Open order — Lukas M., Babolat Pure Aero 98, ordered 2026-05-02". - Focus rings: 2 px indigo-700 outline + 2 px white inset (visible on both white and slate-50 surfaces). Never
outline: none. - Heading order: H1 (page title — see below) → H2 (Today, Recent, Inbox) → H3 inside each card (client name).
Note on the H1: the wireframe omits an explicit page title to keep visual real estate for the queue, but accessibility requires one. Solution: a visually-hidden
<h1>reading the EN string "Dashboard" (DE: "Übersicht"). Sighted users see the section H2s; screen readers anchor to the H1.
- Color is never the only signal: state badges include both color AND an icon AND a label. A color-blind stringer can read every state.
- Hit targets: all card-rows ≥ 44 × 44 px; CTA ≥ 48 px; user-menu hit target ≥ 44 × 44 px (the avatar circle alone is too small — pad it).
- Screen reader: the count chips ("5 open") are announced as part of the section heading via
aria-label="Today, 5 open orders". The state badges are<span role="status">with the full state name in plaintext (not just the icon). prefers-reduced-motion: disables the 200 ms cross-fade on HTMX swaps.
HTMX / progressive-enhancement seams¶
- Initial render: full server-side HTML from
GET /(the dashboard route). No JS required to read the page. - Per-card polling: none. The dashboard is a snapshot at request time. Stefan refreshes the page (or navigates back from a save) to see updates.
- HTMX upgrades (when JS is available):
- The "Mark Strung" affordance on the order detail page (out of Round-1 scope, but the dashboard's "Recent" section will reflect the change on next page load) eventually swaps the affected card via
hx-swap="outerHTML"against/orders/:id/_card. - The Shared-with-me count chip can refresh on focus (
hx-trigger="window:focus") but this is a Round-2 polish, not Round-1 scope. - Without JS: every link still works; no functionality is JS-gated.
i18n affordance¶
| String | Type | Catalogue key |
|---|---|---|
| "Dashboard" (visually-hidden H1) | {% trans %} |
dashboard.title |
| "Today" | {% trans %} |
dashboard.section.today |
| "Recent" | {% trans %} |
dashboard.section.recent |
| "Inbox" | {% trans %} |
dashboard.section.inbox |
| "Shared with me" | {% trans %} |
dashboard.inbox.shared_with_me |
| "Notifications" | {% trans %} |
dashboard.inbox.notifications |
| "N open" | {% trans %} (with Babel.format_decimal) |
dashboard.queue.open_count |
| "Last N" | {% trans %} |
dashboard.recent.last_count |
| "Nothing to string today. Enjoy the quiet." | {% trans %} |
dashboard.empty.queue |
| "No stringing activity yet." | {% trans %} |
dashboard.empty.recent |
| "Add Stringjob" (CTA + header link) | {% trans %} |
dashboard.cta.add_stringjob |
| State badge labels ("Ordered", "Strung", ...) | {% trans %} |
order.state.<name> (shared with order detail screens) |
| Client name ("Lukas M.") | Data | n/a — stored as Person.display_first_name + initial of last. |
| Racket model ("Pure Aero 98") | Data | n/a |
| String spec ("Main 24 kg · Cross 23 kg · BYO") | Format + data | tension formatted with Babel.format_unit; "BYO" is a {% trans %} (order.byo.label); "same" in the cross collapse is {% trans %} (order.cross.same_as_main). |
| Date "2026-05-02" | Format | ISO YYYY-MM-DD per receipt-content OQ-R-2 default. |
| Currency "CHF 48.00" | Format | Babel.format_currency('CHF'). |
DE strings are Iris's later pass — Mira commits the catalogue keys + EN values; DE stays as # fuzzy until Iris signs off.
Open questions for Stefan (with proposed defaults)¶
- Queue cap: how many "Today" cards to show before "Show all"? Proposed default: 5. With 50 jobs/year × ~20 weeks of activity, peak-week open count is plausibly 3–5; 5 is a comfortable ceiling for a phone scroll. Easy to bump later.
- Recent cap: "Last 10" feels right at 50 jobs/year (~5 weeks of stringing context). Proposed default: 10. Configurable per-stringer is a future-round polish.
- Receipt number on Recent cards: shown as
#2026-0042per TR-1. Proposed default: yes, because Stefan often gets a "what was my receipt number for that string job" follow-up text from clients. Hide if it adds noise. - State badge for "Recent" rows: Recent is by definition Strung+, so the badge is redundant — but state-set varies (Strung vs Returned vs Done). Proposed default: show the badge. Cheap, and Stefan can see a "Strung but not yet Paid" gap at a glance.
- "Shared with me" placement: Inbox section, conditional on count > 0. Proposed default: keep. Alternative (always-shown with "0 new") clutters when most stringers won't have shares yet in early V2.
Cross-references¶
- Source requirement: V2 scope M9 + M16, order lifecycle hidden #1, client identity & sharing hidden #1.
- Data model: Order, ClientProfile, Person.
- Linked screens: Add-Stringjob flow (the CTA target). Order detail (out of Round-1).
- Tokens: design-tokens.
- i18n strategy: i18n architecture.
- Issue tracking: racket-book#95.