Files
Live_RC/src/core_views.js
2026-08-24 19:40:39 +02:00

755 lines
28 KiB
JavaScript

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 `
<article class="stat-card">
<p>${label}</p>
<h3>${value}</h3>
<small>${note}</small>
</article>
`;
}
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 = `
<section class="panel dashboard-workflow">
<div class="panel-header">
<div>
<h3>${t("dashboard.get_started")}</h3>
<p class="hint">${t("dashboard.get_started_hint")}</p>
</div>
</div>
<div class="panel-body dashboard-workflow-grid">
<article class="dashboard-workflow-card">
<span class="workflow-number">1</span>
<div><strong>${t("dashboard.step_system")}</strong><p>${t("dashboard.step_system_hint")}</p></div>
<button id="workflowSettings" class="btn" type="button">${t("dashboard.open_settings")}</button>
</article>
<article class="dashboard-workflow-card">
<span class="workflow-number">2</span>
<div><strong>${t("dashboard.step_register")}</strong><p>${t("dashboard.step_register_hint")}</p></div>
<button id="workflowDrivers" class="btn" type="button">${t("dashboard.open_register")}</button>
</article>
<article class="dashboard-workflow-card dashboard-workflow-card-primary">
<span class="workflow-number">3</span>
<div><strong>${t("dashboard.step_create")}</strong><p>${t("dashboard.step_create_hint")}</p></div>
<button id="workflowRaceSetup" class="btn btn-primary" type="button">${t("dashboard.open_race_setup")}</button>
</article>
<article class="dashboard-workflow-card">
<span class="workflow-number">4</span>
<div><strong>${t("dashboard.step_run")}</strong><p>${t("dashboard.step_run_hint")}</p></div>
<div class="actions-inline">
<button id="workflowTiming" class="btn" type="button">${t("dashboard.open_timing")}</button>
<button id="workflowOverlay" class="btn" type="button">${t("dashboard.open_overlay")}</button>
</div>
</article>
</div>
</section>
<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.type === "open_practice" ? t("dashboard.untimed") : `${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>
`;
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("workflowTiming")?.addEventListener("click", () => navigateTo("timing"));
document.getElementById("workflowOverlay")?.addEventListener("click", () => navigateTo("overlay"));
document.getElementById("goEvents")?.addEventListener("click", () => navigateTo("events"));
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 = `
<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" type="button">${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" id="classEditSave" type="button">${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", () => {
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) => `<option value="${c.id}">${escapeHtml(c.name)}</option>`).join("");
const driverSearch = driverBrandFilter().trim().toLowerCase();
const filteredDrivers = state.drivers.filter((driver) =>
!driverSearch || [driver.name, driver.speechName, 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")}" />
<input name="speechName" placeholder="${t("drivers.speech_name_placeholder")}" title="${t("drivers.speech_name_hint")}" />
<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("drivers.speech_name"), 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>${escapeHtml(d.speechName || d.name)}</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" type="button">${t("common.cancel")}</button>
</div>
<form id="driverEditForm" class="panel-body form-grid cols-5">
<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")}" />
<input name="speechName" value="${escapeHtml(editingDriver.speechName || "")}" placeholder="${t("drivers.speech_name_placeholder")}" title="${t("drivers.speech_name_hint")}" />
<p class="form-error" id="driverEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" id="driverEditSave" type="button">${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(),
speechName: String(form.get("speechName") || "").trim(),
})
);
saveState();
renderView();
});
document.getElementById("driverBrandFilter")?.addEventListener("input", (event) => {
const input = event.currentTarget;
if (!(input instanceof HTMLInputElement)) {
return;
}
setDriverBrandFilter(input.value);
renderDriversView(deps);
});
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();
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) =>
item.id === editingDriver.id
? normalizeDriver({
...item,
name: cleanedName,
classId: cleanedClassId || item.classId,
brand: cleanedBrand,
transponder: cleanedTp,
speechName: cleanedSpeechName,
})
: 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;
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" type="button">${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" id="carEditSave" type="button">${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;
}
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();
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,
})
: item
);
setSelectedCarEditId(null);
saveState();
renderView();
};
document.getElementById("carEditForm")?.addEventListener("submit", (event) => {
event.preventDefault();
commitCarEdit();
});
document.getElementById("carEditSave")?.addEventListener("click", commitCarEdit);
}