diff --git a/src/app.js b/src/app.js index 542e495..ca4a92f 100644 --- a/src/app.js +++ b/src/app.js @@ -11,7 +11,7 @@ const NAV_ITEMS = [ { id: "guide", titleKey: "nav.guide", subtitleKey: "nav.guide_sub" }, ]; -const SESSION_TYPES = ["open_practice", "free_practice", "practice", "qualification", "heat", "final"]; +const SESSION_TYPES = ["open_practice", "free_practice", "practice", "qualification", "heat", "final", "team_race"]; const STORAGE_KEY = "rc_timing_control_v1"; const DEFAULT_LANGUAGE = "sv"; @@ -209,6 +209,17 @@ const TRANSLATIONS = { "events.finals_source_hint": "Välj om finalerna ska seedas från practice eller kval.", "events.race_driver_scope": "Race i denna klass använder alla förare i vald klass om sessionen inte har egen deltagarlista.", "events.reserve_bump_slots_hint": "Reserverar tomma platser i högre finaler så bumpade förare kan flyttas in utan att skriva över seedade platser.", + "events.team_race": "Lagrace", + "events.team_race_intro": "Skapa lag för långlopp. Alla passeringar från lagets förare eller bilar summeras till lagets totalvarv i Team Race-sessioner.", + "events.team_name": "Lagnamn", + "events.add_team": "Lägg till lag", + "events.teams": "Lag", + "events.team_drivers": "Lagförare", + "events.team_cars": "Lagbilar", + "events.team_hint": "Välj minst en förare eller bil per lag. Team Race-sessioner summerar lagets totala varv under hela körtiden, t.ex. 4 timmar.", + "events.no_teams": "Inga lag skapade ännu.", + "events.team_standings": "Lagställning", + "events.no_team_results": "Inga teamresultat ännu.", "events.add_session": "Lägg till session", "events.set_active": "Sätt aktiv", "events.assignments": "Tilldelningar", @@ -397,6 +408,7 @@ const TRANSLATIONS = { "session.qualification": "kval", "session.heat": "heat", "session.final": "final", + "session.team_race": "lagrace", "validation.no_assignments": "Ingen förar-/biltilldelning i denna session.", "validation.missing_tp": "En eller flera tilldelade bilar saknar transponder-ID.", "validation.duplicate_tp": "Dubblett-transponder i session: {ids}.", @@ -452,6 +464,12 @@ const TRANSLATIONS = { "guide.open_practice_1": "Använd Open Practice när du vill att systemet bara ska lista alla transpondrar som kommer in.", "guide.open_practice_2": "Om transpondern inte matchar en registrerad förare visas transpondernumret som namn.", "guide.open_practice_3": "Open Practice påverkar inte seedning, kval eller finaler.", + "guide.team_title": "Lagrace / Endurance", + "guide.team_1": "Gå till Race Setup och skapa ett race i rätt klass.", + "guide.team_2": "Öppna Hantera och skapa lag under sektionen Lag.", + "guide.team_3": "Koppla förare och/eller bilar till varje lag. Minst en av dem krävs.", + "guide.team_4": "Skapa en session med typ Team Race och sätt tiden, t.ex. 240 minuter för 4 timmar.", + "guide.team_5": "Starta sessionen i Tidtagning. Alla passeringar från lagets medlemmar summeras till lagets totalvarv.", "overlay.title": "Overlay", "overlay.subtitle": "Extern leaderboard-skärm", "overlay.no_active": "Ingen aktiv session vald.", @@ -693,6 +711,17 @@ const TRANSLATIONS = { "events.finals_source_hint": "Choose whether finals should be seeded from practice or qualifying.", "events.race_driver_scope": "Race mode uses all drivers in the event class unless a session has its own participant list.", "events.reserve_bump_slots_hint": "Reserve empty slots in higher finals so bumped drivers can be inserted without overwriting seeded spots.", + "events.team_race": "Team Race", + "events.team_race_intro": "Create endurance teams. All passings from the team's drivers or cars are added to the team's total laps in Team Race sessions.", + "events.team_name": "Team name", + "events.add_team": "Add team", + "events.teams": "Teams", + "events.team_drivers": "Team drivers", + "events.team_cars": "Team cars", + "events.team_hint": "Select at least one driver or car per team. Team Race sessions sum the team's total laps across the whole race duration, for example 4 hours.", + "events.no_teams": "No teams created yet.", + "events.team_standings": "Team standings", + "events.no_team_results": "No team results yet.", "events.add_session": "Add Session", "events.set_active": "Set Active", "events.assignments": "Assignments", @@ -881,6 +910,7 @@ const TRANSLATIONS = { "session.qualification": "qualification", "session.heat": "heat", "session.final": "final", + "session.team_race": "team race", "validation.no_assignments": "No driver/car assignments in this session.", "validation.missing_tp": "One or more assigned cars are missing transponder ID.", "validation.duplicate_tp": "Duplicate transponder(s) in session: {ids}.", @@ -936,6 +966,12 @@ const TRANSLATIONS = { "guide.open_practice_1": "Use Open Practice when you want the system to simply list every transponder that comes in.", "guide.open_practice_2": "If the transponder does not match a registered driver, the transponder number is shown as the name.", "guide.open_practice_3": "Open Practice does not affect seeding, qualifying or finals.", + "guide.team_title": "Team Race / Endurance", + "guide.team_1": "Go to Race Setup and create a race in the correct class.", + "guide.team_2": "Open Manage and create teams in the Teams section.", + "guide.team_3": "Assign drivers and/or cars to each team. At least one is required.", + "guide.team_4": "Create a session with type Team Race and set the time, for example 240 minutes for 4 hours.", + "guide.team_5": "Start the session in Timing. All passings from the team's members are added to the team's total laps.", "overlay.title": "Overlay", "overlay.subtitle": "External leaderboard screen", "overlay.no_active": "No active session selected.", @@ -1478,6 +1514,15 @@ function normalizeSession(session) { }; } +function normalizeRaceTeam(team) { + return { + id: String(team?.id || uid("team")), + name: String(team?.name || "").trim(), + driverIds: Array.isArray(team?.driverIds) ? team.driverIds.filter(Boolean) : [], + carIds: Array.isArray(team?.carIds) ? team.carIds.filter(Boolean) : [], + }; +} + function normalizeEvent(event) { return { ...event, @@ -1499,6 +1544,7 @@ function normalizeEvent(event) { driverIds: Array.isArray(event?.raceConfig?.driverIds) ? event.raceConfig.driverIds : [], participantsConfigured: Boolean(event?.raceConfig?.participantsConfigured), finalsSource: event?.raceConfig?.finalsSource === "practice" ? "practice" : "qualifying", + teams: Array.isArray(event?.raceConfig?.teams) ? event.raceConfig.teams.map((team) => normalizeRaceTeam(team)).filter((team) => team.name) : [], }, }; } @@ -1924,7 +1970,7 @@ function announcePassing(entry) { return; } if (state.settings.passingSoundMode === "name") { - speakText(entry?.driverName || t("common.unknown_driver")); + speakText(entry?.displayName || entry?.driverName || t("common.unknown_driver")); } } @@ -2702,6 +2748,7 @@ function renderEventManager(eventId) { .map((d) => ``) .join(""); const raceDrivers = event.mode === "race" ? state.drivers.filter((driver) => !event.classId || driver.classId === event.classId) : []; + const raceTeams = event.mode === "race" ? getEventTeams(event) : []; const carOptions = state.cars .map((c) => ``) .join(""); @@ -2747,7 +2794,12 @@ function renderEventManager(eventId) { ${renderTable( [t("table.session"), t("table.type"), t("table.duration"), t("table.start_mode"), t("table.seeding"), t("table.status"), t(event.mode === "track" ? "events.assignments" : "events.participants"), t("events.actions")], sessions.map((s) => { - const assignCount = event.mode === "track" ? (s.assignments || []).length : getSessionEntrants(s).length; + const assignCount = + event.mode === "track" + ? (s.assignments || []).length + : s.type === "team_race" + ? raceTeams.length + : getSessionEntrants(s).length; return `
${t("events.team_race_intro")}
+ +${t("events.team_hint")}
+${t("events.no_teams")}
` + } +