Warn when a driver's transponder collides with a different driver

Registering or editing a driver with a transponder already assigned
to a different driver (e.g. a wrong-class typo) previously saved
silently - the only symptom was that driver's laps never registering
during their heat, with no indication why. Now a confirm dialog names
the conflicting driver and their class before saving; the same
person's own multiple class entries sharing one physical transponder
are correctly never flagged.

Also confirms two things the user asked to verify: the race clock
already counts down for all timed session types (only open practice
counts up unbounded), and a driver not entered in a heat already has
their laps fully discarded rather than recorded - both were already
correct, no changes needed there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 20:20:40 +02:00
parent 50586e8859
commit ba2d78779f
9 changed files with 100 additions and 5 deletions

View File

@@ -866,6 +866,7 @@ const TRANSLATIONS = {
"drivers.brand_placeholder": "Team / märke (valfritt)",
"drivers.brand_filter_placeholder": "Sök namn / transponder / märke / klubb / land",
"drivers.transponder_placeholder": "Personlig transponder (valfritt)",
"drivers.transponder_conflict_confirm": "Transpondern {tp} används redan av {name} ({class}). Spara ändå?",
"drivers.speech_name": "Uttalsnamn",
"drivers.speech_name_placeholder": "Uttal för uppläsning (valfritt)",
"drivers.speech_name_hint": "Skriv namnet som webbläsarrösten ska uttala det. Lämna tomt för vanligt förarnamn.",
@@ -2381,6 +2382,7 @@ const TRANSLATIONS = {
"drivers.brand_placeholder": "Team / brand (optional)",
"drivers.brand_filter_placeholder": "Search name / transponder / brand / club / country",
"drivers.transponder_placeholder": "Personal transponder (optional)",
"drivers.transponder_conflict_confirm": "Transponder {tp} is already used by {name} ({class}). Save anyway?",
"drivers.speech_name": "Spoken name",
"drivers.speech_name_placeholder": "Pronunciation for announcements (optional)",
"drivers.speech_name_hint": "Enter the name as the browser voice should pronounce it. Leave blank to use the regular driver name.",

View File

@@ -1,4 +1,4 @@
import { getDriverProfileGroups, getDriverProfileId } from "./driver_profile_logic.js";
import { getDriverProfileGroups, getDriverProfileId, findTransponderConflict } from "./driver_profile_logic.js";
function getScheduleDriftSummary(state) {
const scheduledSessions = state.sessions
@@ -515,13 +515,18 @@ export function renderDriversView(deps) {
? uid("profile")
: sameNameProfile?.id || uid("profile");
const linkedProfile = profileGroups.find((profile) => profile.id === profileId);
const cleanedTransponder = String(form.get("transponder") || "").trim();
const transponderConflict = findTransponderConflict(state.drivers, { transponder: cleanedTransponder, profileId });
if (transponderConflict && !window.confirm(t("drivers.transponder_conflict_confirm", { tp: cleanedTransponder, name: transponderConflict.name, class: getClassName(transponderConflict.classId) }))) {
return;
}
state.drivers.push(
normalizeDriver({
id: driverId,
name: driverName,
classId: String(form.get("classId")),
brand: String(form.get("brand") || "").trim(),
transponder: String(form.get("transponder") || "").trim(),
transponder: cleanedTransponder,
speechName: String(form.get("speechName") || "").trim(),
passSound: String(form.get("passSound") || "").trim(),
profileId,
@@ -634,6 +639,10 @@ export function renderDriversView(deps) {
setFormError("driverEditError", t("validation.invalid_selection"));
return;
}
const transponderConflict = findTransponderConflict(state.drivers, { transponder: cleanedTp, profileId: cleanedProfileId, excludeDriverId: editingDriver.id });
if (transponderConflict && !window.confirm(t("drivers.transponder_conflict_confirm", { tp: cleanedTp, name: transponderConflict.name, class: getClassName(transponderConflict.classId) }))) {
return;
}
setFormError("driverEditError", "");
state.drivers = state.drivers.map((item) => {
if (item.id === editingDriver.id) {

View File

@@ -44,3 +44,19 @@ export function getDriverProfileReferences(state, reference) {
profile.driverIds.forEach((driverId) => references.add(driverId));
return references;
}
// A transponder is one physical chip, so the same value legitimately repeats across a
// single person's own multi-class driver entries (same profile) - that is not a
// conflict. It only means something went wrong when it also shows up on a driver
// belonging to a DIFFERENT profile, since two different people can't share one chip:
// most often a typo at registration that quietly grabbed someone else's transponder.
export function findTransponderConflict(drivers, { transponder, profileId, excludeDriverId } = {}) {
const value = String(transponder || "").trim();
if (!value) return null;
const list = Array.isArray(drivers) ? drivers : [];
return list.find((driver) => (
String(driver.id || "") !== String(excludeDriverId || "")
&& String(driver.transponder || "").trim() === value
&& getDriverProfileId(driver) !== String(profileId || "")
)) || null;
}