From bafdeb3a3adaef54ad0e050e12a16c382ed043e4 Mon Sep 17 00:00:00 2001 From: larssand Date: Wed, 25 Mar 2026 22:05:31 +0100 Subject: [PATCH] Extract race render and print helpers into module --- src/app.js | 441 ++----------------------------------- src/race_render_helpers.js | 426 +++++++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 427 deletions(-) create mode 100644 src/race_render_helpers.js diff --git a/src/app.js b/src/app.js index 177f8d8..84739c2 100644 --- a/src/app.js +++ b/src/app.js @@ -8,6 +8,8 @@ import { normalizeRaceTeam as normalizeRaceTeamLogic, normalizeStoredRacePreset import { getSessionTypeLabel as getSessionTypeLabelLogic, getStatusLabel as getStatusLabelLogic, isUntimedSession as isUntimedSessionLogic, getActiveSession as getActiveSessionLogic, getSessionTargetMs as getSessionTargetMsLogic, getSessionLapWindow as getSessionLapWindowLogic, isCountedPassing as isCountedPassingLogic, getVisiblePassings as getVisiblePassingsLogic, getPassingValidationLabel as getPassingValidationLabelLogic, getSessionTiming as getSessionTimingLogic, ensureSessionResult as ensureSessionResultLogic, buildLeaderboard as buildLeaderboardLogic, formatLapDelta as formatLapDeltaLogic, formatLeaderboardGap as formatLeaderboardGapLogic, getCompetitorElapsedMs as getCompetitorElapsedMsLogic, getCompetitorPassings as getCompetitorPassingsLogic, getCompetitorSeedMetric as getCompetitorSeedMetricLogic, getSessionEntrants as getSessionEntrantsLogic, buildPracticeStandings as buildPracticeStandingsLogic, getQualifyingPointsValue as getQualifyingPointsValueLogic, isHighPointsTable as isHighPointsTableLogic, compareNumberSet as compareNumberSetLogic, buildQualifyingTieBreakNote as buildQualifyingTieBreakNoteLogic, hasQualifyingPrimaryTie as hasQualifyingPrimaryTieLogic, buildQualifyingStandings as buildQualifyingStandingsLogic, formatTeamActiveMemberLabel as formatTeamActiveMemberLabelLogic, buildTeamRaceStandings as buildTeamRaceStandingsLogic, buildTeamStintLog as buildTeamStintLogLogic, getSessionGridEntries as getSessionGridEntriesLogic, getSessionGridOrder as getSessionGridOrderLogic, ensureSessionDriverOrder as ensureSessionDriverOrderLogic, buildFinalStandings as buildFinalStandingsLogic } from "./timing_logic.js"; +import { renderTeamStintLog as renderTeamStintLogHelper, renderTeamRaceStandings as renderTeamRaceStandingsHelper, getSessionSortWeight as getSessionSortWeightHelper, getDriverDisplayById as getDriverDisplayByIdHelper, renderPositionGrid as renderPositionGridHelper, renderGridEditor as renderGridEditorHelper, getFinalMainLayouts as getFinalMainLayoutsHelper, renderFinalMatrix as renderFinalMatrixHelper, buildPrintBrandBlock as buildPrintBrandBlockHelper, buildRaceStartListsHtml as buildRaceStartListsHtmlHelper, buildRaceResultsHtml as buildRaceResultsHtmlHelper, buildTeamRaceResultsHtml as buildTeamRaceResultsHtmlHelper } from "./race_render_helpers.js"; + import { getManualCorrectionSummary as getManualCorrectionSummaryLogic, applyCompetitorCorrection as applyCompetitorCorrectionLogic, recalculateCompetitorFromPassings as recalculateCompetitorFromPassingsLogic, invalidateCompetitorLastLap as invalidateCompetitorLastLapLogic, restoreCompetitorLastInvalidLap as restoreCompetitorLastInvalidLapLogic, findPassingByUndoMarker as findPassingByUndoMarkerLogic, undoJudgingAdjustment as undoJudgingAdjustmentLogic, getJudgeFilteredRows as getJudgeFilteredRowsLogic, getJudgeFilteredLog as getJudgeFilteredLogLogic } from "./judging_logic.js"; const renderRaceFormatFieldView = (labelKey, hintKey, controlHtml, options = {}) => renderRaceFormatField(labelKey, hintKey, controlHtml, options, { t }); @@ -80,6 +82,18 @@ const ensureSessionDriverOrder = (session) => ensureSessionDriverOrderLogic(sess const buildFinalStandings = (event) => buildFinalStandingsLogic(event, { getSessionsForEvent, buildLeaderboard }); const getManualCorrectionSummary = (row) => getManualCorrectionSummaryLogic(row, { formatLap }); +const renderTeamStintLog = (session, rows) => renderTeamStintLogHelper(session, rows, { t, escapeHtml, buildTeamStintLog, formatTeamActiveMemberLabel, renderTable, formatRaceClock }); +const renderTeamRaceStandings = (event) => renderTeamRaceStandingsHelper(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, renderTeamStintLog }); +const getSessionSortWeight = (session) => getSessionSortWeightHelper(session); +const getDriverDisplayById = (driverId) => getDriverDisplayByIdHelper(driverId, { state, t }); +const renderPositionGrid = (session) => renderPositionGridHelper(session, { t, escapeHtml, getSessionGridEntries }); +const renderGridEditor = (session) => renderGridEditorHelper(session, { t, escapeHtml, ensureSessionDriverOrder, state }); +const getFinalMainLayouts = (event) => getFinalMainLayoutsHelper(event, { getSessionsForEvent, getSessionGridOrder, getDriverDisplayById, t }); +const renderFinalMatrix = (event) => renderFinalMatrixHelper(event, { t, escapeHtml, getFinalMainLayouts, getStatusLabel }); +const buildPrintBrandBlock = (branding) => buildPrintBrandBlockHelper(branding, { escapeHtml }); +const buildRaceStartListsHtml = (event) => buildRaceStartListsHtmlHelper(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable }); +const buildRaceResultsHtml = (event) => buildRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings }); +const buildTeamRaceResultsHtml = (event) => buildTeamRaceResultsHtmlHelper(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog }); const applyCompetitorCorrection = (session, row, options = {}) => applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState }); const recalculateCompetitorFromPassings = (session, rowKey) => recalculateCompetitorFromPassingsLogic(session, rowKey, { ensureSessionResult, getCompetitorPassings, isCountedPassing }); const invalidateCompetitorLastLap = (session, row) => invalidateCompetitorLastLapLogic(session, row, { ensureSessionResult, getCompetitorPassings, isCountedPassing, recalculateCompetitorFromPassings, uid, t, formatLap, saveState }); @@ -7432,176 +7446,6 @@ function formatSeedMetric(metric) { return `${metric.lapCount}/${formatRaceClock(metric.totalMs)}`; } -function renderTeamStintLog(session, rows) { - if (!rows.length) { - return `

${t("events.no_team_results")}

`; - } - - return ` -
- ${rows - .map((row) => { - const stints = buildTeamStintLog(session, row); - return ` -
-
${escapeHtml(row.displayName || row.driverName)}
-
${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
- ${ - stints.length - ? renderTable( - [t("events.slot"), t("table.driver"), t("table.car"), t("table.time"), t("table.duration"), t("table.laps")], - stints.map( - (stint) => ` - - ${stint.index} - ${escapeHtml(stint.driverName || "-")} - ${escapeHtml(stint.carName || "-")} - ${new Date(stint.startTs).toLocaleTimeString()} - ${formatRaceClock(stint.durationMs)} - ${stint.laps} - - ` - ) - ) - : `

${t("timing.no_passings")}

` - } -
- `; - }) - .join("")} -
- `; -} - -function renderTeamRaceStandings(event) { - const groups = buildTeamRaceStandings(event); - if (!groups.length) { - return `

${t("events.no_team_results")}

`; - } - - return groups - .map( - ({ session, rows }) => ` -
-

${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}

- ${ - rows.length - ? renderTable( - [t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")], - rows.map( - (row, index) => ` - - ${index + 1} - -
${escapeHtml(row.displayName || row.driverName)}
-
${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
- - ${row.laps} - ${escapeHtml(row.resultDisplay)} - ${formatLap(row.bestLapMs)} - - ` - ) - ) - : `

${t("events.no_team_results")}

` - } -
-
${t("events.team_stint_log")}
- ${rows.length ? renderTeamStintLog(session, rows) : `

${t("events.no_team_results")}

`} -
-
- ` - ) - .join(""); -} - -function getSessionSortWeight(session) { - const order = { - open_practice: 0, - free_practice: 1, - practice: 2, - qualification: 3, - heat: 4, - final: 5, - team_race: 6, - }; - return order[String(session?.type || "").toLowerCase()] || 99; -} - -function getDriverDisplayById(driverId) { - const driver = state.drivers.find((item) => item.id === driverId); - if (!driver) { - return t("common.unknown_driver"); - } - return driver.transponder ? `${driver.name} (${driver.transponder})` : driver.name; -} - -function renderPositionGrid(session) { - const entries = getSessionGridEntries(session); - if (!entries.length) { - return ""; - } - - return ` -
-

${t("events.position_grid")}

-

${t("timing.position_grid_hint")}

-
- ${entries - .map( - (entry) => ` -
- ${entry.slot} -
- ${escapeHtml(entry.name)} - ${entry.meta ? `
${escapeHtml(entry.meta)}
` : ""} -
-
- ` - ) - .join("")} -
-
- `; -} - -function renderGridEditor(session) { - if (!session) { - return `

${t("events.grid_empty")}

`; - } - - const driverIds = ensureSessionDriverOrder(session); - return ` -
-
- ${escapeHtml(session.name)} -
${t("events.grid_editor_hint")}
-
${t(session.gridCustomized ? "events.grid_locked" : "events.grid_unlocked")}
-
-
- - -
-
-
- ${driverIds - .map((driverId, index) => { - const driver = state.drivers.find((item) => item.id === driverId); - return ` -
- ${index + 1} -
- ${escapeHtml(driver?.name || t("common.unknown_driver"))} -
${escapeHtml(driver?.transponder || "-")}
-
-
- `; - }) - .join("")} -
- `; -} - function clearGeneratedQualifying(eventId) { const generatedIds = state.sessions .filter((session) => session.eventId === eventId && session.type === "qualification" && session.generated) @@ -7626,263 +7470,6 @@ function clearGeneratedFinals(eventId) { }); } -function getFinalMainLayouts(event) { - const finals = getSessionsForEvent(event.id) - .filter((session) => session.type === "final") - .sort((left, right) => { - const leftMain = String(left.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "Z"; - const rightMain = String(right.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "Z"; - if (leftMain !== rightMain) { - return leftMain.localeCompare(rightMain); - } - return String(left.name || "").localeCompare(String(right.name || "")); - }); - - const grouped = new Map(); - finals.forEach((session) => { - const mainKey = String(session.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "A"; - if (!grouped.has(mainKey)) { - grouped.set(mainKey, []); - } - grouped.get(mainKey).push(session); - }); - - return [...grouped.entries()].map(([mainKey, sessions]) => { - const sortedSessions = [...sessions].sort((left, right) => String(left.name || "").localeCompare(String(right.name || ""))); - const baseSession = sortedSessions[0]; - const baseDriverIds = [...getSessionGridOrder(baseSession)]; - const reservedSlots = Math.max(0, Number(baseSession?.reservedBumpSlots || 0) || 0); - const capacity = Math.max( - Number(baseSession?.maxCars || event.raceConfig?.carsPerFinal || 0) || 0, - baseDriverIds.length + reservedSlots - ); - const slots = []; - - for (let index = 0; index < capacity; index += 1) { - const driverId = baseDriverIds[index]; - if (driverId) { - slots.push({ - slot: index + 1, - label: getDriverDisplayById(driverId), - reserved: false, - }); - } else if (index < baseDriverIds.length + reservedSlots) { - slots.push({ - slot: index + 1, - label: t("events.reserved_slot"), - reserved: true, - }); - } else { - slots.push({ - slot: index + 1, - label: "-", - reserved: false, - }); - } - } - - return { - mainKey, - sessions: sortedSessions, - slots, - }; - }); -} - -function renderFinalMatrix(event) { - const mains = getFinalMainLayouts(event); - if (!mains.length) { - return `

${t("events.no_final_matrix")}

`; - } - - return ` -
- ${mains - .map( - (main) => ` -
-
-

${t("events.main")} ${escapeHtml(main.mainKey)}

- ${main.sessions.length} ${escapeHtml(t("events.leg_status").toLowerCase())} -
-
- ${main.slots - .map( - (slot) => ` -
- ${t("events.slot")} ${slot.slot} - ${escapeHtml(slot.label)} -
- ` - ) - .join("")} -
-
- ${main.sessions - .map( - (session) => ` -
- ${escapeHtml(session.name)} - ${escapeHtml(getStatusLabel(session.status))} -
- ` - ) - .join("")} -
-
- ` - ) - .join("")} -
- `; -} - -function buildPrintBrandBlock(branding) { - return ` - - `; -} - -function buildRaceStartListsHtml(event) { - const branding = resolveEventBranding(event); - const sessions = getSessionsForEvent(event.id) - .filter((session) => session.mode === "race") - .sort((left, right) => { - const weightDiff = getSessionSortWeight(left) - getSessionSortWeight(right); - if (weightDiff !== 0) { - return weightDiff; - } - return String(left.name || "").localeCompare(String(right.name || "")); - }); - - return ` - -

${t("events.start_lists")}

- ${sessions - .map((session) => { - const entries = getSessionGridEntries(session); - return ` - - `; - }) - .join("")} - `; -} - -function buildRaceResultsHtml(event) { - const branding = resolveEventBranding(event); - return ` - - - - - - `; -} - -function buildTeamRaceResultsHtml(event) { - const branding = resolveEventBranding(event); - const groups = buildTeamRaceStandings(event); - return ` - - ${groups - .map( - ({ session, rows }) => ` - - ` - ) - .join("")} - `; -} - function openPrintWindow(title, bodyHtml) { const printWindow = window.open("", "_blank", "noopener,noreferrer,width=1200,height=900"); if (!printWindow) { diff --git a/src/race_render_helpers.js b/src/race_render_helpers.js new file mode 100644 index 0000000..04e06d2 --- /dev/null +++ b/src/race_render_helpers.js @@ -0,0 +1,426 @@ +export function renderTeamStintLog(session, rows, { t, escapeHtml, buildTeamStintLog, formatTeamActiveMemberLabel, renderTable, formatRaceClock }) { + if (!rows.length) { + return `

${t("events.no_team_results")}

`; + } + + return ` +
+ ${rows + .map((row) => { + const stints = buildTeamStintLog(session, row); + return ` +
+
${escapeHtml(row.displayName || row.driverName)}
+
${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
+ ${ + stints.length + ? renderTable( + [t("events.slot"), t("table.driver"), t("table.car"), t("table.time"), t("table.duration"), t("table.laps")], + stints.map( + (stint) => ` + + ${stint.index} + ${escapeHtml(stint.driverName || "-")} + ${escapeHtml(stint.carName || "-")} + ${new Date(stint.startTs).toLocaleTimeString()} + ${formatRaceClock(stint.durationMs)} + ${stint.laps} + + ` + ) + ) + : `

${t("timing.no_passings")}

` + } +
+ `; + }) + .join("")} +
+ `; +} + +export function renderTeamRaceStandings(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, renderTeamStintLog }) { + const groups = buildTeamRaceStandings(event); + if (!groups.length) { + return `

${t("events.no_team_results")}

`; + } + + return groups + .map( + ({ session, rows }) => ` +
+

${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}

+ ${ + rows.length + ? renderTable( + [t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")], + rows.map( + (row, index) => ` + + ${index + 1} + +
${escapeHtml(row.displayName || row.driverName)}
+
${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
+ + ${row.laps} + ${escapeHtml(row.resultDisplay)} + ${formatLap(row.bestLapMs)} + + ` + ) + ) + : `

${t("events.no_team_results")}

` + } +
+
${t("events.team_stint_log")}
+ ${rows.length ? renderTeamStintLog(session, rows) : `

${t("events.no_team_results")}

`} +
+
+ ` + ) + .join(""); +} + +export function getSessionSortWeight(session) { + const order = { + open_practice: 0, + free_practice: 1, + practice: 2, + qualification: 3, + heat: 4, + final: 5, + team_race: 6, + }; + return order[String(session?.type || "").toLowerCase()] || 99; +} + +export function getDriverDisplayById(driverId, { state, t }) { + const driver = state.drivers.find((item) => item.id === driverId); + if (!driver) { + return t("common.unknown_driver"); + } + return driver.transponder ? `${driver.name} (${driver.transponder})` : driver.name; +} + +export function renderPositionGrid(session, { t, escapeHtml, getSessionGridEntries }) { + const entries = getSessionGridEntries(session); + if (!entries.length) { + return ""; + } + + return ` +
+

${t("events.position_grid")}

+

${t("timing.position_grid_hint")}

+
+ ${entries + .map( + (entry) => ` +
+ ${entry.slot} +
+ ${escapeHtml(entry.name)} + ${entry.meta ? `
${escapeHtml(entry.meta)}
` : ""} +
+
+ ` + ) + .join("")} +
+
+ `; +} + +export function renderGridEditor(session, { t, escapeHtml, ensureSessionDriverOrder, state }) { + if (!session) { + return `

${t("events.grid_empty")}

`; + } + + const driverIds = ensureSessionDriverOrder(session); + return ` +
+
+ ${escapeHtml(session.name)} +
${t("events.grid_editor_hint")}
+
${t(session.gridCustomized ? "events.grid_locked" : "events.grid_unlocked")}
+
+
+ + +
+
+
+ ${driverIds + .map((driverId, index) => { + const driver = state.drivers.find((item) => item.id === driverId); + return ` +
+ ${index + 1} +
+ ${escapeHtml(driver?.name || t("common.unknown_driver"))} +
${escapeHtml(driver?.transponder || "-")}
+
+
+ `; + }) + .join("")} +
+ `; +} + +export function getFinalMainLayouts(event, { getSessionsForEvent, getSessionGridOrder, getDriverDisplayById, t }) { + const finals = getSessionsForEvent(event.id) + .filter((session) => session.type === "final") + .sort((left, right) => { + const leftMain = String(left.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "Z"; + const rightMain = String(right.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "Z"; + if (leftMain !== rightMain) { + return leftMain.localeCompare(rightMain); + } + return String(left.name || "").localeCompare(String(right.name || "")); + }); + + const grouped = new Map(); + finals.forEach((session) => { + const mainKey = String(session.name || "").match(/^([A-Z])/i)?.[1]?.toUpperCase() || "A"; + if (!grouped.has(mainKey)) { + grouped.set(mainKey, []); + } + grouped.get(mainKey).push(session); + }); + + return [...grouped.entries()].map(([mainKey, sessions]) => { + const sortedSessions = [...sessions].sort((left, right) => String(left.name || "").localeCompare(String(right.name || ""))); + const baseSession = sortedSessions[0]; + const baseDriverIds = [...getSessionGridOrder(baseSession)]; + const reservedSlots = Math.max(0, Number(baseSession?.reservedBumpSlots || 0) || 0); + const capacity = Math.max( + Number(baseSession?.maxCars || event.raceConfig?.carsPerFinal || 0) || 0, + baseDriverIds.length + reservedSlots + ); + const slots = []; + + for (let index = 0; index < capacity; index += 1) { + const driverId = baseDriverIds[index]; + if (driverId) { + slots.push({ + slot: index + 1, + label: getDriverDisplayById(driverId), + reserved: false, + }); + } else if (index < baseDriverIds.length + reservedSlots) { + slots.push({ + slot: index + 1, + label: t("events.reserved_slot"), + reserved: true, + }); + } else { + slots.push({ + slot: index + 1, + label: "-", + reserved: false, + }); + } + } + + return { + mainKey, + sessions: sortedSessions, + slots, + }; + }); +} + +export function renderFinalMatrix(event, { t, escapeHtml, getFinalMainLayouts, getStatusLabel }) { + const mains = getFinalMainLayouts(event); + if (!mains.length) { + return `

${t("events.no_final_matrix")}

`; + } + + return ` +
+ ${mains + .map( + (main) => ` +
+
+

${t("events.main")} ${escapeHtml(main.mainKey)}

+ ${main.sessions.length} ${escapeHtml(t("events.leg_status").toLowerCase())} +
+
+ ${main.slots + .map( + (slot) => ` +
+ ${t("events.slot")} ${slot.slot} + ${escapeHtml(slot.label)} +
+ ` + ) + .join("")} +
+
+ ${main.sessions + .map( + (session) => ` +
+ ${escapeHtml(session.name)} + ${escapeHtml(getStatusLabel(session.status))} +
+ ` + ) + .join("")} +
+
+ ` + ) + .join("")} +
+ `; +} + +export function buildPrintBrandBlock(branding, { escapeHtml }) { + return ` + + `; +} + +export function buildRaceStartListsHtml(event, { t, state, escapeHtml, resolveEventBranding, getSessionsForEvent, getSessionSortWeight, getClassName, buildPrintBrandBlock, getSessionGridEntries, getSessionTypeLabel, getStartModeLabel, renderTable }) { + const branding = resolveEventBranding(event); + const sessions = getSessionsForEvent(event.id) + .filter((session) => session.mode === "race") + .sort((left, right) => { + const weightDiff = getSessionSortWeight(left) - getSessionSortWeight(right); + if (weightDiff !== 0) { + return weightDiff; + } + return String(left.name || "").localeCompare(String(right.name || "")); + }); + + return ` + +

${t("events.start_lists")}

+ ${sessions + .map((session) => { + const entries = getSessionGridEntries(session); + return ` + + `; + }) + .join("")} + `; +} + +export function buildRaceResultsHtml(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings }) { + const branding = resolveEventBranding(event); + return ` + + + + + + `; +} + +export function buildTeamRaceResultsHtml(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog }) { + const branding = resolveEventBranding(event); + const groups = buildTeamRaceStandings(event); + return ` + + ${groups + .map( + ({ session, rows }) => ` + + ` + ) + .join("")} + `; +}