Add persistent driver training history and progression
This commit is contained in:
@@ -9,14 +9,17 @@ export function renderTrainingView(deps) {
|
||||
saveState,
|
||||
renderView,
|
||||
normalizeTrainingState,
|
||||
syncTrainingLedger,
|
||||
buildDriverTrainingStats,
|
||||
buildTrainingRecommendation,
|
||||
getTrainingGoalProgress,
|
||||
getNextTrainingMilestone,
|
||||
getSelectedDriverId,
|
||||
setSelectedDriverId,
|
||||
} = 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)) {
|
||||
@@ -37,6 +40,7 @@ export function renderTrainingView(deps) {
|
||||
const selectedDriver = drivers.find((driver) => driver.id === selectedDriverId);
|
||||
const stats = buildDriverTrainingStats(state, selectedDriverId);
|
||||
const recommendation = buildTrainingRecommendation(stats);
|
||||
const nextMilestone = getNextTrainingMilestone(stats);
|
||||
const goals = state.training.goals.filter((goal) => goal.driverId === selectedDriverId);
|
||||
const driverOptions = drivers
|
||||
.map((driver) => `<option value="${escapeHtml(driver.id)}" ${driver.id === selectedDriverId ? "selected" : ""}>${escapeHtml(driver.name)}</option>`)
|
||||
@@ -76,6 +80,19 @@ export function renderTrainingView(deps) {
|
||||
const targetText = recommendation.targetLapMs
|
||||
? t("training.plan_target", { lap: formatLap(recommendation.targetLapMs) })
|
||||
: t("training.plan_no_target");
|
||||
const lastActivity = stats.lastLapAt
|
||||
? new Date(stats.lastLapAt).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
|
||||
: t("training.no_activity");
|
||||
const dailyRows = stats.dailySummaries.length
|
||||
? stats.dailySummaries.slice(0, 10).map((day) => `
|
||||
<tr>
|
||||
<td>${escapeHtml(new Date(day.latestTimestamp).toLocaleDateString(undefined, { dateStyle: "medium" }))}</td>
|
||||
<td>${day.laps}</td>
|
||||
<td>${formatLap(day.bestLapMs)}</td>
|
||||
<td>${formatLap(day.averageLapMs)}</td>
|
||||
</tr>
|
||||
`).join("")
|
||||
: `<tr><td colspan="4">${escapeHtml(t("training.no_history"))}</td></tr>`;
|
||||
|
||||
dom.view.innerHTML = `
|
||||
<section class="training-hero">
|
||||
@@ -92,11 +109,29 @@ export function renderTrainingView(deps) {
|
||||
|
||||
<div class="grid cols-4 training-stats">
|
||||
${trainingStat(t("training.total_laps"), String(stats.totalLaps), t("training.valid_laps"))}
|
||||
${trainingStat(t("training.sessions"), String(stats.totalSessions), t("training.recorded_sessions"))}
|
||||
${trainingStat(t("training.training_days"), String(stats.trainingDays), t("training.sessions_count", { sessions: stats.totalSessions }))}
|
||||
${trainingStat(t("training.best_lap"), formatLap(stats.bestLapMs), t("training.personal_best"))}
|
||||
${trainingStat(t("training.consistency"), stats.recentLapCount ? `${stats.consistencySpreadPct.toFixed(1)}%` : "-", t("training.last_30_laps"))}
|
||||
</div>
|
||||
|
||||
<div class="training-insight-grid">
|
||||
<section class="training-milestone-card">
|
||||
<div>
|
||||
<span class="training-kicker">${escapeHtml(t("training.next_level"))}</span>
|
||||
<h3>${escapeHtml(t("training.next_level_target", { laps: nextMilestone.targetLaps }))}</h3>
|
||||
<p>${escapeHtml(t("training.goal_remaining", { laps: nextMilestone.remainingLaps }))}</p>
|
||||
</div>
|
||||
<strong>${nextMilestone.currentLaps} / ${nextMilestone.targetLaps}</strong>
|
||||
<div class="training-progress"><span style="width:${nextMilestone.percent.toFixed(2)}%"></span></div>
|
||||
</section>
|
||||
<section class="training-insights">
|
||||
${trainingInsight(t("training.average_lap"), formatLap(stats.averageLapMs))}
|
||||
${trainingInsight(t("training.recent_average"), formatLap(stats.recentAverageMs))}
|
||||
${trainingInsight(t("training.controlled_streak"), t("training.lap_count", { laps: stats.controlledLapStreak }))}
|
||||
${trainingInsight(t("training.last_activity"), lastActivity)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="training-layout">
|
||||
<section class="panel training-plan">
|
||||
<div class="panel-header"><h3>${escapeHtml(t("training.recommendation"))}</h3><span class="badge">20 min</span></div>
|
||||
@@ -127,6 +162,19 @@ export function renderTrainingView(deps) {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header">
|
||||
<h3>${escapeHtml(t("training.history"))}</h3>
|
||||
${stats.personalBestGainMs ? `<span class="badge success">${escapeHtml(t("training.pb_improvement", { lap: formatLap(stats.personalBestGainMs) }))}</span>` : ""}
|
||||
</div>
|
||||
<div class="panel-body table-wrap training-history-wrap">
|
||||
<table class="training-history-table">
|
||||
<thead><tr><th>${escapeHtml(t("training.date"))}</th><th>${escapeHtml(t("training.laps"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.average_lap"))}</th></tr></thead>
|
||||
<tbody>${dailyRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${escapeHtml(t("training.goals"))}</h3><span class="badge">${goals.length}</span></div>
|
||||
<div class="panel-body training-goals">${goalMarkup}</div>
|
||||
@@ -179,3 +227,7 @@ export function renderTrainingView(deps) {
|
||||
function trainingStat(label, value, note) {
|
||||
return `<article class="stat-card"><p>${label}</p><h3>${value}</h3><small>${note}</small></article>`;
|
||||
}
|
||||
|
||||
function trainingInsight(label, value) {
|
||||
return `<div><span>${label}</span><strong>${value}</strong></div>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user