Add a time-of-day race day schedule to Event Manager

Events can now be given a start time and a gap between sessions on a
new "Schedule" panel; every session's planned clock time cascades
automatically in running order (practice, qualifying, finals), and
once a session actually starts or finishes, everything after it
re-anchors to that real time - a late start or a session that ran
long (including any pause) pushes the rest of the day forward with
no manual reschedule step. The scheduled time now also shows on the
printed/PDF start lists.

Printable start lists and manually moving a driver to start last in
their own heat (for a mechanic-time request) already existed, so
this only builds the missing piece: the schedule itself.

Also fixes a real pre-existing bug in the same session-ordering logic
this feature relies on: getSessionSortWeight used `order[type] || 99`,
which silently treated open_practice's own weight of 0 as falsy and
sorted practice sessions dead last instead of first in print/PDF
output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 22:33:57 +02:00
parent a449bec07d
commit cf89086dff
11 changed files with 250 additions and 8 deletions

View File

@@ -11,7 +11,7 @@ import { RACE_AUDIO_EVENT_DEFINITIONS, createRaceAudioEngine, normalizeRaceAudio
import { renderRaceFormatField, renderRaceFormatContextCard, getRaceSummaryItems, getRaceSummaryWarnings, renderManageStatusBadge, renderRaceWizardSteps, renderRaceWizardContent, renderRaceStandingsTable } from "./race_setup_ui.js";
import { normalizeRaceTeam as normalizeRaceTeamLogic, normalizeStoredRacePreset as normalizeStoredRacePresetLogic, getRaceFormatPresets as getRaceFormatPresetsLogic, applyRaceFormatPreset as applyRaceFormatPresetLogic, buildRaceFormatConfigFromForm as buildRaceFormatConfigFromFormLogic, normalizeEvent as normalizeEventLogic, normalizeBrandingConfig as normalizeBrandingConfigLogic, resolveEventBranding as resolveEventBrandingLogic, getRaceManageStatuses as getRaceManageStatusesLogic, buildDefaultRaceWizardDraft as buildDefaultRaceWizardDraftLogic, getRaceWizardPreset as getRaceWizardPresetLogic, applyRaceWizardPresetDefaults as applyRaceWizardPresetDefaultsLogic, ensureRaceParticipantsConfigured as ensureRaceParticipantsConfiguredLogic, buildRaceSession as buildRaceSessionLogic, buildOpenPracticeBundle as buildOpenPracticeBundleLogic, buildTrackSession as buildTrackSessionLogic, createSponsorRounds as createSponsorRoundsLogic, buildRaceSessionsFromWizard as buildRaceSessionsFromWizardLogic, getRaceWizardSessionPlan as getRaceWizardSessionPlanLogic, getEventDrivers as getEventDriversLogic, getEventTeams as getEventTeamsLogic, getTeamDriverPool as getTeamDriverPoolLogic, reassignTeamMembers as reassignTeamMembersLogic, findEventTeamForPassing as findEventTeamForPassingLogic, generateQualifyingForRace as generateQualifyingForRaceLogic, reseedUpcomingQualifying as reseedUpcomingQualifyingLogic, moveDriversIntoQualifyingHeat as moveDriversIntoQualifyingHeatLogic, parseQualifyingHeatName as parseQualifyingHeatNameLogic, generateFinalsForRace as generateFinalsForRaceLogic, applyBumpsForRace as applyBumpsForRaceLogic } from "./event_race_logic.js";
import { normalizeRaceTeam as normalizeRaceTeamLogic, normalizeStoredRacePreset as normalizeStoredRacePresetLogic, getRaceFormatPresets as getRaceFormatPresetsLogic, applyRaceFormatPreset as applyRaceFormatPresetLogic, buildRaceFormatConfigFromForm as buildRaceFormatConfigFromFormLogic, normalizeEvent as normalizeEventLogic, normalizeBrandingConfig as normalizeBrandingConfigLogic, resolveEventBranding as resolveEventBrandingLogic, getRaceManageStatuses as getRaceManageStatusesLogic, buildDefaultRaceWizardDraft as buildDefaultRaceWizardDraftLogic, getRaceWizardPreset as getRaceWizardPresetLogic, applyRaceWizardPresetDefaults as applyRaceWizardPresetDefaultsLogic, ensureRaceParticipantsConfigured as ensureRaceParticipantsConfiguredLogic, buildRaceSession as buildRaceSessionLogic, buildOpenPracticeBundle as buildOpenPracticeBundleLogic, buildTrackSession as buildTrackSessionLogic, createSponsorRounds as createSponsorRoundsLogic, buildRaceSessionsFromWizard as buildRaceSessionsFromWizardLogic, getRaceWizardSessionPlan as getRaceWizardSessionPlanLogic, getEventDrivers as getEventDriversLogic, getEventTeams as getEventTeamsLogic, getTeamDriverPool as getTeamDriverPoolLogic, reassignTeamMembers as reassignTeamMembersLogic, findEventTeamForPassing as findEventTeamForPassingLogic, generateQualifyingForRace as generateQualifyingForRaceLogic, reseedUpcomingQualifying as reseedUpcomingQualifyingLogic, moveDriversIntoQualifyingHeat as moveDriversIntoQualifyingHeatLogic, parseQualifyingHeatName as parseQualifyingHeatNameLogic, generateFinalsForRace as generateFinalsForRaceLogic, applyBumpsForRace as applyBumpsForRaceLogic, buildEventSchedule as buildEventScheduleLogic } from "./event_race_logic.js";
import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getStatusLabelLogic, isUntimedSession as isUntimedSessionLogic, getActiveSession as getActiveSessionLogic, getSessionTargetMs as getSessionTargetMsLogic, getSessionLapWindow as getSessionLapWindowLogic, isCountedPassing as isCountedPassingLogic, getVisiblePassings as getVisiblePassingsLogic, getPassingValidationLabel as getPassingValidationLabelLogic, getSessionTiming as getSessionTimingLogic, ensureSessionResult as ensureSessionResultLogic, buildLeaderboard as buildLeaderboardLogic, formatLapDelta as formatLapDeltaLogic, formatLeaderboardGap as formatLeaderboardGapLogic, getCompetitorElapsedMs as getCompetitorElapsedMsLogic, getCompetitorPassings as getCompetitorPassingsLogic, getCompetitorSeedMetric as getCompetitorSeedMetricLogic, getSessionEntrants as getSessionEntrantsLogic, buildPracticeStandings as buildPracticeStandingsLogic, getQualifyingPointsValue as getQualifyingPointsValueLogic, isHighPointsTable as isHighPointsTableLogic, compareNumberSet as compareNumberSetLogic, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, hasQualifyingPrimaryTie as hasQualifyingPrimaryTieLogic, buildQualifyingStandings as buildQualifyingStandingsLogic, formatTeamActiveMemberLabel as formatTeamActiveMemberLabelLogic, buildTeamRaceStandings as buildTeamRaceStandingsLogic, buildSessionChannelStats as buildSessionChannelStatsLogic, buildTeamStintLog as buildTeamStintLogLogic, getSessionGridEntries as getSessionGridEntriesLogic, getSessionGridOrder as getSessionGridOrderLogic, ensureSessionDriverOrder as ensureSessionDriverOrderLogic, buildFinalStandings as buildFinalStandingsLogic, getTrackRecordMs as getTrackRecordMsLogic } from "./timing_logic.js";
import { normalizeTrackLayouts, getTrackLayoutIdAt, getTrackLayoutLabel, nextTrackLayoutLabel } from "./track_layouts_logic.js";
@@ -120,13 +120,14 @@ const getManualCorrectionSummary = (row) => getManualCorrectionSummaryLogic(row,
const renderTeamStintLog = (session, rows) => renderTeamStintLogHelper(session, rows, { t, escapeHtml, buildTeamStintLog, formatTeamActiveMemberLabel, renderTable, formatRaceClock });
const renderTeamRaceStandings = (event) => renderTeamRaceStandingsHelper(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, formatRaceClock, renderTeamStintLog, buildSessionChannelStats });
const getSessionSortWeight = (session) => getSessionSortWeightHelper(session);
const buildEventSchedule = (event) => buildEventScheduleLogic(event, getSessionsForEvent(event.id), { getSessionSortWeight });
const getDriverDisplayById = (driverId) => getDriverDisplayByIdHelper(driverId, { state, t });
const renderPositionGrid = (session) => renderPositionGridHelper(session, { t, escapeHtml, getSessionGridEntries });
const renderGridEditor = (session) => renderGridEditorHelper(session, { t, escapeHtml, ensureSessionDriverOrder, state });
const getFinalMainLayouts = (event) => getFinalMainLayoutsHelper(event, { getSessionsForEvent, getSessionGridOrder, getDriverDisplayById, t });
const renderFinalMatrix = (event) => renderFinalMatrixHelper(event, { t, escapeHtml, getFinalMainLayouts, getStatusLabel });
const buildPrintBrandBlock = (branding) => buildPrintBrandBlockHelper(branding, { escapeHtml });
const buildRaceStartListsHtml = (event) => buildRaceStartListsHtmlHelper(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable });
const buildRaceStartListsHtml = (event) => buildRaceStartListsHtmlHelper(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable, buildEventSchedule });
const buildRaceResultsHtml = (event) => buildRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings, getSessionsForEvent, renderSessionChannelAnalysis });
const getSessionsForEvent = (eventId) => getSessionsForEventHelper(state, eventId);
const getModeLabel = (mode) => getModeLabelHelper(mode, { t });
@@ -1137,6 +1138,16 @@ const TRANSLATIONS = {
"events.no_practice_results": "Inga practice-resultat ännu.",
"events.no_qualifying_results": "Inga kval-resultat ännu.",
"events.no_final_results": "Inga final-resultat ännu.",
"events.schedule_title": "Tidsschema",
"events.schedule_hint": "Sätt en starttid för dagen så räknas varje sessions planerade klockslag ut automatiskt utifrån längd, efterlöp och paus mellan sessioner. Skjuter en session upp sig (körs över tiden eller pausas) flyttas resten av schemat fram automatiskt.",
"events.schedule_start_time": "Starttid",
"events.schedule_gap": "Paus mellan sessioner (min)",
"events.schedule_save": "Spara schema",
"events.schedule_clear": "Rensa schema",
"events.schedule_not_set": "Inget schema satt ännu. Sätt en starttid ovan.",
"events.schedule_planned": "Planerad tid",
"events.schedule_actual": "Faktisk tid",
"events.schedule_delayed": "Försenad",
"events.final_matrix": "Finalmatris",
"events.print_startlists": "Skriv ut startlistor",
"events.print_results": "Skriv ut resultat",
@@ -2655,6 +2666,16 @@ const TRANSLATIONS = {
"events.no_practice_results": "No practice results yet.",
"events.no_qualifying_results": "No qualifying results yet.",
"events.no_final_results": "No final results yet.",
"events.schedule_title": "Schedule",
"events.schedule_hint": "Set a start time for the day and each session's planned clock time is worked out automatically from its duration, follow-up, and the gap between sessions. If a session runs long or gets paused, the rest of the schedule shifts forward automatically.",
"events.schedule_start_time": "Start time",
"events.schedule_gap": "Gap between sessions (min)",
"events.schedule_save": "Save schedule",
"events.schedule_clear": "Clear schedule",
"events.schedule_not_set": "No schedule set yet. Set a start time above.",
"events.schedule_planned": "Planned time",
"events.schedule_actual": "Actual time",
"events.schedule_delayed": "Delayed",
"events.final_matrix": "Final matrix",
"events.print_startlists": "Print start lists",
"events.print_results": "Print results",
@@ -5035,6 +5056,7 @@ function renderEventManager(eventId) {
normalizeEvent,
ensureRaceParticipantsConfigured,
getSessionsForEvent,
buildEventSchedule,
getSelectedTeamEditId: () => selectedTeamEditId,
setSelectedTeamEditId: (value) => { selectedTeamEditId = value; },
getSelectedSessionEditId: () => selectedSessionEditId,