feat(t-paliad-126): Fristenrechner auto-calc on tab open + input change
The Verfahrensablauf and "Was kommt nach" tabs now render results immediately, without requiring a click on "Fristen berechnen". The button stays as a manual force-recalc affordance. - Pre-select the first proceeding type on load so step 3 has data out of the box. - Pre-select the first trigger event on first event-tab activation (or right after the list loads if the tab was already active). - Auto-recalc on date / proceeding-type / condition-flag change. - Debounce input events to 200ms so spam-edits coalesce into one request, with a per-mode sequence counter so a stale fetch result can never overwrite a fresher one.
This commit is contained in:
@@ -54,6 +54,19 @@ const PARTY_CLASS: Record<string, string> = {
|
||||
|
||||
let lastResponse: DeadlineResponse | null = null;
|
||||
|
||||
// Auto-calc plumbing: a sequence counter prevents stale fetches from clobbering
|
||||
// fresher results, and a single timer debounces rapid input changes.
|
||||
let procCalcSeq = 0;
|
||||
let procCalcTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function scheduleProcCalc(delayMs = 200) {
|
||||
if (procCalcTimer !== null) clearTimeout(procCalcTimer);
|
||||
procCalcTimer = setTimeout(() => {
|
||||
procCalcTimer = null;
|
||||
void calculate();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
onLangChange(() => {
|
||||
if (lastResponse) renderTimeline(lastResponse);
|
||||
// Update trigger event name if a proceeding is selected
|
||||
@@ -163,6 +176,7 @@ function showStep(n: number) {
|
||||
}
|
||||
|
||||
async function calculate() {
|
||||
const seq = ++procCalcSeq;
|
||||
const dateInput = document.getElementById("trigger-date") as HTMLInputElement;
|
||||
const triggerDate = dateInput.value;
|
||||
if (!triggerDate || !selectedType) return;
|
||||
@@ -188,6 +202,7 @@ async function calculate() {
|
||||
}),
|
||||
});
|
||||
|
||||
if (seq !== procCalcSeq) return;
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
console.error("API error:", err);
|
||||
@@ -195,6 +210,7 @@ async function calculate() {
|
||||
}
|
||||
|
||||
const data: DeadlineResponse = await resp.json();
|
||||
if (seq !== procCalcSeq) return;
|
||||
lastResponse = data;
|
||||
renderTimeline(data);
|
||||
showStep(3);
|
||||
@@ -467,39 +483,56 @@ function reset() {
|
||||
showStep(1);
|
||||
}
|
||||
|
||||
function selectProceeding(btn: HTMLButtonElement) {
|
||||
document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
selectedType = btn.dataset.code!;
|
||||
|
||||
// Update trigger event name
|
||||
const name = btn.querySelector("strong")?.textContent || "";
|
||||
document.getElementById("trigger-event")!.textContent = name;
|
||||
|
||||
// Conditional inputs: priority date for EP_GRANT, CCR toggle for UPC_INF.
|
||||
const priorityRow = document.getElementById("priority-date-row");
|
||||
if (priorityRow) priorityRow.style.display = selectedType === "EP_GRANT" ? "" : "none";
|
||||
const ccrRow = document.getElementById("ccr-flag-row");
|
||||
if (ccrRow) ccrRow.style.display = selectedType === "UPC_INF" ? "" : "none";
|
||||
|
||||
showStep(2);
|
||||
scheduleProcCalc(0);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
|
||||
// Proceeding type selection
|
||||
document.querySelectorAll<HTMLButtonElement>(".proceeding-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
selectedType = btn.dataset.code!;
|
||||
|
||||
// Update trigger event name
|
||||
const name = btn.querySelector("strong")?.textContent || "";
|
||||
document.getElementById("trigger-event")!.textContent = name;
|
||||
|
||||
// Conditional inputs: priority date for EP_GRANT, CCR toggle for UPC_INF.
|
||||
const priorityRow = document.getElementById("priority-date-row");
|
||||
if (priorityRow) priorityRow.style.display = selectedType === "EP_GRANT" ? "" : "none";
|
||||
const ccrRow = document.getElementById("ccr-flag-row");
|
||||
if (ccrRow) ccrRow.style.display = selectedType === "UPC_INF" ? "" : "none";
|
||||
|
||||
showStep(2);
|
||||
});
|
||||
btn.addEventListener("click", () => selectProceeding(btn));
|
||||
});
|
||||
|
||||
// Calculate button
|
||||
document.getElementById("calculate-btn")!.addEventListener("click", calculate);
|
||||
// Calculate button — manual force-recalc affordance.
|
||||
document.getElementById("calculate-btn")!.addEventListener("click", () => scheduleProcCalc(0));
|
||||
|
||||
// Also calculate on Enter in date field
|
||||
document.getElementById("trigger-date")!.addEventListener("keydown", (e) => {
|
||||
if ((e as KeyboardEvent).key === "Enter") calculate();
|
||||
// Auto-recalc on input changes. Date `change` covers picker and blur;
|
||||
// `input` covers manual typing. The Enter key on the date field bypasses
|
||||
// debounce for keyboard-savvy users.
|
||||
const dateInput = document.getElementById("trigger-date") as HTMLInputElement;
|
||||
dateInput.addEventListener("change", () => scheduleProcCalc());
|
||||
dateInput.addEventListener("input", () => scheduleProcCalc());
|
||||
dateInput.addEventListener("keydown", (e) => {
|
||||
if ((e as KeyboardEvent).key === "Enter") scheduleProcCalc(0);
|
||||
});
|
||||
|
||||
const priorityInput = document.getElementById("priority-date") as HTMLInputElement | null;
|
||||
if (priorityInput) {
|
||||
priorityInput.addEventListener("change", () => scheduleProcCalc());
|
||||
priorityInput.addEventListener("input", () => scheduleProcCalc());
|
||||
}
|
||||
|
||||
const ccrFlag = document.getElementById("ccr-flag") as HTMLInputElement | null;
|
||||
if (ccrFlag) ccrFlag.addEventListener("change", () => scheduleProcCalc(0));
|
||||
|
||||
// Reset button
|
||||
document.getElementById("reset-btn")!.addEventListener("click", reset);
|
||||
|
||||
@@ -515,6 +548,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
// Event-mode wiring (PR-2: youpc-parity trigger-event lookup)
|
||||
initEventMode();
|
||||
|
||||
// Pre-select the first proceeding button so the deadline list renders
|
||||
// immediately on page load — no click on "Fristen berechnen" required.
|
||||
// This also fires the first auto-calc via scheduleProcCalc().
|
||||
const firstBtn = document.querySelector<HTMLButtonElement>(".proceeding-btn");
|
||||
if (firstBtn) selectProceeding(firstBtn);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
@@ -537,6 +576,11 @@ function initModeTabs() {
|
||||
document.querySelectorAll<HTMLElement>(".mode-panel").forEach((panel) => {
|
||||
panel.hidden = panel.dataset.mode !== mode;
|
||||
});
|
||||
// Auto-calc on tab activation. Procedure mode self-bootstraps via the
|
||||
// pre-selected proceeding button on init, so the only special case is
|
||||
// event mode: pick a default trigger event the first time the tab is
|
||||
// shown so step 3 isn't empty.
|
||||
if (mode === "event") ensureDefaultTriggerEvent();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -581,6 +625,18 @@ let triggerEvents: TriggerEventSummary[] = [];
|
||||
let selectedTrigger: TriggerEventSummary | null = null;
|
||||
let lastEventResponse: EventCalculateResponse | null = null;
|
||||
|
||||
// Auto-calc plumbing for event mode — same shape as procedure mode.
|
||||
let eventCalcSeq = 0;
|
||||
let eventCalcTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
function scheduleEventCalc(delayMs = 200) {
|
||||
if (eventCalcTimer !== null) clearTimeout(eventCalcTimer);
|
||||
eventCalcTimer = setTimeout(() => {
|
||||
eventCalcTimer = null;
|
||||
void calculateEvent();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function showEventStep(n: number) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const el = document.getElementById(`event-step-${i}`);
|
||||
@@ -646,6 +702,10 @@ async function loadTriggerEvents() {
|
||||
}
|
||||
triggerEvents = (await resp.json()) as TriggerEventSummary[];
|
||||
renderEventList("");
|
||||
// If the user already switched to the event tab while the list was
|
||||
// loading, pre-select now so they don't see an empty step 1.
|
||||
const eventTab = document.getElementById("mode-event-tab");
|
||||
if (eventTab?.classList.contains("is-active")) ensureDefaultTriggerEvent();
|
||||
} catch {
|
||||
list.innerHTML = `<li class="event-list-empty">${escHtml(t("deadlines.event.error"))}</li>`;
|
||||
}
|
||||
@@ -658,9 +718,11 @@ function selectTriggerEvent(id: number) {
|
||||
const nameEl = document.getElementById("event-selected-name");
|
||||
if (nameEl) nameEl.textContent = eventName(ev);
|
||||
showEventStep(2);
|
||||
scheduleEventCalc(0);
|
||||
}
|
||||
|
||||
async function calculateEvent() {
|
||||
const seq = ++eventCalcSeq;
|
||||
if (!selectedTrigger) return;
|
||||
const dateInput = document.getElementById("event-date") as HTMLInputElement | null;
|
||||
const triggerDate = dateInput?.value;
|
||||
@@ -672,12 +734,14 @@ async function calculateEvent() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ triggerEventId: selectedTrigger.id, triggerDate }),
|
||||
});
|
||||
if (seq !== eventCalcSeq) return;
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
console.error("event API error:", err);
|
||||
return;
|
||||
}
|
||||
const data = (await resp.json()) as EventCalculateResponse;
|
||||
if (seq !== eventCalcSeq) return;
|
||||
lastEventResponse = data;
|
||||
renderEventResults(data);
|
||||
showEventStep(3);
|
||||
@@ -780,14 +844,33 @@ function initEventMode() {
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("event-calculate-btn")?.addEventListener("click", calculateEvent);
|
||||
document.getElementById("event-date")?.addEventListener("keydown", (e) => {
|
||||
if ((e as KeyboardEvent).key === "Enter") calculateEvent();
|
||||
});
|
||||
// Event-tab calculate button — manual force-recalc affordance.
|
||||
document.getElementById("event-calculate-btn")?.addEventListener("click", () => scheduleEventCalc(0));
|
||||
|
||||
// Auto-recalc when the user changes the event date.
|
||||
const eventDate = document.getElementById("event-date") as HTMLInputElement | null;
|
||||
if (eventDate) {
|
||||
eventDate.addEventListener("change", () => scheduleEventCalc());
|
||||
eventDate.addEventListener("input", () => scheduleEventCalc());
|
||||
eventDate.addEventListener("keydown", (e) => {
|
||||
if ((e as KeyboardEvent).key === "Enter") scheduleEventCalc(0);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("event-reset-btn")?.addEventListener("click", resetEventMode);
|
||||
document.getElementById("event-print-btn")?.addEventListener("click", () => window.print());
|
||||
}
|
||||
|
||||
// Pre-select the first trigger event so the "Was kommt nach" tab renders
|
||||
// immediately with default selection + today's date. Idempotent — only fires
|
||||
// when there's no existing selection. Called both after the event list loads
|
||||
// and when the user first activates the event tab, since either one can be
|
||||
// the trigger depending on which finishes first.
|
||||
function ensureDefaultTriggerEvent() {
|
||||
if (selectedTrigger || triggerEvents.length === 0) return;
|
||||
selectTriggerEvent(triggerEvents[0].id);
|
||||
}
|
||||
|
||||
// Re-render event results when language flips (titles/notes are bilingual).
|
||||
onLangChange(() => {
|
||||
if (lastEventResponse) renderEventResults(lastEventResponse);
|
||||
|
||||
Reference in New Issue
Block a user