stint-/förarbyteslogg per lag

This commit is contained in:
larssand
2026-03-15 14:46:27 +01:00
parent a6e1f4c89c
commit 94bca48f46
2 changed files with 227 additions and 4 deletions

View File

@@ -221,6 +221,10 @@ const TRANSLATIONS = {
"events.team_standings": "Lagställning", "events.team_standings": "Lagställning",
"events.no_team_results": "Inga teamresultat ännu.", "events.no_team_results": "Inga teamresultat ännu.",
"events.edit_team": "Redigera lag", "events.edit_team": "Redigera lag",
"events.team_stint_log": "Stint- och förarbyteslogg",
"events.team_report": "Lagrapport",
"events.print_team_results": "Skriv ut lagrapport",
"events.pdf_team_results": "PDF lagrapport",
"events.add_session": "Lägg till session", "events.add_session": "Lägg till session",
"events.set_active": "Sätt aktiv", "events.set_active": "Sätt aktiv",
"events.assignments": "Tilldelningar", "events.assignments": "Tilldelningar",
@@ -729,6 +733,10 @@ const TRANSLATIONS = {
"events.team_standings": "Team standings", "events.team_standings": "Team standings",
"events.no_team_results": "No team results yet.", "events.no_team_results": "No team results yet.",
"events.edit_team": "Edit team", "events.edit_team": "Edit team",
"events.team_stint_log": "Stint and driver-change log",
"events.team_report": "Team report",
"events.print_team_results": "Print team report",
"events.pdf_team_results": "PDF team report",
"events.add_session": "Add Session", "events.add_session": "Add Session",
"events.set_active": "Set Active", "events.set_active": "Set Active",
"events.assignments": "Assignments", "events.assignments": "Assignments",
@@ -3161,8 +3169,14 @@ function renderEventManager(eventId) {
<section class="panel mt-16"> <section class="panel mt-16">
<div class="panel-header"><h3>${t("events.team_standings")}</h3></div> <div class="panel-header"><h3>${t("events.team_standings")}</h3></div>
<div class="panel-body"> <div class="panel-body">
<div class="actions">
<button id="printTeamResults" class="btn" type="button">${t("events.print_team_results")}</button>
<button id="pdfTeamResults" class="btn" type="button">${t("events.pdf_team_results")}</button>
</div>
<div class="mt-16">
${renderTeamRaceStandings(event)} ${renderTeamRaceStandings(event)}
</div> </div>
</div>
</section> </section>
<section class="panel mt-16"> <section class="panel mt-16">
@@ -3713,6 +3727,10 @@ function renderEventManager(eventId) {
openPrintWindow(`${event.name} - ${t("events.results_overview")}`, buildRaceResultsHtml(event)); openPrintWindow(`${event.name} - ${t("events.results_overview")}`, buildRaceResultsHtml(event));
}); });
document.getElementById("printTeamResults")?.addEventListener("click", () => {
openPrintWindow(`${event.name} - ${t("events.team_report")}`, buildTeamRaceResultsHtml(event));
});
document.getElementById("pdfStartlists")?.addEventListener("click", async () => { document.getElementById("pdfStartlists")?.addEventListener("click", async () => {
await exportRaceStartListsPdf(event); await exportRaceStartListsPdf(event);
}); });
@@ -3721,6 +3739,10 @@ function renderEventManager(eventId) {
await exportRaceResultsPdf(event); await exportRaceResultsPdf(event);
}); });
document.getElementById("pdfTeamResults")?.addEventListener("click", async () => {
await exportTeamRaceResultsPdf(event);
});
document.getElementById("gridResetOrder")?.addEventListener("click", () => { document.getElementById("gridResetOrder")?.addEventListener("click", () => {
if (!selectedGridSession) { if (!selectedGridSession) {
return; return;
@@ -4727,7 +4749,7 @@ function renderLeaderboard(rows) {
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td> <td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
<td> <td>
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div> <div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
${row.teamId && row.subLabel ? `<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(row.subLabel)}</div>` : ""} ${row.teamId ? `<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>` : ""}
</td> </td>
<td>${escapeHtml(row.subLabel || row.carName)}</td> <td>${escapeHtml(row.subLabel || row.carName)}</td>
<td>${escapeHtml(row.transponder)}</td> <td>${escapeHtml(row.transponder)}</td>
@@ -4762,7 +4784,7 @@ function renderOverlayLeaderboard(rows) {
</div> </div>
<div class="overlay-race-driver"> <div class="overlay-race-driver">
<strong>${escapeHtml(row.displayName || row.driverName)}</strong> <strong>${escapeHtml(row.displayName || row.driverName)}</strong>
<span>${escapeHtml(row.teamId && row.subLabel ? `${t("overlay.active_member")}: ${row.subLabel}` : row.subLabel || row.transponder || "-")}</span> <span>${escapeHtml(row.teamId ? `${t("overlay.active_member")}: ${formatTeamActiveMemberLabel(row)}` : row.subLabel || row.transponder || "-")}</span>
<div class="overlay-prediction"> <div class="overlay-prediction">
<div class="overlay-prediction-meta"> <div class="overlay-prediction-meta">
<label>${t("overlay.next_predicted_lap")}</label> <label>${t("overlay.next_predicted_lap")}</label>
@@ -4814,7 +4836,7 @@ function renderTeamOverlay(rows, result, sessionTiming) {
<span class="pos-pill pos-${Math.min(index + 1, 3)}">${index + 1}</span> <span class="pos-pill pos-${Math.min(index + 1, 3)}">${index + 1}</span>
<strong>${escapeHtml(row.displayName || row.driverName)}</strong> <strong>${escapeHtml(row.displayName || row.driverName)}</strong>
<p>${escapeHtml(row.resultDisplay || "-")}</p> <p>${escapeHtml(row.resultDisplay || "-")}</p>
<small>${t("overlay.active_member")}: ${escapeHtml(row.subLabel || "-")}</small> <small>${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</small>
</article> </article>
` `
) )
@@ -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) { function buildTeamRaceStandings(event) {
return getSessionsForEvent(event.id) return getSessionsForEvent(event.id)
.filter((session) => session.type === "team_race") .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 `<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) { function renderTeamRaceStandings(event) {
const groups = buildTeamRaceStandings(event); const groups = buildTeamRaceStandings(event);
if (!groups.length) { if (!groups.length) {
@@ -6108,7 +6215,7 @@ function renderTeamRaceStandings(event) {
<td>${index + 1}</td> <td>${index + 1}</td>
<td> <td>
<div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div> <div class="table-primary">${escapeHtml(row.displayName || row.driverName)}</div>
${row.subLabel ? `<div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(row.subLabel)}</div>` : ""} <div class="table-subnote">${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</div>
</td> </td>
<td>${row.laps}</td> <td>${row.laps}</td>
<td>${escapeHtml(row.resultDisplay)}</td> <td>${escapeHtml(row.resultDisplay)}</td>
@@ -6119,6 +6226,10 @@ function renderTeamRaceStandings(event) {
) )
: `<p>${t("events.no_team_results")}</p>` : `<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> </section>
` `
) )
@@ -6761,6 +6872,52 @@ function buildRaceResultsHtml(event) {
`; `;
} }
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) {
@@ -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) { async function exportSessionHeatSheetPdf(session) {
const event = state.events.find((item) => item.id === session.eventId); const event = state.events.find((item) => item.id === session.eventId);
const branding = resolveEventBranding(event); const branding = resolveEventBranding(event);

View File

@@ -609,6 +609,22 @@ select:focus {
margin: 0 0 10px; 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 { .table-primary {
font-weight: 700; font-weight: 700;
} }