Extract race render and print helpers into module
This commit is contained in:
441
src/app.js
441
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 { 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";
|
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 });
|
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 buildFinalStandings = (event) => buildFinalStandingsLogic(event, { getSessionsForEvent, buildLeaderboard });
|
||||||
|
|
||||||
const getManualCorrectionSummary = (row) => getManualCorrectionSummaryLogic(row, { formatLap });
|
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 applyCompetitorCorrection = (session, row, options = {}) => applyCompetitorCorrectionLogic(session, row, options, { ensureSessionResult, uid, t, formatLap, saveState });
|
||||||
const recalculateCompetitorFromPassings = (session, rowKey) => recalculateCompetitorFromPassingsLogic(session, rowKey, { ensureSessionResult, getCompetitorPassings, isCountedPassing });
|
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 });
|
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)}`;
|
return `${metric.lapCount}/${formatRaceClock(metric.totalMs)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTeamStintLog(session, rows) {
|
|
||||||
if (!rows.length) {
|
|
||||||
return `<p>${t("events.no_team_results")}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="team-log-grid">
|
|
||||||
${rows
|
|
||||||
.map((row) => {
|
|
||||||
const stints = buildTeamStintLog(session, row);
|
|
||||||
return `
|
|
||||||
<article class="team-log-card">
|
|
||||||
<h5>${escapeHtml(row.displayName || row.driverName)}</h5>
|
|
||||||
<div class="hint">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
|
|
||||||
${
|
|
||||||
stints.length
|
|
||||||
? renderTable(
|
|
||||||
[t("events.slot"), t("table.driver"), t("table.car"), t("table.time"), t("table.duration"), t("table.laps")],
|
|
||||||
stints.map(
|
|
||||||
(stint) => `
|
|
||||||
<tr>
|
|
||||||
<td>${stint.index}</td>
|
|
||||||
<td>${escapeHtml(stint.driverName || "-")}</td>
|
|
||||||
<td>${escapeHtml(stint.carName || "-")}</td>
|
|
||||||
<td>${new Date(stint.startTs).toLocaleTimeString()}</td>
|
|
||||||
<td>${formatRaceClock(stint.durationMs)}</td>
|
|
||||||
<td>${stint.laps}</td>
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: `<p>${t("timing.no_passings")}</p>`
|
|
||||||
}
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTeamRaceStandings(event) {
|
|
||||||
const groups = buildTeamRaceStandings(event);
|
|
||||||
if (!groups.length) {
|
|
||||||
return `<p>${t("events.no_team_results")}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups
|
|
||||||
.map(
|
|
||||||
({ session, rows }) => `
|
|
||||||
<section class="team-standings-block">
|
|
||||||
<h4>${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}</h4>
|
|
||||||
${
|
|
||||||
rows.length
|
|
||||||
? renderTable(
|
|
||||||
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
|
|
||||||
rows.map(
|
|
||||||
(row, index) => `
|
|
||||||
<tr>
|
|
||||||
<td>${index + 1}</td>
|
|
||||||
<td>
|
|
||||||
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
|
||||||
<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
|
|
||||||
</td>
|
|
||||||
<td>${row.laps}</td>
|
|
||||||
<td>${escapeHtml(row.resultDisplay)}</td>
|
|
||||||
<td>${formatLap(row.bestLapMs)}</td>
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: `<p>${t("events.no_team_results")}</p>`
|
|
||||||
}
|
|
||||||
<div class="mt-16">
|
|
||||||
<h5>${t("events.team_stint_log")}</h5>
|
|
||||||
${rows.length ? renderTeamStintLog(session, rows) : `<p>${t("events.no_team_results")}</p>`}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.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 `
|
|
||||||
<div class="position-grid mt-16">
|
|
||||||
<h4>${t("events.position_grid")}</h4>
|
|
||||||
<p class="hint">${t("timing.position_grid_hint")}</p>
|
|
||||||
<div class="position-grid-list">
|
|
||||||
${entries
|
|
||||||
.map(
|
|
||||||
(entry) => `
|
|
||||||
<div class="position-grid-item">
|
|
||||||
<span class="pos-pill">${entry.slot}</span>
|
|
||||||
<div>
|
|
||||||
<strong>${escapeHtml(entry.name)}</strong>
|
|
||||||
${entry.meta ? `<div class="hint">${escapeHtml(entry.meta)}</div>` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGridEditor(session) {
|
|
||||||
if (!session) {
|
|
||||||
return `<p>${t("events.grid_empty")}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const driverIds = ensureSessionDriverOrder(session);
|
|
||||||
return `
|
|
||||||
<div class="grid-editor-toolbar">
|
|
||||||
<div>
|
|
||||||
<strong>${escapeHtml(session.name)}</strong>
|
|
||||||
<div class="hint">${t("events.grid_editor_hint")}</div>
|
|
||||||
<div class="hint">${t(session.gridCustomized ? "events.grid_locked" : "events.grid_unlocked")}</div>
|
|
||||||
</div>
|
|
||||||
<div class="actions">
|
|
||||||
<button id="gridToggleLock" class="btn" type="button">${t(session.gridCustomized ? "events.grid_unlock" : "events.grid_lock")}</button>
|
|
||||||
<button id="gridResetOrder" class="btn" type="button">${t("events.grid_reset")}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="drag-list mt-16" id="gridDragList">
|
|
||||||
${driverIds
|
|
||||||
.map((driverId, index) => {
|
|
||||||
const driver = state.drivers.find((item) => item.id === driverId);
|
|
||||||
return `
|
|
||||||
<div class="drag-item" draggable="true" data-index="${index}">
|
|
||||||
<span class="pos-pill">${index + 1}</span>
|
|
||||||
<div>
|
|
||||||
<strong>${escapeHtml(driver?.name || t("common.unknown_driver"))}</strong>
|
|
||||||
<div class="hint">${escapeHtml(driver?.transponder || "-")}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearGeneratedQualifying(eventId) {
|
function clearGeneratedQualifying(eventId) {
|
||||||
const generatedIds = state.sessions
|
const generatedIds = state.sessions
|
||||||
.filter((session) => session.eventId === eventId && session.type === "qualification" && session.generated)
|
.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 `<p>${t("events.no_final_matrix")}</p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="final-matrix">
|
|
||||||
${mains
|
|
||||||
.map(
|
|
||||||
(main) => `
|
|
||||||
<section class="final-card">
|
|
||||||
<div class="final-card-header">
|
|
||||||
<h4>${t("events.main")} ${escapeHtml(main.mainKey)}</h4>
|
|
||||||
<span class="pill">${main.sessions.length} ${escapeHtml(t("events.leg_status").toLowerCase())}</span>
|
|
||||||
</div>
|
|
||||||
<div class="matrix-slots">
|
|
||||||
${main.slots
|
|
||||||
.map(
|
|
||||||
(slot) => `
|
|
||||||
<div class="matrix-slot ${slot.reserved ? "matrix-slot-reserved" : ""}">
|
|
||||||
<span class="pill">${t("events.slot")} ${slot.slot}</span>
|
|
||||||
<strong>${escapeHtml(slot.label)}</strong>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
<div class="matrix-session-list">
|
|
||||||
${main.sessions
|
|
||||||
.map(
|
|
||||||
(session) => `
|
|
||||||
<div class="matrix-session-row">
|
|
||||||
<span>${escapeHtml(session.name)}</span>
|
|
||||||
<span class="pill ${session.status === "finished" ? "pill-green" : ""}">${escapeHtml(getStatusLabel(session.status))}</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPrintBrandBlock(branding) {
|
|
||||||
return `
|
|
||||||
<div class="print-brand-block">
|
|
||||||
${branding.logoDataUrl ? `<img class="print-logo" src="${escapeHtml(branding.logoDataUrl)}" alt="logo" />` : ""}
|
|
||||||
<div>
|
|
||||||
<strong>${escapeHtml(branding.brandName)}</strong>
|
|
||||||
<p>${escapeHtml(branding.brandTagline)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 `
|
|
||||||
<header class="print-header">
|
|
||||||
<div>
|
|
||||||
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
|
|
||||||
<h1>${escapeHtml(event.name)}</h1>
|
|
||||||
<p>${escapeHtml(event.date || "-")}</p>
|
|
||||||
</div>
|
|
||||||
<div class="print-meta">
|
|
||||||
${buildPrintBrandBlock(branding)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<h2>${t("events.start_lists")}</h2>
|
|
||||||
${sessions
|
|
||||||
.map((session) => {
|
|
||||||
const entries = getSessionGridEntries(session);
|
|
||||||
return `
|
|
||||||
<section class="print-block">
|
|
||||||
<h3>${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}</h3>
|
|
||||||
<p>${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(session.startMode))} • ${t("table.duration")}: ${session.durationMin} min</p>
|
|
||||||
${
|
|
||||||
entries.length
|
|
||||||
? renderTable(
|
|
||||||
[t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")],
|
|
||||||
entries.map((entry) => {
|
|
||||||
const driver = state.drivers.find((item) => item.id === entry.id);
|
|
||||||
return `
|
|
||||||
<tr>
|
|
||||||
<td>${entry.slot}</td>
|
|
||||||
<td>${escapeHtml(entry.name)}</td>
|
|
||||||
<td>${escapeHtml(driver?.brand || "-")}</td>
|
|
||||||
<td>${escapeHtml(entry.meta || "-")}</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
: `<p>${t("common.no_entries")}</p>`
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.join("")}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRaceResultsHtml(event) {
|
|
||||||
const branding = resolveEventBranding(event);
|
|
||||||
return `
|
|
||||||
<header class="print-header">
|
|
||||||
<div>
|
|
||||||
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
|
|
||||||
<h1>${escapeHtml(event.name)}</h1>
|
|
||||||
<p>${escapeHtml(event.date || "-")}</p>
|
|
||||||
</div>
|
|
||||||
<div class="print-meta">
|
|
||||||
${buildPrintBrandBlock(branding)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<section class="print-block">
|
|
||||||
<h2>${t("events.practice_standings")}</h2>
|
|
||||||
${renderRaceStandingsTableView(buildPracticeStandings(event), t("events.no_practice_results"))}
|
|
||||||
</section>
|
|
||||||
<section class="print-block">
|
|
||||||
<h2>${t("events.qualifying_standings")}</h2>
|
|
||||||
${renderRaceStandingsTableView(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
|
|
||||||
</section>
|
|
||||||
<section class="print-block">
|
|
||||||
<h2>${t("events.final_standings")}</h2>
|
|
||||||
${renderRaceStandingsTableView(buildFinalStandings(event), t("events.no_final_results"))}
|
|
||||||
</section>
|
|
||||||
<section class="print-block">
|
|
||||||
<h2>${t("events.team_standings")}</h2>
|
|
||||||
${renderTeamRaceStandings(event)}
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTeamRaceResultsHtml(event) {
|
|
||||||
const branding = resolveEventBranding(event);
|
|
||||||
const groups = buildTeamRaceStandings(event);
|
|
||||||
return `
|
|
||||||
<header class="print-header">
|
|
||||||
<div>
|
|
||||||
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
|
|
||||||
<h1>${escapeHtml(event.name)}</h1>
|
|
||||||
<p>${escapeHtml(event.date || "-")}</p>
|
|
||||||
</div>
|
|
||||||
<div class="print-meta">
|
|
||||||
${buildPrintBrandBlock(branding)}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
${groups
|
|
||||||
.map(
|
|
||||||
({ session, rows }) => `
|
|
||||||
<section class="print-block">
|
|
||||||
<h2>${t("events.team_report")} • ${escapeHtml(session.name)}</h2>
|
|
||||||
${
|
|
||||||
rows.length
|
|
||||||
? renderTable(
|
|
||||||
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
|
|
||||||
rows.map(
|
|
||||||
(row, index) => `
|
|
||||||
<tr>
|
|
||||||
<td>${index + 1}</td>
|
|
||||||
<td>${escapeHtml(row.displayName || row.driverName)}</td>
|
|
||||||
<td>${row.laps}</td>
|
|
||||||
<td>${escapeHtml(row.resultDisplay)}</td>
|
|
||||||
<td>${formatLap(row.bestLapMs)}</td>
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: `<p>${t("events.no_team_results")}</p>`
|
|
||||||
}
|
|
||||||
<h3>${t("events.team_stint_log")}</h3>
|
|
||||||
${renderTeamStintLog(session, rows)}
|
|
||||||
</section>
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPrintWindow(title, bodyHtml) {
|
function openPrintWindow(title, bodyHtml) {
|
||||||
const printWindow = window.open("", "_blank", "noopener,noreferrer,width=1200,height=900");
|
const printWindow = window.open("", "_blank", "noopener,noreferrer,width=1200,height=900");
|
||||||
if (!printWindow) {
|
if (!printWindow) {
|
||||||
|
|||||||
426
src/race_render_helpers.js
Normal file
426
src/race_render_helpers.js
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
export function renderTeamStintLog(session, rows, { t, escapeHtml, buildTeamStintLog, formatTeamActiveMemberLabel, renderTable, formatRaceClock }) {
|
||||||
|
if (!rows.length) {
|
||||||
|
return `<p>${t("events.no_team_results")}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="team-log-grid">
|
||||||
|
${rows
|
||||||
|
.map((row) => {
|
||||||
|
const stints = buildTeamStintLog(session, row);
|
||||||
|
return `
|
||||||
|
<article class="team-log-card">
|
||||||
|
<h5>${escapeHtml(row.displayName || row.driverName)}</h5>
|
||||||
|
<div class="hint">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
|
||||||
|
${
|
||||||
|
stints.length
|
||||||
|
? renderTable(
|
||||||
|
[t("events.slot"), t("table.driver"), t("table.car"), t("table.time"), t("table.duration"), t("table.laps")],
|
||||||
|
stints.map(
|
||||||
|
(stint) => `
|
||||||
|
<tr>
|
||||||
|
<td>${stint.index}</td>
|
||||||
|
<td>${escapeHtml(stint.driverName || "-")}</td>
|
||||||
|
<td>${escapeHtml(stint.carName || "-")}</td>
|
||||||
|
<td>${new Date(stint.startTs).toLocaleTimeString()}</td>
|
||||||
|
<td>${formatRaceClock(stint.durationMs)}</td>
|
||||||
|
<td>${stint.laps}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: `<p>${t("timing.no_passings")}</p>`
|
||||||
|
}
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderTeamRaceStandings(event, { t, escapeHtml, buildTeamRaceStandings, getSessionTypeLabel, renderTable, formatTeamActiveMemberLabel, formatLap, renderTeamStintLog }) {
|
||||||
|
const groups = buildTeamRaceStandings(event);
|
||||||
|
if (!groups.length) {
|
||||||
|
return `<p>${t("events.no_team_results")}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
.map(
|
||||||
|
({ session, rows }) => `
|
||||||
|
<section class="team-standings-block">
|
||||||
|
<h4>${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}</h4>
|
||||||
|
${
|
||||||
|
rows.length
|
||||||
|
? renderTable(
|
||||||
|
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
|
||||||
|
rows.map(
|
||||||
|
(row, index) => `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
|
||||||
|
<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
|
||||||
|
</td>
|
||||||
|
<td>${row.laps}</td>
|
||||||
|
<td>${escapeHtml(row.resultDisplay)}</td>
|
||||||
|
<td>${formatLap(row.bestLapMs)}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: `<p>${t("events.no_team_results")}</p>`
|
||||||
|
}
|
||||||
|
<div class="mt-16">
|
||||||
|
<h5>${t("events.team_stint_log")}</h5>
|
||||||
|
${rows.length ? renderTeamStintLog(session, rows) : `<p>${t("events.no_team_results")}</p>`}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.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 `
|
||||||
|
<div class="position-grid mt-16">
|
||||||
|
<h4>${t("events.position_grid")}</h4>
|
||||||
|
<p class="hint">${t("timing.position_grid_hint")}</p>
|
||||||
|
<div class="position-grid-list">
|
||||||
|
${entries
|
||||||
|
.map(
|
||||||
|
(entry) => `
|
||||||
|
<div class="position-grid-item">
|
||||||
|
<span class="pos-pill">${entry.slot}</span>
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(entry.name)}</strong>
|
||||||
|
${entry.meta ? `<div class="hint">${escapeHtml(entry.meta)}</div>` : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderGridEditor(session, { t, escapeHtml, ensureSessionDriverOrder, state }) {
|
||||||
|
if (!session) {
|
||||||
|
return `<p>${t("events.grid_empty")}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const driverIds = ensureSessionDriverOrder(session);
|
||||||
|
return `
|
||||||
|
<div class="grid-editor-toolbar">
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(session.name)}</strong>
|
||||||
|
<div class="hint">${t("events.grid_editor_hint")}</div>
|
||||||
|
<div class="hint">${t(session.gridCustomized ? "events.grid_locked" : "events.grid_unlocked")}</div>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="gridToggleLock" class="btn" type="button">${t(session.gridCustomized ? "events.grid_unlock" : "events.grid_lock")}</button>
|
||||||
|
<button id="gridResetOrder" class="btn" type="button">${t("events.grid_reset")}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="drag-list mt-16" id="gridDragList">
|
||||||
|
${driverIds
|
||||||
|
.map((driverId, index) => {
|
||||||
|
const driver = state.drivers.find((item) => item.id === driverId);
|
||||||
|
return `
|
||||||
|
<div class="drag-item" draggable="true" data-index="${index}">
|
||||||
|
<span class="pos-pill">${index + 1}</span>
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(driver?.name || t("common.unknown_driver"))}</strong>
|
||||||
|
<div class="hint">${escapeHtml(driver?.transponder || "-")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `<p>${t("events.no_final_matrix")}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="final-matrix">
|
||||||
|
${mains
|
||||||
|
.map(
|
||||||
|
(main) => `
|
||||||
|
<section class="final-card">
|
||||||
|
<div class="final-card-header">
|
||||||
|
<h4>${t("events.main")} ${escapeHtml(main.mainKey)}</h4>
|
||||||
|
<span class="pill">${main.sessions.length} ${escapeHtml(t("events.leg_status").toLowerCase())}</span>
|
||||||
|
</div>
|
||||||
|
<div class="matrix-slots">
|
||||||
|
${main.slots
|
||||||
|
.map(
|
||||||
|
(slot) => `
|
||||||
|
<div class="matrix-slot ${slot.reserved ? "matrix-slot-reserved" : ""}">
|
||||||
|
<span class="pill">${t("events.slot")} ${slot.slot}</span>
|
||||||
|
<strong>${escapeHtml(slot.label)}</strong>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
<div class="matrix-session-list">
|
||||||
|
${main.sessions
|
||||||
|
.map(
|
||||||
|
(session) => `
|
||||||
|
<div class="matrix-session-row">
|
||||||
|
<span>${escapeHtml(session.name)}</span>
|
||||||
|
<span class="pill ${session.status === "finished" ? "pill-green" : ""}">${escapeHtml(getStatusLabel(session.status))}</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPrintBrandBlock(branding, { escapeHtml }) {
|
||||||
|
return `
|
||||||
|
<div class="print-brand-block">
|
||||||
|
${branding.logoDataUrl ? `<img class="print-logo" src="${escapeHtml(branding.logoDataUrl)}" alt="logo" />` : ""}
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(branding.brandName)}</strong>
|
||||||
|
<p>${escapeHtml(branding.brandTagline)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `
|
||||||
|
<header class="print-header">
|
||||||
|
<div>
|
||||||
|
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
|
||||||
|
<h1>${escapeHtml(event.name)}</h1>
|
||||||
|
<p>${escapeHtml(event.date || "-")}</p>
|
||||||
|
</div>
|
||||||
|
<div class="print-meta">
|
||||||
|
${buildPrintBrandBlock(branding)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<h2>${t("events.start_lists")}</h2>
|
||||||
|
${sessions
|
||||||
|
.map((session) => {
|
||||||
|
const entries = getSessionGridEntries(session);
|
||||||
|
return `
|
||||||
|
<section class="print-block">
|
||||||
|
<h3>${escapeHtml(session.name)} • ${escapeHtml(getSessionTypeLabel(session.type))}</h3>
|
||||||
|
<p>${t("table.start_mode")}: ${escapeHtml(getStartModeLabel(session.startMode))} • ${t("table.duration")}: ${session.durationMin} min</p>
|
||||||
|
${
|
||||||
|
entries.length
|
||||||
|
? renderTable(
|
||||||
|
[t("events.slot"), t("table.driver"), t("table.brand"), t("table.transponder")],
|
||||||
|
entries.map((entry) => {
|
||||||
|
const driver = state.drivers.find((item) => item.id === entry.id);
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${entry.slot}</td>
|
||||||
|
<td>${escapeHtml(entry.name)}</td>
|
||||||
|
<td>${escapeHtml(driver?.brand || "-")}</td>
|
||||||
|
<td>${escapeHtml(entry.meta || "-")}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: `<p>${t("common.no_entries")}</p>`
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRaceResultsHtml(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, renderRaceStandingsTableView, buildPracticeStandings, buildQualifyingStandings, buildFinalStandings, renderTeamRaceStandings }) {
|
||||||
|
const branding = resolveEventBranding(event);
|
||||||
|
return `
|
||||||
|
<header class="print-header">
|
||||||
|
<div>
|
||||||
|
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
|
||||||
|
<h1>${escapeHtml(event.name)}</h1>
|
||||||
|
<p>${escapeHtml(event.date || "-")}</p>
|
||||||
|
</div>
|
||||||
|
<div class="print-meta">
|
||||||
|
${buildPrintBrandBlock(branding)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<section class="print-block">
|
||||||
|
<h2>${t("events.practice_standings")}</h2>
|
||||||
|
${renderRaceStandingsTableView(buildPracticeStandings(event), t("events.no_practice_results"))}
|
||||||
|
</section>
|
||||||
|
<section class="print-block">
|
||||||
|
<h2>${t("events.qualifying_standings")}</h2>
|
||||||
|
${renderRaceStandingsTableView(buildQualifyingStandings(event), t("events.no_qualifying_results"))}
|
||||||
|
</section>
|
||||||
|
<section class="print-block">
|
||||||
|
<h2>${t("events.final_standings")}</h2>
|
||||||
|
${renderRaceStandingsTableView(buildFinalStandings(event), t("events.no_final_results"))}
|
||||||
|
</section>
|
||||||
|
<section class="print-block">
|
||||||
|
<h2>${t("events.team_standings")}</h2>
|
||||||
|
${renderTeamRaceStandings(event)}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTeamRaceResultsHtml(event, { t, escapeHtml, resolveEventBranding, getClassName, buildPrintBrandBlock, buildTeamRaceStandings, renderTable, formatLap, renderTeamStintLog }) {
|
||||||
|
const branding = resolveEventBranding(event);
|
||||||
|
const groups = buildTeamRaceStandings(event);
|
||||||
|
return `
|
||||||
|
<header class="print-header">
|
||||||
|
<div>
|
||||||
|
<p class="print-kicker">${escapeHtml(getClassName(event.classId))}</p>
|
||||||
|
<h1>${escapeHtml(event.name)}</h1>
|
||||||
|
<p>${escapeHtml(event.date || "-")}</p>
|
||||||
|
</div>
|
||||||
|
<div class="print-meta">
|
||||||
|
${buildPrintBrandBlock(branding)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
${groups
|
||||||
|
.map(
|
||||||
|
({ session, rows }) => `
|
||||||
|
<section class="print-block">
|
||||||
|
<h2>${t("events.team_report")} • ${escapeHtml(session.name)}</h2>
|
||||||
|
${
|
||||||
|
rows.length
|
||||||
|
? renderTable(
|
||||||
|
[t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
|
||||||
|
rows.map(
|
||||||
|
(row, index) => `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${escapeHtml(row.displayName || row.driverName)}</td>
|
||||||
|
<td>${row.laps}</td>
|
||||||
|
<td>${escapeHtml(row.resultDisplay)}</td>
|
||||||
|
<td>${formatLap(row.bestLapMs)}</td>
|
||||||
|
</tr>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: `<p>${t("events.no_team_results")}</p>`
|
||||||
|
}
|
||||||
|
<h3>${t("events.team_stint_log")}</h3>
|
||||||
|
${renderTeamStintLog(session, rows)}
|
||||||
|
</section>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
`;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user