CourtsideDesk — Requirements Document¶
Owner: Alex (Business Analyst)
Status: Session 2 complete — scoring formats, match result structure, round robin standings, and bracket topology resolved. Session 3 addendum (2026-04-10) — DrawFormat catalogue disambiguated for Sam's first packages/db migration (F4 numbering gap, F6 games-per-set, F6 tiebreak trigger).
Last updated: 2026-04-10
1. Project Context¶
CourtsideDesk complements the SwissTennis platform, which is the system of record for Swiss tennis tournaments. SwissTennis handles: publishing tournaments, categories, draws, timetables, and results. It has no public API and provides no friendly UI.
CourtsideDesk does not replace SwissTennis. It provides: 1. A digital signage display for tournament venues 2. An organizer toolset for managing the on-site experience
2. Users / Personas¶
| Persona | Description | Authentication |
|---|---|---|
| Tournament Organizer | Creates and manages tournament data, assigns matches to courts, enters results | Required |
| Player / Visitor | Views live signage screens | None (public) |
Phase: Organizer is the primary persona for v1. Player features are secondary.
3. Data Ingestion — Staged Approach¶
| Stage | Method | Scope |
|---|---|---|
| 1 (v1 launch) | Manual data entry via web forms | All tournament data entered by organizer |
| 2 (future) | File import from SwissTennis exports (Excel .xls/.xlsx and PDF) | Schedule, draws, player registrations, results |
| 3 (future) | Automated web scraping of SwissTennis portal | Full automation, no manual trigger |
| 3 (planned) | Live online import from SwissTennis Advantage JSON servlets | One-shot tournament import, per-event bracket import, schedule mirror, background sync. See specs/swisstennis-api-integration.md. |
[ASSUMPTION] The data model must support all three ingestion methods from day one. No schema migration between stages. Every entity must support a source field indicating origin.
4. Screen Inventory¶
4.1 Digital Signage (public, unauthenticated)¶
SCR-001 — Schedule Screen¶
Purpose: Live court schedule displayed on venue screens.
Layout:
- One column per court (dynamic)
- Each column has one highlighted Main Row = current match on court
- Above main row: upcoming matches, sorted by planned_start_time ASC (next match closest to main row)
- Below main row: completed matches, sorted planned_start_time DESC (most recent closest to main row)
- When a new match is assigned as current, previous current match shifts below
Match display fields:
| Field | Notes |
|---|---|
| Planned date & start time | |
| Actual start time | Set when match begins |
| Status | Planned / On Court / Completed |
| Player 1 & 2 | Format: ([SEED\|WC\|Q]) FirstName LastName (RANKING) — all parts optional |
| Category | Draw name (e.g. "WS 30+ R7/R9") |
| Round label | Optional free-text: "Final", "Semifinal", "QF" |
| Result | Completed matches only — standard tennis score or "WO" |
| Winner highlight | Winning player visually distinguished |
Status transitions:
- Planned → On Court: organizer assigns match to court
- Any → Completed: result entered OR organizer manually marks complete (either triggers Completed)
SCR-002 — Draw / Bracket View¶
Purpose: Display draws on venue screens with live on-court match highlighting.
Screen parameter: Draw ID — operators configure which draw a given screen shows.
Sub-View A — Single Elimination Bracket: - Traditional left-to-right tree - Rounds as vertical columns (R16, QF, SF, Final, etc.) - Per-match fields: same as SCR-001 - On-court matches visually highlighted within the bracket - Winner highlighted and shown advancing to next round
Sub-View B — Round Robin: - Left panel: Result grid — player vs. player matrix, score in each cell - Right panel: Standings table — ranked by tournament standing
Standings columns (left to right): POS, PLAYER, MP, MW, ML, SW, SL, GW, GL
| Column | Meaning |
|---|---|
| POS | Current rank within group |
| PLAYER | Player name |
| MP | Matches played |
| MW | Matches won |
| ML | Matches lost |
| SW | Sets won |
| SL | Sets lost |
| GW | Games won |
| GL | Games lost |
Tiebreak sequence (applied in order when two or more players are tied on MW). Canonical SwissTennis cascade per Rafael ruling (Session 4, 2026-04-10):
| Step | Rule | Scope |
|---|---|---|
| 1 | H2H matches won — recursive mini-table (if 2 tied, direct match; if 3+ tied, sub-sort by wins within the mini-table) | Among tied players only |
| 2 | Overall set ratio (sets won / sets lost) | All group matches |
| 3 | Overall game ratio (games won / games lost) | All group matches |
| 4 | Set ratio within H2H mini-table (second pass, restricted to still-tied players) | Among tied players only |
| 5 | Referee draw — Losentscheid |
App flags for manual intervention — no auto-assignment |
Implementation notes:
- Percentages / ratios use integer cross-multiplication to avoid float drift
- After each step, players that stand apart are finalised; remaining tied group recurses into the next step on the sub-pool only (not the full pool)
- A residual tie after step 4 is surfaced to the caller as {status: "manual-draw-required", partialOrder, tiedGroups} — the standings calculator does not auto-break
- Previous draft had 3 H2H steps before overall (H2H wins → H2H set% → H2H game% → overall set% → overall game% → manual). Corrected to SwissTennis canonical ordering per Rafael domain ruling.
Special result handling in standings calculation: - Walkover (WO) / Default (DEF): win awarded to the opponent; sets array is empty — do NOT impute 6-0 6-0 scores - Retirement (ret.): win awarded to the opponent; sets and games counted up to the retirement point only; last set flagged incomplete and its games are included in GW/GL - Super-tiebreak [10-7]: counts as 1 set won/lost for SW/SL; points do NOT count as games in GW/GL
Both sub-views:
- Real-time refresh
- is_loser_draw = true draws never rendered
- No authentication required
[RESOLVED — Phase 1 ship, 2026-04-10] Bracket topology storage: FK columns on the matches row. Canonical shape is next_match_id self-FK + winner_slot enum (UPPER / LOWER) on every match. The bracket_edges alternative was considered and rejected — the FK-on-row model keeps traversal to a single column chain and fits the packages/scoring computeAdvancement intent-builder cleanly. See packages/db/drizzle/0000_initial_schema.sql for the shipped definition.
[RESOLVED — Rafael] Round robin standings columns and tiebreak sort order — see Sub-View B above.
4.2 Organizer View (authenticated)¶
SCR-003 — Contact Sheet¶
Purpose: Player directory for the current tournament.
Scope: Players registered for the currently open tournament only.
Display: - Table sorted alphabetically by last name (default) - Real-time name search/filter - Columns: Player Name (First Last), Home Phone, Work Phone, Mobile Phone, Email - Phone columns rendered via unified display formatter (see SCR-003.1)
Copy Emails button:
- Copies emails of currently visible/filtered players only
- Semicolon-delimited (;) — ready to paste into Outlook To: field
- Players without email silently excluded
Acceptance criteria: - AC-001: Only players registered for the current tournament shown - AC-002: Default sort: alphabetical by last name A→Z - AC-003: Name filter narrows list in real time - AC-004: Copy Emails copies only filtered/visible players' emails, semicolon-delimited - AC-005: Players without email silently excluded from copy
SCR-003.1 — Unified Phone Number Display (Contact Sheet)¶
Purpose: Render the three phone columns (phone_home, phone_work, phone_mobile) on /contacts in a single, internationally recognised format regardless of how SwissTennis stored them, while leaving the stored values untouched.
Non-goal (explicit): The import pipeline must NOT rewrite, normalise, or canonicalise phone numbers. Storage remains exactly as delivered by SwissTennis (see §6 cols 13–15 and ENTITY-005 phone_home / phone_work / phone_mobile). Formatting is a display-only transformation, applied at render time in the organizer UI.
Display rules:
- Each stored phone value is parsed and rendered in the library's INTERNATIONAL format (E.164-grouped national format with country prefix), e.g.:
- CH: +41 79 123 45 67
- DE: +49 30 12345678
- FR: +33 6 12 34 56 78
- Default parse region: CH — always, no exceptions. Any stored phone value that does NOT begin with a leading country-code prefix (+XX or 00XX) MUST be parsed as a Swiss number. Examples — all of these are Swiss and format to +41 79 123 45 67: 079 123 45 67, 0791234567, 0041791234567, +41 79 123 45 67. There is no heuristic detection of other regions from bare digits; a non-Swiss number is only recognised when it arrives with an explicit +XX / 00XX prefix. Per-tournament / per-org default region is explicitly out of scope for this ticket and tracked as a follow-up.
- Unparseable input fallback: if the library cannot parse the string (garbage, extensions, alphanumeric, etc.), the raw stored value is rendered unchanged. No error, no empty cell, no visual hint or affordance (PO decision: silent raw render — the organizer can't fix SwissTennis data from this screen anyway).
- Null / empty values render as empty cells, same as today.
Toggle:
- Single UI control at the top of the /contacts table, labelled "Formatted phone numbers" (final label / placement owned by Jamie).
- Applies to all rows simultaneously. Per-row toggle is explicitly out of scope.
- Default state: ON (formatted).
- When OFF: every phone cell shows the raw stored value exactly as in the database.
Toggle persistence:
- Persist via browser localStorage, scoped per user/browser (key suggestion: courtsidedesk.contacts.phoneFormat, values "formatted" | "raw").
- No server-side preference, no per-tournament setting. Morgan / Taylor to confirm key naming and SSR-safe access pattern (hydration: server renders formatted default; client effect swaps to persisted value on mount).
Technical note — library choice:
- Use libphonenumber-js (the maintained port of Google's libphonenumber). Tree-shakeable, browser-safe, no hand-rolled regex. Call parsePhoneNumberFromString(value, 'CH') and render phoneNumber.formatInternational(). Catch parse errors and fall through to raw.
- Do NOT introduce a server-side phone normalisation step. Formatting happens in the React component rendering the cell (or a small shared util consumed by it).
Scope boundary:
- Only /contacts (SCR-003) is in scope for this ticket.
- Other surfaces that may display phone numbers (none confirmed in v1 requirements) are out of scope and will be tracked as separate follow-up tickets if/when they appear.
Acceptance criteria:
- AC-006: Phone columns on /contacts render in international format by default for parseable values. Any value without a +XX / 00XX prefix MUST be parsed as Swiss (e.g. 079 123 45 67 → +41 79 123 45 67; 0041791234567 → +41 79 123 45 67).
- AC-007: Numbers already stored with a country code are re-grouped to the library's international format for the detected country (e.g. +4915112345678 → +49 151 12345678).
- AC-008: Unparseable values render as the raw stored string, unchanged, without throwing.
- AC-009: A toggle at the top of the table switches all rows between formatted and raw views; there is no per-row toggle.
- AC-010: Default toggle state on first visit is ON (formatted).
- AC-011: Toggle state persists across page reloads within the same browser via localStorage; is not sent to the server.
- AC-012: Import pipeline writes to phone_home / phone_work / phone_mobile remain byte-for-byte identical to the SwissTennis source (regression test against existing fixtures).
- AC-013: No change to the database schema, API contract, or stored values. Pure UI change.
[ASSUMPTION — confirmed by PO] Default parse region is hard-coded to CH for v1. Any number without a leading +XX / 00XX prefix is Swiss. If a non-Swiss organizer onboards later, a per-tournament default region setting is a separate ticket.
[RESOLVED — PO decision] Unparsed values render silently (raw string, no tooltip, no icon, no styling hint).
[RESOLVED] Shared util location and packages/ui vs. app-local placement are implementer's choice at build time (Morgan / Taylor decide during implementation; PO defers to experts).
SCR-004 — Tournament Setup¶
Purpose: Create and configure a tournament.
Fields: - Tournament name (free text) - Start date / End date (end ≥ start) - Courts: list of free-text names/numbers, each with a sort order - Draws: list of draws, each with a name (e.g. "WS 30+ R7/R9") and format (Single Elimination or Round Robin)
No registration settings (no deadline, no player cap).
SCR-005 — Court Assignment / Match Scheduling¶
Purpose: Operational hub for the organizer on a live tournament day.
Layout:
- Unassigned Matches Panel: Scrollable list of all matches not yet assigned to a court. TBD-player matches freely shown — no blocking.
- Court Columns: One column per court (ordered by sort_order), each with:
- Current match (top, visually distinct)
- Upcoming matches sorted by planned_start_time ASC (nulls last)
Interactions:
| Action | Behaviour |
|---|---|
| Drag from panel → court column | Assigns match; removes from unassigned panel |
| Drag between court columns | Moves match; re-sorts by planned time in new column |
| Drag match back to panel | Clears court_id + planned_start_time; returns to unassigned |
| Inline time edit | Optional; past times valid; saves → re-sort + real-time push to SCR-001 |
| Result entry | Inline on match card; saves → status = COMPLETED + real-time push to SCR-001 + SCR-002 |
Real-time write paths (first-class requirements): 1. Assignment / time change → SCR-001 updates live 2. Result entry → SCR-001 + SCR-002 (bracket advancement) update live
Acceptance criteria: - AC-001: Unassigned matches listed; TBD-player matches shown without blocking - AC-002: Drag to court assigns match; disappears from unassigned panel - AC-003: Drag between courts moves match; re-sorts by planned time - AC-004: Remove from court clears court_id + planned_start_time - AC-005: Inline time edit optional; past times accepted; triggers re-sort - AC-006: Result entry sets status = COMPLETED - AC-007: Assignment/time edit/result entry propagates live to SCR-001 (target: < 2s) - AC-008: Result entry additionally propagates live to SCR-002 - AC-009: Upcoming matches always sorted by planned_start_time ASC; nulls last - AC-010: Completed matches do not clutter the active court column view
5. Data Entities¶
ENTITY-001 — Tournament¶
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
name |
String | e.g. "Stadtmeisterschaft 2026" |
start_date |
Date | |
end_date |
Date | ≥ start_date |
ENTITY-002 — Court¶
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
tournament_id |
UUID FK | Scoped per tournament — no global court registry |
name |
String | Free text: "1", "Centre Court", "Platz 3" |
sort_order |
Integer | Controls display order |
ENTITY-003 — Draw¶
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
tournament_id |
UUID FK | |
name |
String | Opaque label — e.g. "WS 30+ R7/R9". No parsing. |
format |
Enum | SINGLE_ELIMINATION | ROUND_ROBIN |
draw_format_id |
UUID FK | References DrawFormat (ENTITY-010) — scoring format for all matches in this draw |
is_loser_draw |
Boolean | Default false. Stored but never rendered. |
ENTITY-004 — Match¶
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
tournament_id |
UUID FK | |
draw_id |
UUID FK / null | Nullable — standalone matches supported |
court_id |
UUID FK / null | Null until assigned |
planned_start_time |
DateTime / null | Editable; past values valid |
actual_start_time |
DateTime / null | Set when match begins |
status |
Enum | planned | on_court | completed |
player1_id |
UUID FK / null | Nullable for TBD players |
player2_id |
UUID FK / null | Nullable for TBD players |
player1_seed_or_id |
String / null | Seed number or "WC", "Q", "LL" |
player2_seed_or_id |
String / null | |
round_label |
String / null | Free text: "Final", "Semifinal" |
result |
MatchResult / null | Structured set-score result object (see ENTITY-007). Null until completed. |
winner |
Enum / null | player1 | player2 | null |
next_match_id |
UUID FK / null | Elimination brackets — FK → Match; null for the final match in a draw |
winner_slot |
Enum / null | Elimination brackets — UPPER | LOWER; which slot the winner fills in the next match |
source |
Enum | manual | import | scrape |
[RESOLVED] Bracket topology: FK fields on Match row (Option A). next_match_id and winner_slot added above. Double-elimination is explicitly out of scope.
[RESOLVED] Match result format — structured set-score entry confirmed. See ENTITY-007 (MatchResult), ENTITY-008 (SetScore), ENTITY-009 (TiebreakScore).
ENTITY-005 — Player¶
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
license_number |
String / null | SwissTennis Lizenznummer — upsert key on import |
first_name |
String | |
last_name |
String | |
date_of_birth |
Date / null | |
address_street |
String / null | |
address_co |
String / null | |
address_postal_code |
String / null | |
address_city |
String / null | |
address_country |
String / null | |
phone_home |
String / null | |
phone_work |
String / null | |
phone_mobile |
String / null | |
email |
String / null | |
ranking_category |
String / null | Swiss ranking: "R3", "R6", "R9" — this IS the displayed ranking |
ranking_value |
Float / null | Underlying numeric value e.g. 7.284 |
club_code |
String / null | SwissTennis club ID |
club_name |
String / null | Denormalized — no Club entity for now |
source |
Enum | manual | import | scrape |
[ASSUMPTION] license_number is the canonical identity key. On import: upsert if exists, create if not.
[ASSUMPTION] Ranking fields = snapshot at time of last import. Ranking history is out of scope.
[ASSUMPTION] Club denormalized for now; Club entity deferred.
ENTITY-006 — Registration¶
Links a Player to a Tournament + Draw. One record per player per draw entered.
| Field | Type | Notes |
|---|---|---|
id |
UUID | |
player_id |
UUID FK | |
tournament_id |
UUID FK | |
draw_id |
UUID FK | References the draw (Konkurrenz) |
registered_at |
DateTime / null | From SwissTennis Anmeldedatum |
is_seeded |
Boolean | Seeding is per-category |
qualifier_wildcard |
String / null | "Q", "WC", "LL", or null |
restrictions |
String / null | Admin note from SwissTennis |
comment |
String / null | |
is_confirmed |
Boolean | |
is_online_registration |
Boolean | |
is_paid |
Boolean |
ENTITY-007 — MatchResult¶
Embedded object stored on Match. Replaces the former free-text result string.
| Field | Type | Notes |
|---|---|---|
outcome |
Enum | COMPLETED | WALKOVER | DEFAULT | RETIREMENT |
sets |
SetScore[] | Ordered array (ENTITY-008). Empty for WALKOVER and DEFAULT. |
matchTiebreak |
TiebreakScore / null | Populated only for formats with a standalone match tiebreak (F3, F8) |
ENTITY-008 — SetScore¶
One entry per set played.
| Field | Type | Notes |
|---|---|---|
setIndex |
Integer | 0-based position in the match |
setType |
Enum | NORMAL | MATCH_TIEBREAK |
player1Games |
Integer | |
player2Games |
Integer | |
tiebreak |
TiebreakScore / null | Present when a set-level tiebreak was played (ENTITY-009) |
isComplete |
Boolean | false if the set was interrupted by retirement |
ENTITY-009 — TiebreakScore¶
Used for both set-level tiebreaks and standalone match tiebreaks.
| Field | Type | Notes |
|---|---|---|
player1Points |
Integer | |
player2Points |
Integer | Loser's score stored per tennis convention — e.g. 7-6(4) means tiebreak was 7-4 |
targetPoints |
Integer | Winning target (7 for standard TB, 10 for match TB, 5 for Fast4) |
winByTwo |
Boolean | Whether the winner must win by 2 points |
ENTITY-010 — DrawFormat¶
Scoring format configuration. One record per format; referenced by FK on Draw.
Supported formats:
| formatId | Name | Best Of | Games/Set | Final Set Rule | TB Target | No-Ad |
|---|---|---|---|---|---|---|
| F1 | Standard, Advantage Final Set | 3 | 6 | ADVANTAGE | 7 (sets 1–2) | No |
| F2 | Standard, Tiebreak All Sets | 3 | 6 | TIEBREAK | 7 | No |
| F3 | Match Tiebreak (Super TB) | 2+MTB | 6 | MATCH_TIEBREAK | 7 (sets 1–2) / 10 (MTB) | No |
| F5 | Fast4 Best of 3 | 3 | 4 | TIEBREAK | 5 (win by 1) | Yes |
| F5b | Fast4 Best of 5 | 5 | 4 | TIEBREAK | 5 (win by 1) | Yes |
| F6 | Pro Set | 1 | 8 | TIEBREAK | 7 (at 7-7) | No |
| F7 | Short Set No-Ad | 3 | 4 | TIEBREAK | 7 (win by 2) | Yes |
| F8 | Standalone Match Tiebreak | 0+MTB | — | — | 10 | — |
[RESOLVED — Session 3, 2026-04-10] DrawFormat catalogue has 8 rows. F4 is a reserved, intentionally unused id.
The catalogue contains exactly 8 DrawFormat rows: F1, F2, F3, F5, F5b, F6, F7, F8. The id "F4" is deliberately reserved and not seeded — it corresponds to a legacy Swiss club format code that predates this catalogue. Reserving the id (rather than renumbering F5→F4) preserves compatibility with paper draw sheets, tournament bulletins, and future SwissTennis import mappings that may still reference "F4". F8 is a first-class format in the catalogue despite being a degenerate "no sets, match tiebreak only" rule set; the scoring package branches on finalSetRule = MATCH_TIEBREAK with gamesPerSet = 0 to drive MTB-only mode. Morgan's tech stack doc describes this as "7 DrawFormat variants" — that count refers to the seven full scoring rule sets (F1, F2, F3, F5, F5b, F6, F7) and excludes F8 as a degenerate case; the DB-level row count is 8. Morgan's doc needs a clarifying footnote; see Session 3 report.
[RESOLVED — Session 3, 2026-04-10] F6 Pro Set is canonical at 8 games per set.
The "8 or 10" phrasing in earlier drafts has been retired. The 8-game Pro Set is the overwhelmingly dominant variant at Swiss club tournaments; the 10-game variant is rare enough that it does not justify a per-draw override column or a second catalogue row in v1. If the 10-game variant is needed later, a future F6b row can be added with no schema migration — draw_formats is a seed catalogue, not a schema change.
[RESOLVED — Session 3, 2026-04-10] F6 final-set tiebreak triggers at 7-7. The "7-7 or 9-9" phrasing in earlier drafts has been retired. Canonical F6 plays a 7-point tiebreak when the score reaches 7-7 (i.e., first to 8 games, or tiebreak at 7-7). This is the mainstream Swiss club convention for an 8-game Pro Set. The 9-9 variant belongs to a longer Pro Set format that is not in scope for v1. If it becomes required, it will ship as a separate catalogue row (e.g., F6c) and not as a per-draw override.
[ASSUMPTION] Best of 5 / Grand Slam format is reserved for a future release. Do not implement now.
[ASSUMPTION] Scoring format is scoped to the Draw, not the Tournament. Multiple draws within the same tournament may use different formats.
DrawFormat fields:
| Field | Type | Notes |
|---|---|---|
formatId |
UUID | |
name |
String | Human-readable label |
bestOf |
Integer | 1, 3, or 5 (or conceptual value for MTB-only formats) |
gamesPerSet |
Integer | 4, 6, 8, or 10 |
noAd |
Boolean | If true, deuce point decides immediately (no advantage) |
finalSetRule |
Enum | ADVANTAGE | TIEBREAK | MATCH_TIEBREAK |
finalSetTiebreakAt |
Integer / null | Game score at which final-set tiebreak is triggered; null for ADVANTAGE |
standardTiebreakTarget |
Integer | Points to win a set-level tiebreak |
matchTiebreakTarget |
Integer | Points to win a match tiebreak (used when finalSetRule = MATCH_TIEBREAK) |
tiebreakWinByTwo |
Boolean | Whether tiebreak winner must win by 2 points |
Validation rules (application-layer, driven by DrawFormat config):
1. Game scores must not exceed gamesPerSet
2. "Win by 2" at game level is disabled when noAd = true
3. Tiebreak winning score must satisfy >= targetPoints and the applicable win-by rule
4. WALKOVER / DEFAULT: sets array must be empty
5. RETIREMENT: the last set in sets must have isComplete = false
6. Tiebreak loser's score is stored in parentheses in display (e.g. 7-6(4) means tiebreak was 7-4)
6. SwissTennis Export — Field Mapping¶
PlayerList.xls (confirmed format)¶
File structure: tournament header rows 0–2, column headers row 3, data from row 4. One row per registration.
| Col | SwissTennis field | Translation | Entity | Field name |
|---|---|---|---|---|
| 0 | Konkurrenz | Category / Competition | Draw / Registration | draw.name |
| 1 | Anmeldedatum | Registration date | Registration | registered_at |
| 2 | Lizenznummer | License number | Player | license_number |
| 3 | Klub | Club code | Player | club_code |
| 4 | Klub Name | Club name | Player | club_name |
| 5 | Name | Last name | Player | last_name |
| 6 | Vorname | First name | Player | first_name |
| 7 | Geburtsdatum | Date of birth | Player | date_of_birth |
| 8 | Adresse | Street address | Player | address_street |
| 9 | c/o | c/o | Player | address_co |
| 10 | PLZ | Postal code | Player | address_postal_code |
| 11 | Ort | City | Player | address_city |
| 12 | Land | Country | Player | address_country |
| 13 | Tel P | Home phone | Player | phone_home |
| 14 | Tel G | Work phone | Player | phone_work |
| 15 | Mobile | Mobile phone | Player | phone_mobile |
| 16 | Player | email |
||
| 17 | Klassierung | Swiss ranking category | Player | ranking_category |
| 18 | Klass. Wert | Ranking numeric value | Player | ranking_value |
| 19 | Gesetzte | Seeded flag | Registration | is_seeded |
| 20 | Q/WC | Qualifier / Wild Card | Registration | qualifier_wildcard |
| 21 | Einschränkungen | Restrictions | Registration | restrictions |
| 22 | Kommentar | Comment | Registration | comment |
| 23 | bestätigt | Confirmed | Registration | is_confirmed |
| 24 | On-line Anmeldung | Online registration | Registration | is_online_registration |
| 25 | bezahlt | Paid | Registration | is_paid |
Note: Date fields (cols 1, 7) are Excel date serials — must be converted to ISO dates on import.
7. Key Decisions¶
| Decision | Detail |
|---|---|
| Courts are per-tournament | No global court registry |
| Draw names are opaque labels | No parsing of e.g. "WS 30+ R7/R9" |
| Loser draws stored but never rendered | is_loser_draw = true suppressed at display layer |
| Standalone matches supported | draw_id is nullable on Match |
| TBD-player matches freely assignable | No blocking on court assignment |
| Past start times valid | Delayed matches accepted without error |
| Player is a persistent entity | Persists across tournaments; upsert on license_number |
| Real-time write paths are first-class | Assignment and result entry both push live to signage |
| No registration settings | No deadline, no player cap |
| Contact Sheet is tournament-scoped | Filters to current tournament registrations only |
| Copy Emails respects active filter | Copies only visible/filtered players |
| Ranking displayed is Swiss system | ranking_category (R3, R6 etc.) — not ATP/WTA points |
| Match result is structured, not free-text | MatchResult object with SetScore[] and TiebreakScore; replaces score string |
| Scoring format is per-Draw, not per-Tournament | draw_format_id FK on Draw; draws within the same tournament may differ |
| Best of 5 / Grand Slam format deferred | Not in scope for v1 |
| WO/DEFAULT sets array must be empty | Do not impute 6-0 6-0; outcome enum carries the result type |
| Retirement: partial set is included | Games up to retirement point counted; last set flagged isComplete = false |
| Super-tiebreak counts as 1 set | Points do not feed into game totals (GW/GL) |
| RR tiebreak final step is manual (Losentscheid) | App flags the tie for manual referee intervention — no automatic draw |
| Bracket topology: FK fields on Match row (Option A) | next_match_id + winner_slot on Match; double-elimination explicitly out of scope |
| DrawFormat catalogue has 8 rows | F1, F2, F3, F5, F5b, F6, F7, F8. "F4" id is reserved/unused for legacy compatibility. Morgan's "7 variants" count refers to full scoring rule sets only (F8 excluded as degenerate). |
| F6 Pro Set canonicalised at 8 games per set, TB at 7-7 | "8 or 10" and "7-7 or 9-9" variants retired. 10-game / 9-9 variants, if ever needed, ship as separate catalogue rows, not per-draw overrides. |
8. Open Items¶
| Item | Description | Owner | Status |
|---|---|---|---|
| Round robin standings columns | Standard Swiss tennis RR standings columns + tiebreak logic | Rafael | RESOLVED — see SCR-002 Sub-View B and ENTITY-010 |
| Bracket topology storage | FK on Match row vs. bracket_edges table |
Morgan | RESOLVED — Option A: next_match_id + winner_slot FK fields on Match row; double-elimination out of scope |
| Match result format | Free-text assumed — structured set-score entry TBD | PO / Morgan | RESOLVED — structured MatchResult confirmed; see ENTITY-007–009 |
| Unified phone display on /contacts | Display-only formatter + toggle; storage untouched; libphonenumber-js recommended |
Taylor (impl) / Jamie (UX) / Morgan (arch) | SPEC READY — see SCR-003.1 |