fix tränings statse sidan

This commit is contained in:
larssand
2026-08-28 14:12:09 +02:00
parent 34e55ff7f6
commit 0a05f04075
6 changed files with 186 additions and 10 deletions

View File

@@ -335,6 +335,15 @@ const TRANSLATIONS = {
"ui.decoder_offline": "Decoder offline",
"training.eyebrow": "Förarutveckling",
"training.intro": "Följ träning över flera sessioner och gör utvecklingen synlig mellan tävlingarna.",
"training.overview_title": "Träningsöversikt",
"training.overview_intro": "Se klubbens registrerade träning per förare och klass. Välj en förare för personliga mål, rekommendationer och detaljerad statistik.",
"training.select_driver": "Välj förare...",
"training.back_overview": "Till översikten",
"training.registered_drivers": "Registrerade förare",
"training.active_drivers": "Förare med träning",
"training.profile_count_note": "Unika förarprofiler",
"training.with_training_data": "Med giltiga träningsvarv",
"training.overview_class_hint": "Varje klass visas separat så att exempelvis 2WD och 4WD aldrig blandas i varvtider eller statistik.",
"training.driver": "Förare",
"training.class": "Klass",
"training.no_drivers_title": "Ingen förare registrerad",
@@ -1446,6 +1455,15 @@ const TRANSLATIONS = {
"ui.decoder_offline": "Decoder Offline",
"training.eyebrow": "Driver development",
"training.intro": "Track practice across sessions and make progress visible between race days.",
"training.overview_title": "Practice overview",
"training.overview_intro": "Review recorded club practice by driver and class. Select a driver for personal goals, recommendations, and detailed statistics.",
"training.select_driver": "Select driver...",
"training.back_overview": "Back to overview",
"training.registered_drivers": "Registered drivers",
"training.active_drivers": "Drivers with practice",
"training.profile_count_note": "Unique driver profiles",
"training.with_training_data": "With valid practice laps",
"training.overview_class_hint": "Every class is listed separately so lap times and statistics from categories such as 2WD and 4WD are never combined.",
"training.driver": "Driver",
"training.class": "Class",
"training.no_drivers_title": "No driver registered",
@@ -3527,6 +3545,10 @@ function renderNav() {
button.className = `nav-item ${item.id === currentView ? "active" : ""}`;
button.textContent = t(item.titleKey);
button.addEventListener("click", () => {
if (item.id === "training") {
selectedTrainingDriverId = "";
selectedTrainingClassId = "";
}
currentView = item.id;
updateViewUrl(currentView);
renderNav();

View File

@@ -30,17 +30,17 @@ export function renderTrainingView(deps) {
const profiles = getDriverProfileGroups(state)
.sort((left, right) => String(left.representative?.name || "").localeCompare(String(right.representative?.name || "")));
let selectedProfileId = String(getSelectedDriverId() || "");
let selectedProfile = profiles.find((profile) => profile.id === selectedProfileId || profile.driverIds.includes(selectedProfileId));
if (!selectedProfile) {
selectedProfile = profiles[0] || null;
selectedProfileId = selectedProfile?.id || "";
setSelectedDriverId(selectedProfileId);
} else if (selectedProfileId !== selectedProfile.id) {
let selectedProfile = resolveTrainingProfile(profiles, selectedProfileId);
if (selectedProfileId && !selectedProfile) {
selectedProfileId = "";
setSelectedDriverId("");
setSelectedClassId("");
} else if (selectedProfile && selectedProfileId !== selectedProfile.id) {
selectedProfileId = selectedProfile.id;
setSelectedDriverId(selectedProfileId);
}
if (!selectedProfileId || !selectedProfile) {
if (!profiles.length) {
dom.view.innerHTML = `
<section class="panel training-empty">
<div class="panel-header"><h3>${escapeHtml(t("training.no_drivers_title"))}</h3></div>
@@ -50,6 +50,11 @@ export function renderTrainingView(deps) {
return;
}
if (!selectedProfileId || !selectedProfile) {
renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, profiles, buildDriverTrainingStats, getNextTrainingMilestone, setSelectedDriverId, setSelectedClassId, renderView });
return;
}
let selectedClassId = String(getSelectedClassId() || "");
if (!selectedProfile.classIds.includes(selectedClassId)) {
selectedClassId = selectedProfile.classIds[0] || "";
@@ -68,7 +73,7 @@ export function renderTrainingView(deps) {
));
const profileShare = (state.training.shares || []).find((share) => profileReferences.has(String(share.driverId || "")) && share.enabled);
const publicProfileUrl = profileShare ? buildPublicProfileUrl(profileShare.token, state.settings?.language) : "";
const driverOptions = profiles
const driverOptions = `<option value="">${escapeHtml(t("training.overview_title"))}</option>` + profiles
.map((profile) => {
const classNames = profile.classIds
.map((classId) => state.classes.find((item) => item.id === classId)?.name || "")
@@ -167,6 +172,7 @@ export function renderTrainingView(deps) {
<select id="training-class">${classOptions}</select>
</label>
<div class="actions training-hero-actions">
<button class="btn" id="trainingBackOverview" type="button">${escapeHtml(t("training.back_overview"))}</button>
${profileShare ? `
<button class="btn btn-primary" id="trainingHeroOpenProfile" type="button">${escapeHtml(t("training.start_personal"))}</button>
<button class="btn" id="trainingHeroCopyProfile" type="button">${escapeHtml(t("training.copy_mobile_link"))}</button>
@@ -349,6 +355,11 @@ export function renderTrainingView(deps) {
</div>
`;
document.getElementById("trainingBackOverview")?.addEventListener("click", () => {
setSelectedDriverId("");
setSelectedClassId("");
renderView();
});
document.getElementById("training-driver")?.addEventListener("change", (event) => {
setSelectedDriverId(event.currentTarget.value);
setSelectedClassId("");
@@ -440,6 +451,112 @@ export function renderTrainingView(deps) {
});
}
export function resolveTrainingProfile(profiles, selectedId) {
const reference = String(selectedId || "");
if (!reference) return null;
return (Array.isArray(profiles) ? profiles : []).find((profile) => (
profile.id === reference || profile.driverIds.includes(reference)
)) || null;
}
export function buildTrainingOverviewRows(state, profiles, buildDriverTrainingStats) {
return (Array.isArray(profiles) ? profiles : []).flatMap((profile) => {
const classIds = profile.classIds.length ? profile.classIds : [""];
return classIds.map((classId) => ({
profile,
driver: profile.representative,
classId,
className: state.classes.find((item) => String(item.id || "") === String(classId || ""))?.name || classId || "-",
stats: buildDriverTrainingStats(state, profile.id, classId),
}));
}).sort((left, right) => (
right.stats.totalLaps - left.stats.totalLaps
|| String(left.driver?.name || "").localeCompare(String(right.driver?.name || ""))
|| String(left.className).localeCompare(String(right.className))
));
}
function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, profiles, buildDriverTrainingStats, getNextTrainingMilestone, setSelectedDriverId, setSelectedClassId, renderView }) {
const rows = buildTrainingOverviewRows(state, profiles, buildDriverTrainingStats);
const ledger = Array.isArray(state.training?.ledger) ? state.training.ledger : [];
const activeProfiles = new Set(rows.filter((row) => row.stats.totalLaps > 0).map((row) => row.profile.id)).size;
const recordedSessions = new Set(ledger.map((entry) => String(entry.sessionId || "")).filter(Boolean)).size;
const driverOptions = profiles.map((profile) => (
`<option value="${escapeHtml(profile.id)}">${escapeHtml(profile.representative?.name || "-")}</option>`
)).join("");
const tableRows = rows.map((row, index) => {
const milestone = getNextTrainingMilestone(row.stats);
const lastActivity = row.stats.lastLapAt
? new Date(row.stats.lastLapAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
: "-";
return `
<tr>
<td>${index + 1}</td>
<td><button class="training-club-driver" data-driver-id="${escapeHtml(row.profile.id)}" data-class-id="${escapeHtml(row.classId)}" type="button">${escapeHtml(row.driver?.name || "-")}</button></td>
<td>${escapeHtml(row.className)}</td>
<td>${row.stats.totalLaps}</td>
<td>${row.stats.trainingDays}</td>
<td>${formatLap(row.stats.bestLapMs)}</td>
<td>${escapeHtml(lastActivity)}</td>
<td>${milestone.targetLaps}</td>
</tr>
`;
}).join("");
dom.view.innerHTML = `
<section class="training-hero training-overview-hero">
<div>
<span class="training-kicker">${escapeHtml(t("training.eyebrow"))}</span>
<h3>${escapeHtml(t("training.overview_title"))}</h3>
<p>${escapeHtml(t("training.overview_intro"))}</p>
</div>
<div class="training-hero-controls">
<label class="training-driver-select">
<span>${escapeHtml(t("training.driver"))}</span>
<select id="training-overview-driver">
<option value="">${escapeHtml(t("training.select_driver"))}</option>
${driverOptions}
</select>
</label>
</div>
</section>
<div class="grid cols-4 training-stats">
${trainingStat(t("training.total_laps"), String(ledger.length), t("training.valid_laps"))}
${trainingStat(t("training.registered_drivers"), String(profiles.length), t("training.profile_count_note"))}
${trainingStat(t("training.active_drivers"), String(activeProfiles), t("training.with_training_data"))}
${trainingStat(t("training.sessions"), String(recordedSessions), t("training.recorded_sessions"))}
</div>
<section class="panel mt-16">
<div class="panel-header"><h3>${escapeHtml(t("training.club_progress"))}</h3><span class="badge">${rows.length}</span></div>
<div class="panel-body">
<p class="hint">${escapeHtml(t("training.overview_class_hint"))}</p>
<div class="table-wrap training-history-wrap">
<table class="training-history-table training-club-table">
<thead><tr><th>#</th><th>${escapeHtml(t("training.driver"))}</th><th>${escapeHtml(t("training.class"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.training_days"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.last_activity"))}</th><th>${escapeHtml(t("training.next_target"))}</th></tr></thead>
<tbody>${tableRows}</tbody>
</table>
</div>
</div>
</section>
`;
document.getElementById("training-overview-driver")?.addEventListener("change", (event) => {
if (!event.currentTarget.value) return;
setSelectedDriverId(event.currentTarget.value);
setSelectedClassId("");
renderView();
});
document.querySelectorAll(".training-club-driver").forEach((button) => {
button.addEventListener("click", () => {
setSelectedDriverId(button.dataset.driverId || "");
setSelectedClassId(button.dataset.classId || "");
renderView();
});
});
}
function trainingStat(label, value, note) {
return `<article class="stat-card"><p>${label}</p><h3>${value}</h3><small>${note}</small></article>`;
}