förbättreing tränings
This commit is contained in:
@@ -137,15 +137,42 @@ export function renderTrainingView(deps) {
|
||||
</tr>
|
||||
`).join("")
|
||||
: `<tr><td colspan="4">${escapeHtml(t("training.no_history"))}</td></tr>`;
|
||||
const unlockedAchievements = (stats.achievements || []).filter((achievement) => achievement.unlocked);
|
||||
const nextAchievement = (stats.achievements || []).find((achievement) => !achievement.unlocked);
|
||||
const visibleAchievements = [...unlockedAchievements.slice(-5), ...(nextAchievement ? [nextAchievement] : [])];
|
||||
const achievementMarkup = visibleAchievements.length
|
||||
? visibleAchievements.map((achievement) => `
|
||||
<article class="training-achievement ${achievement.unlocked ? "is-unlocked" : "is-next"}">
|
||||
<span class="training-achievement-mark">${achievement.unlocked ? "OK" : `${Math.round(achievement.percent)}%`}</span>
|
||||
<div>
|
||||
<strong>${escapeHtml(t(`training.achievement_${achievement.id}`))}</strong>
|
||||
<small>${escapeHtml(t(achievement.unlocked ? "training.achievement_unlocked" : "training.achievement_next"))}</small>
|
||||
</div>
|
||||
</article>
|
||||
`).join("")
|
||||
: `<p class="hint">${escapeHtml(t("training.achievement_empty"))}</p>`;
|
||||
const paceTrend = !stats.trendSampleReady
|
||||
? t("training.trend_collecting")
|
||||
: stats.averageTrendMs > 0
|
||||
? t("training.trend_faster", { lap: formatLap(stats.averageTrendMs) })
|
||||
: stats.averageTrendMs < 0
|
||||
? t("training.trend_slower", { lap: formatLap(Math.abs(stats.averageTrendMs)) })
|
||||
: t("training.trend_stable");
|
||||
const consistencyTrend = !stats.trendSampleReady
|
||||
? t("training.trend_collecting_short")
|
||||
: stats.consistencyTrendPct > 0.1
|
||||
? t("training.consistency_better", { value: stats.consistencyTrendPct.toFixed(1) })
|
||||
: stats.consistencyTrendPct < -0.1
|
||||
? t("training.consistency_building")
|
||||
: t("training.trend_stable");
|
||||
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) => {
|
||||
.sort((left, right) => right.stats.lastLapAt - left.stats.lastLapAt || String(left.driver?.name || "").localeCompare(String(right.driver?.name || "")))
|
||||
.map((entry) => {
|
||||
const milestone = getNextTrainingMilestone(entry.stats);
|
||||
return `
|
||||
<tr class="${entry.profile.id === selectedProfileId ? "is-selected" : ""}">
|
||||
<td>${index + 1}</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>
|
||||
@@ -209,6 +236,27 @@ export function renderTrainingView(deps) {
|
||||
${trainingStat(t("training.consistency"), stats.recentLapCount ? `${stats.consistencySpreadPct.toFixed(1)}%` : "-", t("training.last_30_laps"))}
|
||||
</div>
|
||||
|
||||
<div class="training-development-grid">
|
||||
<section class="training-development-card">
|
||||
<div class="training-section-title">
|
||||
<div><span class="training-kicker">${escapeHtml(t("training.your_progress"))}</span><h3>${escapeHtml(t("training.recent_trend"))}</h3></div>
|
||||
<span class="badge">30 + 30 ${escapeHtml(t("training.laps").toLowerCase())}</span>
|
||||
</div>
|
||||
<div class="training-trend-metrics">
|
||||
${trainingInsight(t("training.pace_trend"), paceTrend)}
|
||||
${trainingInsight(t("training.consistency_trend"), consistencyTrend)}
|
||||
</div>
|
||||
<p>${escapeHtml(t("training.trend_hint"))}</p>
|
||||
</section>
|
||||
<section class="training-development-card">
|
||||
<div class="training-section-title">
|
||||
<div><span class="training-kicker">${escapeHtml(t("training.personal_credit"))}</span><h3>${escapeHtml(t("training.achievements"))}</h3></div>
|
||||
<span class="badge success">${unlockedAchievements.length}/${(stats.achievements || []).length}</span>
|
||||
</div>
|
||||
<div class="training-achievements">${achievementMarkup}</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="training-focus-grid">
|
||||
<section class="training-milestone-card">
|
||||
<div>
|
||||
@@ -347,7 +395,7 @@ export function renderTrainingView(deps) {
|
||||
</summary>
|
||||
<div class="training-disclosure-body 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.laps"))}</th><th>${escapeHtml(t("training.training_days"))}</th><th>${escapeHtml(t("training.best_lap"))}</th><th>${escapeHtml(t("training.next_target"))}</th></tr></thead>
|
||||
<thead><tr><th>${escapeHtml(t("training.driver"))}</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.next_target"))}</th></tr></thead>
|
||||
<tbody>${clubRows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -470,28 +518,62 @@ export function buildTrainingOverviewRows(state, profiles, buildDriverTrainingSt
|
||||
stats: buildDriverTrainingStats(state, profile.id, classId),
|
||||
}));
|
||||
}).sort((left, right) => (
|
||||
right.stats.totalLaps - left.stats.totalLaps
|
||||
right.stats.lastLapAt - left.stats.lastLapAt
|
||||
|| String(left.driver?.name || "").localeCompare(String(right.driver?.name || ""))
|
||||
|| String(left.className).localeCompare(String(right.className))
|
||||
));
|
||||
}
|
||||
|
||||
export function buildTrainingProgressFeed(rows, limit = 6) {
|
||||
return (Array.isArray(rows) ? rows : [])
|
||||
.filter((row) => Number(row?.stats?.totalLaps || 0) > 0)
|
||||
.sort((left, right) => Number(right.stats.lastLapAt || 0) - Number(left.stats.lastLapAt || 0))
|
||||
.slice(0, Math.max(1, Number(limit || 6)))
|
||||
.map((row) => {
|
||||
const stats = row.stats;
|
||||
if (stats.personalBestGainMs > 0) return { ...row, kind: "personal_best", value: stats.personalBestGainMs };
|
||||
if (stats.trendSampleReady && stats.averageTrendMs > 0) return { ...row, kind: "average_gain", value: stats.averageTrendMs };
|
||||
if (stats.bestFiveMinuteResult) return { ...row, kind: "five_minute", value: stats.bestFiveMinuteResult.lapCount };
|
||||
if (stats.controlledLapStreak >= 5) return { ...row, kind: "controlled_streak", value: stats.controlledLapStreak };
|
||||
return { ...row, kind: "practice_laps", value: stats.totalLaps };
|
||||
});
|
||||
}
|
||||
|
||||
function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, profiles, buildDriverTrainingStats, getNextTrainingMilestone, setSelectedDriverId, setSelectedClassId, renderView }) {
|
||||
const rows = buildTrainingOverviewRows(state, profiles, buildDriverTrainingStats);
|
||||
const progressFeed = buildTrainingProgressFeed(rows);
|
||||
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 progressFeedMarkup = progressFeed.length
|
||||
? progressFeed.map((entry) => {
|
||||
const message = entry.kind === "personal_best"
|
||||
? t("training.feed_personal_best", { lap: formatLap(entry.value) })
|
||||
: entry.kind === "average_gain"
|
||||
? t("training.feed_average_gain", { lap: formatLap(entry.value) })
|
||||
: entry.kind === "five_minute"
|
||||
? t("training.feed_five_minute", { laps: entry.value })
|
||||
: entry.kind === "controlled_streak"
|
||||
? t("training.feed_controlled_streak", { laps: entry.value })
|
||||
: t("training.feed_practice_laps", { laps: entry.value });
|
||||
return `
|
||||
<button class="training-progress-entry training-club-driver" data-driver-id="${escapeHtml(entry.profile.id)}" data-class-id="${escapeHtml(entry.classId)}" type="button">
|
||||
<span class="training-progress-initial">${escapeHtml(String(entry.driver?.name || "?").trim().charAt(0).toUpperCase() || "?")}</span>
|
||||
<span><strong>${escapeHtml(entry.driver?.name || "-")}</strong><small>${escapeHtml(entry.className)} · ${escapeHtml(message)}</small></span>
|
||||
</button>
|
||||
`;
|
||||
}).join("")
|
||||
: `<p class="hint">${escapeHtml(t("training.feed_empty"))}</p>`;
|
||||
const tableRows = rows.map((row) => {
|
||||
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>
|
||||
@@ -528,13 +610,18 @@ function renderTrainingOverview({ state, dom, t, escapeHtml, formatLap, profiles
|
||||
${trainingStat(t("training.sessions"), String(recordedSessions), t("training.recorded_sessions"))}
|
||||
</div>
|
||||
|
||||
<section class="panel mt-16 training-progress-panel">
|
||||
<div class="panel-header"><div><span class="training-kicker">${escapeHtml(t("training.progress_not_positions"))}</span><h3>${escapeHtml(t("training.progress_feed"))}</h3></div></div>
|
||||
<div class="panel-body training-progress-feed">${progressFeedMarkup}</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<thead><tr><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>
|
||||
|
||||
Reference in New Issue
Block a user