Schedule Scenarios
Overview
A Schedule Scenario is a named, alternate ("contingency") scheduling plan a tournament director builds off the current schedule — a rain plan, a "compress onto the indoor courts", a "shift the afternoon back two hours". Scenarios let a director prepare and compare several plans without disturbing the live schedule, then commit one as the official schedule when needed.
Key characteristics:
- Persisted first-class on
tournamentRecord.scheduling.scenarios— never a legacy extension, and never emitted to public / arena read surfaces. - Many named scenarios per tournament; a director cycles through them and commits at most one.
- Resolved matchUp placements only (courts / times / order) — a scenario never re-runs the round-level scheduler.
- Commit is a direct hand-off — a scenario's
placementsarray is, by construction, abulkScheduleMatchUpsmatchUpDetailspayload. - Uncompleted matchUps only — applying a scenario skips completed matchUps automatically (inherited from
bulkScheduleMatchUps).
Data Structure
Scenarios are stored as an array on the tournament's scheduling group:
type ScheduleScenario = {
scenarioId: string; // generated on add when not supplied
scenarioName: string; // required, human-readable
scheduledDates?: string[]; // ISO 'YYYY-MM-DD' dates the plan re-plans
createdAt?: string; // caller-provided ISO string (factory does not stamp wall-clock)
updatedAt?: string; // caller-provided ISO string
notes?: string;
basedOnHash?: string; // fingerprint of the official schedule at authoring time (drift detection — Phase 1)
placements: ScenarioPlacement[];
};
type ScenarioPlacement = {
tournamentId: string;
matchUpId: string;
schedule: MatchUpSchedule; // scheduledDate, scheduledTime, courtId, venueId, courtOrder, timeModifiers, …
};
createdAt / updatedAt follow the calledAt idiom — the caller (TMX) provides ISO strings; the factory never reads the wall clock, so behaviour stays deterministic.
Managing Scenarios
import { scheduleGovernor } from 'tods-competition-factory';
Create
const { scenarioId } = engine.addScheduleScenario({
scenario: {
scenarioName: 'Rain plan — Saturday indoor',
scheduledDates: ['2024-05-04'],
placements: [
{
tournamentId,
matchUpId,
schedule: { scheduledDate: '2024-05-04', scheduledTime: '10:00', courtId, courtOrder: 1 },
},
// …
],
},
});
scenarioName is required; scenarioId is generated when omitted. The scenario is validated before it is stored (shape + that each placement targets a known tournamentId).
Read
engine.getScheduleScenarios(); // → { scenarios: ScheduleScenario[] }
engine.getScheduleScenario({ scenarioId }); // → { scenario } | { error }
Update / Remove
engine.updateScheduleScenario({ scenarioId, updates: { scenarioName: 'Rain plan v2', notes: 'moved to indoor' } });
engine.removeScheduleScenario({ scenarioId });
updates are merged over the existing scenario (the scenarioId is preserved) and the result is re-validated.
Committing a Scenario
applyScheduleScenario commits a scenario's placements as the official schedule:
const result = engine.applyScheduleScenario({ scenarioId });
// → { success: true, applied: <matchUps scheduled>, ... }
Because the placements are a bulkScheduleMatchUps payload:
- Completed matchUps are skipped by default (pass
scheduleCompletedMatchUps: trueto override). removePriorValuesdefaults totrue, matching TMX grid-drop semantics (a re-dated matchUp sheds stale grid position — see Scheduling Conflicts).- The scenario is left in place after a commit; the caller decides whether to
removeScheduleScenario.
Rendering a plan — the unofficial overlay
getScenarioScheduleProjection produces the "Plan mode" view: the official schedule with a scenario's placements laid on top, without writing to any matchUp. Each cell is tagged official or planned, and double-booking conflicts in the projected plan are detected via the shared-facility mergeFacilitySchedule (SAME_COURT_ORDER / SAME_SCHEDULED_TIME).
const { scheduleCells, grid, conflicts, plannedMatchUpIds, skippedCompletedMatchUpIds } =
engine.getScenarioScheduleProjection({ scenarioId, venueIds /* optional filter */ });
scheduleCells—ScheduleCell[](the shared-facility contract) each withscenarioStatus: 'official' | 'planned'.grid— the same cells arranged into a venue → court → date grid (FacilityScheduleGrid).conflicts— advisory double-bookings in the projected plan (detection only, never blocks).plannedMatchUpIds/skippedCompletedMatchUpIds— what the plan moves vs. the completed matchUps it can't.
Completed matchUps keep their official placement (a commit skips them) and are reported in skippedCompletedMatchUpIds.
Drift detection & rebase
A scenario is anchored to the official schedule as it stood when it was authored (or last rebased) via basedOnHash — a fingerprint over the official placement + status of the scenario's matchUps. Editing a scenario (rename, notes, moving placements) does not re-anchor it, so an "out of date" alert persists until the director explicitly reconciles.
const status = engine.getScheduleScenarioStatus({ scenarioId });
// {
// outOfDate, // the official baseline moved since authoring/rebase
// currentHash, basedOnHash,
// completedMatchUpIds, // placements now completed → skipped on commit
// missingMatchUpIds, // placements whose matchUp no longer exists
// applicableMatchUpIds, // placements that would actually be scheduled
// }
When a director has reviewed the changes and wants to accept current state as the new baseline:
engine.rebaseScheduleScenario({ scenarioId }); // recomputes basedOnHash → outOfDate clears
The client-side "Plan" mode UI (mode toggle, scenario switcher, drift banner, "Make official" commit) consumes these three read/rebase methods plus applyScheduleScenario.