Skip to main content

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 placements array is, by construction, a bulkScheduleMatchUps matchUpDetails payload.
  • 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: true to override).
  • removePriorValues defaults to true, 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 */ });
  • scheduleCellsScheduleCell[] (the shared-facility contract) each with scenarioStatus: '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
note

The client-side "Plan" mode UI (mode toggle, scenario switcher, drift banner, "Make official" commit) consumes these three read/rebase methods plus applyScheduleScenario.