427 lines
16 KiB
JavaScript
427 lines
16 KiB
JavaScript
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("")}
|
|
`;
|
|
}
|