Skip to content

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:

  1. What do I need to string today? — the open queue.
  2. What did I just finish? — recent strung activity (so Stefan can verify a save landed).
  3. 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

  • 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 key dashboard.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_at ascending (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 coffee icon, 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_at descending (most-recent strung first).
  • Filter: strung_at IS NOT NULL AND deleted_at IS NULL, limit 10.
  • Card content: state badge, strung_at date, 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 on md+.
  • Hit target: 48 × 48 px minimum; on sm the 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)

  1. 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.
  2. 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.
  3. Receipt number on Recent cards: shown as #2026-0042 per 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.
  4. 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.
  5. "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