diff --git a/src/app.js b/src/app.js index f83d0d0..4c9252f 100644 --- a/src/app.js +++ b/src/app.js @@ -12,7 +12,7 @@ import { renderDashboardView, renderClassesView, renderDriversView, renderCarsVi import { renderGuideView, renderOverlayPageView } from "./misc_views.js"; import { getSessionsForEventHelper, getModeLabelHelper, normalizeStartModeHelper, getStartModeLabelHelper, getClassNameHelper, getEventNameHelper, renderAssignmentListView, renderSessionsTableView } from "./event_common.js"; -import { renderEventWorkspaceMarkup } from "./event_views.js"; +import { renderEventWorkspaceView } from "./event_workspace_controller.js"; import { renderEventManagerMarkup } from "./event_manager_view.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"; @@ -2812,302 +2812,42 @@ function renderRaceSetup() { } function renderEventWorkspace(mode) { - const isRaceMode = mode === "race"; - if (isRaceMode) { - ensureRaceWizardDraft(); - } - const filteredEvents = state.events.filter((event) => event.mode === mode); - const editingEvent = filteredEvents.find((event) => event.id === selectedEventEditId) || null; - - dom.view.innerHTML = renderEventWorkspaceMarkup(mode, { + renderEventWorkspaceView({ + mode, state, + dom, t, escapeHtml, renderTable, renderRaceWizardStepsView, renderRaceWizardContentView, - raceWizardDraft, - raceWizardStep, + getRaceWizardDraft: () => raceWizardDraft, + setRaceWizardDraft: (value) => { raceWizardDraft = value; }, + getRaceWizardStep: () => raceWizardStep, + setRaceWizardStep: (value) => { raceWizardStep = value; }, + ensureRaceWizardDraft, getDriversForClass, getRaceWizardPreset, getSessionsForEvent, getClassName, getModeLabel, - editingEvent, + getSelectedEventEditId: () => selectedEventEditId, + setSelectedEventEditId: (value) => { selectedEventEditId = value; }, + renderView, + renderEventManager, + uid, + normalizeEvent, + applyRaceFormatPreset, + buildRaceSessionsFromWizard, + buildDefaultRaceWizardDraft, + applyRaceWizardPresetDefaults, + setSelectedTeamEditId: (value) => { selectedTeamEditId = value; }, + setSelectedSessionEditId: (value) => { selectedSessionEditId = value; }, + setFormError, + bindModalShell, + isValidIsoDate, + saveState, }); - - if (isRaceMode) { - const persistWizardStepOne = () => { - const form = document.getElementById("raceWizardStepForm"); - if (!(form instanceof HTMLFormElement)) { - return true; - } - const data = new FormData(form); - const nextName = String(data.get("name") || "").trim(); - const nextDate = String(data.get("date") || "").trim(); - const nextClassId = String(data.get("classId") || "").trim(); - const nextPresetId = String(data.get("presetId") || "club_qualifying").trim() || "club_qualifying"; - if (!nextName || !nextDate || !nextClassId) { - return false; - } - const classChanged = raceWizardDraft.classId !== nextClassId; - raceWizardDraft.name = nextName; - raceWizardDraft.date = nextDate; - raceWizardDraft.classId = nextClassId; - if (classChanged) { - raceWizardDraft.driverIds = getDriversForClass(nextClassId).map((driver) => driver.id); - } - if (raceWizardDraft.presetId !== nextPresetId) { - applyRaceWizardPresetDefaults(raceWizardDraft, nextPresetId); - } - return true; - }; - - const persistWizardParticipants = () => { - raceWizardDraft.driverIds = Array.from(document.querySelectorAll(".wizard-participant:checked")).map((node) => node.value); - return true; - }; - - const persistWizardPlan = () => { - const form = document.getElementById("raceWizardPlanForm"); - if (!(form instanceof HTMLFormElement)) { - return true; - } - const data = new FormData(form); - raceWizardDraft.createPractice = data.get("createPractice") === "on"; - raceWizardDraft.practiceSessions = Math.max(0, Number(data.get("practiceSessions") || 0) || 0); - raceWizardDraft.createQualifying = data.get("createQualifying") === "on"; - raceWizardDraft.qualifyingRounds = Math.max(0, Number(data.get("qualifyingRounds") || 0) || 0); - raceWizardDraft.createTeamRace = data.get("createTeamRace") === "on"; - raceWizardDraft.teamRaceDurationMin = Math.max(1, Number(data.get("teamRaceDurationMin") || 1) || 1); - return raceWizardDraft.createPractice || raceWizardDraft.createQualifying || raceWizardDraft.createTeamRace; - }; - - document.getElementById("raceWizardReset")?.addEventListener("click", () => { - raceWizardDraft = applyRaceWizardPresetDefaults(buildDefaultRaceWizardDraft(), "club_qualifying"); - raceWizardStep = 1; - renderView(); - }); - - document.getElementById("raceWizardPrev")?.addEventListener("click", () => { - if (raceWizardStep === 2) { - persistWizardParticipants(); - } - if (raceWizardStep === 3) { - persistWizardPlan(); - } - raceWizardStep = Math.max(1, raceWizardStep - 1); - renderView(); - }); - - document.getElementById("raceWizardNext")?.addEventListener("click", () => { - let valid = true; - if (raceWizardStep === 1) { - valid = persistWizardStepOne(); - } else if (raceWizardStep === 2) { - valid = persistWizardParticipants(); - } else if (raceWizardStep === 3) { - valid = persistWizardPlan(); - } - if (!valid) { - return; - } - raceWizardStep = Math.min(4, raceWizardStep + 1); - renderView(); - }); - - const wizardBasicsForm = document.getElementById("raceWizardStepForm"); - const syncWizardBasicsDraft = () => { - if (!(wizardBasicsForm instanceof HTMLFormElement)) { - return; - } - const data = new FormData(wizardBasicsForm); - raceWizardDraft.name = String(data.get("name") || "").trim(); - raceWizardDraft.date = String(data.get("date") || raceWizardDraft.date || "").trim(); - }; - - wizardBasicsForm?.querySelector('[name="classId"]')?.addEventListener("change", (event) => { - syncWizardBasicsDraft(); - const nextClassId = String(event.currentTarget?.value || "").trim(); - if (!nextClassId) { - return; - } - const classChanged = raceWizardDraft.classId !== nextClassId; - raceWizardDraft.classId = nextClassId; - if (classChanged) { - raceWizardDraft.driverIds = getDriversForClass(nextClassId).map((driver) => driver.id); - } - renderView(); - }); - - wizardBasicsForm?.querySelector('[name="presetId"]')?.addEventListener("change", (event) => { - syncWizardBasicsDraft(); - const nextPresetId = String(event.currentTarget?.value || "club_qualifying").trim() || "club_qualifying"; - applyRaceWizardPresetDefaults(raceWizardDraft, nextPresetId); - renderView(); - }); - - const wizardPlanForm = document.getElementById("raceWizardPlanForm"); - const toggleWizardPlanField = (toggleName, fieldName) => { - const toggle = wizardPlanForm?.querySelector(`[name="${toggleName}"]`); - const field = wizardPlanForm?.querySelector(`[name="${fieldName}"]`); - if (!(toggle instanceof HTMLInputElement) || !(field instanceof HTMLInputElement)) { - return; - } - const applyDisabledState = () => { - field.disabled = !toggle.checked; - }; - applyDisabledState(); - toggle.addEventListener("change", applyDisabledState); - }; - toggleWizardPlanField("createPractice", "practiceSessions"); - toggleWizardPlanField("createQualifying", "qualifyingRounds"); - toggleWizardPlanField("createTeamRace", "teamRaceDurationMin"); - - document.getElementById("wizardSelectAllParticipants")?.addEventListener("click", () => { - raceWizardDraft.driverIds = getDriversForClass(raceWizardDraft.classId).map((driver) => driver.id); - renderView(); - }); - - document.getElementById("wizardClearParticipants")?.addEventListener("click", () => { - raceWizardDraft.driverIds = []; - renderView(); - }); - - document.getElementById("raceWizardCreate")?.addEventListener("click", () => { - const selectedDrivers = raceWizardDraft.driverIds.length ? raceWizardDraft.driverIds : getDriversForClass(raceWizardDraft.classId).map((driver) => driver.id); - const event = normalizeEvent({ - id: uid("event"), - name: raceWizardDraft.name.trim(), - date: raceWizardDraft.date, - classId: raceWizardDraft.classId, - mode, - }); - applyRaceFormatPreset(event, raceWizardDraft.presetId); - event.raceConfig.driverIds = selectedDrivers; - event.raceConfig.participantsConfigured = true; - state.events.push(event); - buildRaceSessionsFromWizard(event, raceWizardDraft).forEach((session) => state.sessions.push(session)); - selectedTeamEditId = null; - selectedSessionEditId = null; - raceWizardDraft = applyRaceWizardPresetDefaults(buildDefaultRaceWizardDraft(), "club_qualifying"); - raceWizardStep = 1; - saveState(); - renderView(); - renderEventManager(event.id); - }); - } else { - document.getElementById("eventForm")?.addEventListener("submit", (e) => { - e.preventDefault(); - const form = new FormData(e.currentTarget); - const event = { - id: uid("event"), - name: String(form.get("name")).trim(), - date: String(form.get("date")), - classId: String(form.get("classId")), - mode, - }; - state.events.push(normalizeEvent(event)); - saveState(); - renderView(); - }); - } - - filteredEvents.forEach((e) => { - document.getElementById(`event-edit-${e.id}`)?.addEventListener("click", () => { - selectedEventEditId = e.id; - renderView(); - }); - - document.getElementById(`event-delete-${e.id}`)?.addEventListener("click", () => { - const sessionIds = getSessionsForEvent(e.id).map((s) => s.id); - state.events = state.events.filter((x) => x.id !== e.id); - state.sessions = state.sessions.filter((x) => x.eventId !== e.id); - sessionIds.forEach((id) => delete state.resultsBySession[id]); - if (state.activeSessionId && sessionIds.includes(state.activeSessionId)) { - state.activeSessionId = null; - } - saveState(); - renderView(); - }); - - document.getElementById(`event-manage-${e.id}`)?.addEventListener("click", () => { - renderEventManager(e.id); - }); - }); - - document.getElementById("eventEditCancel")?.addEventListener("click", () => { - selectedEventEditId = null; - renderView(); - }); - - document.getElementById("eventEditCancelFooter")?.addEventListener("click", () => { - selectedEventEditId = null; - renderView(); - }); - - document.getElementById("eventEditModalOverlay")?.addEventListener("click", (event) => { - if (event.target?.id === "eventEditModalOverlay") { - selectedEventEditId = null; - renderView(); - } - }); - - bindModalShell("eventEditModalOverlay", () => { - selectedEventEditId = null; - renderView(); - }); - - const commitEventEdit = () => { - if (!editingEvent) { - return; - } - const formNode = document.getElementById("eventEditForm"); - if (!(formNode instanceof HTMLFormElement)) { - return; - } - const form = new FormData(formNode); - const cleanedName = String(form.get("name") || "").trim(); - const cleanedDate = String(form.get("date") || "").trim(); - const cleanedClassId = String(form.get("classId") || "").trim(); - if (!cleanedName) { - setFormError("eventEditError", t("validation.required_name")); - return; - } - if (!cleanedDate) { - setFormError("eventEditError", t("validation.required_date")); - return; - } - if (!isValidIsoDate(cleanedDate)) { - setFormError("eventEditError", t("validation.invalid_date")); - return; - } - if (cleanedClassId && !state.classes.some((item) => item.id === cleanedClassId)) { - setFormError("eventEditError", t("validation.invalid_selection")); - return; - } - setFormError("eventEditError", ""); - state.events = state.events.map((item) => - item.id === editingEvent.id - ? normalizeEvent({ - ...item, - name: cleanedName, - date: cleanedDate, - classId: cleanedClassId || item.classId, - }) - : item - ); - selectedEventEditId = null; - saveState(); - renderView(); - }; - - document.getElementById("eventEditForm")?.addEventListener("submit", (event) => { - event.preventDefault(); - commitEventEdit(); - }); - - document.getElementById("eventEditSave")?.addEventListener("click", commitEventEdit); } function renderEventManager(eventId) { diff --git a/src/event_workspace_controller.js b/src/event_workspace_controller.js new file mode 100644 index 0000000..7304cb9 --- /dev/null +++ b/src/event_workspace_controller.js @@ -0,0 +1,344 @@ +import { renderEventWorkspaceMarkup } from "./event_views.js"; + +export function renderEventWorkspaceView(context) { + const { + mode, + state, + dom, + t, + escapeHtml, + renderTable, + renderRaceWizardStepsView, + renderRaceWizardContentView, + getRaceWizardDraft, + setRaceWizardDraft, + getRaceWizardStep, + setRaceWizardStep, + ensureRaceWizardDraft, + getDriversForClass, + getRaceWizardPreset, + getSessionsForEvent, + getClassName, + getModeLabel, + getSelectedEventEditId, + setSelectedEventEditId, + renderView, + renderEventManager, + uid, + normalizeEvent, + applyRaceFormatPreset, + buildRaceSessionsFromWizard, + buildDefaultRaceWizardDraft, + applyRaceWizardPresetDefaults, + setSelectedTeamEditId, + setSelectedSessionEditId, + setFormError, + bindModalShell, + isValidIsoDate, + saveState, + } = context; + + const isRaceMode = mode === "race"; + if (isRaceMode) { + ensureRaceWizardDraft(); + } + const filteredEvents = state.events.filter((event) => event.mode === mode); + const editingEvent = filteredEvents.find((event) => event.id === getSelectedEventEditId()) || null; + const raceWizardDraft = getRaceWizardDraft(); + const raceWizardStep = getRaceWizardStep(); + + dom.view.innerHTML = renderEventWorkspaceMarkup(mode, { + state, + t, + escapeHtml, + renderTable, + renderRaceWizardStepsView, + renderRaceWizardContentView, + raceWizardDraft, + raceWizardStep, + getDriversForClass, + getRaceWizardPreset, + getSessionsForEvent, + getClassName, + getModeLabel, + editingEvent, + }); + + if (isRaceMode) { + const persistWizardStepOne = () => { + const form = document.getElementById("raceWizardStepForm"); + if (!(form instanceof HTMLFormElement)) { + return true; + } + const data = new FormData(form); + const nextName = String(data.get("name") || "").trim(); + const nextDate = String(data.get("date") || "").trim(); + const nextClassId = String(data.get("classId") || "").trim(); + const nextPresetId = String(data.get("presetId") || "club_qualifying").trim() || "club_qualifying"; + if (!nextName || !nextDate || !nextClassId) { + return false; + } + const draft = getRaceWizardDraft(); + const classChanged = draft.classId !== nextClassId; + draft.name = nextName; + draft.date = nextDate; + draft.classId = nextClassId; + if (classChanged) { + draft.driverIds = getDriversForClass(nextClassId).map((driver) => driver.id); + } + if (draft.presetId !== nextPresetId) { + applyRaceWizardPresetDefaults(draft, nextPresetId); + } + return true; + }; + + const persistWizardParticipants = () => { + const draft = getRaceWizardDraft(); + draft.driverIds = Array.from(document.querySelectorAll(".wizard-participant:checked")).map((node) => node.value); + return true; + }; + + const persistWizardPlan = () => { + const form = document.getElementById("raceWizardPlanForm"); + if (!(form instanceof HTMLFormElement)) { + return true; + } + const data = new FormData(form); + const draft = getRaceWizardDraft(); + draft.createPractice = data.get("createPractice") === "on"; + draft.practiceSessions = Math.max(0, Number(data.get("practiceSessions") || 0) || 0); + draft.createQualifying = data.get("createQualifying") === "on"; + draft.qualifyingRounds = Math.max(0, Number(data.get("qualifyingRounds") || 0) || 0); + draft.createTeamRace = data.get("createTeamRace") === "on"; + draft.teamRaceDurationMin = Math.max(1, Number(data.get("teamRaceDurationMin") || 1) || 1); + return draft.createPractice || draft.createQualifying || draft.createTeamRace; + }; + + document.getElementById("raceWizardReset")?.addEventListener("click", () => { + setRaceWizardDraft(applyRaceWizardPresetDefaults(buildDefaultRaceWizardDraft(), "club_qualifying")); + setRaceWizardStep(1); + renderView(); + }); + + document.getElementById("raceWizardPrev")?.addEventListener("click", () => { + if (getRaceWizardStep() === 2) { + persistWizardParticipants(); + } + if (getRaceWizardStep() === 3) { + persistWizardPlan(); + } + setRaceWizardStep(Math.max(1, getRaceWizardStep() - 1)); + renderView(); + }); + + document.getElementById("raceWizardNext")?.addEventListener("click", () => { + let valid = true; + if (getRaceWizardStep() === 1) { + valid = persistWizardStepOne(); + } else if (getRaceWizardStep() === 2) { + valid = persistWizardParticipants(); + } else if (getRaceWizardStep() === 3) { + valid = persistWizardPlan(); + } + if (!valid) { + return; + } + setRaceWizardStep(Math.min(4, getRaceWizardStep() + 1)); + renderView(); + }); + + const wizardBasicsForm = document.getElementById("raceWizardStepForm"); + const syncWizardBasicsDraft = () => { + if (!(wizardBasicsForm instanceof HTMLFormElement)) { + return; + } + const data = new FormData(wizardBasicsForm); + const draft = getRaceWizardDraft(); + draft.name = String(data.get("name") || "").trim(); + draft.date = String(data.get("date") || draft.date || "").trim(); + }; + + wizardBasicsForm?.querySelector('[name="classId"]')?.addEventListener("change", (event) => { + syncWizardBasicsDraft(); + const nextClassId = String(event.currentTarget?.value || "").trim(); + if (!nextClassId) { + return; + } + const draft = getRaceWizardDraft(); + const classChanged = draft.classId !== nextClassId; + draft.classId = nextClassId; + if (classChanged) { + draft.driverIds = getDriversForClass(nextClassId).map((driver) => driver.id); + } + renderView(); + }); + + wizardBasicsForm?.querySelector('[name="presetId"]')?.addEventListener("change", (event) => { + syncWizardBasicsDraft(); + const nextPresetId = String(event.currentTarget?.value || "club_qualifying").trim() || "club_qualifying"; + applyRaceWizardPresetDefaults(getRaceWizardDraft(), nextPresetId); + renderView(); + }); + + const wizardPlanForm = document.getElementById("raceWizardPlanForm"); + const toggleWizardPlanField = (toggleName, fieldName) => { + const toggle = wizardPlanForm?.querySelector(`[name="${toggleName}"]`); + const field = wizardPlanForm?.querySelector(`[name="${fieldName}"]`); + if (!(toggle instanceof HTMLInputElement) || !(field instanceof HTMLInputElement)) { + return; + } + const applyDisabledState = () => { + field.disabled = !toggle.checked; + }; + applyDisabledState(); + toggle.addEventListener("change", applyDisabledState); + }; + toggleWizardPlanField("createPractice", "practiceSessions"); + toggleWizardPlanField("createQualifying", "qualifyingRounds"); + toggleWizardPlanField("createTeamRace", "teamRaceDurationMin"); + + document.getElementById("wizardSelectAllParticipants")?.addEventListener("click", () => { + const draft = getRaceWizardDraft(); + draft.driverIds = getDriversForClass(draft.classId).map((driver) => driver.id); + renderView(); + }); + + document.getElementById("wizardClearParticipants")?.addEventListener("click", () => { + getRaceWizardDraft().driverIds = []; + renderView(); + }); + + document.getElementById("raceWizardCreate")?.addEventListener("click", () => { + const draft = getRaceWizardDraft(); + const selectedDrivers = draft.driverIds.length ? draft.driverIds : getDriversForClass(draft.classId).map((driver) => driver.id); + const event = normalizeEvent({ + id: uid("event"), + name: draft.name.trim(), + date: draft.date, + classId: draft.classId, + mode, + }); + applyRaceFormatPreset(event, draft.presetId); + event.raceConfig.driverIds = selectedDrivers; + event.raceConfig.participantsConfigured = true; + state.events.push(event); + buildRaceSessionsFromWizard(event, draft).forEach((session) => state.sessions.push(session)); + setSelectedTeamEditId(null); + setSelectedSessionEditId(null); + setRaceWizardDraft(applyRaceWizardPresetDefaults(buildDefaultRaceWizardDraft(), "club_qualifying")); + setRaceWizardStep(1); + saveState(); + renderView(); + renderEventManager(event.id); + }); + } else { + document.getElementById("eventForm")?.addEventListener("submit", (e) => { + e.preventDefault(); + const form = new FormData(e.currentTarget); + const event = { + id: uid("event"), + name: String(form.get("name")).trim(), + date: String(form.get("date")), + classId: String(form.get("classId")), + mode, + }; + state.events.push(normalizeEvent(event)); + saveState(); + renderView(); + }); + } + + filteredEvents.forEach((e) => { + document.getElementById(`event-edit-${e.id}`)?.addEventListener("click", () => { + setSelectedEventEditId(e.id); + renderView(); + }); + + document.getElementById(`event-delete-${e.id}`)?.addEventListener("click", () => { + const sessionIds = getSessionsForEvent(e.id).map((s) => s.id); + state.events = state.events.filter((x) => x.id !== e.id); + state.sessions = state.sessions.filter((x) => x.eventId !== e.id); + sessionIds.forEach((id) => delete state.resultsBySession[id]); + if (state.activeSessionId && sessionIds.includes(state.activeSessionId)) { + state.activeSessionId = null; + } + saveState(); + renderView(); + }); + + document.getElementById(`event-manage-${e.id}`)?.addEventListener("click", () => { + renderEventManager(e.id); + }); + }); + + document.getElementById("eventEditCancel")?.addEventListener("click", () => { + setSelectedEventEditId(null); + renderView(); + }); + + document.getElementById("eventEditCancelFooter")?.addEventListener("click", () => { + setSelectedEventEditId(null); + renderView(); + }); + + document.getElementById("eventEditModalOverlay")?.addEventListener("click", (event) => { + if (event.target?.id === "eventEditModalOverlay") { + setSelectedEventEditId(null); + renderView(); + } + }); + + bindModalShell("eventEditModalOverlay", () => { + setSelectedEventEditId(null); + renderView(); + }); + + const commitEventEdit = () => { + if (!editingEvent) { + return; + } + const formNode = document.getElementById("eventEditForm"); + if (!(formNode instanceof HTMLFormElement)) { + return; + } + const form = new FormData(formNode); + const cleanedName = String(form.get("name") || "").trim(); + const cleanedDate = String(form.get("date") || "").trim(); + const cleanedClassId = String(form.get("classId") || "").trim(); + if (!cleanedName) { + setFormError("eventEditError", t("validation.required_name")); + return; + } + if (!cleanedDate) { + setFormError("eventEditError", t("validation.required_date")); + return; + } + if (!isValidIsoDate(cleanedDate)) { + setFormError("eventEditError", t("validation.invalid_date")); + return; + } + if (cleanedClassId && !state.classes.some((item) => item.id === cleanedClassId)) { + setFormError("eventEditError", t("validation.invalid_selection")); + return; + } + setFormError("eventEditError", ""); + state.events = state.events.map((item) => + item.id === editingEvent.id + ? normalizeEvent({ + ...item, + name: cleanedName, + date: cleanedDate, + classId: cleanedClassId || item.classId, + }) + : item + ); + setSelectedEventEditId(null); + saveState(); + renderView(); + }; + + document.getElementById("eventEditForm")?.addEventListener("submit", (event) => { + event.preventDefault(); + commitEventEdit(); + }); +}