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);
+ });
+ });
+ }
+
+}