Skip to content

feat: tracks own date, location, availability, events, shifts, etc. #71

Description

@ethnjs

Background

A tournament today is a single scheduled thing: one location, one start_date/end_date, one division list. TournamentTrack exists but is only a named bucket (id, tournament_id, name, is_archived, allow_confirm) used for per-member status. Everything schedule-shaped hangs off the tournament and, below that, off dates — shifts validate against the tournament's date range, and the reserved form field keys bake a date into the key string (availability_20270213, lunch_20270213_protein).

Real tournaments aren't shaped like that. An umbrella regional runs Day 1 at UCI on 02/13 in division C and Day 2 at Northwood on 02/20 in division B, plus undated tracks for test writing and review. Date is the wrong scope key — track is. A date range is also the wrong shape: Feb 13 plus Feb 20 is two days, not an eight-day span, and nothing today can say that.

Motivation

Multi-site, multi-day, multi-division tournaments can't be represented at all right now, and a TD running one has to either fake it as a single blob or create separate tournaments that don't share a roster. Making tracks the scope key also gives event assignments (#70) the event↔track link they need, and gives member profile display configuration (#69) a stable key to hang on instead of a date string that changes whenever a tournament moves.

Proposed changes

Tracks own when, where, and what. A track gains a primary/cosmetic distinction, its own start and end dates, its own venue (a university or a free-text location, never both), and its own division list. A primary track must have all of them; a cosmetic track — test writing, review — leaves them all empty. Every tournament needs at least one live primary track.

The tournament derives its display from its primary tracks and loses its own location, dates, and division columns entirely. It exposes a list of dates — the union of each primary track's own days — rather than a start/end range, so a two-day tournament three weeks apart reports two days and not twenty-two. A tournament with exactly one primary track renders as the single line it does today; more than one renders per-track rows. Cosmetic tracks never affect this. First and last day survive internally for age derivation, auto-archiving, and sorting, but never appear in a response.

Shifts belong to a track and validate against that track's date range instead of the tournament's. Cosmetic tracks have no shifts, and therefore no availability. The shift list becomes readable by any tournament member rather than requiring event-management permission — it carries nothing confidential, and other surfaces need the catalog.

Events link to tracks explicitly, through a new many-to-many table rather than being derived through their shifts. Derivation doesn't work: a cosmetic track has no shifts by construction, so test writing could never hold an event. Attaching a shift to an event automatically links that shift's track, so a TD only ever adds links by hand for undated tracks. Both an event's tracks and its shifts become properties set on the event itself, replacing the current per-link attach/detach routes.

Events lose their own start and end times. An event's schedule is the list of days its shifts fall on. This also settles a question this refactor opens — a gap day between two tracks can't be an event time, because no shift falls there — so the tournament-bounds check on events is deleted rather than repointed at a track. An event on a cosmetic track correctly has no days at all. The only guard that survives is that two shifts on one event still can't overlap, which is the rule assignments will build on.

Reserved form field keys re-key from dates to tracks. Availability, lunch, and event-preference keys all take a track id in place of the date, and event preference collapses to exactly one question per track. Availability's "wrong day" validation becomes a wrong-track check; event-preference options validate against the events actually linked to that track, so a question on test writing offers exactly the test-writing events. Where an availability question also sets track status, its option values collapse from a list of per-track statuses to a single status — the field's own track.

Write-through storage re-scopes to match. Stored lunch selections swap their date for a track; stored event preferences gain a track and drop their field key, since one question per track makes the track the whole scope. Member-facing read builders group by track. Display-configuration keys move from availability_day:{date} and lunch_category:{cat} to track-scoped equivalents.

Track archiving stops being a TD toggle and becomes a deletion lifecycle. Deleting a track with no TD-authored references — no shifts, no linked events, no form fields naming it — deletes it immediately, cascading the member data (statuses, availability, lunch, preferences) that hangs off it. Deleting one that is referenced marks it pending deletion instead, with the response naming the blocking shifts, events, and fields and the count of member responses that would be lost. A pending-delete track disappears from every surface except the tracks section of tournament settings, where a TD can restore it. Repointing the last blocking reference — moving a shift or a form field to another track — purges the track automatically. Deleting or demoting the last live primary track, or clearing a primary track's dates, venue, or division, is rejected. Both lifecycle transitions write audit-log entries.

Frontend. The tracks settings page and its sidebar entry are removed; a full per-track editor moves into general settings, which loses its own location, date, and division fields. The delete flow there shows the blocking references and response-loss count, and lists pending-delete tracks with a restore action. Tournament creation keeps its current simple mode, silently creating one primary track named after the tournament, and gains an advanced mode with a repeatable track editor. The shifts page gets a track selector. The form builder replaces its day pickers with track pickers, filters shift and event options to the field's track (greying unlinked events with a reason), and collapses the track-status control to a single status menu. The members table and display-config surfaces move to the new keys. Every frontend read of the tournament's start and end date is rewritten against the date list — a range would claim the tournament runs on days it doesn't.

No data migration of presets or member responses. Dev data only: the migration reverts every reserved field to a plain question, keeping its label, description, and option labels, and clears the four write-through tables. TDs re-bind presets to tracks by hand.

What will not change

  • Assignments (feat: tournament event assignments #70) are out of scope. The event↔track link established here is what they build on. Note that this supersedes that issue's "tracks will not gain any notion of owning a set of events" — they now do, as a TD-managed link, though assignment itself still stays scoped per event.
  • Member self-service editing is out of scope and stays with member profile (feat: member profile #69) — this issue changes the shape of what those routes write, not who can write it.
  • Display configuration itself is unchanged (feat: member profile #69) — the same things can be hidden from the same surfaces; only the key format moves from dates to tracks. Saved configs referencing old keys are ignored.
  • Tournament start and end date do not come back under another name. They remain internal helpers and never appear in a response.
  • Server-side age filtering still doesn't exist. The 18+/21+ derivations just repoint from the tournament's start date to its first day.
  • Events' existing times are not preserved. An event's schedule becomes meaningful again only once it has shifts; no rule converts one into the other.
  • The track status field grammar is unchanged — it stays multi-track through its option values rather than taking a track in its key.

Completion criteria

  1. Migrating a pre-change database gives every tournament exactly one primary track carrying its old location, dates, and division; every shift points at that track; every event is linked to the tracks of its shifts; and no reserved field key survives.
  2. A tournament with primary tracks on Feb 13 and Feb 20 reports exactly those two dates — not the span between them — everywhere dates are shown.
  3. Simple-mode creation produces one primary track and an unchanged tournament header; advanced mode with two dated tracks at different venues and divisions produces per-track rows.
  4. A cosmetic track with no dates rejects shifts and availability but accepts lunch, and an event-preference question on it offers an event linked to both it and a dated track, despite the cosmetic track having no shifts.
  5. Deleting an unreferenced track removes it and its member data immediately; deleting a referenced one marks it pending deletion with the blocking references and response-loss count named, hides it everywhere but tournament settings, and allows restore.
  6. Repointing the last blocking reference off a pending-delete track purges it automatically, and both transitions appear in the audit log.
  7. Deleting or demoting the last live primary track, or clearing a primary track's dates, venue, or division, is rejected.
  8. The backend test suite passes, with the migration check confirming models match the migration head, and new coverage for the track lifecycle rules and guards.
  9. npx tsc --noEmit is clean on the frontend — the proof that every consumer of the removed tournament location, date, and division fields was updated.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

backendbreaking-changeRemoves or alters existing behavior requiring a migration, coordination, or careful rolloutdatabaseUpdates database schemas or modelsenhancementNew feature or requesteventsformsfrontendtournaments

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions