Extract core dashboard and admin views into module
This commit is contained in:
628
src/app.js
628
src/app.js
@@ -8,6 +8,8 @@ import { normalizeRaceTeam as normalizeRaceTeamLogic, normalizeStoredRacePreset
|
||||
|
||||
import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getStatusLabelLogic, isUntimedSession as isUntimedSessionLogic, getActiveSession as getActiveSessionLogic, getSessionTargetMs as getSessionTargetMsLogic, getSessionLapWindow as getSessionLapWindowLogic, isCountedPassing as isCountedPassingLogic, getVisiblePassings as getVisiblePassingsLogic, getPassingValidationLabel as getPassingValidationLabelLogic, getSessionTiming as getSessionTimingLogic, ensureSessionResult as ensureSessionResultLogic, buildLeaderboard as buildLeaderboardLogic, formatLapDelta as formatLapDeltaLogic, formatLeaderboardGap as formatLeaderboardGapLogic, getCompetitorElapsedMs as getCompetitorElapsedMsLogic, getCompetitorPassings as getCompetitorPassingsLogic, getCompetitorSeedMetric as getCompetitorSeedMetricLogic, getSessionEntrants as getSessionEntrantsLogic, buildPracticeStandings as buildPracticeStandingsLogic, getQualifyingPointsValue as getQualifyingPointsValueLogic, isHighPointsTable as isHighPointsTableLogic, compareNumberSet as compareNumberSetLogic, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, hasQualifyingPrimaryTie as hasQualifyingPrimaryTieLogic, buildQualifyingStandings as buildQualifyingStandingsLogic, formatTeamActiveMemberLabel as formatTeamActiveMemberLabelLogic, buildTeamRaceStandings as buildTeamRaceStandingsLogic, buildTeamStintLog as buildTeamStintLogLogic, getSessionGridEntries as getSessionGridEntriesLogic, getSessionGridOrder as getSessionGridOrderLogic, ensureSessionDriverOrder as ensureSessionDriverOrderLogic, buildFinalStandings as buildFinalStandingsLogic } from "./timing_logic.js";
|
||||
|
||||
import { renderDashboardView, renderClassesView, renderDriversView, renderCarsView } from "./core_views.js";
|
||||
|
||||
import { renderTeamStintLog as renderTeamStintLogHelper, renderTeamRaceStandings as renderTeamRaceStandingsHelper, getSessionSortWeight as getSessionSortWeightHelper, getDriverDisplayById as getDriverDisplayByIdHelper, renderPositionGrid as renderPositionGridHelper, renderGridEditor as renderGridEditorHelper, getFinalMainLayouts as getFinalMainLayoutsHelper, renderFinalMatrix as renderFinalMatrixHelper, buildPrintBrandBlock as buildPrintBrandBlockHelper, buildRaceStartListsHtml as buildRaceStartListsHtmlHelper, buildRaceResultsHtml as buildRaceResultsHtmlHelper, buildTeamRaceResultsHtml as buildTeamRaceResultsHtmlHelper } from "./race_render_helpers.js";
|
||||
|
||||
import { getManualCorrectionSummary as getManualCorrectionSummaryLogic, applyCompetitorCorrection as applyCompetitorCorrectionLogic, recalculateCompetitorFromPassings as recalculateCompetitorFromPassingsLogic, invalidateCompetitorLastLap as invalidateCompetitorLastLapLogic, restoreCompetitorLastInvalidLap as restoreCompetitorLastInvalidLapLogic, findPassingByUndoMarker as findPassingByUndoMarkerLogic, undoJudgingAdjustment as undoJudgingAdjustmentLogic, getJudgeFilteredRows as getJudgeFilteredRowsLogic, getJudgeFilteredLog as getJudgeFilteredLogLogic } from "./judging_logic.js";
|
||||
@@ -94,6 +96,10 @@ const buildPrintBrandBlock = (branding) => buildPrintBrandBlockHelper(branding,
|
||||
const buildRaceStartListsHtml = (event) => buildRaceStartListsHtmlHelper(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable });
|
||||
const buildRaceResultsHtml = (event) => buildRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings });
|
||||
const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog });
|
||||
const renderDashboard = () => renderDashboardView({ state, dom, t, backend, getActiveSession, getStatusLabel, getSessionTypeLabel, getEventName, getModeLabel, getBackendUrl, formatLap, renderSessionsTable, setCurrentView: (view) => { currentView = view; }, renderNav, renderView, connectDecoder, disconnectDecoder, openOverlayWindow, ensureAudioContext, playPassingBeep, playFinishSiren, escapeHtml });
|
||||
const renderClasses = () => renderClassesView({ state, dom, t, selectedClassEditId: () => selectedClassEditId, setSelectedClassEditId: (value) => { selectedClassEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell });
|
||||
const renderDrivers = () => renderDriversView({ state, dom, t, driverBrandFilter: () => driverBrandFilter, setDriverBrandFilter: (value) => { driverBrandFilter = value; }, selectedDriverEditId: () => selectedDriverEditId, setSelectedDriverEditId: (value) => { selectedDriverEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeDriver, getClassName });
|
||||
const renderCars = () => renderCarsView({ state, dom, t, carBrandFilter: () => carBrandFilter, setCarBrandFilter: (value) => { carBrandFilter = value; }, selectedCarEditId: () => selectedCarEditId, setSelectedCarEditId: (value) => { selectedCarEditId = value; }, uid, saveState, renderView, renderTable, escapeHtml, setFormError, bindModalShell, normalizeCar });
|
||||
const applyCompetitorCorrection = (session, row, options = {}) => applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
|
||||
const recalculateCompetitorFromPassings = (session, rowKey) => recalculateCompetitorFromPassingsLogic(session, rowKey, { ensureSessionResult, getCompetitorPassings, isCountedPassing });
|
||||
const invalidateCompetitorLastLap = (session, row) => invalidateCompetitorLastLapLogic(session, row, { ensureSessionResult, getCompetitorPassings, isCountedPassing, recalculateCompetitorFromPassings, uid, t, formatLap, saveState });
|
||||
@@ -3181,628 +3187,6 @@ function handleSessionTimerTick() {
|
||||
return { changed: true };
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
const active = getActiveSession();
|
||||
const schedule = getScheduleDriftSummary();
|
||||
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 = `
|
||||
<div class="grid cols-4">
|
||||
${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"))}
|
||||
</div>
|
||||
|
||||
<div class="panel-row">
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>${t("dashboard.live_session")}</h3>
|
||||
<span class="pill ${active ? "pill-green" : ""}">${active ? getStatusLabel(active.status) : t("dashboard.idle")}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${
|
||||
active
|
||||
? `<p><strong>${active.name}</strong> (${getSessionTypeLabel(active.type)})</p>
|
||||
<p>${getEventName(active.eventId)} • ${getModeLabel(active.mode)}</p>
|
||||
<p>${t("dashboard.duration")}: ${active.durationMin} min</p>`
|
||||
: `<p>${t("dashboard.no_session")}</p>`
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>${t("dashboard.live_board")}</h3>
|
||||
</div>
|
||||
<div class="panel-body dashboard-live-stack">
|
||||
<p class="hint">${t("dashboard.live_note")}</p>
|
||||
<div class="dashboard-live-grid">
|
||||
<article class="dashboard-live-card">
|
||||
<span>${t("dashboard.decoder_feed")}</span>
|
||||
<strong>${state.decoder.connected ? t("timing.connected") : t("timing.disconnected")}</strong>
|
||||
<small>${escapeHtml(decoderUrl)}</small>
|
||||
</article>
|
||||
<article class="dashboard-live-card">
|
||||
<span>${t("dashboard.backend_link")}</span>
|
||||
<strong>${backend.available ? t("settings.online") : t("settings.offline")}</strong>
|
||||
<small>${escapeHtml(backendUrl)}</small>
|
||||
</article>
|
||||
<article class="dashboard-live-card">
|
||||
<span>${t("dashboard.audio_profile")}</span>
|
||||
<strong>${state.settings.audioEnabled ? audioProfile : t("settings.passing_sound_off")}</strong>
|
||||
<small>${state.settings.finishVoiceEnabled ? t("settings.finish_voice") : "-"}</small>
|
||||
</article>
|
||||
<article class="dashboard-live-card">
|
||||
<span>${t("dashboard.schedule_drift")}</span>
|
||||
<strong>${
|
||||
schedule
|
||||
? `${schedule.driftMs === 0 ? t("dashboard.on_time") : schedule.driftMs < 0 ? t("dashboard.ahead") : t("dashboard.behind")} ${formatLap(Math.abs(schedule.driftMs))}`
|
||||
: "-"
|
||||
}</strong>
|
||||
<small>${
|
||||
schedule
|
||||
? `${t("dashboard.schedule_plan")}: ${formatLap(schedule.plannedMs)} • ${t("dashboard.schedule_actual")}: ${formatLap(schedule.actualMs)}`
|
||||
: "-"
|
||||
}</small>
|
||||
</article>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="goEvents" class="btn btn-primary">${t("dashboard.create_event")}</button>
|
||||
<button id="goTiming" class="btn">${t("dashboard.open_timing")}</button>
|
||||
<button id="openDashboardOverlay" class="btn">${t("timing.open_overlay")}</button>
|
||||
<button id="connectNow" class="btn">${t("dashboard.connect_decoder")}</button>
|
||||
<button id="disconnectNow" class="btn">${t("timing.disconnect")}</button>
|
||||
<button id="dashboardTestAudio" class="btn">${t("settings.test_audio")}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h3>${t("dashboard.recent_sessions")}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${renderSessionsTable(state.sessions.slice(-8).reverse())}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
document.getElementById("goEvents")?.addEventListener("click", () => {
|
||||
currentView = "events";
|
||||
renderNav();
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("goTiming")?.addEventListener("click", () => {
|
||||
currentView = "timing";
|
||||
renderNav();
|
||||
renderView();
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getScheduleDriftSummary() {
|
||||
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 `
|
||||
<article class="stat-card">
|
||||
<p>${label}</p>
|
||||
<h3>${value}</h3>
|
||||
<small>${note}</small>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderClasses() {
|
||||
const editingClass = state.classes.find((item) => item.id === selectedClassEditId) || null;
|
||||
dom.view.innerHTML = `
|
||||
<div class="panel-row">
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("classes.create")}</h3></div>
|
||||
<form id="classForm" class="form-row panel-body">
|
||||
<input required name="name" placeholder="${t("classes.placeholder")}" />
|
||||
<button class="btn btn-primary" type="submit">${t("classes.add")}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("classes.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
${renderTable(
|
||||
[t("table.name"), t("events.actions")],
|
||||
state.classes.map(
|
||||
(c) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(c.name)}</td>
|
||||
<td class="actions-inline">
|
||||
<button id="class-edit-${c.id}" class="btn">${t("common.edit")}</button>
|
||||
<button id="class-delete-${c.id}" class="btn btn-danger">${t("common.delete")}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${
|
||||
editingClass
|
||||
? `
|
||||
<div class="modal-overlay" id="classEditModalOverlay">
|
||||
<div class="modal-card">
|
||||
<div class="panel-header">
|
||||
<h3>${t("common.edit")}</h3>
|
||||
<button class="btn" id="classEditCancel">${t("common.cancel")}</button>
|
||||
</div>
|
||||
<form id="classEditForm" class="panel-body form-grid cols-2">
|
||||
<input name="name" required value="${escapeHtml(editingClass.name)}" placeholder="${t("classes.placeholder")}" />
|
||||
<p class="form-error" id="classEditError" hidden></p>
|
||||
<div class="actions-inline">
|
||||
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
|
||||
<button class="btn" id="classEditCancelFooter" type="button">${t("common.cancel")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
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", () => {
|
||||
selectedClassEditId = 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", () => {
|
||||
selectedClassEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("classEditCancelFooter")?.addEventListener("click", () => {
|
||||
selectedClassEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("classEditModalOverlay")?.addEventListener("click", (event) => {
|
||||
if (event.target?.id === "classEditModalOverlay") {
|
||||
selectedClassEditId = null;
|
||||
renderView();
|
||||
}
|
||||
});
|
||||
|
||||
bindModalShell("classEditModalOverlay", () => {
|
||||
selectedClassEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("classEditForm")?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (!editingClass) {
|
||||
return;
|
||||
}
|
||||
const form = new FormData(event.currentTarget);
|
||||
const cleaned = String(form.get("name") || "").trim();
|
||||
if (!cleaned) {
|
||||
setFormError("classEditError", t("validation.required_name"));
|
||||
return;
|
||||
}
|
||||
setFormError("classEditError", "");
|
||||
editingClass.name = cleaned;
|
||||
selectedClassEditId = null;
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
}
|
||||
|
||||
function renderDrivers() {
|
||||
const classOptions = state.classes
|
||||
.map((c) => `<option value="${c.id}">${escapeHtml(c.name)}</option>`)
|
||||
.join("");
|
||||
const driverSearch = driverBrandFilter.trim().toLowerCase();
|
||||
const filteredDrivers = state.drivers.filter((driver) =>
|
||||
!driverSearch ||
|
||||
[driver.name, driver.transponder, driver.brand]
|
||||
.map((value) => String(value || "").toLowerCase())
|
||||
.some((value) => value.includes(driverSearch))
|
||||
);
|
||||
const editingDriver = state.drivers.find((driver) => driver.id === selectedDriverEditId) || null;
|
||||
|
||||
dom.view.innerHTML = `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("drivers.create")}</h3></div>
|
||||
<form id="driverForm" class="panel-body form-grid cols-5">
|
||||
<input required name="name" placeholder="${t("drivers.name_placeholder")}" />
|
||||
<select name="classId">${classOptions}</select>
|
||||
<input name="brand" placeholder="${t("drivers.brand_placeholder")}" />
|
||||
<input name="transponder" placeholder="${t("drivers.transponder_placeholder")}" />
|
||||
<button class="btn btn-primary" type="submit">${t("drivers.add")}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("drivers.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<input id="driverBrandFilter" value="${escapeHtml(driverBrandFilter)}" placeholder="${t("drivers.brand_filter_placeholder")}" />
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${renderTable(
|
||||
[t("table.name"), t("table.class"), t("table.brand"), t("table.transponder"), t("events.actions")],
|
||||
filteredDrivers.map(
|
||||
(d) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(d.name)}</td>
|
||||
<td>${escapeHtml(getClassName(d.classId))}</td>
|
||||
<td>${escapeHtml(d.brand || "-")}</td>
|
||||
<td>${escapeHtml(d.transponder || "-")}</td>
|
||||
<td class="actions-inline">
|
||||
<button id="driver-edit-${d.id}" class="btn">${t("common.edit")}</button>
|
||||
<button id="driver-delete-${d.id}" class="btn btn-danger">${t("common.delete")}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${
|
||||
editingDriver
|
||||
? `
|
||||
<div class="modal-overlay" id="driverEditModalOverlay">
|
||||
<div class="modal-card">
|
||||
<div class="panel-header">
|
||||
<h3>${t("common.edit")}</h3>
|
||||
<button class="btn" id="driverEditCancel">${t("common.cancel")}</button>
|
||||
</div>
|
||||
<form id="driverEditForm" class="panel-body form-grid cols-4">
|
||||
<input name="name" required value="${escapeHtml(editingDriver.name)}" placeholder="${t("drivers.name_placeholder")}" />
|
||||
<select name="classId">
|
||||
${state.classes
|
||||
.map(
|
||||
(item) =>
|
||||
`<option value="${item.id}" ${item.id === editingDriver.classId ? "selected" : ""}>${escapeHtml(item.name)}</option>`
|
||||
)
|
||||
.join("")}
|
||||
</select>
|
||||
<input name="brand" value="${escapeHtml(editingDriver.brand || "")}" placeholder="${t("drivers.brand_placeholder")}" />
|
||||
<input name="transponder" value="${escapeHtml(editingDriver.transponder || "")}" placeholder="${t("drivers.transponder_placeholder")}" />
|
||||
<p class="form-error" id="driverEditError" hidden></p>
|
||||
<div class="actions-inline">
|
||||
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
|
||||
<button class="btn" id="driverEditCancelFooter" type="button">${t("common.cancel")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
document.getElementById("driverForm")?.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
const form = new FormData(e.currentTarget);
|
||||
state.drivers.push(
|
||||
normalizeDriver({
|
||||
id: uid("driver"),
|
||||
name: String(form.get("name")).trim(),
|
||||
classId: String(form.get("classId")),
|
||||
brand: String(form.get("brand") || "").trim(),
|
||||
transponder: String(form.get("transponder") || "").trim(),
|
||||
})
|
||||
);
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("driverBrandFilter")?.addEventListener("input", (event) => {
|
||||
const input = event.currentTarget;
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
driverBrandFilter = input.value;
|
||||
renderDrivers();
|
||||
});
|
||||
|
||||
state.drivers.forEach((d) => {
|
||||
document.getElementById(`driver-edit-${d.id}`)?.addEventListener("click", () => {
|
||||
selectedDriverEditId = 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", () => {
|
||||
selectedDriverEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("driverEditCancelFooter")?.addEventListener("click", () => {
|
||||
selectedDriverEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("driverEditModalOverlay")?.addEventListener("click", (event) => {
|
||||
if (event.target?.id === "driverEditModalOverlay") {
|
||||
selectedDriverEditId = null;
|
||||
renderView();
|
||||
}
|
||||
});
|
||||
|
||||
bindModalShell("driverEditModalOverlay", () => {
|
||||
selectedDriverEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("driverEditForm")?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (!editingDriver) {
|
||||
return;
|
||||
}
|
||||
const form = new FormData(event.currentTarget);
|
||||
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();
|
||||
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", "");
|
||||
editingDriver.name = cleanedName;
|
||||
editingDriver.classId = cleanedClassId || editingDriver.classId;
|
||||
editingDriver.brand = cleanedBrand;
|
||||
editingDriver.transponder = cleanedTp;
|
||||
selectedDriverEditId = null;
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
}
|
||||
|
||||
function renderCars() {
|
||||
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;
|
||||
dom.view.innerHTML = `
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("cars.create")}</h3></div>
|
||||
<form id="carForm" class="panel-body form-grid cols-4">
|
||||
<input required name="name" placeholder="${t("cars.name_placeholder")}" />
|
||||
<input name="brand" placeholder="${t("cars.brand_placeholder")}" />
|
||||
<input required name="transponder" placeholder="${t("cars.transponder_placeholder")}" />
|
||||
<button class="btn btn-primary" type="submit">${t("cars.add")}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header"><h3>${t("cars.title")}</h3></div>
|
||||
<div class="panel-body">
|
||||
<input id="carBrandFilter" value="${escapeHtml(carBrandFilter)}" placeholder="${t("cars.brand_filter_placeholder")}" />
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
${renderTable(
|
||||
[t("table.car"), t("table.brand"), t("table.transponder"), t("events.actions")],
|
||||
filteredCars.map(
|
||||
(c) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(c.name)}</td>
|
||||
<td>${escapeHtml(c.brand || "-")}</td>
|
||||
<td>${escapeHtml(c.transponder)}</td>
|
||||
<td class="actions-inline">
|
||||
<button id="car-edit-${c.id}" class="btn">${t("common.edit")}</button>
|
||||
<button id="car-delete-${c.id}" class="btn btn-danger">${t("common.delete")}</button>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${
|
||||
editingCar
|
||||
? `
|
||||
<div class="modal-overlay" id="carEditModalOverlay">
|
||||
<div class="modal-card">
|
||||
<div class="panel-header">
|
||||
<h3>${t("common.edit")}</h3>
|
||||
<button class="btn" id="carEditCancel">${t("common.cancel")}</button>
|
||||
</div>
|
||||
<form id="carEditForm" class="panel-body form-grid cols-4">
|
||||
<input name="name" required value="${escapeHtml(editingCar.name)}" placeholder="${t("cars.name_placeholder")}" />
|
||||
<input name="brand" value="${escapeHtml(editingCar.brand || "")}" placeholder="${t("cars.brand_placeholder")}" />
|
||||
<input
|
||||
name="transponder"
|
||||
required
|
||||
value="${escapeHtml(editingCar.transponder || "")}"
|
||||
placeholder="${t("cars.transponder_placeholder")}"
|
||||
/>
|
||||
<p class="form-error" id="carEditError" hidden></p>
|
||||
<div class="actions-inline">
|
||||
<button class="btn btn-primary" type="submit">${t("common.save")}</button>
|
||||
<button class="btn" id="carEditCancelFooter" type="button">${t("common.cancel")}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
|
||||
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(),
|
||||
})
|
||||
);
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("carBrandFilter")?.addEventListener("input", (event) => {
|
||||
const input = event.currentTarget;
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
carBrandFilter = input.value;
|
||||
renderCars();
|
||||
});
|
||||
|
||||
state.cars.forEach((c) => {
|
||||
document.getElementById(`car-edit-${c.id}`)?.addEventListener("click", () => {
|
||||
selectedCarEditId = 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", () => {
|
||||
selectedCarEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("carEditCancelFooter")?.addEventListener("click", () => {
|
||||
selectedCarEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("carEditModalOverlay")?.addEventListener("click", (event) => {
|
||||
if (event.target?.id === "carEditModalOverlay") {
|
||||
selectedCarEditId = null;
|
||||
renderView();
|
||||
}
|
||||
});
|
||||
|
||||
bindModalShell("carEditModalOverlay", () => {
|
||||
selectedCarEditId = null;
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("carEditForm")?.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
if (!editingCar) {
|
||||
return;
|
||||
}
|
||||
const form = new FormData(event.currentTarget);
|
||||
const cleanedName = String(form.get("name") || "").trim();
|
||||
const cleanedBrand = String(form.get("brand") || "").trim();
|
||||
const cleanedTp = String(form.get("transponder") || "").trim();
|
||||
if (!cleanedName) {
|
||||
setFormError("carEditError", t("validation.required_name"));
|
||||
return;
|
||||
}
|
||||
if (!cleanedTp) {
|
||||
setFormError("carEditError", t("validation.required_transponder"));
|
||||
return;
|
||||
}
|
||||
setFormError("carEditError", "");
|
||||
editingCar.name = cleanedName;
|
||||
editingCar.brand = cleanedBrand;
|
||||
editingCar.transponder = cleanedTp;
|
||||
selectedCarEditId = null;
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
}
|
||||
|
||||
function renderEvents() {
|
||||
renderEventWorkspace("track");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user