755 lines
28 KiB
JavaScript
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);
|
|
}
|