import { getDriverProfileGroups, getDriverProfileId } from "./driver_profile_logic.js"; function getScheduleDriftSummary(state) { const scheduledSessions = state.sessions .filter((session) => Number(session.startedAt || 0) > 0) .sort((left, right) => Number(left.startedAt || 0) - Number(right.startedAt || 0)); if (!scheduledSessions.length) { return null; } const nowTs = Date.now(); const plannedMs = scheduledSessions.reduce( (sum, session) => sum + Math.max(1, Number(session.durationMin || 0) || 0) * 60000 + Math.max(0, Number(session.followUpSec || 0) || 0) * 1000, 0 ); const actualMs = scheduledSessions.reduce((sum, session) => { const startedAt = Number(session.startedAt || 0) || 0; const endedAt = Number(session.endedAt || 0) || (session.status === "finished" ? nowTs : 0); if (!startedAt) { return sum; } const effectiveEnd = endedAt || nowTs; return sum + Math.max(0, effectiveEnd - startedAt); }, 0); return { plannedMs, actualMs, driftMs: actualMs - plannedMs, }; } function statCard(label, value, note) { return `

${label}

${value}

${note}
`; } export function renderDashboardView(deps) { const { state, dom, t, backend, getActiveSession, getStatusLabel, getSessionTypeLabel, getEventName, getModeLabel, getBackendUrl, formatLap, renderSessionsTable, setCurrentView, updateViewUrl, renderNav, renderView, connectDecoder, disconnectDecoder, openOverlayWindow, ensureAudioContext, playPassingBeep, playFinishSiren, escapeHtml, } = deps; const active = getActiveSession(); const schedule = getScheduleDriftSummary(state); const totalPassings = Object.values(state.resultsBySession || {}).reduce((sum, x) => sum + (x?.passings?.length || 0), 0); const backendUrl = getBackendUrl(); const decoderUrl = state.settings.wsUrl || "-"; const audioProfile = state.settings.passingSoundMode === "name" ? t("settings.passing_sound_name") : state.settings.passingSoundMode === "beep" ? t("settings.passing_sound_beep") : t("settings.passing_sound_off"); dom.view.innerHTML = `

${t("dashboard.get_started")}

${t("dashboard.get_started_hint")}

1
${t("dashboard.step_system")}

${t("dashboard.step_system_hint")}

2
${t("dashboard.step_register")}

${t("dashboard.step_register_hint")}

3
${t("dashboard.step_create")}

${t("dashboard.step_create_hint")}

4
${t("dashboard.step_run")}

${t("dashboard.step_run_hint")}

${statCard(t("dashboard.events"), String(state.events.length), t("dashboard.created"))} ${statCard(t("dashboard.drivers"), String(state.drivers.length), t("dashboard.registered"))} ${statCard(t("dashboard.cars"), String(state.cars.length), t("dashboard.track_fleet"))} ${statCard(t("dashboard.passings"), String(totalPassings), t("dashboard.captured"))}

${t("dashboard.live_session")}

${active ? getStatusLabel(active.status) : t("dashboard.idle")}
${ active ? `

${active.name} (${getSessionTypeLabel(active.type)})

${getEventName(active.eventId)} • ${getModeLabel(active.mode)}

${t("dashboard.duration")}: ${active.type === "open_practice" ? t("dashboard.untimed") : `${active.durationMin} min`}

` : `

${t("dashboard.no_session")}

` }

${t("dashboard.live_board")}

${t("dashboard.live_note")}

${t("dashboard.decoder_feed")} ${state.decoder.connected ? t("timing.connected") : t("timing.disconnected")} ${escapeHtml(decoderUrl)} ${state.settings.splitDecoderEnabled ? `${t("timing.split_decoder")}: ${state.decoder.splitConnected ? t("timing.connected") : t("timing.disconnected")}` : ""} ${state.settings.pitDecoderEnabled ? `${t("table.pit")}: ${Number(state.decoder.pitEntryConnected) + Number(state.decoder.pitExitConnected)}/2` : ""}
${t("dashboard.backend_link")} ${backend.available ? t("settings.online") : t("settings.offline")} ${escapeHtml(backendUrl)}
${t("dashboard.audio_profile")} ${state.settings.audioEnabled ? audioProfile : t("settings.passing_sound_off")} ${state.settings.finishVoiceEnabled ? t("settings.finish_voice") : "-"}
${t("dashboard.schedule_drift")} ${ schedule ? `${schedule.driftMs === 0 ? t("dashboard.on_time") : schedule.driftMs < 0 ? t("dashboard.ahead") : t("dashboard.behind")} ${formatLap(Math.abs(schedule.driftMs))}` : "-" } ${ schedule ? `${t("dashboard.schedule_plan")}: ${formatLap(schedule.plannedMs)} • ${t("dashboard.schedule_actual")}: ${formatLap(schedule.actualMs)}` : "-" }

${t("dashboard.recent_sessions")}

${renderSessionsTable(state.sessions.slice(-8).reverse())}
`; const navigateTo = (view) => { setCurrentView(view); updateViewUrl(view); renderNav(); renderView(); }; document.getElementById("workflowSettings")?.addEventListener("click", () => navigateTo("settings")); document.getElementById("workflowDrivers")?.addEventListener("click", () => navigateTo("drivers")); document.getElementById("workflowRaceSetup")?.addEventListener("click", () => navigateTo("race_setup")); document.getElementById("workflowRaceDay")?.addEventListener("click", () => navigateTo("race_day")); document.getElementById("workflowTiming")?.addEventListener("click", () => navigateTo("timing")); document.getElementById("workflowOverlay")?.addEventListener("click", () => navigateTo("overlay")); document.getElementById("goEvents")?.addEventListener("click", () => navigateTo("events")); document.getElementById("goRaceDay")?.addEventListener("click", () => navigateTo("race_day")); document.getElementById("goTiming")?.addEventListener("click", () => navigateTo("timing")); document.getElementById("connectNow")?.addEventListener("click", connectDecoder); document.getElementById("disconnectNow")?.addEventListener("click", disconnectDecoder); document.getElementById("openDashboardOverlay")?.addEventListener("click", openOverlayWindow); document.getElementById("dashboardTestAudio")?.addEventListener("click", () => { ensureAudioContext(); playPassingBeep(); if (state.settings.finishVoiceEnabled) { setTimeout(playFinishSiren, 220); } }); } export function renderClassesView(deps) { const { state, dom, t, selectedClassEditId, setSelectedClassEditId, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell } = deps; const editingClass = state.classes.find((item) => item.id === selectedClassEditId()) || null; dom.view.innerHTML = `

${t("classes.create")}

${t("classes.title")}

${renderTable( [t("table.name"), t("events.actions")], state.classes.map( (c) => ` ${escapeHtml(c.name)} ` ) )}
${ editingClass ? ` ` : "" } `; document.getElementById("classForm")?.addEventListener("submit", (e) => { e.preventDefault(); const form = new FormData(e.currentTarget); state.classes.push({ id: uid("class"), name: String(form.get("name")).trim() }); saveState(); renderView(); }); state.classes.forEach((item) => { document.getElementById(`class-edit-${item.id}`)?.addEventListener("click", () => { setSelectedClassEditId(item.id); renderView(); }); document.getElementById(`class-delete-${item.id}`)?.addEventListener("click", () => { state.classes = state.classes.filter((x) => x.id !== item.id); saveState(); renderView(); }); }); document.getElementById("classEditCancel")?.addEventListener("click", () => { setSelectedClassEditId(null); renderView(); }); document.getElementById("classEditCancelFooter")?.addEventListener("click", () => { setSelectedClassEditId(null); renderView(); }); document.getElementById("classEditModalOverlay")?.addEventListener("click", (event) => { if (event.target?.id === "classEditModalOverlay") { setSelectedClassEditId(null); renderView(); } }); bindModalShell("classEditModalOverlay", () => { setSelectedClassEditId(null); renderView(); }); const commitClassEdit = () => { if (!editingClass) { return; } const formNode = document.getElementById("classEditForm"); if (!(formNode instanceof HTMLFormElement)) { return; } const form = new FormData(formNode); const cleaned = String(form.get("name") || "").trim(); if (!cleaned) { setFormError("classEditError", t("validation.required_name")); return; } setFormError("classEditError", ""); state.classes = state.classes.map((item) => (item.id === editingClass.id ? { ...item, name: cleaned } : item)); setSelectedClassEditId(null); saveState(); renderView(); }; document.getElementById("classEditForm")?.addEventListener("submit", (event) => { event.preventDefault(); commitClassEdit(); }); document.getElementById("classEditSave")?.addEventListener("click", commitClassEdit); } export function renderDriversView(deps) { const { state, dom, t, driverBrandFilter, setDriverBrandFilter, selectedDriverEditId, setSelectedDriverEditId, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName, } = deps; const classOptions = state.classes.map((c) => ``).join(""); const driverSearch = driverBrandFilter().trim().toLowerCase(); const filteredDrivers = state.drivers.filter((driver) => !driverSearch || [driver.name, driver.speechName, driver.transponder, driver.brand, driver.club, driver.country].map((value) => String(value || "").toLowerCase()).some((value) => value.includes(driverSearch)) ); const editingDriver = state.drivers.find((driver) => driver.id === selectedDriverEditId()) || null; const profileGroups = getDriverProfileGroups(state); const profileOptions = profileGroups .map((profile) => { const profileName = profile.representative?.name || t("common.unknown_driver"); const classNames = profile.classIds.map((classId) => getClassName(classId)).filter(Boolean).join(" / "); const label = classNames ? `${profileName} - ${classNames}` : profileName; return ``; }) .join(""); const editingProfileId = editingDriver ? getDriverProfileId(editingDriver) : ""; const passingSoundOptions = (selected = "") => [``, ...Array.from({ length: 8 }, (_item, index) => { const value = `pass_${String(index + 1).padStart(2, "0")}.wav`; return ``; })].join(""); const existingClubs = [...new Set(state.drivers.map((driver) => String(driver.club || "").trim()).filter(Boolean))].sort(); const existingCountries = [...new Set([ "Sverige", "Norge", "Finland", "Danmark", "Island", "Tyskland", "Nederländerna", "Belgien", "Frankrike", "Storbritannien", "Polen", "Tjeckien", "Österrike", "Schweiz", ...state.drivers.map((driver) => String(driver.country || "").trim()).filter(Boolean), ])].sort(); dom.view.innerHTML = `

${t("drivers.create")}

${existingClubs.map((club) => ``).join("")} ${existingCountries.map((country) => ``).join("")}

${t("drivers.title")}

${renderTable( [t("table.name"), t("table.class"), t("drivers.club"), t("drivers.country"), t("table.brand"), t("table.transponder"), t("drivers.training_profile"), t("drivers.speech_name"), t("events.actions")], filteredDrivers.map( (d) => { const profile = profileGroups.find((item) => item.id === getDriverProfileId(d)); return ` ${escapeHtml(d.name)} ${escapeHtml(getClassName(d.classId))} ${escapeHtml(profile?.club || d.club || "-")} ${escapeHtml(profile?.country || d.country || "-")} ${escapeHtml(d.brand || "-")} ${escapeHtml(d.transponder || "-")} ${escapeHtml(profile?.representative?.name || d.name)}${profile?.drivers.length > 1 ? ` · ${profile.drivers.length}` : ""} ${escapeHtml(d.speechName || d.name)} `; } ) )}
${ editingDriver ? ` ` : "" } `; document.getElementById("driverForm")?.addEventListener("submit", (e) => { e.preventDefault(); const form = new FormData(e.currentTarget); const driverId = uid("driver"); const driverName = String(form.get("name") || "").trim(); const requestedProfileId = String(form.get("profileId") || "").trim(); const sameNameProfile = profileGroups.find((profile) => ( String(profile.representative?.name || "").trim().toLocaleLowerCase().replace(/\s+/g, " ") === driverName.toLocaleLowerCase().replace(/\s+/g, " ") )); const profileId = requestedProfileId && requestedProfileId !== "__new__" ? requestedProfileId : requestedProfileId === "__new__" ? uid("profile") : sameNameProfile?.id || uid("profile"); const linkedProfile = profileGroups.find((profile) => profile.id === profileId); state.drivers.push( normalizeDriver({ id: driverId, name: driverName, classId: String(form.get("classId")), brand: String(form.get("brand") || "").trim(), transponder: String(form.get("transponder") || "").trim(), speechName: String(form.get("speechName") || "").trim(), passSound: String(form.get("passSound") || "").trim(), profileId, club: String(form.get("club") || linkedProfile?.club || "").trim(), country: String(form.get("country") || linkedProfile?.country || "").trim(), }) ); saveState(); renderView(); }); document.getElementById("driverBrandFilter")?.addEventListener("input", (event) => { const input = event.currentTarget; if (!(input instanceof HTMLInputElement)) { return; } setDriverBrandFilter(input.value); renderDriversView(deps); }); const bindProfileMetadataFill = (formId) => { const formNode = document.getElementById(formId); const profileSelect = formNode?.querySelector('select[name="profileId"]'); profileSelect?.addEventListener("change", () => { const profile = profileGroups.find((item) => item.id === profileSelect.value); if (!profile) return; const clubInput = formNode.querySelector('input[name="club"]'); const countryInput = formNode.querySelector('input[name="country"]'); if (clubInput instanceof HTMLInputElement) clubInput.value = profile.club || ""; if (countryInput instanceof HTMLInputElement) countryInput.value = profile.country || ""; }); }; bindProfileMetadataFill("driverForm"); bindProfileMetadataFill("driverEditForm"); state.drivers.forEach((d) => { document.getElementById(`driver-edit-${d.id}`)?.addEventListener("click", () => { setSelectedDriverEditId(d.id); renderView(); }); document.getElementById(`driver-delete-${d.id}`)?.addEventListener("click", () => { state.drivers = state.drivers.filter((x) => x.id !== d.id); state.sessions.forEach((s) => { s.assignments = (s.assignments || []).filter((a) => a.driverId !== d.id); }); saveState(); renderView(); }); }); document.getElementById("driverEditCancel")?.addEventListener("click", () => { setSelectedDriverEditId(null); renderView(); }); document.getElementById("driverEditCancelFooter")?.addEventListener("click", () => { setSelectedDriverEditId(null); renderView(); }); document.getElementById("driverEditModalOverlay")?.addEventListener("click", (event) => { if (event.target?.id === "driverEditModalOverlay") { setSelectedDriverEditId(null); renderView(); } }); bindModalShell("driverEditModalOverlay", () => { setSelectedDriverEditId(null); renderView(); }); const commitDriverEdit = () => { if (!editingDriver) { return; } const formNode = document.getElementById("driverEditForm"); if (!(formNode instanceof HTMLFormElement)) { return; } const form = new FormData(formNode); const cleanedName = String(form.get("name") || "").trim(); const cleanedClassId = String(form.get("classId") || "").trim(); const cleanedBrand = String(form.get("brand") || "").trim(); const cleanedTp = String(form.get("transponder") || "").trim(); const cleanedSpeechName = String(form.get("speechName") || "").trim(); const cleanedPassSound = String(form.get("passSound") || "").trim(); const requestedProfileId = String(form.get("profileId") || "").trim(); const cleanedProfileId = requestedProfileId === "__new__" ? uid("profile") : requestedProfileId || editingProfileId; const cleanedClub = String(form.get("club") || "").trim(); const cleanedCountry = String(form.get("country") || "").trim(); if (!cleanedName) { setFormError("driverEditError", t("validation.required_name")); return; } if (cleanedClassId && !state.classes.some((item) => item.id === cleanedClassId)) { setFormError("driverEditError", t("validation.invalid_selection")); return; } setFormError("driverEditError", ""); state.drivers = state.drivers.map((item) => { if (item.id === editingDriver.id) { return normalizeDriver({ ...item, name: cleanedName, classId: cleanedClassId || item.classId, brand: cleanedBrand, transponder: cleanedTp, speechName: cleanedSpeechName, passSound: cleanedPassSound, profileId: cleanedProfileId, club: cleanedClub, country: cleanedCountry, }); } if (getDriverProfileId(item) === cleanedProfileId) { return normalizeDriver({ ...item, club: cleanedClub, country: cleanedCountry }); } return item; }); setSelectedDriverEditId(null); saveState(); renderView(); }; document.getElementById("driverEditForm")?.addEventListener("submit", (event) => { event.preventDefault(); commitDriverEdit(); }); document.getElementById("driverEditSave")?.addEventListener("click", commitDriverEdit); } export function renderCarsView(deps) { const { state, dom, t, carBrandFilter, setCarBrandFilter, selectedCarEditId, setSelectedCarEditId, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar, } = deps; const carSearch = carBrandFilter().trim().toLowerCase(); const filteredCars = state.cars.filter((car) => !carSearch || [car.name, car.transponder, car.brand].map((value) => String(value || "").toLowerCase()).some((value) => value.includes(carSearch)) ); const editingCar = state.cars.find((car) => car.id === selectedCarEditId()) || null; const passingSoundOptions = (selected = "") => [``, ...Array.from({ length: 8 }, (_item, index) => { const value = `pass_${String(index + 1).padStart(2, "0")}.wav`; return ``; })].join(""); dom.view.innerHTML = `

${t("cars.create")}

${t("cars.title")}

${renderTable( [t("table.car"), t("table.brand"), t("table.transponder"), t("events.actions")], filteredCars.map( (c) => ` ${escapeHtml(c.name)} ${escapeHtml(c.brand || "-")} ${escapeHtml(c.transponder)} ` ) )}
${ editingCar ? ` ` : "" } `; document.getElementById("carForm")?.addEventListener("submit", (e) => { e.preventDefault(); const form = new FormData(e.currentTarget); state.cars.push( normalizeCar({ id: uid("car"), name: String(form.get("name")).trim(), brand: String(form.get("brand") || "").trim(), transponder: String(form.get("transponder")).trim(), passSound: String(form.get("passSound") || "").trim(), }) ); saveState(); renderView(); }); document.getElementById("carBrandFilter")?.addEventListener("input", (event) => { const input = event.currentTarget; if (!(input instanceof HTMLInputElement)) { return; } setCarBrandFilter(input.value); renderCarsView(deps); }); state.cars.forEach((c) => { document.getElementById(`car-edit-${c.id}`)?.addEventListener("click", () => { setSelectedCarEditId(c.id); renderView(); }); document.getElementById(`car-delete-${c.id}`)?.addEventListener("click", () => { state.cars = state.cars.filter((x) => x.id !== c.id); state.sessions.forEach((s) => { s.assignments = (s.assignments || []).filter((a) => a.carId !== c.id); }); saveState(); renderView(); }); }); document.getElementById("carEditCancel")?.addEventListener("click", () => { setSelectedCarEditId(null); renderView(); }); document.getElementById("carEditCancelFooter")?.addEventListener("click", () => { setSelectedCarEditId(null); renderView(); }); document.getElementById("carEditModalOverlay")?.addEventListener("click", (event) => { if (event.target?.id === "carEditModalOverlay") { setSelectedCarEditId(null); renderView(); } }); bindModalShell("carEditModalOverlay", () => { setSelectedCarEditId(null); renderView(); }); const commitCarEdit = () => { if (!editingCar) { return; } const formNode = document.getElementById("carEditForm"); if (!(formNode instanceof HTMLFormElement)) { return; } const form = new FormData(formNode); const cleanedName = String(form.get("name") || "").trim(); const cleanedBrand = String(form.get("brand") || "").trim(); const cleanedTp = String(form.get("transponder") || "").trim(); const cleanedPassSound = String(form.get("passSound") || "").trim(); if (!cleanedName) { setFormError("carEditError", t("validation.required_name")); return; } if (!cleanedTp) { setFormError("carEditError", t("validation.required_transponder")); return; } setFormError("carEditError", ""); state.cars = state.cars.map((item) => item.id === editingCar.id ? normalizeCar({ ...item, name: cleanedName, brand: cleanedBrand, transponder: cleanedTp, passSound: cleanedPassSound, }) : item ); setSelectedCarEditId(null); saveState(); renderView(); }; document.getElementById("carEditForm")?.addEventListener("submit", (event) => { event.preventDefault(); commitCarEdit(); }); document.getElementById("carEditSave")?.addEventListener("click", commitCarEdit); }