Score Dashboard

Summary

Score review and analysis tab within the event detail page. Sortable tables, filtering, CSV export, and void actions for event scores. Scores are submitted from mobile/PWA — the dashboard is read + manage only.

Shared contracts

This spec consumes the following from dashboard-shared:

  • Data table: score table with filters and pagination
  • Status badges: voided scores (line-through text-red-500)
  • Confirm dialog: void score
  • Role-gating: admin/instructor can void; all org members can view event scores; admin/instructor/RO can export CSV
  • Route ownership: Scores tab within /events/:id (no own routes)

Requirements

  • Score table with shooter name, drill, zones, time, points, hit factor, and date
  • Filter scores by shooter, drill, and voided status
  • Void scores with confirmation dialog (admin/instructor only)
  • CSV export of score data
  • Pagination via cursor-based “Load More”

Context

The scores tab lives inside EventDetail (owned by event-builder). It receives event data via signals from the parent component but fetches its own score data via ListScores(event_id).

Important: Scores are not created from the dashboard. They are submitted by shooters via mobile apps or PWA. The dashboard provides view, filter, export, and void capabilities for organizers.

API-to-UI mapping

ScoreService RPCs used

UI actionRPCKey request fieldsNotes
List event scoresListScoresevent_id, shooter_id (filter), drill_id (filter), final_only, paginationCursor-based, timestamp DESC
Void scoreVoidScorescore_idAdmin/instructor only. Confirmation required.
Get single scoreGetScorescore_idFor detail expansion (optional)

Score fields

FieldTypeSourceEditable?
idUUIDServerNo
shooter display_namestringResolved from shooter_idNo
drill_namestringDenormalized in scoreNo
drill_typeDrillTypeDenormalizedNo
calculationCalculationMethodDenormalizedNo
zone_breakdown[{zone_name, hits}]SubmittedNo
timedouble?SubmittedNo
pointsint32?Server-computedNo
hit_factordouble?Server-computedNo
penaltiesint32?SubmittedNo
passbool?Server-computed (PassFail)No
voidedboolServerVia VoidScore
notesstring?SubmittedNo
timestampTimestampSubmitted (when shot)No
created_atTimestampServerNo

All score fields are read-only from the dashboard. The only mutation is voiding.

Filtering

FilterInput typeAPI fieldNotes
ShooterDropdown (from roster)shooter_id”All Shooters” default
DrillDropdown (from event drills)drill_id”All Drills” default
Show voidedCheckboxfinal_onlyUnchecked = final_only: true (hide voided). Checked = final_only: false (show all).

Filters are dropdowns populated from the event’s roster and drill assignments (already loaded by parent). No server round-trip to populate filter options.

ListScores authorization

Per server rules:

  • Org members can view all event scores (event_id scoped)
  • Non-members get PermissionDenied
  • Dashboard users are always org members (enforced by session)

Component

ScoresTab

Rendered inside EventDetail when the Scores tab is active.

Props (received from parent):

  • event: Signal<Option<EventDto>> — event metadata
  • drills: Signal<Vec<EventDrillDto>> — assigned drills (for filter dropdown)
  • roster: Signal<Vec<RosterDto>> — roster entries (for shooter filter dropdown)

Layout:

  • Header: “Scores” + score count + “Export CSV” button (admin/instructor/RO)
  • Filter row: Shooter dropdown, Drill dropdown, “Show voided” checkbox
  • Score table
  • “Load More” button (cursor pagination)

Score table columns:

ColumnContentSort?
ShooterDisplay name (resolved)No (server doesn’t support)
Drilldrill_nameNo
TypeCalculation method badgeNo
ZonesAbbreviated zone breakdown (e.g., “A:5 C:3 D:1”)No
TimeFormatted seconds (e.g., “4.32s”) or ”-“No
PointsInteger or ”-“No
Hit Factor2 decimal places (e.g., “6.48”) or ”-“No
PassCheckmark / X / ”-” (PassFail only)No
DateFormatted timestampNo
ActionsVoid buttonNo

Row styling:

  • Voided scores: entire row uses line-through text-red-500 bg-red-50 (per shared badge contract)
  • Voided scores show “(Voided)” label in place of Void button

Shooter name resolution: Uses enriched roster data from GetEvent (see dashboard-shared server API gaps — roster entries must include display_name). The roster signal (shared from parent) provides the user_id → display_name mapping. Scores also contain drill_name denormalized, so no drill lookup is needed. ListOrgMembers is NOT used here (restricted to admin/instructor, but all org members can view scores).

Void flow:

  1. Click “Void” button on score row (admin/instructor only)
  2. Confirm dialog: “Void this score? This cannot be undone.”
  3. VoidScore(score_id) called
  4. On success: update score in local state (set voided=true), show toast
  5. On error: show error alert
  6. Voided scores remain visible with strikethrough styling (unless “Show voided” is unchecked)

CSV export:

  1. Click “Export CSV” button
  2. Client-side: iterate all loaded scores, generate CSV string
  3. Trigger browser download as {event_name}_scores_{date}.csv
  4. CSV columns: Shooter, Drill, Type, Zones, Time, Points, Hit Factor, Pass, Penalties, Notes, Date, Voided
  5. Note: exports only currently loaded scores. If pagination has more, show info: “Showing {loaded} of {total} scores. Load all before exporting.”

Empty state: “No scores recorded yet. Scores will appear here as shooters submit them from the mobile app.”

Live scores (deferred): WatchScores RPC exists but returns Unimplemented on server. Live streaming is deferred to a future item. Note in UI is not needed — scores refresh on tab focus or manual refresh.

New API module

apps/dashboard/src/api/score.rs:

DTOs:

  • ScoreDto — all score fields (see field table above)
  • ZoneCountDto — zone_name, hits
  • ListScoresRequest — event_id, shooter_id, drill_id, final_only, pagination
  • ListScoresResponse — scores, pagination
  • VoidScoreRequest — score_id

Methods:

  • list_scores(token, req), void_score(token, score_id)

Test plan

  • List scores: Navigate to event with scores → scores tab shows table with all score data
  • Filter by shooter: Select shooter from dropdown → table shows only that shooter’s scores
  • Filter by drill: Select drill → table shows only scores for that drill
  • Combined filters: Shooter + drill → intersection shown
  • Show/hide voided: Check “Show voided” → voided scores appear with strikethrough. Uncheck → voided hidden.
  • Void score: Click Void → confirm → score shows strikethrough + “(Voided)” label
  • CSV export: Click export → CSV file downloads with correct data
  • Authorization: Member cannot see Void button. RO can view and export but not void.
  • Empty state: New event with no scores → shows empty message
  • Pagination: Event with many scores → “Load More” fetches next page

Acceptance criteria

  1. Scores tab within event detail shows all event scores in a table with shooter name, drill, zones, time, points, hit factor, and date.
  2. Shooter and drill dropdown filters narrow the displayed scores; dropdowns are populated from event roster and drill assignments.
  3. “Show voided” checkbox toggles voided score visibility; voided scores display with strikethrough styling.
  4. Void button (admin/instructor only) triggers confirmation dialog; voiding updates the score row immediately without full reload.
  5. CSV export downloads all loaded scores as a CSV file with all columns.
  6. Shooter names are resolved from enriched roster data in GetEvent response (see dashboard-shared server API gaps).
  7. Pagination via “Load More” fetches additional score pages.
  8. Empty state shows helpful message when no scores exist.
  9. RO users can view and export but cannot void. Members can view only. Server enforces authorization as fallback.
  10. End-to-end: Create event (event-builder) → add drills → add roster (roster-builder) → scores submitted via mobile → scores tab shows submitted scores → admin voids one → voided score shows strikethrough → CSV export includes voided column.