Add race setup status badges and summary warnings
This commit is contained in:
@@ -42,6 +42,8 @@ JMK RB RaceController is an RC timing and race-control system with support for s
|
|||||||
- `Basic / Advanced` mode for `Race Format`
|
- `Basic / Advanced` mode for `Race Format`
|
||||||
- a right-side race summary card
|
- a right-side race summary card
|
||||||
- a preset-aware summary card that changes between endurance and normal race focus
|
- a preset-aware summary card that changes between endurance and normal race focus
|
||||||
|
- status badges for `Setup`, `Format`, `Generation`, and `Live / results`
|
||||||
|
- summary warnings when participants, sessions, teams, or lap-window settings are missing
|
||||||
- separate `Race actions` for generation, reseeding and bump-up
|
- separate `Race actions` for generation, reseeding and bump-up
|
||||||
- reorganized in-app `Guide` with overview cards and two-column sections
|
- reorganized in-app `Guide` with overview cards and two-column sections
|
||||||
- explicit race participant selection
|
- explicit race participant selection
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he
|
|||||||
- `Grundläge / Avancerat` för `Raceformat`
|
- `Grundläge / Avancerat` för `Raceformat`
|
||||||
- en sammanfattningspanel till höger
|
- en sammanfattningspanel till höger
|
||||||
- ett presetstyrt sammanfattningskort som växlar mellan endurance- och vanligt race-fokus
|
- ett presetstyrt sammanfattningskort som växlar mellan endurance- och vanligt race-fokus
|
||||||
|
- statusbrickor för `Setup`, `Format`, `Generering` och `Live / resultat`
|
||||||
|
- sammanfattningsvarningar när deltagare, sessioner, lag eller varvfönster saknas
|
||||||
- separata `Race actions` för generering, reseeding och bump-up
|
- separata `Race actions` för generering, reseeding och bump-up
|
||||||
- omgjord inbyggd `Guide` med översiktskort och tvåkolumnslayout
|
- omgjord inbyggd `Guide` med översiktskort och tvåkolumnslayout
|
||||||
- välj exakt vilka förare som är med i racet
|
- välj exakt vilka förare som är med i racet
|
||||||
|
|||||||
123
src/app.js
123
src/app.js
@@ -203,6 +203,17 @@ const TRANSLATIONS = {
|
|||||||
"events.context_endurance_hint": "I Grundläge döljs kval- och finalfälten här eftersom endurance normalt byggs runt Team Race. Öppna Avancerat om eventet även ska ha stödheat eller finaler.",
|
"events.context_endurance_hint": "I Grundläge döljs kval- och finalfälten här eftersom endurance normalt byggs runt Team Race. Öppna Avancerat om eventet även ska ha stödheat eller finaler.",
|
||||||
"events.context_rules_title": "Valideringsexempel",
|
"events.context_rules_title": "Valideringsexempel",
|
||||||
"events.context_rules_hint": "Exempel på kort teknisk bana: 11s min / 60s max. Justera efter verklig varvtid så statistik och stintar blir rimliga.",
|
"events.context_rules_hint": "Exempel på kort teknisk bana: 11s min / 60s max. Justera efter verklig varvtid så statistik och stintar blir rimliga.",
|
||||||
|
"events.status_complete": "Klar",
|
||||||
|
"events.status_pending": "Väntar",
|
||||||
|
"events.status_attention": "Åtgärda",
|
||||||
|
"events.summary_warnings_title": "Kontrollpunkter",
|
||||||
|
"events.summary_warning_no_participants": "Inga racedeltagare är valda ännu.",
|
||||||
|
"events.summary_warning_no_sessions": "Inga sessioner finns ännu. Skapa dem i wizarden eller via Race actions.",
|
||||||
|
"events.summary_warning_no_qualifying": "Inga kvalsessioner finns ännu.",
|
||||||
|
"events.summary_warning_no_finals": "Inga finalsessioner finns ännu.",
|
||||||
|
"events.summary_warning_no_teams": "Endurance/Team Race saknar lag. Lägg upp lag i Setup innan körning.",
|
||||||
|
"events.summary_warning_invalid_lap_window": "Min/max varvtid ser fel ut. Max måste vara större än min.",
|
||||||
|
"guide.manage_steps_6": "6. Statusbrickorna visar om varje steg är klart, väntar eller behöver åtgärdas. Sammanfattningen till höger visar också varningar om deltagare, lag eller sessioner saknas.",
|
||||||
"guide.race_wizard_title": "Create Race Wizard",
|
"guide.race_wizard_title": "Create Race Wizard",
|
||||||
"guide.race_wizard_1": "1. Börja i Race Setup och använd wizarden när du skapar ett nytt race, i stället för att bygga allt direkt i Hantera.",
|
"guide.race_wizard_1": "1. Börja i Race Setup och använd wizarden när du skapar ett nytt race, i stället för att bygga allt direkt i Hantera.",
|
||||||
"guide.race_wizard_2": "2. Steg 1 sätter namn, datum, klass och preset. Presetet fyller rimliga standardvärden innan du finjusterar något.",
|
"guide.race_wizard_2": "2. Steg 1 sätter namn, datum, klass och preset. Presetet fyller rimliga standardvärden innan du finjusterar något.",
|
||||||
@@ -919,6 +930,17 @@ const TRANSLATIONS = {
|
|||||||
"events.context_endurance_hint": "Basic mode hides most qualifying and finals fields here because endurance is normally built around Team Race. Open Advanced if the event also needs support heats or finals.",
|
"events.context_endurance_hint": "Basic mode hides most qualifying and finals fields here because endurance is normally built around Team Race. Open Advanced if the event also needs support heats or finals.",
|
||||||
"events.context_rules_title": "Validation example",
|
"events.context_rules_title": "Validation example",
|
||||||
"events.context_rules_hint": "Example for a short technical track: 11s min / 60s max. Adjust to the real lap pace so stats and stints remain meaningful.",
|
"events.context_rules_hint": "Example for a short technical track: 11s min / 60s max. Adjust to the real lap pace so stats and stints remain meaningful.",
|
||||||
|
"events.status_complete": "Complete",
|
||||||
|
"events.status_pending": "Pending",
|
||||||
|
"events.status_attention": "Needs action",
|
||||||
|
"events.summary_warnings_title": "Checkpoints",
|
||||||
|
"events.summary_warning_no_participants": "No race participants are selected yet.",
|
||||||
|
"events.summary_warning_no_sessions": "No sessions exist yet. Create them in the wizard or via Race Actions.",
|
||||||
|
"events.summary_warning_no_qualifying": "No qualifying sessions exist yet.",
|
||||||
|
"events.summary_warning_no_finals": "No finals exist yet.",
|
||||||
|
"events.summary_warning_no_teams": "Endurance/Team Race has no teams yet. Add teams in Setup before running.",
|
||||||
|
"events.summary_warning_invalid_lap_window": "The min/max lap window looks invalid. Max must be greater than min.",
|
||||||
|
"guide.manage_steps_6": "6. The status badges show whether each step is complete, pending, or needs action. The summary on the right also warns when participants, teams, or sessions are missing.",
|
||||||
"guide.race_wizard_title": "Create Race Wizard",
|
"guide.race_wizard_title": "Create Race Wizard",
|
||||||
"guide.race_wizard_1": "1. Start in Race Setup and use the wizard when creating a new race instead of building everything directly in Manage.",
|
"guide.race_wizard_1": "1. Start in Race Setup and use the wizard when creating a new race instead of building everything directly in Manage.",
|
||||||
"guide.race_wizard_2": "2. Step 1 sets name, date, class and preset. The preset fills sensible defaults before you fine-tune anything.",
|
"guide.race_wizard_2": "2. Step 1 sets name, date, class and preset. The preset fills sensible defaults before you fine-tune anything.",
|
||||||
@@ -3744,6 +3766,8 @@ function renderEventManager(eventId) {
|
|||||||
const showBasicFinalFields = raceFormatAdvanced || !isEndurancePreset;
|
const showBasicFinalFields = raceFormatAdvanced || !isEndurancePreset;
|
||||||
const selectedParticipantCount = event.mode === "race" ? (event.raceConfig.participantsConfigured ? (event.raceConfig.driverIds || []).length : raceDrivers.length) : 0;
|
const selectedParticipantCount = event.mode === "race" ? (event.raceConfig.participantsConfigured ? (event.raceConfig.driverIds || []).length : raceDrivers.length) : 0;
|
||||||
const raceSummaryItems = event.mode === "race" ? getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset) : [];
|
const raceSummaryItems = event.mode === "race" ? getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset) : [];
|
||||||
|
const raceSummaryWarnings = event.mode === "race" ? getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selectedPreset) : [];
|
||||||
|
const manageStatuses = event.mode === "race" ? getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selectedPreset) : null;
|
||||||
const gridSessions = event.mode === "race" ? sessions.filter((session) => normalizeStartMode(session.startMode) === "position") : [];
|
const gridSessions = event.mode === "race" ? sessions.filter((session) => normalizeStartMode(session.startMode) === "position") : [];
|
||||||
if (selectedGridSessionId && !gridSessions.some((session) => session.id === selectedGridSessionId)) {
|
if (selectedGridSessionId && !gridSessions.some((session) => session.id === selectedGridSessionId)) {
|
||||||
selectedGridSessionId = "";
|
selectedGridSessionId = "";
|
||||||
@@ -3902,20 +3926,20 @@ function renderEventManager(eventId) {
|
|||||||
? `
|
? `
|
||||||
<section class="panel mt-16">
|
<section class="panel mt-16">
|
||||||
<div class="panel-body manage-step-grid">
|
<div class="panel-body manage-step-grid">
|
||||||
<article class="manage-step-card">
|
<article class="manage-step-card manage-step-card-${manageStatuses?.setup || "pending"}">
|
||||||
<strong>${t("events.manage_step_setup")}</strong>
|
<div class="manage-step-head"><strong>${t("events.manage_step_setup")}</strong>${renderManageStatusBadge(manageStatuses?.setup || "pending")}</div>
|
||||||
<p>${t("events.manage_step_setup_hint")}</p>
|
<p>${t("events.manage_step_setup_hint")}</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="manage-step-card">
|
<article class="manage-step-card manage-step-card-${manageStatuses?.format || "pending"}">
|
||||||
<strong>${t("events.manage_step_format")}</strong>
|
<div class="manage-step-head"><strong>${t("events.manage_step_format")}</strong>${renderManageStatusBadge(manageStatuses?.format || "pending")}</div>
|
||||||
<p>${t("events.manage_step_format_hint")}</p>
|
<p>${t("events.manage_step_format_hint")}</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="manage-step-card">
|
<article class="manage-step-card manage-step-card-${manageStatuses?.generation || "pending"}">
|
||||||
<strong>${t("events.manage_step_generate")}</strong>
|
<div class="manage-step-head"><strong>${t("events.manage_step_generate")}</strong>${renderManageStatusBadge(manageStatuses?.generation || "pending")}</div>
|
||||||
<p>${t("events.manage_step_generate_hint")}</p>
|
<p>${t("events.manage_step_generate_hint")}</p>
|
||||||
</article>
|
</article>
|
||||||
<article class="manage-step-card">
|
<article class="manage-step-card manage-step-card-${manageStatuses?.live || "pending"}">
|
||||||
<strong>${t("events.manage_step_live")}</strong>
|
<div class="manage-step-head"><strong>${t("events.manage_step_live")}</strong>${renderManageStatusBadge(manageStatuses?.live || "pending")}</div>
|
||||||
<p>${t("events.manage_step_live_hint")}</p>
|
<p>${t("events.manage_step_live_hint")}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -4267,6 +4291,14 @@ function renderEventManager(eventId) {
|
|||||||
<div class="panel-header"><h3>${t("events.race_summary")}</h3></div>
|
<div class="panel-header"><h3>${t("events.race_summary")}</h3></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p class="hint">${t("events.race_summary_hint")}</p>
|
<p class="hint">${t("events.race_summary_hint")}</p>
|
||||||
|
${raceSummaryWarnings.length ? `
|
||||||
|
<div class="race-summary-warnings">
|
||||||
|
<strong>${t("events.summary_warnings_title")}</strong>
|
||||||
|
<ul>
|
||||||
|
${raceSummaryWarnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join("")}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
<div class="race-summary-list">
|
<div class="race-summary-list">
|
||||||
${raceSummaryItems
|
${raceSummaryItems
|
||||||
.map(
|
.map(
|
||||||
@@ -5788,7 +5820,7 @@ function renderGuide() {
|
|||||||
|
|
||||||
<div class="guide-section-grid mt-16">
|
<div class="guide-section-grid mt-16">
|
||||||
${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])}
|
${renderGuidePanel("guide.race_title", ["guide.race_1", "guide.race_2", "guide.race_3", "guide.race_4", "guide.race_4a", "guide.race_5", "guide.race_6", "guide.race_7", "guide.race_8", "guide.race_9", "guide.race_10"])}
|
||||||
${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5"])}
|
${renderGuidePanel("guide.manage_steps_title", ["guide.manage_steps_1", "guide.manage_steps_2", "guide.manage_steps_3", "guide.manage_steps_4", "guide.manage_steps_5", "guide.manage_steps_6"])}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="guide-section-grid mt-16">
|
<div class="guide-section-grid mt-16">
|
||||||
@@ -7740,6 +7772,79 @@ function getRaceSummaryItems(event, sessions, raceDrivers, selectedPreset) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRaceSummaryWarnings(event, sessions, raceDrivers, raceTeams, selectedPreset) {
|
||||||
|
const cfg = event.raceConfig || {};
|
||||||
|
const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
|
||||||
|
const warnings = [];
|
||||||
|
const isEndurancePreset = selectedPreset?.id === "endurance";
|
||||||
|
const hasQualifying = sessions.some((session) => session.type === "qualification");
|
||||||
|
const hasFinals = sessions.some((session) => session.type === "final");
|
||||||
|
const hasAnySession = sessions.length > 0;
|
||||||
|
if (participantCount <= 0) {
|
||||||
|
warnings.push(t("events.summary_warning_no_participants"));
|
||||||
|
}
|
||||||
|
if (!hasAnySession) {
|
||||||
|
warnings.push(t("events.summary_warning_no_sessions"));
|
||||||
|
}
|
||||||
|
if (!isEndurancePreset && !hasQualifying) {
|
||||||
|
warnings.push(t("events.summary_warning_no_qualifying"));
|
||||||
|
}
|
||||||
|
if (!isEndurancePreset && !hasFinals) {
|
||||||
|
warnings.push(t("events.summary_warning_no_finals"));
|
||||||
|
}
|
||||||
|
if (isEndurancePreset && raceTeams.length <= 0) {
|
||||||
|
warnings.push(t("events.summary_warning_no_teams"));
|
||||||
|
}
|
||||||
|
const minLapMs = Math.max(0, Number(cfg.minLapMs || 0) || 0);
|
||||||
|
const maxLapMs = Math.max(0, Number(cfg.maxLapMs || 0) || 0);
|
||||||
|
if (maxLapMs > 0 && maxLapMs <= minLapMs) {
|
||||||
|
warnings.push(t("events.summary_warning_invalid_lap_window"));
|
||||||
|
}
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRaceManageStatuses(event, sessions, raceDrivers, raceTeams, selectedPreset) {
|
||||||
|
const cfg = event.raceConfig || {};
|
||||||
|
const participantCount = cfg.participantsConfigured ? (cfg.driverIds || []).length : raceDrivers.length;
|
||||||
|
const isEndurancePreset = selectedPreset?.id === "endurance";
|
||||||
|
const hasQualifying = sessions.some((session) => session.type === "qualification");
|
||||||
|
const hasFinals = sessions.some((session) => session.type === "final");
|
||||||
|
const hasTeamRace = sessions.some((session) => session.type === "team_race");
|
||||||
|
const hasStartedSession = sessions.some((session) => ["running", "finished", "stopped"].includes(String(session.status || "").toLowerCase()) || session.startedAt);
|
||||||
|
const practiceRows = buildPracticeStandings(event);
|
||||||
|
const qualifyingRows = buildQualifyingStandings(event);
|
||||||
|
const finalRows = buildFinalStandings(event);
|
||||||
|
const lapWindowValid = Math.max(0, Number(cfg.maxLapMs || 0) || 0) > Math.max(0, Number(cfg.minLapMs || 0) || 0);
|
||||||
|
|
||||||
|
const setupStatus = participantCount > 0 && (!isEndurancePreset || raceTeams.length > 0)
|
||||||
|
? "complete"
|
||||||
|
: participantCount > 0 || raceTeams.length > 0
|
||||||
|
? "attention"
|
||||||
|
: "pending";
|
||||||
|
|
||||||
|
const formatStatus = lapWindowValid ? "complete" : "attention";
|
||||||
|
|
||||||
|
const generationReady = isEndurancePreset ? hasTeamRace : hasQualifying || hasFinals;
|
||||||
|
const generationStatus = generationReady ? "complete" : sessions.length > 0 ? "attention" : "pending";
|
||||||
|
|
||||||
|
const liveStatus = hasStartedSession || practiceRows.length > 0 || qualifyingRows.length > 0 || finalRows.length > 0
|
||||||
|
? "complete"
|
||||||
|
: generationReady
|
||||||
|
? "pending"
|
||||||
|
: "pending";
|
||||||
|
|
||||||
|
return {
|
||||||
|
setup: setupStatus,
|
||||||
|
format: formatStatus,
|
||||||
|
generation: generationStatus,
|
||||||
|
live: liveStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderManageStatusBadge(status) {
|
||||||
|
return `<span class="status-chip status-chip-${status}">${t(`events.status_${status}`)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function getDriversForClass(classId) {
|
function getDriversForClass(classId) {
|
||||||
return state.drivers.filter((driver) => !classId || driver.classId === classId);
|
return state.drivers.filter((driver) => !classId || driver.classId === classId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,6 +372,13 @@ body {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manage-step-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.wizard-step span {
|
.wizard-step span {
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
@@ -404,6 +411,46 @@ body {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 82px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip-complete {
|
||||||
|
background: rgba(46, 204, 113, 0.12);
|
||||||
|
border-color: rgba(46, 204, 113, 0.35);
|
||||||
|
color: #9ef0bf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip-pending {
|
||||||
|
background: rgba(136, 149, 178, 0.12);
|
||||||
|
border-color: rgba(136, 149, 178, 0.28);
|
||||||
|
color: #c9d3e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip-attention {
|
||||||
|
background: rgba(245, 166, 35, 0.12);
|
||||||
|
border-color: rgba(245, 166, 35, 0.34);
|
||||||
|
color: #ffd391;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-step-card-complete {
|
||||||
|
border-color: rgba(46, 204, 113, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manage-step-card-attention {
|
||||||
|
border-color: rgba(245, 166, 35, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
.race-wizard-footer {
|
.race-wizard-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -524,6 +571,26 @@ body {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.race-summary-warnings {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid rgba(245, 166, 35, 0.34);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(245, 166, 35, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.race-summary-warnings strong {
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.race-summary-warnings ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-header-inline {
|
.panel-header-inline {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user