`;
})
.join("");
popup.style.display = "flex";
}
function applyView() {
const tableWrap = document.getElementById("events-table-wrap") as HTMLElement;
const calWrap = document.getElementById("events-calendar-wrap") as HTMLElement;
const summary = document.getElementById("events-summary") as HTMLElement;
// Active state on selector buttons.
document.querySelectorAll("[data-event-view]").forEach((b) => {
b.classList.toggle("events-view-btn-active", b.dataset.eventView === currentView);
});
// Body class so CSS can hook in if needed downstream.
document.body.classList.toggle("events-view-cards", currentView === "cards");
document.body.classList.toggle("events-view-list", currentView === "list");
document.body.classList.toggle("events-view-calendar", currentView === "calendar");
// Cards view = the original layout (5-card summary + table).
// List view = no summary cards, table only — gives more vertical space
// and matches users' mental model of a flat list.
// Calendar view = month grid; cards + table both hidden.
summary.style.display = currentView === "cards" ? "" : "none";
tableWrap.style.display = currentView === "calendar" ? "none" : "";
calWrap.hidden = currentView !== "calendar";
if (currentView === "calendar" && loadedOK) renderCalendar();
}
function wireRowHandlers(tbody: HTMLElement) {
tbody.querySelectorAll(".events-row").forEach((row) => {
const id = row.dataset.id!;
const type = row.dataset.type!;
row.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
if (
target.closest(".frist-complete-cb") ||
target.closest(".frist-reopen-btn") ||
target.closest("a")
) return;
window.location.href = type === "deadline" ? `/deadlines/${id}` : `/appointments/${id}`;
});
const cb = row.querySelector(".frist-complete-cb");
if (cb && !cb.disabled) {
cb.addEventListener("change", async () => {
if (!cb.checked) return;
cb.disabled = true;
try {
const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
if (resp.ok) {
await Promise.all([loadList(), loadSummary()]);
} else {
cb.checked = false;
cb.disabled = false;
}
} catch {
cb.checked = false;
cb.disabled = false;
}
});
}
const reopenBtn = row.querySelector(".frist-reopen-btn");
if (reopenBtn) {
reopenBtn.addEventListener("click", async () => {
reopenBtn.disabled = true;
try {
const resp = await fetch(`/api/deadlines/${id}/reopen`, { method: "PATCH" });
if (resp.ok) {
await Promise.all([loadList(), loadSummary()]);
} else {
reopenBtn.disabled = false;
}
} catch {
reopenBtn.disabled = false;
}
});
}
});
}
function isFilterPristine(): boolean {
return (
statusFilter === defaultStatusFor(currentType) &&
!projectFilter &&
!appointmentTypeFilter &&
(eventTypeFilter?.toQueryValue() ?? "") === ""
);
}
function applyTypeVisibility() {
// Page-level chrome — heading, subtitle, actions, type chips' active state.
const heading = document.getElementById("events-heading");
const subtitle = document.getElementById("events-subtitle");
const title = document.getElementById("events-title");
const isAppointment = currentType === "appointment";
const isDeadline = currentType === "deadline";
const isAll = currentType === "all";
if (heading) heading.textContent = isAppointment ? t("appointments.list.heading") : t("deadlines.list.heading");
if (subtitle) subtitle.textContent = isAppointment ? t("appointments.list.subtitle") : t("deadlines.list.subtitle");
if (title) title.textContent = isAppointment ? t("appointments.list.title") : t("deadlines.list.title");
// Single "Neu …" CTA per type. View axis is the calendar/list/cards selector.
toggleDisplay("events-action-new-deadline", isDeadline || isAll, "inline-flex");
toggleDisplay("events-action-new-appointment", isAppointment || isAll, "inline-flex");
// Empty-state CTAs.
toggleDisplay("events-empty-cta-deadline", isDeadline || isAll, "inline-flex");
toggleDisplay("events-empty-cta-appointment", isAppointment || isAll, "inline-flex");
// Status filter applies to all types — for appointments the option set
// narrows to date buckets only (Heute / Diese Woche / Nächste Woche /
// Später / Alle). populateStatusFilter handles the option swap and may
// reset statusFilter when switching from a deadline-only value.
populateStatusFilter();
// Event-type multi-select is deadline-only (appointments have no event_types).
toggleFilterPair("events-filter-event-type", !isAppointment);
// The panel is a popup the trigger owns via `panel.hidden`. Never stamp
// `display: block` on it from the type filter — that overrides the
// `.multi-panel[hidden]` CSS rule and leaves the panel visible on larger
// screens. When switching to appointment view, force-close it.
const eventTypePanel = document.getElementById("events-filter-event-type-panel") as HTMLElement | null;
if (eventTypePanel) {
eventTypePanel.style.display = "";
if (isAppointment) eventTypePanel.hidden = true;
}
// Termin-Typ is appointment-only.
toggleFilterPair("events-filter-appointment-type", !isDeadline);
// "Nur persönliche" applies uniformly to deadlines + appointments now
// (t-paliad-128) — items where the caller is the creator. Always available.
// Update chip active state.
document.querySelectorAll('[data-event-type]').forEach((b) => {
b.classList.toggle("agenda-chip-active", b.dataset.eventType === currentType);
});
// Refresh overdue card visibility on type change.
applyOverdueState(parseInt(document.getElementById("events-sum-overdue")?.textContent ?? "0", 10));
// Sidebar highlight (t-paliad-115). The bare /events HTML SSRs with
// currentPath="/events" so neither Fristen nor Termine sidebar entries
// match — we re-highlight at hydration based on the active type so the
// user always sees the correct nav entry lit. Same logic for BottomNav,
// which today doesn't carry these entries but stays consistent if it
// ever does.
applySidebarTypeHighlight();
}
function applySidebarTypeHighlight() {
const items = document.querySelectorAll(".sidebar-item");
for (const a of items) {
const href = a.getAttribute("href") ?? "";
if (href === "/events?type=deadline") {
a.classList.toggle("active", currentType === "deadline");
} else if (href === "/events?type=appointment") {
a.classList.toggle("active", currentType === "appointment");
}
}
}
function toggleDisplay(id: string, show: boolean, displayWhenShown = "block") {
const el = document.getElementById(id);
if (!el) return;
el.style.display = show ? displayWhenShown : "none";
}
// toggleFilterPair shows/hides the wrapping .filter-group so the label,
// control, and (for the multi-select) the .multi-anchor collapse together
// when a filter doesn't apply for the current type.
function toggleFilterPair(controlID: string, show: boolean) {
const ctrl = document.getElementById(controlID);
const group = ctrl?.closest(".filter-group");
if (group) group.style.display = show ? "" : "none";
}
function syncURLParams() {
const url = new URL(window.location.href);
url.searchParams.delete("type");
url.searchParams.delete("view");
url.searchParams.delete("status");
url.searchParams.delete("project_id");
url.searchParams.delete("personal_only");
url.searchParams.delete("event_type");
url.searchParams.delete("type_filter");
// Default type per route comes from window.__PALIAD_EVENTS__; only
// surface in the URL when the user opted into something else.
if (currentType !== defaultType()) url.searchParams.set("type", currentType);
if (currentView !== "cards") url.searchParams.set("view", currentView);
if (statusFilter && statusFilter !== defaultStatusFor(currentType)) {
url.searchParams.set("status", statusFilter);
}
if (projectFilter) url.searchParams.set("project_id", projectFilter);
if (currentType !== "appointment") {
const eventQuery = eventTypeFilter?.toQueryValue() ?? "";
if (eventQuery) url.searchParams.set("event_type", eventQuery);
}
if (currentType === "appointment" && appointmentTypeFilter) {
url.searchParams.set("type_filter", appointmentTypeFilter);
}
window.history.replaceState(null, "", url.toString());
}
// populateStatusFilter rebuilds the Status `