Add a time-of-day race day schedule to Event Manager

Events can now be given a start time and a gap between sessions on a
new "Schedule" panel; every session's planned clock time cascades
automatically in running order (practice, qualifying, finals), and
once a session actually starts or finishes, everything after it
re-anchors to that real time - a late start or a session that ran
long (including any pause) pushes the rest of the day forward with
no manual reschedule step. The scheduled time now also shows on the
printed/PDF start lists.

Printable start lists and manually moving a driver to start last in
their own heat (for a mechanic-time request) already existed, so
this only builds the missing piece: the schedule itself.

Also fixes a real pre-existing bug in the same session-ordering logic
this feature relies on: getSessionSortWeight used `order[type] || 99`,
which silently treated open_practice's own weight of 0 as falsy and
sorted practice sessions dead last instead of first in print/PDF
output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
larssand
2026-09-10 22:33:57 +02:00
parent a449bec07d
commit cf89086dff
11 changed files with 250 additions and 8 deletions

View File

@@ -1,9 +1,21 @@
function formatDateTimeLocalValue(ms) {
const date = new Date(ms);
date.setSeconds(0, 0);
date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
return date.toISOString().slice(0, 16);
}
function formatClockTime(ms) {
return new Date(ms).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });
}
export function renderEventManagerMarkup(context) {
const {
event,
t,
escapeHtml,
sessions,
eventSchedule,
sessionTypeChoices,
sessionTypeHintKey,
raceTeams,
@@ -742,6 +754,39 @@ export function renderEventManagerMarkup(context) {
</section>
` : ""}
<section class="panel mt-16">
<div class="panel-header"><h3>${t("events.schedule_title")}</h3></div>
<div class="panel-body">
<p class="hint">${t("events.schedule_hint")}</p>
<form id="scheduleForm" class="form-grid cols-3">
<label>${t("events.schedule_start_time")}<input type="datetime-local" name="scheduleStartAt" value="${event.raceConfig?.scheduleStartAt ? escapeHtml(formatDateTimeLocalValue(event.raceConfig.scheduleStartAt)) : ""}" /></label>
<label>${t("events.schedule_gap")}<input type="number" min="0" step="1" name="scheduleGapMin" value="${Number(event.raceConfig?.scheduleGapMin ?? 3)}" /></label>
<div class="actions">
<button class="btn btn-primary" type="submit">${t("events.schedule_save")}</button>
${event.raceConfig?.scheduleStartAt ? `<button id="scheduleClear" class="btn btn-danger" type="button">${t("events.schedule_clear")}</button>` : ""}
</div>
</form>
${eventSchedule && eventSchedule.length
? renderTable(
[t("table.session"), t("table.type"), t("events.schedule_planned"), t("events.schedule_actual"), ""],
eventSchedule.map((row) => {
const session = sessions.find((item) => item.id === row.sessionId);
if (!session) return "";
return `
<tr>
<td>${escapeHtml(session.name)}</td>
<td>${escapeHtml(getSessionTypeLabel(session.type))}</td>
<td>${escapeHtml(formatClockTime(row.plannedStart))} - ${escapeHtml(formatClockTime(row.plannedEnd))}</td>
<td>${row.actualStart ? escapeHtml(formatClockTime(row.actualStart)) + (row.actualEnd ? ` - ${escapeHtml(formatClockTime(row.actualEnd))}` : "") : "-"}</td>
<td>${row.delayed ? `<span class="pill pill-warn">${t("events.schedule_delayed")}</span>` : ""}</td>
</tr>
`;
})
)
: `<p class="hint">${t("events.schedule_not_set")}</p>`}
</div>
</section>
<section class="panel mt-16">
<div class="panel-header"><h3>${t("events.final_matrix")}</h3></div>
<div class="panel-body">