Add moving a driver between qualifying heats

The user asked whether a lone driver left in the last heat (e.g. 7
drivers at 3 per heat) could be moved into another heat manually.
Investigated first: neither "Reseed" (redistributes every not-yet-run
heat by current standings, not a targeted move) nor the grid editor
(reorders start position within one heat, never moves a driver
between heats) could do this - there was genuinely no path to it.

moveDriversIntoQualifyingHeat (src/event_race_logic.js) adds the same
"checking them elsewhere is the move" pattern already used for team
rosters: a new driver checklist on the qualification session edit
form pulls a checked driver out of whichever other heat they're
currently on, scoped to the same round only ("Q2 H2" never touches
"Q1"/"Q3", since heat composition can legitimately differ round to
round after a reseed). The checklist names the driver's current heat
before saving, mirroring the team-roster warning.

Also confirmed, per the user's earlier questions, already correct
with no changes needed: practice's per-driver "best 3 laps"/"best
5-minute result" stats, and that heats are already seeded fastest-
first from practice results. Flagged one real, deliberately unbuilt
gap while investigating: a true per-driver rolling "5 minutes from
your own first crossing" qualifying *heat* clock isn't supported -
only the practice stats work that way today.

Verified with new unit tests for moveDriversIntoQualifyingHeat and
the extracted parseQualifyingHeatName helper, and a real headless-
browser run reproducing the exact reported scenario end to end
through the actual UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 18:18:01 +02:00
parent e9883188fe
commit b1b65a9cf9
9 changed files with 166 additions and 10 deletions

View File

@@ -21,6 +21,7 @@ export function renderEventManagerMarkup(context) {
raceDrivers,
teamDriverPool,
showTeamsSection,
parseQualifyingHeatName,
state,
getDriverDisplayById,
raceFormatAdvanced,
@@ -54,6 +55,21 @@ export function renderEventManagerMarkup(context) {
raceTeams.find((team) => team.id !== excludeTeamId && team.driverIds.includes(driverId))?.name || "";
const carTeamElsewhere = (carId, excludeTeamId = "") =>
raceTeams.find((team) => team.id !== excludeTeamId && team.carIds.includes(carId))?.name || "";
// Same idea as driverTeamElsewhere above, but for qualifying heats: checking a driver
// onto this heat moves them out of whichever other heat in the SAME round they're
// currently on (see moveDriversIntoQualifyingHeat) - name that heat so the admin sees the
// move coming before saving, not after.
const driverHeatElsewhere = (driverId, targetSession) => {
const targetParsed = parseQualifyingHeatName(targetSession?.name);
if (!targetParsed) return "";
const other = sessions.find((session) =>
session.id !== targetSession.id
&& session.type === "qualification"
&& (session.driverIds || []).includes(driverId)
&& parseQualifyingHeatName(session.name)?.round === targetParsed.round
);
return other?.name || "";
};
const sessionChannelMarkup = sessions
.map((session) => ({ session, markup: renderSessionChannelAnalysis(session) }))
.filter((entry) => entry.markup)
@@ -864,6 +880,29 @@ export function renderEventManagerMarkup(context) {
<input name="maxCars" type="number" min="1" value="${editingSession.maxCars || ""}" />
<small>${t("events.session_max_cars_hint")}</small>
</label>
${
event.mode === "race" && editingSession.type === "qualification"
? `
<div class="span-5">
<h4>${t("events.heat_drivers")}</h4>
<p class="hint">${t("events.heat_drivers_hint")}</p>
<div class="check-grid">
${raceDrivers
.map((driver) => {
const elsewhere = driverHeatElsewhere(driver.id, editingSession);
return `
<label class="check-card">
<input type="checkbox" name="heatDriverIds" value="${driver.id}" ${(editingSession.driverIds || []).includes(driver.id) ? "checked" : ""} />
<span>${escapeHtml(driver.name)}${driver.transponder ? ` (${escapeHtml(driver.transponder)})` : ""}${elsewhere ? `<small>${escapeHtml(t("events.heat_member_elsewhere", { heat: elsewhere }))}</small>` : ""}</span>
</label>
`;
})
.join("")}
</div>
</div>
`
: ""
}
<p class="form-error" id="sessionEditError" hidden></p>
<div class="actions-inline">
<button class="btn btn-primary" type="submit">${t("common.save")}</button>