import { sortableTable, sortValuesAttribute } from "./list_sorting.js"; function buildArchiveRows(state, { getEventName, getClassName, getTrackLayoutLabel }) { return (state.sessions || []) .filter((session) => session.status === "finished") .map((session) => { const event = state.events.find((item) => item.id === session.eventId) || null; const result = state.resultsBySession?.[session.id] || null; const competitors = result ? Object.values(result.competitors || {}) : []; const winner = competitors .slice() .sort((left, right) => (Number(right.laps || 0) - Number(left.laps || 0)) || (Number(left.totalElapsedMs || 0) - Number(right.totalElapsedMs || 0)))[0] || null; return { session, event, result, competitors, eventName: event ? getEventName(event.id) : session.name, className: event ? getClassName(event.classId) : "", date: event?.date || (session.endedAt ? new Date(session.endedAt).toISOString().slice(0, 10) : ""), winnerName: winner?.displayName || winner?.driverName || "", adjustmentCount: (result?.adjustments || []).length, trackLayoutLabel: getTrackLayoutLabel(state.trackLayouts, session.trackLayoutId), }; }) .sort((left, right) => Number(right.session.endedAt || 0) - Number(left.session.endedAt || 0)); } function matchesArchiveFilters(row, filters) { const search = filters.search.trim().toLowerCase(); if (search && !`${row.eventName} ${row.session.name}`.toLowerCase().includes(search)) return false; if (filters.classId && row.event?.classId !== filters.classId) return false; if (filters.driverId && !row.competitors.some((competitor) => String(competitor.driverId || "") === filters.driverId)) return false; if (filters.dateFrom && row.date && row.date < filters.dateFrom) return false; if (filters.dateTo && row.date && row.date > filters.dateTo) return false; return true; } function renderArchiveDetail(row, deps) { const { t, escapeHtml, formatLap, formatRaceClock, renderTable, buildLeaderboard, getCompetitorPassings } = deps; const { session, event, result } = row; const leaderboard = buildLeaderboard(session); const adjustments = result?.adjustments || []; return `

${escapeHtml(row.eventName)} - ${escapeHtml(session.name)}${row.trackLayoutLabel ? ` ${escapeHtml(row.trackLayoutLabel)}` : ""}

${event ? `` : ""} ${event ? `` : ""}

${t("archive.standings")}

${leaderboard.length ? renderTable( [t("table.pos"), t("table.driver"), t("table.laps"), t("table.best_lap"), t("timing.total_time")], leaderboard.map((rowItem, index) => ` ${index + 1} ${escapeHtml(rowItem.displayName || rowItem.driverName || t("common.unknown_driver"))} ${rowItem.laps} ${formatLap(rowItem.bestLapMs)} ${escapeHtml(rowItem.resultDisplay || "-")} `) ) : `

${t("archive.no_standings")}

`}

${t("archive.penalties")}

${adjustments.length ? renderTable( [t("table.time"), t("table.driver"), t("archive.action"), t("archive.detail")], adjustments.slice().reverse().map((adjustment) => ` ${escapeHtml(new Date(adjustment.ts || 0).toLocaleTimeString())} ${escapeHtml(adjustment.displayName || "-")} ${escapeHtml(t(`archive.adjustment_${adjustment.category}`) || adjustment.category || "-")} ${escapeHtml(adjustment.detail || "-")} `) ) : `

${t("archive.no_penalties")}

`}

${t("archive.lap_history")}

${leaderboard.map((rowItem) => { const passings = getCompetitorPassings(session, rowItem); return `
${escapeHtml(rowItem.displayName || rowItem.driverName || t("common.unknown_driver"))} - ${rowItem.laps} ${t("table.laps")} ${passings.length ? renderTable( [t("table.lap"), t("table.time"), t("table.last_lap")], passings.map((passing, index) => ` ${index + 1} ${formatRaceClock(Math.max(0, passing.timestamp - (session.startedAt || passing.timestamp)))} ${formatLap(passing.lapMs)} `) ) : `

${t("timing.no_lap_history")}

`}
`; }).join("") || `

${t("archive.no_standings")}

`}
`; } export function renderResultsArchiveView(deps) { const { state, dom, t, escapeHtml, formatLap, formatRaceClock, renderTable, getClassName, getEventName, buildLeaderboard, getCompetitorPassings, exportRaceResultsPdf, buildRacePackagePayload, downloadJsonFile, sanitizeFilenameSegment, getArchiveFilters, setArchiveFilters, getSelectedSessionId, setSelectedSessionId, getTrackLayoutLabel, } = deps; const filters = getArchiveFilters(); const allRows = buildArchiveRows(state, { getEventName, getClassName, getTrackLayoutLabel }); const filteredRows = allRows.filter((row) => matchesArchiveFilters(row, filters)); const selectedSessionId = getSelectedSessionId(); const selectedRow = selectedSessionId ? allRows.find((row) => row.session.id === selectedSessionId) : null; const classOptions = state.classes.map((c) => ``).join(""); const driverOptions = state.drivers.map((d) => ``).join(""); const table = sortableTable( renderTable( [t("archive.date"), t("archive.event"), t("archive.session"), t("archive.class"), t("archive.layout"), t("archive.drivers"), t("archive.winner"), t("archive.penalties_short"), ""], filteredRows.map((row) => ` ${escapeHtml(row.date || "-")} ${escapeHtml(row.eventName)} ${escapeHtml(row.session.name)} ${escapeHtml(row.className || "-")} ${escapeHtml(row.trackLayoutLabel || "-")} ${row.competitors.length} ${escapeHtml(row.winnerName || "-")} ${row.adjustmentCount || "-"} `) ), "archive-sessions", [1, 2, 3, 4, 6] ); dom.view.innerHTML = `

${t("archive.title")}

${t("archive.hint")}

${filteredRows.length ? table : `

${t("archive.no_results")}

`}
${selectedRow ? renderArchiveDetail(selectedRow, { t, escapeHtml, formatLap, formatRaceClock, renderTable, buildLeaderboard, getCompetitorPassings }) : ""} `; const rerender = () => renderResultsArchiveView(deps); document.getElementById("archiveSearch")?.addEventListener("input", (event) => { const input = event.currentTarget; if (!(input instanceof HTMLInputElement)) return; // A full re-render replaces this input's DOM node, which would otherwise drop focus and // cursor position on every keystroke - restore both immediately after. const caret = input.selectionStart; setArchiveFilters({ ...filters, search: input.value }); rerender(); const restored = document.getElementById("archiveSearch"); if (restored instanceof HTMLInputElement) { restored.focus(); restored.setSelectionRange(caret, caret); } }); document.getElementById("archiveClassFilter")?.addEventListener("change", (event) => { setArchiveFilters({ ...filters, classId: event.currentTarget.value }); rerender(); }); document.getElementById("archiveDriverFilter")?.addEventListener("change", (event) => { setArchiveFilters({ ...filters, driverId: event.currentTarget.value }); rerender(); }); document.getElementById("archiveDateFrom")?.addEventListener("change", (event) => { setArchiveFilters({ ...filters, dateFrom: event.currentTarget.value }); rerender(); }); document.getElementById("archiveDateTo")?.addEventListener("change", (event) => { setArchiveFilters({ ...filters, dateTo: event.currentTarget.value }); rerender(); }); document.querySelectorAll("[data-archive-open]").forEach((button) => { button.addEventListener("click", () => { setSelectedSessionId(button.dataset.archiveOpen); rerender(); }); }); document.getElementById("archiveDetailClose")?.addEventListener("click", () => { setSelectedSessionId(""); rerender(); }); if (selectedRow?.event) { document.getElementById("archiveExportPdf")?.addEventListener("click", () => { exportRaceResultsPdf(selectedRow.event); }); document.getElementById("archiveExportJson")?.addEventListener("click", () => { const payload = buildRacePackagePayload(selectedRow.event.id); downloadJsonFile(`${sanitizeFilenameSegment(selectedRow.event.name)}_race_package.json`, payload); }); } }