fix tränings statse sidan
This commit is contained in:
22
src/app.js
22
src/app.js
@@ -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();
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user