@@ -4814,7 +4836,7 @@ function renderTeamOverlay(rows, result, sessionTiming) {
${index + 1}
${escapeHtml(row.displayName || row.driverName)}
${escapeHtml(row.resultDisplay || "-")}
-
${t("overlay.active_member")}: ${escapeHtml(row.subLabel || "-")}
+
${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
`
)
@@ -6077,6 +6099,14 @@ function renderRaceStandingsTable(rows, emptyLabel) {
);
}
+function formatTeamActiveMemberLabel(rowOrPassing) {
+ if (!rowOrPassing) {
+ return "-";
+ }
+ const parts = [rowOrPassing.driverName || "", rowOrPassing.carName || ""].filter(Boolean);
+ return parts.join(" • ") || rowOrPassing.subLabel || "-";
+}
+
function buildTeamRaceStandings(event) {
return getSessionsForEvent(event.id)
.filter((session) => session.type === "team_race")
@@ -6087,6 +6117,83 @@ function buildTeamRaceStandings(event) {
}));
}
+function buildTeamStintLog(session, row) {
+ const passings = getCompetitorPassings(session, row);
+ if (!passings.length) {
+ return [];
+ }
+
+ const stints = [];
+ let current = null;
+
+ passings.forEach((passing) => {
+ const memberLabel = formatTeamActiveMemberLabel(passing);
+ const memberKey = `${passing.driverId || passing.driverName || "-"}|${passing.carId || passing.carName || "-"}`;
+ if (!current || current.memberKey !== memberKey) {
+ current = {
+ memberKey,
+ memberLabel,
+ driverName: passing.driverName || "-",
+ carName: passing.carName || "-",
+ startTs: passing.timestamp,
+ endTs: passing.timestamp,
+ laps: 1,
+ };
+ stints.push(current);
+ return;
+ }
+ current.endTs = passing.timestamp;
+ current.laps += 1;
+ });
+
+ return stints.map((stint, index) => ({
+ ...stint,
+ index: index + 1,
+ durationMs: Math.max(0, stint.endTs - stint.startTs),
+ }));
+}
+
+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) {
@@ -6108,7 +6215,7 @@ function renderTeamRaceStandings(event) {
${index + 1} |
${escapeHtml(row.displayName || row.driverName)}
- ${row.subLabel ? `${t("overlay.active_member")}: ${escapeHtml(row.subLabel)} ` : ""}
+ ${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}
|
${row.laps} |
${escapeHtml(row.resultDisplay)} |
@@ -6119,6 +6226,10 @@ function renderTeamRaceStandings(event) {
)
: `
${t("events.no_team_results")}
`
}
+
+
${t("events.team_stint_log")}
+ ${rows.length ? renderTeamStintLog(session, rows) : `
${t("events.no_team_results")}
`}
+
`
)
@@ -6761,6 +6872,52 @@ function buildRaceResultsHtml(event) {
`;
}
+function buildTeamRaceResultsHtml(event) {
+ const branding = resolveEventBranding(event);
+ const groups = buildTeamRaceStandings(event);
+ return `
+
+ ${groups
+ .map(
+ ({ session, rows }) => `
+
+ ${t("events.team_report")} • ${escapeHtml(session.name)}
+ ${
+ 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)} |
+ ${row.laps} |
+ ${escapeHtml(row.resultDisplay)} |
+ ${formatLap(row.bestLapMs)} |
+
+ `
+ )
+ )
+ : `${t("events.no_team_results")}
`
+ }
+ ${t("events.team_stint_log")}
+ ${renderTeamStintLog(session, rows)}
+
+ `
+ )
+ .join("")}
+ `;
+}
+
function openPrintWindow(title, bodyHtml) {
const printWindow = window.open("", "_blank", "noopener,noreferrer,width=1200,height=900");
if (!printWindow) {
@@ -6946,6 +7103,56 @@ async function exportRaceResultsPdf(event) {
});
}
+async function exportTeamRaceResultsPdf(event) {
+ const branding = resolveEventBranding(event);
+ const sections = [];
+ buildTeamRaceStandings(event).forEach(({ session, rows }) => {
+ sections.push(
+ buildPdfSection(
+ `${t("events.team_report")} • ${session.name}`,
+ [t("table.pos"), t("events.team_name"), t("table.laps"), t("table.result"), t("table.best_lap")],
+ rows.map((row, index) => [
+ String(index + 1),
+ row.displayName || row.driverName || "-",
+ String(row.laps || 0),
+ row.resultDisplay || "-",
+ formatLap(row.bestLapMs),
+ ])
+ )
+ );
+
+ rows.forEach((row) => {
+ const stints = buildTeamStintLog(session, row);
+ sections.push(
+ buildPdfSection(
+ `${session.name} • ${row.displayName || row.driverName} • ${t("events.team_stint_log")}`,
+ [t("events.slot"), t("table.driver"), t("table.car"), t("table.time"), t("table.duration"), t("table.laps")],
+ stints.map((stint) => [
+ String(stint.index),
+ stint.driverName || "-",
+ stint.carName || "-",
+ new Date(stint.startTs).toLocaleTimeString(),
+ formatRaceClock(stint.durationMs),
+ String(stint.laps || 0),
+ ])
+ )
+ );
+ });
+ });
+
+ await requestPdfExport({
+ filename: `${event.name.replaceAll(/\s+/g, "_")}_team_report.pdf`,
+ title: event.name,
+ subtitle: `${t("events.team_report")} • ${getClassName(event.classId)} • ${event.date || "-"}`,
+ brandName: branding.brandName,
+ brandTagline: branding.brandTagline,
+ footer: branding.pdfFooter,
+ theme: branding.pdfTheme,
+ logoDataUrl: await ensurePdfLogoDataUrl(branding.logoDataUrl),
+ sections,
+ });
+}
+
async function exportSessionHeatSheetPdf(session) {
const event = state.events.find((item) => item.id === session.eventId);
const branding = resolveEventBranding(event);
diff --git a/src/styles.css b/src/styles.css
index a89a8b5..e573de6 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -609,6 +609,22 @@ select:focus {
margin: 0 0 10px;
}
+.team-log-grid {
+ display: grid;
+ gap: 12px;
+}
+
+.team-log-card {
+ border: 1px solid var(--line);
+ border-radius: 14px;
+ padding: 12px;
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.team-log-card h5 {
+ margin: 0 0 8px;
+}
+
.table-primary {
font-weight: 700;
}