stint-/förarbyteslogg per lag
This commit is contained in:
215
src/app.js
215
src/app.js
@@ -221,6 +221,10 @@ const TRANSLATIONS = {
|
||||
"events.team_standings": "Lagställning",
|
||||
"events.no_team_results": "Inga teamresultat ännu.",
|
||||
"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.set_active": "Sätt aktiv",
|
||||
"events.assignments": "Tilldelningar",
|
||||
@@ -729,6 +733,10 @@ const TRANSLATIONS = {
|
||||
"events.team_standings": "Team standings",
|
||||
"events.no_team_results": "No team results yet.",
|
||||
"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.set_active": "Set Active",
|
||||
"events.assignments": "Assignments",
|
||||
@@ -3161,8 +3169,14 @@ function renderEventManager(eventId) {
|
||||
<section class="panel mt-16">
|
||||
<div class="panel-header"><h3>${t("events.team_standings")}</h3></div>
|
||||
<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)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
@@ -3713,6 +3727,10 @@ function renderEventManager(eventId) {
|
||||
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 () => {
|
||||
await exportRaceStartListsPdf(event);
|
||||
});
|
||||
@@ -3721,6 +3739,10 @@ function renderEventManager(eventId) {
|
||||
await exportRaceResultsPdf(event);
|
||||
});
|
||||
|
||||
document.getElementById("pdfTeamResults")?.addEventListener("click", async () => {
|
||||
await exportTeamRaceResultsPdf(event);
|
||||
});
|
||||
|
||||
document.getElementById("gridResetOrder")?.addEventListener("click", () => {
|
||||
if (!selectedGridSession) {
|
||||
return;
|
||||
@@ -4727,7 +4749,7 @@ function renderLeaderboard(rows) {
|
||||
<td><span class="pos-pill ${posClass}">${idx + 1}</span></td>
|
||||
<td>
|
||||
<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>${escapeHtml(row.subLabel || row.carName)}</td>
|
||||
<td>${escapeHtml(row.transponder)}</td>
|
||||
@@ -4762,7 +4784,7 @@ function renderOverlayLeaderboard(rows) {
|
||||
</div>
|
||||
<div class="overlay-race-driver">
|
||||
<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-meta">
|
||||
<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>
|
||||
<strong>${escapeHtml(row.displayName || row.driverName)}</strong>
|
||||
<p>${escapeHtml(row.resultDisplay || "-")}</p>
|
||||
<small>${t("overlay.active_member")}: ${escapeHtml(row.subLabel || "-")}</small>
|
||||
<small>${t("overlay.active_member")}: ${escapeHtml(formatTeamActiveMemberLabel(row))}</small>
|
||||
</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) {
|
||||
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 `<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) {
|
||||
@@ -6108,7 +6215,7 @@ function renderTeamRaceStandings(event) {
|
||||
<td>${index + 1}</td>
|
||||
<td>
|
||||
<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>${row.laps}</td>
|
||||
<td>${escapeHtml(row.resultDisplay)}</td>
|
||||
@@ -6119,6 +6226,10 @@ function renderTeamRaceStandings(event) {
|
||||
)
|
||||
: `<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>
|
||||
`
|
||||
)
|
||||
@@ -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) {
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user