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:
@@ -672,6 +672,16 @@ export function generateQualifyingForRace(event, { buildPracticeStandings, getEv
|
||||
|
||||
|
||||
|
||||
// Generated qualifying sessions are named "Q<round> H<heat>" (see generateQualifyingForRace)
|
||||
// with no separate round/heat fields of their own - shared here so every place that needs
|
||||
// to know which round/heat a session belongs to parses the same way.
|
||||
export function parseQualifyingHeatName(name) {
|
||||
const match = String(name || "").match(/^Q(\d+)\s+H(\d+)/i);
|
||||
return match ? { round: Number(match[1]), heat: Number(match[2]) } : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function reseedUpcomingQualifying(event, { buildQualifyingStandings, buildPracticeStandings, getEventDrivers, getSessionsForEvent, chunkArray }) {
|
||||
const standings = buildQualifyingStandings(event);
|
||||
const sourceRows =
|
||||
@@ -695,12 +705,12 @@ export function reseedUpcomingQualifying(event, { buildQualifyingStandings, buil
|
||||
getSessionsForEvent(event.id)
|
||||
.filter((session) => session.type === "qualification" && session.generated && session.status === "ready")
|
||||
.forEach((session) => {
|
||||
const match = String(session.name || "").match(/^Q(\d+)\s+H(\d+)/i);
|
||||
const round = match ? Number(match[1]) : 0;
|
||||
// The regex above already captures the heat number - sorting the full session name
|
||||
// as a string instead ("H10" < "H2" lexicographically) used to scramble the grid
|
||||
// for any round with 10+ heats.
|
||||
const heatNumber = match ? Number(match[2]) : 0;
|
||||
const parsed = parseQualifyingHeatName(session.name);
|
||||
const round = parsed?.round ?? 0;
|
||||
// parseQualifyingHeatName already captures the heat number - sorting the full
|
||||
// session name as a string instead ("H10" < "H2" lexicographically) used to scramble
|
||||
// the grid for any round with 10+ heats.
|
||||
const heatNumber = parsed?.heat ?? 0;
|
||||
if (!sessionsByRound.has(round)) {
|
||||
sessionsByRound.set(round, []);
|
||||
}
|
||||
@@ -737,6 +747,36 @@ export function reseedUpcomingQualifying(event, { buildQualifyingStandings, buil
|
||||
|
||||
|
||||
|
||||
// Moving a driver onto a heat's roster (there is no separate "move" action - ticking them
|
||||
// on the target heat's edit form IS the move) must pull them out of whichever other heat
|
||||
// they're currently gridded in, or they end up double-entered - the same class of bug
|
||||
// already fixed for team rosters. Scoped to the target heat's own round only ("Q2 H2" never
|
||||
// touches "Q1"/"Q3"), since heat composition can legitimately differ round to round after a
|
||||
// reseed, and an admin fixing one round's lone-driver heat should not silently reshuffle
|
||||
// rounds they didn't ask to change.
|
||||
export function moveDriversIntoQualifyingHeat(sessions, targetSessionId, driverIds) {
|
||||
const list = Array.isArray(sessions) ? sessions : [];
|
||||
const target = list.find((session) => session.id === targetSessionId);
|
||||
const parsedTarget = target ? parseQualifyingHeatName(target.name) : null;
|
||||
if (!parsedTarget) {
|
||||
return;
|
||||
}
|
||||
const driverSet = new Set(driverIds);
|
||||
list.forEach((session) => {
|
||||
if (session.id === targetSessionId || session.type !== "qualification") {
|
||||
return;
|
||||
}
|
||||
const parsed = parseQualifyingHeatName(session.name);
|
||||
if (!parsed || parsed.round !== parsedTarget.round) {
|
||||
return;
|
||||
}
|
||||
session.driverIds = (session.driverIds || []).filter((id) => !driverSet.has(id));
|
||||
session.manualGridIds = (session.manualGridIds || []).filter((id) => !driverSet.has(id));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function generateFinalsForRace(event, { buildPracticeStandings, buildQualifyingStandings, clearGeneratedFinals, state, normalizeStartMode, normalizeSession, uid, chunkArray, t }) {
|
||||
const sourceRows =
|
||||
event.raceConfig?.finalsSource === "practice"
|
||||
|
||||
Reference in New Issue
Block a user