Add class-specific driver training profiles and metadata
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user