Extract core dashboard and admin views into module
This commit is contained in:
664
src/core_views.js
Normal file
664
src/core_views.js
Normal file
@@ -0,0 +1,664 @@
|
||||
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,
|
||||
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 = `
|
||||
<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", () => {
|
||||
setCurrentView("events");
|
||||
renderNav();
|
||||
renderView();
|
||||
});
|
||||
|
||||
document.getElementById("goTiming")?.addEventListener("click", () => {
|
||||
setCurrentView("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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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">${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", () => {
|
||||
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();
|
||||
});
|
||||
|
||||
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;
|
||||
setSelectedClassEditId(null);
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
}
|
||||
|
||||
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.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;
|
||||
}
|
||||
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();
|
||||
});
|
||||
|
||||
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;
|
||||
setSelectedDriverEditId(null);
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
}
|
||||
|
||||
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">${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;
|
||||
}
|
||||
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();
|
||||
});
|
||||
|
||||
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;
|
||||
setSelectedCarEditId(null);
|
||||
saveState();
|
||||
renderView();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user