From 920763601b7b14a6c5e3c5bf77690f1b226b5e4b Mon Sep 17 00:00:00 2001 From: larssand Date: Fri, 20 Mar 2026 14:46:49 +0100 Subject: [PATCH] Add race setup status badges and summary warnings --- README.md | 2 + README.sv.md | 2 + src/app.js | 123 +++++++++++++++++++++++++++++++++++++++++++++---- src/styles.css | 67 +++++++++++++++++++++++++++ 4 files changed, 185 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1455a2c..64ee901 100644 --- a/README.md +++ b/README.md @@ -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` - a right-side race summary card - 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 - reorganized in-app `Guide` with overview cards and two-column sections - explicit race participant selection diff --git a/README.sv.md b/README.sv.md index de068fe..55d8a3d 100644 --- a/README.sv.md +++ b/README.sv.md @@ -40,6 +40,8 @@ RC timing app med sponsor-eventflöde (delade bilar/transpondrar mellan olika he - `Grundläge / Avancerat` för `Raceformat` - en sammanfattningspanel till höger - 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 - omgjord inbyggd `Guide` med översiktskort och tvåkolumnslayout - välj exakt vilka förare som är med i racet diff --git a/src/app.js b/src/app.js index 6d29d80..7630d3d 100644 --- a/src/app.js +++ b/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_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.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_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.", @@ -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_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.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_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.", @@ -3744,6 +3766,8 @@ function renderEventManager(eventId) { const showBasicFinalFields = raceFormatAdvanced || !isEndurancePreset; 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 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") : []; if (selectedGridSessionId && !gridSessions.some((session) => session.id === selectedGridSessionId)) { selectedGridSessionId = ""; @@ -3902,20 +3926,20 @@ function renderEventManager(eventId) { ? `
-
- ${t("events.manage_step_setup")} +
+
${t("events.manage_step_setup")}${renderManageStatusBadge(manageStatuses?.setup || "pending")}

${t("events.manage_step_setup_hint")}

-
- ${t("events.manage_step_format")} +
+
${t("events.manage_step_format")}${renderManageStatusBadge(manageStatuses?.format || "pending")}

${t("events.manage_step_format_hint")}

-
- ${t("events.manage_step_generate")} +
+
${t("events.manage_step_generate")}${renderManageStatusBadge(manageStatuses?.generation || "pending")}

${t("events.manage_step_generate_hint")}

-
- ${t("events.manage_step_live")} +
+
${t("events.manage_step_live")}${renderManageStatusBadge(manageStatuses?.live || "pending")}

${t("events.manage_step_live_hint")}

@@ -4267,6 +4291,14 @@ function renderEventManager(eventId) {

${t("events.race_summary")}

${t("events.race_summary_hint")}

+ ${raceSummaryWarnings.length ? ` +
+ ${t("events.summary_warnings_title")} +
    + ${raceSummaryWarnings.map((warning) => `
  • ${escapeHtml(warning)}
  • `).join("")} +
+
+ ` : ""}
${raceSummaryItems .map( @@ -5788,7 +5820,7 @@ function renderGuide() {
${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"])}
@@ -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 `${t(`events.status_${status}`)}`; +} + function getDriversForClass(classId) { return state.drivers.filter((driver) => !classId || driver.classId === classId); } diff --git a/src/styles.css b/src/styles.css index e52f5af..6a7f7aa 100644 --- a/src/styles.css +++ b/src/styles.css @@ -372,6 +372,13 @@ body { gap: 6px; } +.manage-step-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; +} + .wizard-step span { width: 26px; height: 26px; @@ -404,6 +411,46 @@ body { 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 { display: flex; justify-content: space-between; @@ -524,6 +571,26 @@ body { 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 { padding: 0; border: 0;