Extract event manager controller into module
This commit is contained in:
781
src/app.js
781
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 { getSessionsForEventHelper, getModeLabelHelper, normalizeStartModeHelper, getStartModeLabelHelper, getClassNameHelper, getEventNameHelper, renderAssignmentListView, renderSessionsTableView } from "./event_common.js";
|
||||||
import { renderEventWorkspaceView } from "./event_workspace_controller.js";
|
import { renderEventWorkspaceView } from "./event_workspace_controller.js";
|
||||||
import { renderEventManagerMarkup } from "./event_manager_view.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 { 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";
|
import { connectDecoderHelper, disconnectDecoderHelper, processDecoderMessageHelper } from "./decoder_runtime.js";
|
||||||
|
|
||||||
@@ -2851,92 +2852,40 @@ function renderEventWorkspace(mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderEventManager(eventId) {
|
function renderEventManager(eventId) {
|
||||||
const event = state.events.find((e) => e.id === eventId);
|
renderEventManagerView({
|
||||||
if (!event) {
|
eventId,
|
||||||
return;
|
state,
|
||||||
}
|
|
||||||
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) => `<option value="${d.id}">${escapeHtml(d.name)}</option>`)
|
|
||||||
.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) => `<option value="${c.id}">${escapeHtml(c.name)} (${escapeHtml(c.transponder)})</option>`)
|
|
||||||
.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,
|
|
||||||
t,
|
t,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
sessions,
|
normalizeEvent,
|
||||||
sessionTypeChoices,
|
ensureRaceParticipantsConfigured,
|
||||||
sessionTypeHintKey,
|
getSessionsForEvent,
|
||||||
raceTeams,
|
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,
|
getSessionEntrants,
|
||||||
getSessionTypeLabel,
|
getSessionTypeLabel,
|
||||||
getStartModeLabel,
|
getStartModeLabel,
|
||||||
getStatusLabel,
|
getStatusLabel,
|
||||||
normalizeStartMode,
|
|
||||||
branding,
|
|
||||||
driverOptions,
|
|
||||||
carOptions,
|
|
||||||
manageStatuses,
|
|
||||||
renderManageStatusBadgeView,
|
renderManageStatusBadgeView,
|
||||||
selectedParticipantCount,
|
|
||||||
raceDrivers,
|
|
||||||
teamDriverPool,
|
|
||||||
state,
|
|
||||||
getDriverDisplayById,
|
getDriverDisplayById,
|
||||||
raceFormatAdvanced,
|
|
||||||
racePresets,
|
|
||||||
selectedPreset,
|
|
||||||
isEndurancePreset,
|
|
||||||
showBasicQualifyingFields,
|
|
||||||
showBasicFinalFields,
|
|
||||||
renderRaceFormatContextCardView,
|
renderRaceFormatContextCardView,
|
||||||
renderRaceFormatFieldView,
|
renderRaceFormatFieldView,
|
||||||
raceSummaryWarnings,
|
|
||||||
raceSummaryItems,
|
|
||||||
selectedGridSession,
|
|
||||||
renderGridEditor,
|
renderGridEditor,
|
||||||
renderRaceStandingsTableView,
|
renderRaceStandingsTableView,
|
||||||
buildPracticeStandings,
|
buildPracticeStandings,
|
||||||
@@ -2944,653 +2893,49 @@ function renderEventManager(eventId) {
|
|||||||
buildFinalStandings,
|
buildFinalStandings,
|
||||||
renderTeamRaceStandings,
|
renderTeamRaceStandings,
|
||||||
renderFinalMatrix,
|
renderFinalMatrix,
|
||||||
editingTeam,
|
|
||||||
editingSession,
|
|
||||||
getModeLabel,
|
getModeLabel,
|
||||||
renderTable,
|
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) {
|
function getFreePracticeSessions(eventId) {
|
||||||
return getSessionsForEvent(eventId).filter((session) => session.type === "free_practice");
|
return getSessionsForEvent(eventId).filter((session) => session.type === "free_practice");
|
||||||
}
|
}
|
||||||
|
|||||||
828
src/event_manager_controller.js
Normal file
828
src/event_manager_controller.js
Normal file
@@ -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) => `<option value="${d.id}">${escapeHtml(d.name)}</option>`)
|
||||||
|
.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) => `<option value="${c.id}">${escapeHtml(c.name)} (${escapeHtml(c.transponder)})</option>`)
|
||||||
|
.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user