Add class-specific driver training profiles and metadata

This commit is contained in:
larssand
2026-08-24 22:05:50 +02:00
parent 3aeb77a042
commit 6f02e955a5
14 changed files with 559 additions and 128 deletions

View File

@@ -1,3 +1,5 @@
import { getDriverProfileGroups, getDriverProfileId } from "./driver_profile_logic.js";
function getScheduleDriftSummary(state) {
const scheduledSessions = state.sessions
.filter((session) => Number(session.startedAt || 0) > 0)
@@ -373,9 +375,24 @@ export function renderDriversView(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))
!driverSearch || [driver.name, driver.speechName, driver.transponder, driver.brand, driver.club, driver.country].map((value) => String(value || "").toLowerCase()).some((value) => value.includes(driverSearch))
);
const editingDriver = state.drivers.find((driver) => driver.id === selectedDriverEditId()) || null;
const profileGroups = getDriverProfileGroups(state);
const profileOptions = profileGroups
.map((profile) => {
const profileName = profile.representative?.name || t("common.unknown_driver");
const classNames = profile.classIds.map((classId) => getClassName(classId)).filter(Boolean).join(" / ");
const label = classNames ? `${profileName} - ${classNames}` : profileName;
return `<option value="${escapeHtml(profile.id)}">${escapeHtml(label)}</option>`;
})
.join("");
const editingProfileId = editingDriver ? getDriverProfileId(editingDriver) : "";
const existingClubs = [...new Set(state.drivers.map((driver) => String(driver.club || "").trim()).filter(Boolean))].sort();
const existingCountries = [...new Set([
"Sverige", "Norge", "Finland", "Danmark", "Island", "Tyskland", "Nederländerna", "Belgien", "Frankrike", "Storbritannien", "Polen", "Tjeckien", "Österrike", "Schweiz",
...state.drivers.map((driver) => String(driver.country || "").trim()).filter(Boolean),
])].sort();
dom.view.innerHTML = `
<section class="panel">
@@ -386,6 +403,15 @@ export function renderDriversView(deps) {
<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")}" />
<input name="club" list="driverClubOptions" placeholder="${t("drivers.club_placeholder")}" />
<input name="country" list="driverCountryOptions" placeholder="${t("drivers.country_placeholder")}" />
<label>${t("drivers.training_profile")}<select name="profileId" title="${t("drivers.training_profile_hint")}">
<option value="">${t("drivers.training_profile_auto")}</option>
${profileOptions}
<option value="__new__">${t("drivers.training_profile_new")}</option>
</select></label>
<datalist id="driverClubOptions">${existingClubs.map((club) => `<option value="${escapeHtml(club)}"></option>`).join("")}</datalist>
<datalist id="driverCountryOptions">${existingCountries.map((country) => `<option value="${escapeHtml(country)}"></option>`).join("")}</datalist>
<button class="btn btn-primary" type="submit">${t("drivers.add")}</button>
</form>
</section>
@@ -397,21 +423,27 @@ export function renderDriversView(deps) {
</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")],
[t("table.name"), t("table.class"), t("drivers.club"), t("drivers.country"), t("table.brand"), t("table.transponder"), t("drivers.training_profile"), t("drivers.speech_name"), t("events.actions")],
filteredDrivers.map(
(d) => `
(d) => {
const profile = profileGroups.find((item) => item.id === getDriverProfileId(d));
return `
<tr>
<td>${escapeHtml(d.name)}</td>
<td>${escapeHtml(getClassName(d.classId))}</td>
<td>${escapeHtml(profile?.club || d.club || "-")}</td>
<td>${escapeHtml(profile?.country || d.country || "-")}</td>
<td>${escapeHtml(d.brand || "-")}</td>
<td>${escapeHtml(d.transponder || "-")}</td>
<td>${escapeHtml(profile?.representative?.name || d.name)}${profile?.drivers.length > 1 ? ` · ${profile.drivers.length}` : ""}</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>
@@ -436,6 +468,16 @@ export function renderDriversView(deps) {
<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")}" />
<input name="club" list="driverClubOptions" value="${escapeHtml(profileGroups.find((profile) => profile.id === editingProfileId)?.club || editingDriver.club || "")}" placeholder="${t("drivers.club_placeholder")}" />
<input name="country" list="driverCountryOptions" value="${escapeHtml(profileGroups.find((profile) => profile.id === editingProfileId)?.country || editingDriver.country || "")}" placeholder="${t("drivers.country_placeholder")}" />
<label>${t("drivers.training_profile")}<select name="profileId" title="${t("drivers.training_profile_hint")}">
${profileGroups.map((profile) => {
const profileName = profile.representative?.name || t("common.unknown_driver");
const classNames = profile.classIds.map((classId) => getClassName(classId)).filter(Boolean).join(" / ");
return `<option value="${escapeHtml(profile.id)}" ${profile.id === editingProfileId ? "selected" : ""}>${escapeHtml(classNames ? `${profileName} - ${classNames}` : profileName)}</option>`;
}).join("")}
<option value="__new__">${t("drivers.training_profile_separate")}</option>
</select></label>
<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>
@@ -452,14 +494,29 @@ export function renderDriversView(deps) {
document.getElementById("driverForm")?.addEventListener("submit", (e) => {
e.preventDefault();
const form = new FormData(e.currentTarget);
const driverId = uid("driver");
const driverName = String(form.get("name") || "").trim();
const requestedProfileId = String(form.get("profileId") || "").trim();
const sameNameProfile = profileGroups.find((profile) => (
String(profile.representative?.name || "").trim().toLocaleLowerCase().replace(/\s+/g, " ") === driverName.toLocaleLowerCase().replace(/\s+/g, " ")
));
const profileId = requestedProfileId && requestedProfileId !== "__new__"
? requestedProfileId
: requestedProfileId === "__new__"
? uid("profile")
: sameNameProfile?.id || uid("profile");
const linkedProfile = profileGroups.find((profile) => profile.id === profileId);
state.drivers.push(
normalizeDriver({
id: uid("driver"),
name: String(form.get("name")).trim(),
id: driverId,
name: driverName,
classId: String(form.get("classId")),
brand: String(form.get("brand") || "").trim(),
transponder: String(form.get("transponder") || "").trim(),
speechName: String(form.get("speechName") || "").trim(),
profileId,
club: String(form.get("club") || linkedProfile?.club || "").trim(),
country: String(form.get("country") || linkedProfile?.country || "").trim(),
})
);
saveState();
@@ -475,6 +532,21 @@ export function renderDriversView(deps) {
renderDriversView(deps);
});
const bindProfileMetadataFill = (formId) => {
const formNode = document.getElementById(formId);
const profileSelect = formNode?.querySelector('select[name="profileId"]');
profileSelect?.addEventListener("change", () => {
const profile = profileGroups.find((item) => item.id === profileSelect.value);
if (!profile) return;
const clubInput = formNode.querySelector('input[name="club"]');
const countryInput = formNode.querySelector('input[name="country"]');
if (clubInput instanceof HTMLInputElement) clubInput.value = profile.club || "";
if (countryInput instanceof HTMLInputElement) countryInput.value = profile.country || "";
});
};
bindProfileMetadataFill("driverForm");
bindProfileMetadataFill("driverEditForm");
state.drivers.forEach((d) => {
document.getElementById(`driver-edit-${d.id}`)?.addEventListener("click", () => {
setSelectedDriverEditId(d.id);
@@ -527,6 +599,10 @@ export function renderDriversView(deps) {
const cleanedBrand = String(form.get("brand") || "").trim();
const cleanedTp = String(form.get("transponder") || "").trim();
const cleanedSpeechName = String(form.get("speechName") || "").trim();
const requestedProfileId = String(form.get("profileId") || "").trim();
const cleanedProfileId = requestedProfileId === "__new__" ? uid("profile") : requestedProfileId || editingProfileId;
const cleanedClub = String(form.get("club") || "").trim();
const cleanedCountry = String(form.get("country") || "").trim();
if (!cleanedName) {
setFormError("driverEditError", t("validation.required_name"));
return;
@@ -536,18 +612,25 @@ export function renderDriversView(deps) {
return;
}
setFormError("driverEditError", "");
state.drivers = state.drivers.map((item) =>
item.id === editingDriver.id
? normalizeDriver({
state.drivers = state.drivers.map((item) => {
if (item.id === editingDriver.id) {
return normalizeDriver({
...item,
name: cleanedName,
classId: cleanedClassId || item.classId,
brand: cleanedBrand,
transponder: cleanedTp,
speechName: cleanedSpeechName,
})
: item
);
profileId: cleanedProfileId,
club: cleanedClub,
country: cleanedCountry,
});
}
if (getDriverProfileId(item) === cleanedProfileId) {
return normalizeDriver({ ...item, club: cleanedClub, country: cleanedCountry });
}
return item;
});
setSelectedDriverEditId(null);
saveState();
renderView();