From 1aa55e32123180cc70c231856b6d7172dae03fb3 Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 26 Mar 2026 20:46:04 +0100 Subject: [PATCH] Extract event manager controller into module --- src/app.js | 781 +++--------------------------- src/event_manager_controller.js | 828 ++++++++++++++++++++++++++++++++ 2 files changed, 891 insertions(+), 718 deletions(-) create mode 100644 src/event_manager_controller.js diff --git a/src/app.js b/src/app.js index 4c9252f..0082bc1 100644 --- a/src/app.js +++ b/src/app.js @@ -14,6 +14,7 @@ import { renderGuideView, renderOverlayPageView } from "./misc_views.js"; import { getSessionsForEventHelper, getModeLabelHelper, normalizeStartModeHelper, getStartModeLabelHelper, getClassNameHelper, getEventNameHelper, renderAssignmentListView, renderSessionsTableView } from "./event_common.js"; import { renderEventWorkspaceView } from "./event_workspace_controller.js"; import { renderEventManagerMarkup } from "./event_manager_view.js"; +import { renderEventManagerView } from "./event_manager_controller.js"; import { createDefaultAmmcConfigHelper, getManagedWsUrlHelper, loadAmmcConfigFromBackendHelper, saveAmmcConfigToBackendHelper, refreshAmmcStatusHelper, startManagedAmmcHelper, stopManagedAmmcHelper, applyPersistedStateHelper, hydrateFromBackendHelper, scheduleBackendSyncHelper, syncStateToBackendHelper, pingBackendHelper, checkAppVersionHelper, startAppVersionPollingHelper, startOverlaySyncHelper, startOverlayRotationHelper, startOverlayLiveRefreshHelper, ensureAudioContextHelper, playPassingBeepHelper, playFinishSirenHelper, playLeaderCueHelper, playStartCueHelper, playBestLapCueHelper, pushOverlayEventHelper, speakTextHelper, announcePassingHelper, announceRaceFinishedHelper, handleSessionTimerTickHelper, tickClockHelper } from "./runtime_services.js"; import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js"; @@ -2851,92 +2852,40 @@ function renderEventWorkspace(mode) { } function renderEventManager(eventId) { - const event = state.events.find((e) => e.id === eventId); - if (!event) { - return; - } - const normalizedEvent = normalizeEvent(event); - if (normalizedEvent !== event) { - Object.assign(event, normalizedEvent); - } - ensureRaceParticipantsConfigured(event); - - const sessions = getSessionsForEvent(eventId); - const eventManageArea = document.getElementById("eventManageArea"); - if (!eventManageArea) { - return; - } - - const driverOptions = state.drivers - .map((d) => ``) - .join(""); - const teamDriverPool = event.mode === "race" ? getTeamDriverPool(event) : { drivers: [], fallback: false }; - const raceDrivers = event.mode === "race" ? teamDriverPool.drivers : []; - const raceTeams = event.mode === "race" ? getEventTeams(event) : []; - if (selectedTeamEditId && !raceTeams.some((team) => team.id === selectedTeamEditId)) { - selectedTeamEditId = null; - } - const editingTeam = event.mode === "race" ? raceTeams.find((team) => team.id === selectedTeamEditId) || null : null; - const carOptions = state.cars - .map((c) => ``) - .join(""); - const branding = normalizeBrandingConfig(event.branding); - const editingSession = sessions.find((session) => session.id === selectedSessionEditId) || null; - const sessionTypeChoices = getSessionTypeChoices(event.mode); - const sessionTypeHintKey = event.mode === "track" ? "events.session_type_hint_track" : "events.session_type_hint_race"; - const racePresets = getRaceFormatPresets(); - const selectedPreset = racePresets.find((preset) => preset.id === event.raceConfig.presetId) || racePresets[0]; - const isEndurancePreset = event.mode === "race" && selectedPreset?.id === "endurance"; - const showBasicQualifyingFields = raceFormatAdvanced || !isEndurancePreset; - const showBasicFinalFields = raceFormatAdvanced || !isEndurancePreset; - const selectedParticipantCount = event.mode === "race" ? (event.raceConfig.participantsConfigured ? (event.raceConfig.driverIds || []).length : raceDrivers.length) : 0; - const raceSummaryItems = event.mode === "race" ? getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset, { t, getStartModeLabel }) : []; - const raceSummaryWarnings = event.mode === "race" ? getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selectedPreset, { t }) : []; - const manageStatuses = event.mode === "race" ? getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selectedPreset) : null; - const gridSessions = event.mode === "race" ? sessions.filter((session) => normalizeStartMode(session.startMode) === "position") : []; - if (selectedGridSessionId && !gridSessions.some((session) => session.id === selectedGridSessionId)) { - selectedGridSessionId = ""; - } - const selectedGridSession = - gridSessions.find((session) => session.id === selectedGridSessionId) || gridSessions[0] || null; - if (!selectedGridSessionId && selectedGridSession) { - selectedGridSessionId = selectedGridSession.id; - } - - eventManageArea.innerHTML = renderEventManagerMarkup({ - event, + renderEventManagerView({ + eventId, + state, t, escapeHtml, - sessions, - sessionTypeChoices, - sessionTypeHintKey, - raceTeams, + normalizeEvent, + ensureRaceParticipantsConfigured, + getSessionsForEvent, + getSelectedTeamEditId: () => selectedTeamEditId, + setSelectedTeamEditId: (value) => { selectedTeamEditId = value; }, + getSelectedSessionEditId: () => selectedSessionEditId, + setSelectedSessionEditId: (value) => { selectedSessionEditId = value; }, + getSelectedGridSessionId: () => selectedGridSessionId, + setSelectedGridSessionId: (value) => { selectedGridSessionId = value; }, + getRaceFormatAdvanced: () => raceFormatAdvanced, + setRaceFormatAdvanced: (value) => { raceFormatAdvanced = value; }, + getTeamDriverPool, + getEventTeams, + normalizeBrandingConfig, + getSessionTypeChoices, + getRaceFormatPresets, + getRaceSummaryItems, + getRaceSummaryWarnings, + getRaceManageStatuses, + normalizeStartMode, + renderEventManagerMarkup, getSessionEntrants, getSessionTypeLabel, getStartModeLabel, getStatusLabel, - normalizeStartMode, - branding, - driverOptions, - carOptions, - manageStatuses, renderManageStatusBadgeView, - selectedParticipantCount, - raceDrivers, - teamDriverPool, - state, getDriverDisplayById, - raceFormatAdvanced, - racePresets, - selectedPreset, - isEndurancePreset, - showBasicQualifyingFields, - showBasicFinalFields, renderRaceFormatContextCardView, renderRaceFormatFieldView, - raceSummaryWarnings, - raceSummaryItems, - selectedGridSession, renderGridEditor, renderRaceStandingsTableView, buildPracticeStandings, @@ -2944,653 +2893,49 @@ function renderEventManager(eventId) { buildFinalStandings, renderTeamRaceStandings, renderFinalMatrix, - editingTeam, - editingSession, getModeLabel, renderTable, + bindModalShell, + setFormError, + saveState, + renderView, + rerenderEventManager: renderEventManager, + updateHeaderState, + uid, + normalizeSession, + buildSessionHeatSheetHtml, + openPrintWindow, + exportSessionHeatSheet, + exportSessionHeatSheetPdf, + getSelectedAssignmentSessionId, + autoAssignTrackSession, + renderAssignmentList, + normalizeRaceTeam, + buildRaceFormatConfigFromForm, + applyRaceFormatPreset, + normalizeStoredRacePreset, + generateQualifyingForRace, + clearGeneratedQualifying, + reseedUpcomingQualifying, + generateFinalsForRace, + clearGeneratedFinals, + applyBumpsForRace, + buildRacePackagePayload, + downloadJsonFile, + sanitizeFilenameSegment, + importRacePackagePayload, + buildRaceStartListsHtml, + buildRaceResultsHtml, + buildTeamRaceResultsHtml, + exportRaceStartListsPdf, + exportRaceResultsPdf, + exportTeamRaceResultsPdf, + ensureSessionDriverOrder, + reorderList, + getEventName, }); - - const bindManageJump = (node) => { - const triggerJump = () => { - const targetId = node.getAttribute("data-target") || ""; - const target = targetId ? document.getElementById(targetId) : null; - if (target) { - target.scrollIntoView({ behavior: "smooth", block: "start" }); - } - }; - node.addEventListener("click", triggerJump); - node.addEventListener("keydown", (event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - triggerJump(); - } - }); - }; - - eventManageArea.querySelectorAll(".summary-warning-link, .manage-step-card-link").forEach((node) => { - bindManageJump(node); - }); - - document.getElementById("eventBrandingForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - event.branding = normalizeBrandingConfig({ - ...event.branding, - brandName: String(form.get("brandName") || "").trim(), - brandTagline: String(form.get("brandTagline") || "").trim(), - pdfFooter: String(form.get("pdfFooter") || "").trim(), - pdfTheme: String(form.get("pdfTheme") || "").trim(), - }); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("eventLogoUpload")?.addEventListener("change", (eventInput) => { - const input = eventInput.currentTarget; - const file = input instanceof HTMLInputElement ? input.files?.[0] : null; - if (!file) { - return; - } - const reader = new FileReader(); - reader.onload = () => { - event.branding = normalizeBrandingConfig({ - ...event.branding, - logoDataUrl: typeof reader.result === "string" ? reader.result : "", - }); - saveState(); - renderEventManager(eventId); - }; - reader.readAsDataURL(file); - }); - - document.getElementById("eventLogoClear")?.addEventListener("click", () => { - event.branding = normalizeBrandingConfig({ - ...event.branding, - logoDataUrl: "", - }); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("sessionForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - state.sessions.push(normalizeSession({ - id: uid("session"), - eventId, - name: String(form.get("name")).trim(), - type: String(form.get("type")), - durationMin: Number(form.get("durationMin")), - followUpSec: Math.max(0, Number(form.get("followUpSec") || 0) || 0), - startMode: String(form.get("startMode") || "mass"), - seedBestLapCount: Math.max(0, Number(form.get("seedBestLapCount") || 0) || 0), - seedMethod: String(form.get("seedMethod") || "best_sum"), - staggerGapSec: Math.max(0, Number(form.get("staggerGapSec") || 0) || 0), - maxCars: Number(form.get("maxCars") || 0) || null, - mode: event.mode, - status: "ready", - startedAt: null, - endedAt: null, - finishedByTimer: false, - assignments: [], - })); - saveState(); - renderEventManager(eventId); - updateHeaderState(); - }); - - sessions.forEach((s) => { - document.getElementById(`session-edit-${s.id}`)?.addEventListener("click", () => { - selectedSessionEditId = s.id; - renderEventManager(eventId); - }); - - document.getElementById(`session-active-${s.id}`)?.addEventListener("click", () => { - state.activeSessionId = s.id; - saveState(); - updateHeaderState(); - renderView(); - }); - - document.getElementById(`session-delete-${s.id}`)?.addEventListener("click", () => { - state.sessions = state.sessions.filter((x) => x.id !== s.id); - delete state.resultsBySession[s.id]; - if (state.activeSessionId === s.id) { - state.activeSessionId = null; - } - saveState(); - renderEventManager(eventId); - updateHeaderState(); - }); - - document.getElementById(`session-grid-${s.id}`)?.addEventListener("click", () => { - ensureSessionDriverOrder(s); - selectedGridSessionId = s.id; - saveState(); - renderEventManager(eventId); - }); - - document.getElementById(`session-sheet-print-${s.id}`)?.addEventListener("click", () => { - openPrintWindow(`${getEventName(eventId)} - ${s.name}`, buildSessionHeatSheetHtml(s)); - }); - - document.getElementById(`session-sheet-export-${s.id}`)?.addEventListener("click", () => { - exportSessionHeatSheet(s); - }); - - document.getElementById(`session-sheet-pdf-${s.id}`)?.addEventListener("click", async () => { - await exportSessionHeatSheetPdf(s); - }); - }); - - document.getElementById("sessionEditCancel")?.addEventListener("click", () => { - selectedSessionEditId = null; - renderEventManager(eventId); - }); - - document.getElementById("sessionEditCancelFooter")?.addEventListener("click", () => { - selectedSessionEditId = null; - renderEventManager(eventId); - }); - - document.getElementById("sessionEditModalOverlay")?.addEventListener("click", (event) => { - if (event.target?.id === "sessionEditModalOverlay") { - selectedSessionEditId = null; - renderEventManager(eventId); - } - }); - - bindModalShell("sessionEditModalOverlay", () => { - selectedSessionEditId = null; - renderEventManager(eventId); - }); - - document.getElementById("sessionEditForm")?.addEventListener("submit", (event) => { - event.preventDefault(); - if (!editingSession) { - return; - } - const form = new FormData(event.currentTarget); - const cleanedName = String(form.get("name") || "").trim(); - const cleanedDuration = Number(form.get("durationMin") || editingSession.durationMin || 5) || 0; - if (!cleanedName) { - setFormError("sessionEditError", t("validation.required_name")); - return; - } - if (cleanedDuration < 1) { - setFormError("sessionEditError", t("validation.required_duration")); - return; - } - setFormError("sessionEditError", ""); - editingSession.name = cleanedName; - editingSession.type = String(form.get("type") || editingSession.type); - editingSession.durationMin = Math.max(1, cleanedDuration); - editingSession.followUpSec = Math.max(0, Number(form.get("followUpSec") || 0) || 0); - editingSession.startMode = normalizeStartMode(String(form.get("startMode") || editingSession.startMode || "mass")); - editingSession.seedBestLapCount = Math.max(0, Number(form.get("seedBestLapCount") || 0) || 0); - editingSession.seedMethod = ["best_sum", "average", "consecutive"].includes(String(form.get("seedMethod") || "").toLowerCase()) - ? String(form.get("seedMethod")).toLowerCase() - : "best_sum"; - editingSession.staggerGapSec = Math.max(0, Number(form.get("staggerGapSec") || 0) || 0); - editingSession.maxCars = Number(form.get("maxCars") || 0) || null; - selectedSessionEditId = null; - saveState(); - renderEventManager(eventId); - }); - - if (event.mode === "track") { - document.getElementById("sponsorRoundsForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - const qualificationRounds = Number(form.get("qualificationRounds") || 0); - const heatRounds = Number(form.get("heatRounds") || 0); - const finalRounds = Number(form.get("finalRounds") || 0); - const durationMin = Number(form.get("roundDuration") || 5); - - createSponsorRounds(eventId, { - qualificationRounds, - heatRounds, - finalRounds, - durationMin, - }); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("assignForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - const sessionId = String(form.get("sessionId")); - const session = state.sessions.find((x) => x.id === sessionId); - if (!session) { - return; - } - - const driverId = String(form.get("driverId")); - const carId = String(form.get("carId")); - const car = state.cars.find((x) => x.id === carId); - if (!car) { - return; - } - - const duplicateCar = (session.assignments || []).find((a) => a.carId === carId); - if (duplicateCar) { - alert(t("events.duplicate_car")); - return; - } - - const duplicateDriver = (session.assignments || []).find((a) => a.driverId === driverId); - if (duplicateDriver) { - alert(t("events.duplicate_driver")); - return; - } - - const duplicateTp = (session.assignments || []).find((a) => { - const existingCar = state.cars.find((x) => x.id === a.carId); - return existingCar?.transponder && existingCar.transponder === car.transponder; - }); - if (duplicateTp) { - alert(t("events.duplicate_tp")); - return; - } - - session.assignments = session.assignments || []; - session.assignments.push({ id: uid("as"), driverId, carId }); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("autoAssignSession")?.addEventListener("click", () => { - const sessionId = getSelectedAssignmentSessionId(); - if (!sessionId) { - return; - } - autoAssignTrackSession(event, sessionId); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("clearAssignSession")?.addEventListener("click", () => { - const sessionId = getSelectedAssignmentSessionId(); - if (!sessionId) { - return; - } - const session = state.sessions.find((x) => x.id === sessionId); - if (!session) { - return; - } - session.assignments = []; - saveState(); - renderEventManager(eventId); - }); - - renderAssignmentList(eventId); - } - - if (event.mode === "race") { - const persistRaceParticipants = () => { - const selectedIds = Array.from(document.querySelectorAll(".race-participant:checked")).map((node) => node.value); - event.raceConfig.driverIds = selectedIds; - event.raceConfig.participantsConfigured = true; - saveState(); - }; - - document.querySelectorAll(".race-participant").forEach((node) => { - node.addEventListener("change", persistRaceParticipants); - }); - - document.getElementById("selectAllParticipants")?.addEventListener("click", () => { - document.querySelectorAll(".race-participant").forEach((node) => { - node.checked = true; - }); - persistRaceParticipants(); - }); - - document.getElementById("clearParticipants")?.addEventListener("click", () => { - document.querySelectorAll(".race-participant").forEach((node) => { - node.checked = false; - }); - persistRaceParticipants(); - }); - - document.getElementById("teamForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - const name = String(form.get("teamName") || "").trim(); - const driverIds = form.getAll("teamDriverIds").map(String).filter(Boolean); - const carIds = form.getAll("teamCarIds").map(String).filter(Boolean); - if (!name || (!driverIds.length && !carIds.length)) { - return; - } - const createdTeam = normalizeRaceTeam({ id: uid("team"), name, driverIds, carIds }); - event.raceConfig.teams = [...getEventTeams(event), createdTeam]; - selectedTeamEditId = createdTeam.id; - saveState(); - renderEventManager(eventId); - }); - - raceTeams.forEach((team) => { - document.getElementById(`team-edit-${team.id}`)?.addEventListener("click", () => { - selectedTeamEditId = team.id; - renderEventManager(eventId); - }); - - document.getElementById(`team-delete-${team.id}`)?.addEventListener("click", () => { - event.raceConfig.teams = getEventTeams(event).filter((item) => item.id !== team.id); - if (selectedTeamEditId === team.id) { - selectedTeamEditId = null; - } - saveState(); - renderEventManager(eventId); - }); - }); - - document.getElementById("teamEditCancel")?.addEventListener("click", () => { - selectedTeamEditId = null; - renderEventManager(eventId); - }); - - document.getElementById("teamEditCancelFooter")?.addEventListener("click", () => { - selectedTeamEditId = null; - renderEventManager(eventId); - }); - - document.getElementById("teamEditModalOverlay")?.addEventListener("click", (modalEvent) => { - if (modalEvent.target?.id === "teamEditModalOverlay") { - selectedTeamEditId = null; - renderEventManager(eventId); - } - }); - - bindModalShell("teamEditModalOverlay", () => { - selectedTeamEditId = null; - renderEventManager(eventId); - }); - - document.getElementById("teamEditForm")?.addEventListener("submit", (submitEvent) => { - submitEvent.preventDefault(); - if (!editingTeam) { - return; - } - const form = new FormData(submitEvent.currentTarget); - const name = String(form.get("teamName") || "").trim(); - const driverIds = form.getAll("teamDriverIds").map(String).filter(Boolean); - const carIds = form.getAll("teamCarIds").map(String).filter(Boolean); - if (!name) { - setFormError("teamEditError", t("validation.required_name")); - return; - } - if (!driverIds.length && !carIds.length) { - setFormError("teamEditError", t("validation.invalid_selection")); - return; - } - setFormError("teamEditError", ""); - event.raceConfig.teams = getEventTeams(event).map((team) => - team.id === editingTeam.id ? normalizeRaceTeam({ ...team, name, driverIds, carIds }) : team - ); - selectedTeamEditId = null; - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("raceFormatBasicToggle")?.addEventListener("click", () => { - raceFormatAdvanced = false; - renderEventManager(eventId); - }); - - document.getElementById("raceFormatAdvancedToggle")?.addEventListener("click", () => { - raceFormatAdvanced = true; - renderEventManager(eventId); - }); - - document.getElementById("raceFormatForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - event.raceConfig = buildRaceFormatConfigFromForm(form, event); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("applyRacePreset")?.addEventListener("click", () => { - const formElement = document.getElementById("raceFormatForm"); - if (!(formElement instanceof HTMLFormElement)) { - return; - } - const form = new FormData(formElement); - applyRaceFormatPreset(event, String(form.get("presetId") || "custom")); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("saveRacePreset")?.addEventListener("click", () => { - const formElement = document.getElementById("raceFormatForm"); - if (!(formElement instanceof HTMLFormElement)) { - return; - } - const form = new FormData(formElement); - const presetName = String(form.get("presetName") || "").trim(); - if (!presetName) { - return; - } - const config = buildRaceFormatConfigFromForm(form, event); - const selectedPresetId = String(form.get("presetId") || "custom"); - const existingCustomPreset = (state.settings.racePresets || []).find((preset) => preset.id === selectedPresetId); - const presetId = existingCustomPreset ? existingCustomPreset.id : uid("preset"); - const storedPreset = normalizeStoredRacePreset({ - id: presetId, - name: presetName, - values: { - qualifyingScoring: config.qualifyingScoring, - qualifyingRounds: config.qualifyingRounds, - carsPerHeat: config.carsPerHeat, - qualDurationMin: config.qualDurationMin, - qualStartMode: config.qualStartMode, - qualSeedLapCount: config.qualSeedLapCount, - qualSeedMethod: config.qualSeedMethod, - countedQualRounds: config.countedQualRounds, - qualifyingPointsTable: config.qualifyingPointsTable, - qualifyingTieBreak: config.qualifyingTieBreak, - carsPerFinal: config.carsPerFinal, - finalLegs: config.finalLegs, - countedFinalLegs: config.countedFinalLegs, - finalDurationMin: config.finalDurationMin, - finalStartMode: config.finalStartMode, - followUpSec: config.followUpSec, - minLapMs: config.minLapMs, - maxLapMs: config.maxLapMs, - bumpCount: config.bumpCount, - reserveBumpSlots: config.reserveBumpSlots, - finalsSource: config.finalsSource, - }, - }); - const otherPresets = (state.settings.racePresets || []).filter((preset) => preset.id !== presetId); - state.settings.racePresets = [...otherPresets, storedPreset]; - event.raceConfig = { ...config, presetId }; - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("deleteRacePreset")?.addEventListener("click", () => { - const formElement = document.getElementById("raceFormatForm"); - if (!(formElement instanceof HTMLFormElement)) { - return; - } - const form = new FormData(formElement); - const presetId = String(form.get("presetId") || "custom"); - if (!(state.settings.racePresets || []).some((preset) => preset.id === presetId)) { - return; - } - state.settings.racePresets = (state.settings.racePresets || []).filter((preset) => preset.id !== presetId); - event.raceConfig.presetId = "custom"; - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("generateQualifying")?.addEventListener("click", () => { - const created = generateQualifyingForRace(event); - saveState(); - renderEventManager(eventId); - if (created > 0) { - alert(t("events.generated_qualifying")); - } - }); - - document.getElementById("clearGeneratedQualifying")?.addEventListener("click", () => { - clearGeneratedQualifying(event.id); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("reseedQualifying")?.addEventListener("click", () => { - const result = reseedUpcomingQualifying(event); - saveState(); - renderEventManager(eventId); - const messages = []; - if (result.updated > 0) { - messages.push(t("events.reseed_done")); - } else { - messages.push(t("events.no_reseed_done")); - } - if (result.locked > 0) { - messages.push(t("events.reseed_locked", { count: result.locked })); - } - alert(messages.join("\n")); - }); - - document.getElementById("generateFinals")?.addEventListener("click", () => { - const created = generateFinalsForRace(event); - saveState(); - renderEventManager(eventId); - if (created > 0) { - alert(t("events.finals_generated")); - } - }); - - document.getElementById("clearGeneratedFinals")?.addEventListener("click", () => { - clearGeneratedFinals(event.id); - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("applyBumps")?.addEventListener("click", () => { - const applied = applyBumpsForRace(event); - saveState(); - renderEventManager(eventId); - alert(t(applied > 0 ? "events.bumps_applied" : "events.no_bumps_applied")); - }); - - document.getElementById("exportRacePackage")?.addEventListener("click", () => { - const payload = buildRacePackagePayload(eventId); - downloadJsonFile(`${sanitizeFilenameSegment(event.name)}_race_package.json`, payload); - }); - - document.getElementById("importRacePackage")?.addEventListener("change", (importEvent) => { - const input = importEvent.currentTarget; - const file = input instanceof HTMLInputElement ? input.files?.[0] : null; - if (!file) { - return; - } - const reader = new FileReader(); - reader.onload = () => { - try { - const parsed = JSON.parse(String(reader.result || "{}")); - importRacePackagePayload(parsed); - } catch (error) { - alert(t("settings.import_failed", { msg: error instanceof Error ? error.message : String(error) })); - } - }; - reader.readAsText(file); - }); - - document.getElementById("printStartlists")?.addEventListener("click", () => { - openPrintWindow(`${event.name} - ${t("events.start_lists")}`, buildRaceStartListsHtml(event)); - }); - - document.getElementById("printResults")?.addEventListener("click", () => { - openPrintWindow(`${event.name} - ${t("events.results_overview")}`, buildRaceResultsHtml(event)); - }); - - document.getElementById("printTeamResults")?.addEventListener("click", () => { - openPrintWindow(`${event.name} - ${t("events.team_report")}`, buildTeamRaceResultsHtml(event)); - }); - - document.getElementById("pdfStartlists")?.addEventListener("click", async () => { - await exportRaceStartListsPdf(event); - }); - - document.getElementById("pdfResults")?.addEventListener("click", async () => { - await exportRaceResultsPdf(event); - }); - - document.getElementById("pdfTeamResults")?.addEventListener("click", async () => { - await exportTeamRaceResultsPdf(event); - }); - - document.getElementById("gridResetOrder")?.addEventListener("click", () => { - if (!selectedGridSession) { - return; - } - selectedGridSession.driverIds = getSessionEntrants(selectedGridSession) - .map((driver) => driver.id) - .filter(Boolean); - selectedGridSession.manualGridIds = [...selectedGridSession.driverIds]; - selectedGridSession.gridCustomized = false; - saveState(); - renderEventManager(eventId); - }); - - document.getElementById("gridToggleLock")?.addEventListener("click", () => { - if (!selectedGridSession) { - return; - } - if (!selectedGridSession.gridCustomized) { - selectedGridSession.manualGridIds = [...ensureSessionDriverOrder(selectedGridSession)]; - selectedGridSession.gridCustomized = true; - } else { - selectedGridSession.manualGridIds = [...selectedGridSession.driverIds]; - selectedGridSession.gridCustomized = false; - } - saveState(); - renderEventManager(eventId); - }); - - let dragIndex = null; - document.querySelectorAll("#gridDragList .drag-item").forEach((node) => { - node.addEventListener("dragstart", () => { - dragIndex = Number(node.dataset.index); - node.classList.add("drag-item-active"); - }); - node.addEventListener("dragend", () => { - dragIndex = null; - node.classList.remove("drag-item-active"); - }); - node.addEventListener("dragover", (dragEvent) => { - dragEvent.preventDefault(); - node.classList.add("drag-item-over"); - }); - node.addEventListener("dragleave", () => { - node.classList.remove("drag-item-over"); - }); - node.addEventListener("drop", (dropEvent) => { - dropEvent.preventDefault(); - node.classList.remove("drag-item-over"); - if (!selectedGridSession || dragIndex === null) { - return; - } - const dropIndex = Number(node.dataset.index); - if (Number.isNaN(dropIndex) || dropIndex === dragIndex) { - return; - } - selectedGridSession.manualGridIds = reorderList(ensureSessionDriverOrder(selectedGridSession), dragIndex, dropIndex); - selectedGridSession.gridCustomized = true; - saveState(); - renderEventManager(eventId); - }); - }); - } } - function getFreePracticeSessions(eventId) { return getSessionsForEvent(eventId).filter((session) => session.type === "free_practice"); } diff --git a/src/event_manager_controller.js b/src/event_manager_controller.js new file mode 100644 index 0000000..52cf6fa --- /dev/null +++ b/src/event_manager_controller.js @@ -0,0 +1,828 @@ +export function renderEventManagerView(context) { + const { + eventId, + state, + t, + escapeHtml, + normalizeEvent, + ensureRaceParticipantsConfigured, + getSessionsForEvent, + getSelectedTeamEditId, + setSelectedTeamEditId, + getSelectedSessionEditId, + setSelectedSessionEditId, + getSelectedGridSessionId, + setSelectedGridSessionId, + getRaceFormatAdvanced, + setRaceFormatAdvanced, + getTeamDriverPool, + getEventTeams, + normalizeBrandingConfig, + getSessionTypeChoices, + getRaceFormatPresets, + getRaceSummaryItems, + getRaceSummaryWarnings, + getRaceManageStatuses, + normalizeStartMode, + renderEventManagerMarkup, + getSessionEntrants, + getSessionTypeLabel, + getStartModeLabel, + getStatusLabel, + renderManageStatusBadgeView, + getDriverDisplayById, + renderRaceFormatContextCardView, + renderRaceFormatFieldView, + renderGridEditor, + renderRaceStandingsTableView, + buildPracticeStandings, + buildQualifyingStandings, + buildFinalStandings, + renderTeamRaceStandings, + renderFinalMatrix, + getModeLabel, + renderTable, + bindModalShell, + setFormError, + saveState, + renderView, + rerenderEventManager, + updateHeaderState, + uid, + normalizeSession, + buildSessionHeatSheetHtml, + openPrintWindow, + exportSessionHeatSheet, + exportSessionHeatSheetPdf, + getSelectedAssignmentSessionId, + autoAssignTrackSession, + renderAssignmentList, + normalizeRaceTeam, + buildRaceFormatConfigFromForm, + applyRaceFormatPreset, + normalizeStoredRacePreset, + generateQualifyingForRace, + clearGeneratedQualifying, + reseedUpcomingQualifying, + generateFinalsForRace, + clearGeneratedFinals, + applyBumpsForRace, + buildRacePackagePayload, + downloadJsonFile, + sanitizeFilenameSegment, + importRacePackagePayload, + buildRaceStartListsHtml, + buildRaceResultsHtml, + buildTeamRaceResultsHtml, + exportRaceStartListsPdf, + exportRaceResultsPdf, + exportTeamRaceResultsPdf, + ensureSessionDriverOrder, + reorderList, + getEventName, + } = context; + + const selectedTeamEditId = getSelectedTeamEditId(); + const selectedSessionEditId = getSelectedSessionEditId(); + const selectedGridSessionId = getSelectedGridSessionId(); + const raceFormatAdvanced = getRaceFormatAdvanced(); + + const event = state.events.find((e) => e.id === eventId); + if (!event) { + return; + } + const normalizedEvent = normalizeEvent(event); + if (normalizedEvent !== event) { + Object.assign(event, normalizedEvent); + } + ensureRaceParticipantsConfigured(event); + + const sessions = getSessionsForEvent(eventId); + const eventManageArea = document.getElementById("eventManageArea"); + if (!eventManageArea) { + return; + } + + const driverOptions = state.drivers + .map((d) => ``) + .join(""); + const teamDriverPool = event.mode === "race" ? getTeamDriverPool(event) : { drivers: [], fallback: false }; + const raceDrivers = event.mode === "race" ? teamDriverPool.drivers : []; + const raceTeams = event.mode === "race" ? getEventTeams(event) : []; + if (selectedTeamEditId && !raceTeams.some((team) => team.id === selectedTeamEditId)) { + setSelectedTeamEditId(null); + } + const editingTeam = event.mode === "race" ? raceTeams.find((team) => team.id === selectedTeamEditId) || null : null; + const carOptions = state.cars + .map((c) => ``) + .join(""); + const branding = normalizeBrandingConfig(event.branding); + const editingSession = sessions.find((session) => session.id === selectedSessionEditId) || null; + const sessionTypeChoices = getSessionTypeChoices(event.mode); + const sessionTypeHintKey = event.mode === "track" ? "events.session_type_hint_track" : "events.session_type_hint_race"; + const racePresets = getRaceFormatPresets(); + const selectedPreset = racePresets.find((preset) => preset.id === event.raceConfig.presetId) || racePresets[0]; + const isEndurancePreset = event.mode === "race" && selectedPreset?.id === "endurance"; + const showBasicQualifyingFields = raceFormatAdvanced || !isEndurancePreset; + const showBasicFinalFields = raceFormatAdvanced || !isEndurancePreset; + const selectedParticipantCount = event.mode === "race" ? (event.raceConfig.participantsConfigured ? (event.raceConfig.driverIds || []).length : raceDrivers.length) : 0; + const raceSummaryItems = event.mode === "race" ? getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset, { t, getStartModeLabel }) : []; + const raceSummaryWarnings = event.mode === "race" ? getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selectedPreset, { t }) : []; + const manageStatuses = event.mode === "race" ? getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selectedPreset) : null; + const gridSessions = event.mode === "race" ? sessions.filter((session) => normalizeStartMode(session.startMode) === "position") : []; + if (selectedGridSessionId && !gridSessions.some((session) => session.id === selectedGridSessionId)) { + setSelectedGridSessionId(""); + } + const selectedGridSession = + gridSessions.find((session) => session.id === selectedGridSessionId) || gridSessions[0] || null; + if (!selectedGridSessionId && selectedGridSession) { + setSelectedGridSessionId(selectedGridSession.id); + } + + eventManageArea.innerHTML = renderEventManagerMarkup({ + event, + t, + escapeHtml, + sessions, + sessionTypeChoices, + sessionTypeHintKey, + raceTeams, + getSessionEntrants, + getSessionTypeLabel, + getStartModeLabel, + getStatusLabel, + normalizeStartMode, + branding, + driverOptions, + carOptions, + manageStatuses, + renderManageStatusBadgeView, + selectedParticipantCount, + raceDrivers, + teamDriverPool, + state, + getDriverDisplayById, + raceFormatAdvanced, + racePresets, + selectedPreset, + isEndurancePreset, + showBasicQualifyingFields, + showBasicFinalFields, + renderRaceFormatContextCardView, + renderRaceFormatFieldView, + raceSummaryWarnings, + raceSummaryItems, + selectedGridSession, + renderGridEditor, + renderRaceStandingsTableView, + buildPracticeStandings, + buildQualifyingStandings, + buildFinalStandings, + renderTeamRaceStandings, + renderFinalMatrix, + editingTeam, + editingSession, + getModeLabel, + renderTable, + }); + + const bindManageJump = (node) => { + const triggerJump = () => { + const targetId = node.getAttribute("data-target") || ""; + const target = targetId ? document.getElementById(targetId) : null; + if (target) { + target.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }; + node.addEventListener("click", triggerJump); + node.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + triggerJump(); + } + }); + }; + + eventManageArea.querySelectorAll(".summary-warning-link, .manage-step-card-link").forEach((node) => { + bindManageJump(node); + }); + + document.getElementById("eventBrandingForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + event.branding = normalizeBrandingConfig({ + ...event.branding, + brandName: String(form.get("brandName") || "").trim(), + brandTagline: String(form.get("brandTagline") || "").trim(), + pdfFooter: String(form.get("pdfFooter") || "").trim(), + pdfTheme: String(form.get("pdfTheme") || "").trim(), + }); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("eventLogoUpload")?.addEventListener("change", (eventInput) => { + const input = eventInput.currentTarget; + const file = input instanceof HTMLInputElement ? input.files?.[0] : null; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = () => { + event.branding = normalizeBrandingConfig({ + ...event.branding, + logoDataUrl: typeof reader.result === "string" ? reader.result : "", + }); + saveState(); + rerenderEventManager(eventId); + }; + reader.readAsDataURL(file); + }); + + document.getElementById("eventLogoClear")?.addEventListener("click", () => { + event.branding = normalizeBrandingConfig({ + ...event.branding, + logoDataUrl: "", + }); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("sessionForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + state.sessions.push(normalizeSession({ + id: uid("session"), + eventId, + name: String(form.get("name")).trim(), + type: String(form.get("type")), + durationMin: Number(form.get("durationMin")), + followUpSec: Math.max(0, Number(form.get("followUpSec") || 0) || 0), + startMode: String(form.get("startMode") || "mass"), + seedBestLapCount: Math.max(0, Number(form.get("seedBestLapCount") || 0) || 0), + seedMethod: String(form.get("seedMethod") || "best_sum"), + staggerGapSec: Math.max(0, Number(form.get("staggerGapSec") || 0) || 0), + maxCars: Number(form.get("maxCars") || 0) || null, + mode: event.mode, + status: "ready", + startedAt: null, + endedAt: null, + finishedByTimer: false, + assignments: [], + })); + saveState(); + rerenderEventManager(eventId); + updateHeaderState(); + }); + + sessions.forEach((s) => { + document.getElementById(`session-edit-${s.id}`)?.addEventListener("click", () => { + setSelectedSessionEditId(s.id); + rerenderEventManager(eventId); + }); + + document.getElementById(`session-active-${s.id}`)?.addEventListener("click", () => { + state.activeSessionId = s.id; + saveState(); + updateHeaderState(); + renderView(); + }); + + document.getElementById(`session-delete-${s.id}`)?.addEventListener("click", () => { + state.sessions = state.sessions.filter((x) => x.id !== s.id); + delete state.resultsBySession[s.id]; + if (state.activeSessionId === s.id) { + state.activeSessionId = null; + } + saveState(); + rerenderEventManager(eventId); + updateHeaderState(); + }); + + document.getElementById(`session-grid-${s.id}`)?.addEventListener("click", () => { + ensureSessionDriverOrder(s); + setSelectedGridSessionId(s.id); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById(`session-sheet-print-${s.id}`)?.addEventListener("click", () => { + openPrintWindow(`${getEventName(eventId)} - ${s.name}`, buildSessionHeatSheetHtml(s)); + }); + + document.getElementById(`session-sheet-export-${s.id}`)?.addEventListener("click", () => { + exportSessionHeatSheet(s); + }); + + document.getElementById(`session-sheet-pdf-${s.id}`)?.addEventListener("click", async () => { + await exportSessionHeatSheetPdf(s); + }); + }); + + document.getElementById("sessionEditCancel")?.addEventListener("click", () => { + setSelectedSessionEditId(null); + rerenderEventManager(eventId); + }); + + document.getElementById("sessionEditCancelFooter")?.addEventListener("click", () => { + setSelectedSessionEditId(null); + rerenderEventManager(eventId); + }); + + document.getElementById("sessionEditModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "sessionEditModalOverlay") { + setSelectedSessionEditId(null); + rerenderEventManager(eventId); + } + }); + + bindModalShell("sessionEditModalOverlay", () => { + setSelectedSessionEditId(null); + rerenderEventManager(eventId); + }); + + document.getElementById("sessionEditForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + if (!editingSession) { + return; + } + const form = new FormData(event.currentTarget); + const cleanedName = String(form.get("name") || "").trim(); + const cleanedDuration = Number(form.get("durationMin") || editingSession.durationMin || 5) || 0; + if (!cleanedName) { + setFormError("sessionEditError", t("validation.required_name")); + return; + } + if (cleanedDuration < 1) { + setFormError("sessionEditError", t("validation.required_duration")); + return; + } + setFormError("sessionEditError", ""); + editingSession.name = cleanedName; + editingSession.type = String(form.get("type") || editingSession.type); + editingSession.durationMin = Math.max(1, cleanedDuration); + editingSession.followUpSec = Math.max(0, Number(form.get("followUpSec") || 0) || 0); + editingSession.startMode = normalizeStartMode(String(form.get("startMode") || editingSession.startMode || "mass")); + editingSession.seedBestLapCount = Math.max(0, Number(form.get("seedBestLapCount") || 0) || 0); + editingSession.seedMethod = ["best_sum", "average", "consecutive"].includes(String(form.get("seedMethod") || "").toLowerCase()) + ? String(form.get("seedMethod")).toLowerCase() + : "best_sum"; + editingSession.staggerGapSec = Math.max(0, Number(form.get("staggerGapSec") || 0) || 0); + editingSession.maxCars = Number(form.get("maxCars") || 0) || null; + setSelectedSessionEditId(null); + saveState(); + rerenderEventManager(eventId); + }); + + if (event.mode === "track") { + document.getElementById("sponsorRoundsForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + const qualificationRounds = Number(form.get("qualificationRounds") || 0); + const heatRounds = Number(form.get("heatRounds") || 0); + const finalRounds = Number(form.get("finalRounds") || 0); + const durationMin = Number(form.get("roundDuration") || 5); + + createSponsorRounds(eventId, { + qualificationRounds, + heatRounds, + finalRounds, + durationMin, + }); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("assignForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + const sessionId = String(form.get("sessionId")); + const session = state.sessions.find((x) => x.id === sessionId); + if (!session) { + return; + } + + const driverId = String(form.get("driverId")); + const carId = String(form.get("carId")); + const car = state.cars.find((x) => x.id === carId); + if (!car) { + return; + } + + const duplicateCar = (session.assignments || []).find((a) => a.carId === carId); + if (duplicateCar) { + alert(t("events.duplicate_car")); + return; + } + + const duplicateDriver = (session.assignments || []).find((a) => a.driverId === driverId); + if (duplicateDriver) { + alert(t("events.duplicate_driver")); + return; + } + + const duplicateTp = (session.assignments || []).find((a) => { + const existingCar = state.cars.find((x) => x.id === a.carId); + return existingCar?.transponder && existingCar.transponder === car.transponder; + }); + if (duplicateTp) { + alert(t("events.duplicate_tp")); + return; + } + + session.assignments = session.assignments || []; + session.assignments.push({ id: uid("as"), driverId, carId }); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("autoAssignSession")?.addEventListener("click", () => { + const sessionId = getSelectedAssignmentSessionId(); + if (!sessionId) { + return; + } + autoAssignTrackSession(event, sessionId); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("clearAssignSession")?.addEventListener("click", () => { + const sessionId = getSelectedAssignmentSessionId(); + if (!sessionId) { + return; + } + const session = state.sessions.find((x) => x.id === sessionId); + if (!session) { + return; + } + session.assignments = []; + saveState(); + rerenderEventManager(eventId); + }); + + renderAssignmentList(eventId); + } + + if (event.mode === "race") { + const persistRaceParticipants = () => { + const selectedIds = Array.from(document.querySelectorAll(".race-participant:checked")).map((node) => node.value); + event.raceConfig.driverIds = selectedIds; + event.raceConfig.participantsConfigured = true; + saveState(); + }; + + document.querySelectorAll(".race-participant").forEach((node) => { + node.addEventListener("change", persistRaceParticipants); + }); + + document.getElementById("selectAllParticipants")?.addEventListener("click", () => { + document.querySelectorAll(".race-participant").forEach((node) => { + node.checked = true; + }); + persistRaceParticipants(); + }); + + document.getElementById("clearParticipants")?.addEventListener("click", () => { + document.querySelectorAll(".race-participant").forEach((node) => { + node.checked = false; + }); + persistRaceParticipants(); + }); + + document.getElementById("teamForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + const name = String(form.get("teamName") || "").trim(); + const driverIds = form.getAll("teamDriverIds").map(String).filter(Boolean); + const carIds = form.getAll("teamCarIds").map(String).filter(Boolean); + if (!name || (!driverIds.length && !carIds.length)) { + return; + } + const createdTeam = normalizeRaceTeam({ id: uid("team"), name, driverIds, carIds }); + event.raceConfig.teams = [...getEventTeams(event), createdTeam]; + setSelectedTeamEditId(createdTeam.id); + saveState(); + rerenderEventManager(eventId); + }); + + raceTeams.forEach((team) => { + document.getElementById(`team-edit-${team.id}`)?.addEventListener("click", () => { + setSelectedTeamEditId(team.id); + rerenderEventManager(eventId); + }); + + document.getElementById(`team-delete-${team.id}`)?.addEventListener("click", () => { + event.raceConfig.teams = getEventTeams(event).filter((item) => item.id !== team.id); + if (selectedTeamEditId === team.id) { + setSelectedTeamEditId(null); + } + saveState(); + rerenderEventManager(eventId); + }); + }); + + document.getElementById("teamEditCancel")?.addEventListener("click", () => { + setSelectedTeamEditId(null); + rerenderEventManager(eventId); + }); + + document.getElementById("teamEditCancelFooter")?.addEventListener("click", () => { + setSelectedTeamEditId(null); + rerenderEventManager(eventId); + }); + + document.getElementById("teamEditModalOverlay")?.addEventListener("click", (modalEvent) => { + if (modalEvent.target?.id === "teamEditModalOverlay") { + setSelectedTeamEditId(null); + rerenderEventManager(eventId); + } + }); + + bindModalShell("teamEditModalOverlay", () => { + setSelectedTeamEditId(null); + rerenderEventManager(eventId); + }); + + document.getElementById("teamEditForm")?.addEventListener("submit", (submitEvent) => { + submitEvent.preventDefault(); + if (!editingTeam) { + return; + } + const form = new FormData(submitEvent.currentTarget); + const name = String(form.get("teamName") || "").trim(); + const driverIds = form.getAll("teamDriverIds").map(String).filter(Boolean); + const carIds = form.getAll("teamCarIds").map(String).filter(Boolean); + if (!name) { + setFormError("teamEditError", t("validation.required_name")); + return; + } + if (!driverIds.length && !carIds.length) { + setFormError("teamEditError", t("validation.invalid_selection")); + return; + } + setFormError("teamEditError", ""); + event.raceConfig.teams = getEventTeams(event).map((team) => + team.id === editingTeam.id ? normalizeRaceTeam({ ...team, name, driverIds, carIds }) : team + ); + setSelectedTeamEditId(null); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("raceFormatBasicToggle")?.addEventListener("click", () => { + setRaceFormatAdvanced(false); + rerenderEventManager(eventId); + }); + + document.getElementById("raceFormatAdvancedToggle")?.addEventListener("click", () => { + setRaceFormatAdvanced(true); + rerenderEventManager(eventId); + }); + + document.getElementById("raceFormatForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + event.raceConfig = buildRaceFormatConfigFromForm(form, event); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("applyRacePreset")?.addEventListener("click", () => { + const formElement = document.getElementById("raceFormatForm"); + if (!(formElement instanceof HTMLFormElement)) { + return; + } + const form = new FormData(formElement); + applyRaceFormatPreset(event, String(form.get("presetId") || "custom")); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("saveRacePreset")?.addEventListener("click", () => { + const formElement = document.getElementById("raceFormatForm"); + if (!(formElement instanceof HTMLFormElement)) { + return; + } + const form = new FormData(formElement); + const presetName = String(form.get("presetName") || "").trim(); + if (!presetName) { + return; + } + const config = buildRaceFormatConfigFromForm(form, event); + const selectedPresetId = String(form.get("presetId") || "custom"); + const existingCustomPreset = (state.settings.racePresets || []).find((preset) => preset.id === selectedPresetId); + const presetId = existingCustomPreset ? existingCustomPreset.id : uid("preset"); + const storedPreset = normalizeStoredRacePreset({ + id: presetId, + name: presetName, + values: { + qualifyingScoring: config.qualifyingScoring, + qualifyingRounds: config.qualifyingRounds, + carsPerHeat: config.carsPerHeat, + qualDurationMin: config.qualDurationMin, + qualStartMode: config.qualStartMode, + qualSeedLapCount: config.qualSeedLapCount, + qualSeedMethod: config.qualSeedMethod, + countedQualRounds: config.countedQualRounds, + qualifyingPointsTable: config.qualifyingPointsTable, + qualifyingTieBreak: config.qualifyingTieBreak, + carsPerFinal: config.carsPerFinal, + finalLegs: config.finalLegs, + countedFinalLegs: config.countedFinalLegs, + finalDurationMin: config.finalDurationMin, + finalStartMode: config.finalStartMode, + followUpSec: config.followUpSec, + minLapMs: config.minLapMs, + maxLapMs: config.maxLapMs, + bumpCount: config.bumpCount, + reserveBumpSlots: config.reserveBumpSlots, + finalsSource: config.finalsSource, + }, + }); + const otherPresets = (state.settings.racePresets || []).filter((preset) => preset.id !== presetId); + state.settings.racePresets = [...otherPresets, storedPreset]; + event.raceConfig = { ...config, presetId }; + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("deleteRacePreset")?.addEventListener("click", () => { + const formElement = document.getElementById("raceFormatForm"); + if (!(formElement instanceof HTMLFormElement)) { + return; + } + const form = new FormData(formElement); + const presetId = String(form.get("presetId") || "custom"); + if (!(state.settings.racePresets || []).some((preset) => preset.id === presetId)) { + return; + } + state.settings.racePresets = (state.settings.racePresets || []).filter((preset) => preset.id !== presetId); + event.raceConfig.presetId = "custom"; + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("generateQualifying")?.addEventListener("click", () => { + const created = generateQualifyingForRace(event); + saveState(); + rerenderEventManager(eventId); + if (created > 0) { + alert(t("events.generated_qualifying")); + } + }); + + document.getElementById("clearGeneratedQualifying")?.addEventListener("click", () => { + clearGeneratedQualifying(event.id); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("reseedQualifying")?.addEventListener("click", () => { + const result = reseedUpcomingQualifying(event); + saveState(); + rerenderEventManager(eventId); + const messages = []; + if (result.updated > 0) { + messages.push(t("events.reseed_done")); + } else { + messages.push(t("events.no_reseed_done")); + } + if (result.locked > 0) { + messages.push(t("events.reseed_locked", { count: result.locked })); + } + alert(messages.join("\n")); + }); + + document.getElementById("generateFinals")?.addEventListener("click", () => { + const created = generateFinalsForRace(event); + saveState(); + rerenderEventManager(eventId); + if (created > 0) { + alert(t("events.finals_generated")); + } + }); + + document.getElementById("clearGeneratedFinals")?.addEventListener("click", () => { + clearGeneratedFinals(event.id); + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("applyBumps")?.addEventListener("click", () => { + const applied = applyBumpsForRace(event); + saveState(); + rerenderEventManager(eventId); + alert(t(applied > 0 ? "events.bumps_applied" : "events.no_bumps_applied")); + }); + + document.getElementById("exportRacePackage")?.addEventListener("click", () => { + const payload = buildRacePackagePayload(eventId); + downloadJsonFile(`${sanitizeFilenameSegment(event.name)}_race_package.json`, payload); + }); + + document.getElementById("importRacePackage")?.addEventListener("change", (importEvent) => { + const input = importEvent.currentTarget; + const file = input instanceof HTMLInputElement ? input.files?.[0] : null; + if (!file) { + return; + } + const reader = new FileReader(); + reader.onload = () => { + try { + const parsed = JSON.parse(String(reader.result || "{}")); + importRacePackagePayload(parsed); + } catch (error) { + alert(t("settings.import_failed", { msg: error instanceof Error ? error.message : String(error) })); + } + }; + reader.readAsText(file); + }); + + document.getElementById("printStartlists")?.addEventListener("click", () => { + openPrintWindow(`${event.name} - ${t("events.start_lists")}`, buildRaceStartListsHtml(event)); + }); + + document.getElementById("printResults")?.addEventListener("click", () => { + openPrintWindow(`${event.name} - ${t("events.results_overview")}`, buildRaceResultsHtml(event)); + }); + + document.getElementById("printTeamResults")?.addEventListener("click", () => { + openPrintWindow(`${event.name} - ${t("events.team_report")}`, buildTeamRaceResultsHtml(event)); + }); + + document.getElementById("pdfStartlists")?.addEventListener("click", async () => { + await exportRaceStartListsPdf(event); + }); + + document.getElementById("pdfResults")?.addEventListener("click", async () => { + await exportRaceResultsPdf(event); + }); + + document.getElementById("pdfTeamResults")?.addEventListener("click", async () => { + await exportTeamRaceResultsPdf(event); + }); + + document.getElementById("gridResetOrder")?.addEventListener("click", () => { + if (!selectedGridSession) { + return; + } + selectedGridSession.driverIds = getSessionEntrants(selectedGridSession) + .map((driver) => driver.id) + .filter(Boolean); + selectedGridSession.manualGridIds = [...selectedGridSession.driverIds]; + selectedGridSession.gridCustomized = false; + saveState(); + rerenderEventManager(eventId); + }); + + document.getElementById("gridToggleLock")?.addEventListener("click", () => { + if (!selectedGridSession) { + return; + } + if (!selectedGridSession.gridCustomized) { + selectedGridSession.manualGridIds = [...ensureSessionDriverOrder(selectedGridSession)]; + selectedGridSession.gridCustomized = true; + } else { + selectedGridSession.manualGridIds = [...selectedGridSession.driverIds]; + selectedGridSession.gridCustomized = false; + } + saveState(); + rerenderEventManager(eventId); + }); + + let dragIndex = null; + document.querySelectorAll("#gridDragList .drag-item").forEach((node) => { + node.addEventListener("dragstart", () => { + dragIndex = Number(node.dataset.index); + node.classList.add("drag-item-active"); + }); + node.addEventListener("dragend", () => { + dragIndex = null; + node.classList.remove("drag-item-active"); + }); + node.addEventListener("dragover", (dragEvent) => { + dragEvent.preventDefault(); + node.classList.add("drag-item-over"); + }); + node.addEventListener("dragleave", () => { + node.classList.remove("drag-item-over"); + }); + node.addEventListener("drop", (dropEvent) => { + dropEvent.preventDefault(); + node.classList.remove("drag-item-over"); + if (!selectedGridSession || dragIndex === null) { + return; + } + const dropIndex = Number(node.dataset.index); + if (Number.isNaN(dropIndex) || dropIndex === dragIndex) { + return; + } + selectedGridSession.manualGridIds = reorderList(ensureSessionDriverOrder(selectedGridSession), dragIndex, dropIndex); + selectedGridSession.gridCustomized = true; + saveState(); + rerenderEventManager(eventId); + }); + }); + } + +}