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, getDriverProfileReferences } from "./driver_profile_logic.js";
export function renderTrainingView(deps) {
const {
state,
@@ -18,18 +20,26 @@ export function renderTrainingView(deps) {
getNextTrainingMilestone,
getSelectedDriverId,
setSelectedDriverId,
getSelectedClassId,
setSelectedClassId,
} = deps;
state.training = normalizeTrainingState(state.training);
syncTrainingLedger();
const drivers = [...state.drivers].sort((left, right) => String(left.name).localeCompare(String(right.name)));
let selectedDriverId = String(getSelectedDriverId() || "");
if (!drivers.some((driver) => driver.id === selectedDriverId)) {
selectedDriverId = drivers[0]?.id || "";
setSelectedDriverId(selectedDriverId);
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) {
selectedProfileId = selectedProfile.id;
setSelectedDriverId(selectedProfileId);
}
if (!selectedDriverId) {
if (!selectedProfileId || !selectedProfile) {
dom.view.innerHTML = `
<section class="panel training-empty">
<div class="panel-header"><h3>${escapeHtml(t("training.no_drivers_title"))}</h3></div>
@@ -39,16 +49,38 @@ export function renderTrainingView(deps) {
return;
}
const selectedDriver = drivers.find((driver) => driver.id === selectedDriverId);
const stats = buildDriverTrainingStats(state, selectedDriverId);
const activePractice = buildDriverActivePracticeStats(state, selectedDriverId);
let selectedClassId = String(getSelectedClassId() || "");
if (!selectedProfile.classIds.includes(selectedClassId)) {
selectedClassId = selectedProfile.classIds[0] || "";
setSelectedClassId(selectedClassId);
}
const selectedDriver = selectedProfile.representative;
const profileReferences = getDriverProfileReferences(state, selectedProfileId);
const stats = buildDriverTrainingStats(state, selectedProfileId, selectedClassId);
const activePractice = buildDriverActivePracticeStats(state, selectedProfileId, Date.now(), selectedClassId);
const recommendation = buildTrainingRecommendation(stats);
const nextMilestone = getNextTrainingMilestone(stats);
const goals = state.training.goals.filter((goal) => goal.driverId === selectedDriverId);
const profileShare = (state.training.shares || []).find((share) => share.driverId === selectedDriverId && share.enabled);
const goals = state.training.goals.filter((goal) => (
profileReferences.has(String(goal.driverId || ""))
&& (!goal.classId || String(goal.classId) === selectedClassId)
));
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 = drivers
.map((driver) => `<option value="${escapeHtml(driver.id)}" ${driver.id === selectedDriverId ? "selected" : ""}>${escapeHtml(driver.name)}</option>`)
const driverOptions = profiles
.map((profile) => {
const classNames = profile.classIds
.map((classId) => state.classes.find((item) => item.id === classId)?.name || "")
.filter(Boolean)
.join(" / ");
const label = classNames ? `${profile.representative?.name || "-"} - ${classNames}` : profile.representative?.name || "-";
return `<option value="${escapeHtml(profile.id)}" ${profile.id === selectedProfileId ? "selected" : ""}>${escapeHtml(label)}</option>`;
})
.join("");
const classOptions = selectedProfile.classIds
.map((classId) => {
const className = state.classes.find((item) => item.id === classId)?.name || classId;
return `<option value="${escapeHtml(classId)}" ${classId === selectedClassId ? "selected" : ""}>${escapeHtml(className)}</option>`;
})
.join("");
const goalMarkup = goals.length
@@ -98,15 +130,16 @@ export function renderTrainingView(deps) {
</tr>
`).join("")
: `<tr><td colspan="4">${escapeHtml(t("training.no_history"))}</td></tr>`;
const clubRows = drivers
.map((driver) => ({ driver, stats: driver.id === selectedDriverId ? stats : buildDriverTrainingStats(state, driver.id) }))
.sort((left, right) => right.stats.totalLaps - left.stats.totalLaps || String(left.driver.name).localeCompare(String(right.driver.name)))
const clubRows = profiles
.filter((profile) => !selectedClassId || profile.classIds.includes(selectedClassId))
.map((profile) => ({ profile, driver: profile.representative, stats: profile.id === selectedProfileId ? stats : buildDriverTrainingStats(state, profile.id, selectedClassId) }))
.sort((left, right) => right.stats.totalLaps - left.stats.totalLaps || String(left.driver?.name || "").localeCompare(String(right.driver?.name || "")))
.map((entry, index) => {
const milestone = getNextTrainingMilestone(entry.stats);
return `
<tr class="${entry.driver.id === selectedDriverId ? "is-selected" : ""}">
<tr class="${entry.profile.id === selectedProfileId ? "is-selected" : ""}">
<td>${index + 1}</td>
<td><button class="training-club-driver" data-driver-id="${escapeHtml(entry.driver.id)}" type="button">${escapeHtml(entry.driver.name)}</button></td>
<td><button class="training-club-driver" data-driver-id="${escapeHtml(entry.profile.id)}" data-class-id="${escapeHtml(selectedClassId)}" type="button">${escapeHtml(entry.driver?.name || "-")}</button></td>
<td>${entry.stats.totalLaps}</td>
<td>${entry.stats.trainingDays}</td>
<td>${formatLap(entry.stats.bestLapMs)}</td>
@@ -127,6 +160,10 @@ export function renderTrainingView(deps) {
<span>${escapeHtml(t("training.driver"))}</span>
<select id="training-driver">${driverOptions}</select>
</label>
<label class="training-driver-select">
<span>${escapeHtml(t("training.class"))}</span>
<select id="training-class">${classOptions}</select>
</label>
<div class="actions training-hero-actions">
${profileShare ? `
<button class="btn btn-primary" id="trainingHeroOpenProfile" type="button">${escapeHtml(t("training.start_personal"))}</button>
@@ -288,11 +325,16 @@ export function renderTrainingView(deps) {
document.getElementById("training-driver")?.addEventListener("change", (event) => {
setSelectedDriverId(event.currentTarget.value);
setSelectedClassId("");
renderView();
});
document.getElementById("training-class")?.addEventListener("change", (event) => {
setSelectedClassId(event.currentTarget.value);
renderView();
});
document.getElementById("trainingHeroEnableProfile")?.addEventListener("click", () => {
replacePublicProfileShare(state, selectedDriverId);
replacePublicProfileShare(state, selectedProfileId, profileReferences);
saveState();
renderView();
});
@@ -316,7 +358,8 @@ export function renderTrainingView(deps) {
const targetLaps = Math.max(1, Math.round(Number(form.get("targetLaps") || 0) || 0));
state.training.goals.push({
id: uid("goal"),
driverId: selectedDriverId,
driverId: selectedProfileId,
classId: selectedClassId,
targetLaps,
title: String(form.get("title") || "").trim(),
reward: String(form.get("reward") || "").trim(),
@@ -341,7 +384,7 @@ export function renderTrainingView(deps) {
});
document.getElementById("trainingPublicProfileEnable")?.addEventListener("click", () => {
replacePublicProfileShare(state, selectedDriverId);
replacePublicProfileShare(state, selectedProfileId, profileReferences);
saveState();
renderView();
});
@@ -353,18 +396,19 @@ export function renderTrainingView(deps) {
});
document.getElementById("trainingPublicProfileRotate")?.addEventListener("click", () => {
if (!confirm(t("training.replace_link_confirm"))) return;
replacePublicProfileShare(state, selectedDriverId);
replacePublicProfileShare(state, selectedProfileId, profileReferences);
saveState();
renderView();
});
document.getElementById("trainingPublicProfileDisable")?.addEventListener("click", () => {
state.training.shares = state.training.shares.map((share) => share.driverId === selectedDriverId ? { ...share, enabled: false } : share);
state.training.shares = state.training.shares.map((share) => profileReferences.has(String(share.driverId || "")) ? { ...share, enabled: false } : share);
saveState();
renderView();
});
document.querySelectorAll(".training-club-driver").forEach((button) => {
button.addEventListener("click", () => {
setSelectedDriverId(button.dataset.driverId || "");
setSelectedClassId(button.dataset.classId || "");
renderView();
});
});
@@ -390,14 +434,14 @@ function generateShareToken() {
return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
}
function replacePublicProfileShare(state, driverId) {
function replacePublicProfileShare(state, driverId, profileReferences = new Set([driverId])) {
const nextShare = {
driverId,
token: generateShareToken(),
enabled: true,
createdAt: new Date().toISOString(),
};
state.training.shares = [...state.training.shares.filter((share) => share.driverId !== driverId), nextShare];
state.training.shares = [...state.training.shares.filter((share) => !profileReferences.has(String(share.driverId || ""))), nextShare];
}
async function copyText(value) {