Merge: deadlines/{id} notfound + Invalid Date list (t-paliad-039)
This commit is contained in:
@@ -58,7 +58,7 @@ export function renderAppointmentsCalendar(): string {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="frist-calendar" id="termin-calendar">
|
||||
<div className="frist-calendar" id="appointment-calendar">
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.mon">Mo</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.tue">Di</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.wed">Mi</div>
|
||||
@@ -66,10 +66,10 @@ export function renderAppointmentsCalendar(): string {
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.fri">Fr</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.sat">Sa</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.sun">So</div>
|
||||
<div id="termin-cal-grid" className="frist-cal-grid" />
|
||||
<div id="appointment-cal-grid" className="frist-cal-grid" />
|
||||
</div>
|
||||
|
||||
<p className="akten-events-empty" id="termin-cal-empty" style="display:none" data-i18n="termine.kalender.empty">
|
||||
<p className="akten-events-empty" id="appointment-cal-empty" style="display:none" data-i18n="termine.kalender.empty">
|
||||
Keine Termine im ausgewählten Zeitraum.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -19,48 +19,48 @@ export function renderAppointmentsDetail(): string {
|
||||
<div className="container container-narrow">
|
||||
<a href="/appointments" className="akten-back-link" data-i18n="termine.detail.back">← Zurück zur Übersicht</a>
|
||||
|
||||
<div id="termin-loading" className="akten-loading" data-i18n="termine.detail.loading">Lädt…</div>
|
||||
<div id="termin-not-found" style="display:none" className="akten-empty">
|
||||
<div id="appointment-loading" className="akten-loading" data-i18n="termine.detail.loading">Lädt…</div>
|
||||
<div id="appointment-not-found" style="display:none" className="akten-empty">
|
||||
<h2 data-i18n="termine.detail.notfound">Termin nicht gefunden</h2>
|
||||
<p data-i18n="termine.detail.notfound.hint">Der Termin existiert nicht oder Sie haben keine Berechtigung.</p>
|
||||
</div>
|
||||
|
||||
<div id="termin-body" style="display:none">
|
||||
<div id="appointment-body" style="display:none">
|
||||
<div className="tool-header">
|
||||
<span className="termin-type-badge" id="termin-type-badge" />
|
||||
<h1 id="termin-title-display" />
|
||||
<p className="tool-subtitle" id="termin-time-display" />
|
||||
<span className="termin-type-badge" id="appointment-type-badge" />
|
||||
<h1 id="appointment-title-display" />
|
||||
<p className="tool-subtitle" id="appointment-time-display" />
|
||||
</div>
|
||||
|
||||
<div id="termin-project-row" className="akten-detail-meta-row" style="display:none">
|
||||
<div id="appointment-project-row" className="akten-detail-meta-row" style="display:none">
|
||||
<span className="akten-detail-meta-label" data-i18n="termine.detail.akte">Akte:</span>
|
||||
<a id="termin-project-link" className="akten-ref-link" />
|
||||
<a id="appointment-project-link" className="akten-ref-link" />
|
||||
</div>
|
||||
|
||||
<section className="termin-notes-section">
|
||||
<h2 className="frist-section-heading" data-i18n="notizen.section.title">Notizen</h2>
|
||||
<div id="notes-container" className="notiz-container" data-parent-type="termin" />
|
||||
<div id="notes-container" className="notiz-container" data-parent-type="appointment" />
|
||||
</section>
|
||||
|
||||
<form id="termin-edit-form" className="akten-form">
|
||||
<form id="appointment-edit-form" className="akten-form">
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-title-edit" data-i18n="termine.field.title">Titel</label>
|
||||
<input type="text" id="termin-title-edit" required />
|
||||
<label htmlFor="appointment-title-edit" data-i18n="termine.field.title">Titel</label>
|
||||
<input type="text" id="appointment-title-edit" required />
|
||||
</div>
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-start-edit" data-i18n="termine.field.start">Beginn</label>
|
||||
<input type="datetime-local" id="termin-start-edit" required />
|
||||
<label htmlFor="appointment-start-edit" data-i18n="termine.field.start">Beginn</label>
|
||||
<input type="datetime-local" id="appointment-start-edit" required />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-end-edit" data-i18n="termine.field.end">Ende (optional)</label>
|
||||
<input type="datetime-local" id="termin-end-edit" />
|
||||
<label htmlFor="appointment-end-edit" data-i18n="termine.field.end">Ende (optional)</label>
|
||||
<input type="datetime-local" id="appointment-end-edit" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-type-edit" data-i18n="termine.field.type">Typ</label>
|
||||
<select id="termin-type-edit">
|
||||
<label htmlFor="appointment-type-edit" data-i18n="termine.field.type">Typ</label>
|
||||
<select id="appointment-type-edit">
|
||||
<option value="" data-i18n="termine.field.type.none">Kein Typ</option>
|
||||
<option value="hearing" data-i18n="termine.type.hearing">Verhandlung</option>
|
||||
<option value="meeting" data-i18n="termine.type.meeting">Besprechung</option>
|
||||
@@ -69,19 +69,19 @@ export function renderAppointmentsDetail(): string {
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-location-edit" data-i18n="termine.field.location">Ort</label>
|
||||
<input type="text" id="termin-location-edit" />
|
||||
<label htmlFor="appointment-location-edit" data-i18n="termine.field.location">Ort</label>
|
||||
<input type="text" id="appointment-location-edit" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-description-edit" data-i18n="termine.field.description">Beschreibung</label>
|
||||
<textarea id="termin-description-edit" rows={3} />
|
||||
<label htmlFor="appointment-description-edit" data-i18n="termine.field.description">Beschreibung</label>
|
||||
<textarea id="appointment-description-edit" rows={3} />
|
||||
</div>
|
||||
|
||||
<p className="form-msg" id="termin-edit-msg" />
|
||||
<p className="form-msg" id="appointment-edit-msg" />
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="button" id="termin-delete-btn" className="btn-danger" data-i18n="termine.detail.delete">Termin löschen</button>
|
||||
<button type="button" id="appointment-delete-btn" className="btn-danger" data-i18n="termine.detail.delete">Termin löschen</button>
|
||||
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="termine.detail.save">Änderungen speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -18,19 +18,19 @@ export function renderAppointmentsNew(): string {
|
||||
<section className="tool-page">
|
||||
<div className="container container-narrow">
|
||||
<div className="tool-header">
|
||||
<a href="/appointments" className="akten-back-link" id="termin-neu-back" data-i18n="termine.neu.back">← Zurück zur Übersicht</a>
|
||||
<a href="/appointments" className="akten-back-link" id="appointment-new-back" data-i18n="termine.neu.back">← Zurück zur Übersicht</a>
|
||||
<h1 data-i18n="termine.neu.heading">Neuer Termin</h1>
|
||||
<p className="tool-subtitle" data-i18n="termine.neu.subtitle">
|
||||
Persönlich oder einer Akte zugeordnet. Bei aktiver CalDAV-Synchronisation erscheint der Termin auch im externen Kalender.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="termin-neu-form" className="akten-form" autocomplete="off">
|
||||
<form id="appointment-new-form" className="akten-form" autocomplete="off">
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-title" data-i18n="termine.field.title">Titel</label>
|
||||
<label htmlFor="appointment-title" data-i18n="termine.field.title">Titel</label>
|
||||
<input
|
||||
type="text"
|
||||
id="termin-title"
|
||||
id="appointment-title"
|
||||
required
|
||||
placeholder="z.B. Mündliche Verhandlung"
|
||||
data-i18n-placeholder="termine.field.title.placeholder"
|
||||
@@ -39,19 +39,19 @@ export function renderAppointmentsNew(): string {
|
||||
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-start" data-i18n="termine.field.start">Beginn</label>
|
||||
<input type="datetime-local" id="termin-start" required />
|
||||
<label htmlFor="appointment-start" data-i18n="termine.field.start">Beginn</label>
|
||||
<input type="datetime-local" id="appointment-start" required />
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-end" data-i18n="termine.field.end">Ende (optional)</label>
|
||||
<input type="datetime-local" id="termin-end" />
|
||||
<label htmlFor="appointment-end" data-i18n="termine.field.end">Ende (optional)</label>
|
||||
<input type="datetime-local" id="appointment-end" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-type" data-i18n="termine.field.type">Typ</label>
|
||||
<select id="termin-type">
|
||||
<label htmlFor="appointment-type" data-i18n="termine.field.type">Typ</label>
|
||||
<select id="appointment-type">
|
||||
<option value="" data-i18n="termine.field.type.none">Kein Typ</option>
|
||||
<option value="hearing" data-i18n="termine.type.hearing">Verhandlung</option>
|
||||
<option value="meeting" data-i18n="termine.type.meeting">Besprechung</option>
|
||||
@@ -60,27 +60,27 @@ export function renderAppointmentsNew(): string {
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-project" data-i18n="termine.field.akte">Akte (optional)</label>
|
||||
<select id="termin-project">
|
||||
<label htmlFor="appointment-project" data-i18n="termine.field.akte">Akte (optional)</label>
|
||||
<select id="appointment-project">
|
||||
<option value="" data-i18n="termine.field.akte.none">Persönlicher Termin</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-location" data-i18n="termine.field.location">Ort (optional)</label>
|
||||
<input type="text" id="termin-location" placeholder="z.B. UPC LD München" data-i18n-placeholder="termine.field.location.placeholder" />
|
||||
<label htmlFor="appointment-location" data-i18n="termine.field.location">Ort (optional)</label>
|
||||
<input type="text" id="appointment-location" placeholder="z.B. UPC LD München" data-i18n-placeholder="termine.field.location.placeholder" />
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="termin-description" data-i18n="termine.field.description">Beschreibung (optional)</label>
|
||||
<textarea id="termin-description" rows={3} placeholder="Hinweise, Tagesordnung, nächste Schritte…" data-i18n-placeholder="termine.field.description.placeholder" />
|
||||
<label htmlFor="appointment-description" data-i18n="termine.field.description">Beschreibung (optional)</label>
|
||||
<textarea id="appointment-description" rows={3} placeholder="Hinweise, Tagesordnung, nächste Schritte…" data-i18n-placeholder="termine.field.description.placeholder" />
|
||||
</div>
|
||||
|
||||
<p className="form-msg" id="termin-neu-msg" />
|
||||
<p className="form-msg" id="appointment-new-msg" />
|
||||
|
||||
<div className="form-actions">
|
||||
<a href="/appointments" id="termin-neu-cancel" className="btn-cancel" data-i18n="termine.neu.cancel">Abbrechen</a>
|
||||
<a href="/appointments" id="appointment-new-cancel" className="btn-cancel" data-i18n="termine.neu.cancel">Abbrechen</a>
|
||||
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="termine.neu.submit">Termin anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function renderAppointments(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="frist-summary-cards" id="termin-summary">
|
||||
<div className="frist-summary-cards" id="appointments-summary">
|
||||
<button type="button" className="frist-summary-card termin-card-today" data-range="today">
|
||||
<span className="frist-summary-dot" />
|
||||
<span className="frist-summary-count" id="sum-today">0</span>
|
||||
@@ -56,8 +56,8 @@ export function renderAppointments(): string {
|
||||
|
||||
<div className="akten-controls">
|
||||
<div className="akten-filter-row">
|
||||
<label className="akten-filter-label" htmlFor="termin-filter-type" data-i18n="termine.filter.type">Typ</label>
|
||||
<select id="termin-filter-type" className="akten-select">
|
||||
<label className="akten-filter-label" htmlFor="appointment-filter-type" data-i18n="termine.filter.type">Typ</label>
|
||||
<select id="appointment-filter-type" className="akten-select">
|
||||
<option value="" data-i18n="termine.filter.type.all">Alle Typen</option>
|
||||
<option value="hearing" data-i18n="termine.type.hearing">Verhandlung</option>
|
||||
<option value="meeting" data-i18n="termine.type.meeting">Besprechung</option>
|
||||
@@ -65,27 +65,27 @@ export function renderAppointments(): string {
|
||||
<option value="deadline_hearing" data-i18n="termine.type.deadline_hearing">Fristverhandlung</option>
|
||||
</select>
|
||||
|
||||
<label className="akten-filter-label" htmlFor="termin-filter-project" data-i18n="termine.filter.akte">Akte</label>
|
||||
<select id="termin-filter-project" className="akten-select">
|
||||
<label className="akten-filter-label" htmlFor="appointment-filter-project" data-i18n="termine.filter.akte">Akte</label>
|
||||
<select id="appointment-filter-project" className="akten-select">
|
||||
<option value="" data-i18n="termine.filter.akte.all">Alle Akten & persönlich</option>
|
||||
<option value="__personal__" data-i18n="termine.filter.akte.personal">Nur persönliche</option>
|
||||
</select>
|
||||
|
||||
<label className="akten-filter-label" htmlFor="termin-filter-from" data-i18n="termine.filter.from">Von</label>
|
||||
<input type="date" id="termin-filter-from" className="akten-select" />
|
||||
<label className="akten-filter-label" htmlFor="termin-filter-to" data-i18n="termine.filter.to">Bis</label>
|
||||
<input type="date" id="termin-filter-to" className="akten-select" />
|
||||
<label className="akten-filter-label" htmlFor="appointment-filter-from" data-i18n="termine.filter.from">Von</label>
|
||||
<input type="date" id="appointment-filter-from" className="akten-select" />
|
||||
<label className="akten-filter-label" htmlFor="appointment-filter-to" data-i18n="termine.filter.to">Bis</label>
|
||||
<input type="date" id="appointment-filter-to" className="akten-select" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="termine-unavailable" className="akten-unavailable" style="display:none">
|
||||
<div id="appointments-unavailable" className="akten-unavailable" style="display:none">
|
||||
<p data-i18n="termine.unavailable">
|
||||
Terminverwaltung zurzeit nicht verfügbar — bitte Administrator kontaktieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="akten-table-wrap">
|
||||
<table className="akten-table fristen-table" id="termine-table">
|
||||
<table className="akten-table fristen-table" id="appointments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
@@ -96,11 +96,11 @@ export function renderAppointments(): string {
|
||||
<th data-i18n="termine.col.type">Typ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="termine-body" />
|
||||
<tbody id="appointments-body" />
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="akten-empty" id="termine-empty" style="display:none">
|
||||
<div className="akten-empty" id="appointments-empty" style="display:none">
|
||||
<h2 data-i18n="termine.empty.title">Keine Termine vorhanden</h2>
|
||||
<p data-i18n="termine.empty.hint">
|
||||
Sobald Termine angelegt werden, erscheinen sie hier.
|
||||
@@ -108,7 +108,7 @@ export function renderAppointments(): string {
|
||||
<a href="/appointments/new" className="btn-primary btn-cta-lime" data-i18n="termine.list.new">Neuer Termin</a>
|
||||
</div>
|
||||
|
||||
<div className="akten-empty akten-empty-filtered" id="termine-empty-filtered" style="display:none">
|
||||
<div className="akten-empty akten-empty-filtered" id="appointments-empty-filtered" style="display:none">
|
||||
<p data-i18n="termine.empty.filtered">Keine Termine mit diesen Filtern.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ function isoDate(year: number, month: number, day: number): string {
|
||||
return `${year}-${m}-${d}`;
|
||||
}
|
||||
|
||||
async function loadTermine() {
|
||||
async function loadAppointments() {
|
||||
// Pull a wide window (current month plus a little buffer either side).
|
||||
// We could narrow this, but the user typically navigates ±1-2 months
|
||||
// and the dataset is small.
|
||||
@@ -97,7 +97,7 @@ function render() {
|
||||
);
|
||||
}
|
||||
|
||||
const grid = document.getElementById("termin-cal-grid")!;
|
||||
const grid = document.getElementById("appointment-cal-grid")!;
|
||||
grid.innerHTML = cells.join("");
|
||||
|
||||
grid.querySelectorAll<HTMLElement>(".frist-cal-cell-has").forEach((cell) => {
|
||||
@@ -110,7 +110,7 @@ function render() {
|
||||
const iso = tt.start_at.slice(0, 10);
|
||||
return iso >= monthStart && iso <= monthEnd;
|
||||
});
|
||||
const empty = document.getElementById("termin-cal-empty")!;
|
||||
const empty = document.getElementById("appointment-cal-empty")!;
|
||||
empty.style.display = hasInMonth ? "none" : "";
|
||||
}
|
||||
|
||||
@@ -188,6 +188,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
initNav();
|
||||
initPopup();
|
||||
onLangChange(render);
|
||||
await loadTermine();
|
||||
await loadAppointments();
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -20,12 +20,12 @@ interface Project {
|
||||
title: string;
|
||||
}
|
||||
|
||||
let termin: Appointment | null = null;
|
||||
let appointment: Appointment | null = null;
|
||||
let project: Project | null = null;
|
||||
|
||||
function parseTerminID(): string | null {
|
||||
function parseAppointmentID(): string | null {
|
||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||
if (parts[0] !== "termine" || !parts[1]) return null;
|
||||
if (parts[0] !== "appointments" || !parts[1]) return null;
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
@@ -57,18 +57,18 @@ function esc(s: string): string {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
async function loadTermin(id: string): Promise<boolean> {
|
||||
async function loadAppointment(id: string): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(`/api/appointments/${id}`);
|
||||
if (!resp.ok) return false;
|
||||
termin = await resp.json();
|
||||
appointment = await resp.json();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAkte(id: string) {
|
||||
async function loadProject(id: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/projects/${id}`);
|
||||
if (resp.ok) project = await resp.json();
|
||||
@@ -78,57 +78,57 @@ async function loadAkte(id: string) {
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (!termin) return;
|
||||
document.getElementById("termin-title-display")!.textContent = termin.title;
|
||||
if (!appointment) return;
|
||||
document.getElementById("appointment-title-display")!.textContent = appointment.title;
|
||||
|
||||
const time = termin.end_at
|
||||
? `${fmtDateTime(termin.start_at)} \u2014 ${fmtDateTime(termin.end_at)}`
|
||||
: fmtDateTime(termin.start_at);
|
||||
document.getElementById("termin-time-display")!.textContent = time;
|
||||
const time = appointment.end_at
|
||||
? `${fmtDateTime(appointment.start_at)} — ${fmtDateTime(appointment.end_at)}`
|
||||
: fmtDateTime(appointment.start_at);
|
||||
document.getElementById("appointment-time-display")!.textContent = time;
|
||||
|
||||
const badge = document.getElementById("termin-type-badge")!;
|
||||
if (termin.appointment_type) {
|
||||
badge.textContent = t(`termine.type.${termin.appointment_type}`) || termin.appointment_type;
|
||||
badge.className = `termin-type-badge termin-type-${termin.appointment_type}`;
|
||||
const badge = document.getElementById("appointment-type-badge")!;
|
||||
if (appointment.appointment_type) {
|
||||
badge.textContent = t(`termine.type.${appointment.appointment_type}`) || appointment.appointment_type;
|
||||
badge.className = `termin-type-badge termin-type-${appointment.appointment_type}`;
|
||||
badge.style.display = "";
|
||||
} else {
|
||||
badge.style.display = "none";
|
||||
}
|
||||
|
||||
const akteRow = document.getElementById("termin-project-row")!;
|
||||
if (termin.project_id && project) {
|
||||
const link = document.getElementById("termin-project-link") as HTMLAnchorElement;
|
||||
const projectRow = document.getElementById("appointment-project-row")!;
|
||||
if (appointment.project_id && project) {
|
||||
const link = document.getElementById("appointment-project-link") as HTMLAnchorElement;
|
||||
link.href = `/projects/${project.id}`;
|
||||
link.textContent = `${project.reference || ""} \u2014 ${project.title}`;
|
||||
akteRow.style.display = "";
|
||||
link.textContent = `${project.reference || ""} — ${project.title}`;
|
||||
projectRow.style.display = "";
|
||||
} else {
|
||||
akteRow.style.display = "none";
|
||||
projectRow.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function fillEditForm() {
|
||||
if (!termin) return;
|
||||
(document.getElementById("termin-title-edit") as HTMLInputElement).value = termin.title;
|
||||
(document.getElementById("termin-start-edit") as HTMLInputElement).value = toLocalInput(termin.start_at);
|
||||
(document.getElementById("termin-end-edit") as HTMLInputElement).value = toLocalInput(termin.end_at);
|
||||
(document.getElementById("termin-type-edit") as HTMLSelectElement).value = termin.appointment_type ?? "";
|
||||
(document.getElementById("termin-location-edit") as HTMLInputElement).value = termin.location ?? "";
|
||||
(document.getElementById("termin-description-edit") as HTMLTextAreaElement).value = termin.description ?? "";
|
||||
if (!appointment) return;
|
||||
(document.getElementById("appointment-title-edit") as HTMLInputElement).value = appointment.title;
|
||||
(document.getElementById("appointment-start-edit") as HTMLInputElement).value = toLocalInput(appointment.start_at);
|
||||
(document.getElementById("appointment-end-edit") as HTMLInputElement).value = toLocalInput(appointment.end_at);
|
||||
(document.getElementById("appointment-type-edit") as HTMLSelectElement).value = appointment.appointment_type ?? "";
|
||||
(document.getElementById("appointment-location-edit") as HTMLInputElement).value = appointment.location ?? "";
|
||||
(document.getElementById("appointment-description-edit") as HTMLTextAreaElement).value = appointment.description ?? "";
|
||||
}
|
||||
|
||||
async function saveEdit(ev: Event) {
|
||||
ev.preventDefault();
|
||||
if (!termin) return;
|
||||
const msg = document.getElementById("termin-edit-msg")!;
|
||||
if (!appointment) return;
|
||||
const msg = document.getElementById("appointment-edit-msg")!;
|
||||
const submitBtn = (ev.target as HTMLFormElement).querySelector<HTMLButtonElement>("button[type=submit]")!;
|
||||
msg.textContent = "";
|
||||
|
||||
const title = (document.getElementById("termin-title-edit") as HTMLInputElement).value.trim();
|
||||
const startRaw = (document.getElementById("termin-start-edit") as HTMLInputElement).value;
|
||||
const endRaw = (document.getElementById("termin-end-edit") as HTMLInputElement).value;
|
||||
const type = (document.getElementById("termin-type-edit") as HTMLSelectElement).value;
|
||||
const location = (document.getElementById("termin-location-edit") as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById("termin-description-edit") as HTMLTextAreaElement).value;
|
||||
const title = (document.getElementById("appointment-title-edit") as HTMLInputElement).value.trim();
|
||||
const startRaw = (document.getElementById("appointment-start-edit") as HTMLInputElement).value;
|
||||
const endRaw = (document.getElementById("appointment-end-edit") as HTMLInputElement).value;
|
||||
const type = (document.getElementById("appointment-type-edit") as HTMLSelectElement).value;
|
||||
const location = (document.getElementById("appointment-location-edit") as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById("appointment-description-edit") as HTMLTextAreaElement).value;
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
title,
|
||||
@@ -141,13 +141,13 @@ async function saveEdit(ev: Event) {
|
||||
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/appointments/${termin.id}`, {
|
||||
const resp = await fetch(`/api/appointments/${appointment.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (resp.ok) {
|
||||
termin = await resp.json();
|
||||
appointment = await resp.json();
|
||||
renderHeader();
|
||||
msg.textContent = t("termine.detail.saved");
|
||||
msg.className = "form-msg form-msg-ok";
|
||||
@@ -164,50 +164,50 @@ async function saveEdit(ev: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTermin() {
|
||||
if (!termin) return;
|
||||
async function deleteAppointment() {
|
||||
if (!appointment) return;
|
||||
if (!confirm(t("termine.detail.delete.confirm"))) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/appointments/${termin.id}`, { method: "DELETE" });
|
||||
const resp = await fetch(`/api/appointments/${appointment.id}`, { method: "DELETE" });
|
||||
if (resp.ok || resp.status === 204) {
|
||||
window.location.href = "/appointments";
|
||||
} else {
|
||||
const data = await resp.json().catch(() => ({}) as { error?: string });
|
||||
const msg = document.getElementById("termin-edit-msg")!;
|
||||
const msg = document.getElementById("appointment-edit-msg")!;
|
||||
msg.textContent = data.error || t("termine.error.generic");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
}
|
||||
} catch {
|
||||
const msg = document.getElementById("termin-edit-msg")!;
|
||||
const msg = document.getElementById("appointment-edit-msg")!;
|
||||
msg.textContent = t("termine.error.generic");
|
||||
msg.className = "form-msg form-msg-error";
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const id = parseTerminID();
|
||||
const loading = document.getElementById("termin-loading")!;
|
||||
const body = document.getElementById("termin-body")!;
|
||||
const notFound = document.getElementById("termin-not-found")!;
|
||||
const id = parseAppointmentID();
|
||||
const loading = document.getElementById("appointment-loading")!;
|
||||
const body = document.getElementById("appointment-body")!;
|
||||
const notFound = document.getElementById("appointment-not-found")!;
|
||||
if (!id) {
|
||||
loading.style.display = "none";
|
||||
notFound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
const ok = await loadTermin(id);
|
||||
if (!ok || !termin) {
|
||||
const ok = await loadAppointment(id);
|
||||
if (!ok || !appointment) {
|
||||
loading.style.display = "none";
|
||||
notFound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
if (termin.project_id) await loadAkte(termin.project_id);
|
||||
if (appointment.project_id) await loadProject(appointment.project_id);
|
||||
loading.style.display = "none";
|
||||
body.style.display = "";
|
||||
renderHeader();
|
||||
fillEditForm();
|
||||
|
||||
document.getElementById("termin-edit-form")!.addEventListener("submit", saveEdit);
|
||||
document.getElementById("termin-delete-btn")!.addEventListener("click", deleteTermin);
|
||||
document.getElementById("appointment-edit-form")!.addEventListener("submit", saveEdit);
|
||||
document.getElementById("appointment-delete-btn")!.addEventListener("click", deleteAppointment);
|
||||
|
||||
const notes = document.getElementById("notes-container");
|
||||
if (notes) {
|
||||
|
||||
@@ -15,7 +15,7 @@ function esc(s: string): string {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
async function loadAkten() {
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const resp = await fetch("/api/projects");
|
||||
if (resp.ok) allProjects = await resp.json();
|
||||
@@ -24,49 +24,46 @@ async function loadAkten() {
|
||||
}
|
||||
}
|
||||
|
||||
function populateAkten() {
|
||||
const sel = document.getElementById("termin-project") as HTMLSelectElement;
|
||||
function populateProjects() {
|
||||
const sel = document.getElementById("appointment-project") as HTMLSelectElement;
|
||||
const opts: string[] = [
|
||||
`<option value="">${esc(t("termine.field.project.none"))}</option>`,
|
||||
];
|
||||
for (const a of allProjects) {
|
||||
opts.push(
|
||||
`<option value="${esc(a.id)}">${esc(a.reference || "")} \u2014 ${esc(a.title)}</option>`,
|
||||
`<option value="${esc(a.id)}">${esc(a.reference || "")} — ${esc(a.title)}</option>`,
|
||||
);
|
||||
}
|
||||
sel.innerHTML = opts.join("");
|
||||
|
||||
// Pre-select Project from query (?project_id=...)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ak = params.get("project_id");
|
||||
if (ak) sel.value = ak;
|
||||
}
|
||||
|
||||
function preFillStart() {
|
||||
const start = document.getElementById("termin-start") as HTMLInputElement;
|
||||
const start = document.getElementById("appointment-start") as HTMLInputElement;
|
||||
const now = new Date();
|
||||
// Round to next quarter-hour for sensible default.
|
||||
now.setMinutes(now.getMinutes() + (15 - (now.getMinutes() % 15)));
|
||||
now.setSeconds(0);
|
||||
now.setMilliseconds(0);
|
||||
// datetime-local expects "YYYY-MM-DDTHH:MM"
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
start.value = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
||||
}
|
||||
|
||||
async function submitForm(ev: Event) {
|
||||
ev.preventDefault();
|
||||
const msg = document.getElementById("termin-neu-msg")!;
|
||||
const msg = document.getElementById("appointment-new-msg")!;
|
||||
const submitBtn = (ev.target as HTMLFormElement).querySelector<HTMLButtonElement>("button[type=submit]")!;
|
||||
msg.textContent = "";
|
||||
|
||||
const title = (document.getElementById("termin-title") as HTMLInputElement).value.trim();
|
||||
const startRaw = (document.getElementById("termin-start") as HTMLInputElement).value;
|
||||
const endRaw = (document.getElementById("termin-end") as HTMLInputElement).value;
|
||||
const type = (document.getElementById("termin-type") as HTMLSelectElement).value;
|
||||
const akteID = (document.getElementById("termin-project") as HTMLSelectElement).value;
|
||||
const location = (document.getElementById("termin-location") as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById("termin-description") as HTMLTextAreaElement).value.trim();
|
||||
const title = (document.getElementById("appointment-title") as HTMLInputElement).value.trim();
|
||||
const startRaw = (document.getElementById("appointment-start") as HTMLInputElement).value;
|
||||
const endRaw = (document.getElementById("appointment-end") as HTMLInputElement).value;
|
||||
const type = (document.getElementById("appointment-type") as HTMLSelectElement).value;
|
||||
const projectID = (document.getElementById("appointment-project") as HTMLSelectElement).value;
|
||||
const location = (document.getElementById("appointment-location") as HTMLInputElement).value.trim();
|
||||
const description = (document.getElementById("appointment-description") as HTMLTextAreaElement).value.trim();
|
||||
|
||||
if (!title || !startRaw) {
|
||||
msg.textContent = t("termine.error.required");
|
||||
@@ -80,7 +77,7 @@ async function submitForm(ev: Event) {
|
||||
};
|
||||
if (endRaw) payload.end_at = new Date(endRaw).toISOString();
|
||||
if (type) payload.appointment_type = type;
|
||||
if (akteID) payload.project_id = akteID;
|
||||
if (projectID) payload.project_id = projectID;
|
||||
if (location) payload.location = location;
|
||||
if (description) payload.description = description;
|
||||
|
||||
@@ -110,8 +107,8 @@ async function submitForm(ev: Event) {
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
await loadAkten();
|
||||
populateAkten();
|
||||
await loadProjects();
|
||||
populateProjects();
|
||||
preFillStart();
|
||||
document.getElementById("termin-neu-form")!.addEventListener("submit", submitForm);
|
||||
document.getElementById("appointment-new-form")!.addEventListener("submit", submitForm);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ interface Appointment {
|
||||
appointment_type?: string;
|
||||
project_reference?: string;
|
||||
project_title?: string;
|
||||
projekt_office?: string;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
@@ -33,7 +32,7 @@ const PERSONAL = "__personal__";
|
||||
let allAppointments: Appointment[] = [];
|
||||
let allProjects: Project[] = [];
|
||||
let typeFilter = "";
|
||||
let akteFilter = "";
|
||||
let projectFilter = "";
|
||||
let fromFilter = "";
|
||||
let toFilter = "";
|
||||
let loadedOK = false;
|
||||
@@ -42,7 +41,7 @@ function urlParams(): URLSearchParams {
|
||||
return new URLSearchParams(window.location.search);
|
||||
}
|
||||
|
||||
async function loadAkten() {
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const resp = await fetch("/api/projects");
|
||||
if (resp.ok) allProjects = await resp.json();
|
||||
@@ -69,20 +68,20 @@ function setCount(id: string, n: number) {
|
||||
if (el) el.textContent = String(n);
|
||||
}
|
||||
|
||||
async function loadTermine() {
|
||||
const unavailable = document.getElementById("termine-unavailable")!;
|
||||
async function loadAppointments() {
|
||||
const unavailable = document.getElementById("appointments-unavailable")!;
|
||||
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (typeFilter) params.set("type", typeFilter);
|
||||
if (akteFilter && akteFilter !== PERSONAL) params.set("project_id", akteFilter);
|
||||
if (projectFilter && projectFilter !== PERSONAL) params.set("project_id", projectFilter);
|
||||
if (fromFilter) params.set("from", fromFilter);
|
||||
if (toFilter) params.set("to", toFilter);
|
||||
const resp = await fetch(`/api/appointments?${params.toString()}`);
|
||||
if (resp.status === 503) {
|
||||
unavailable.style.display = "block";
|
||||
tableWrap.style.display = "none";
|
||||
document.getElementById("termine-empty")!.style.display = "none";
|
||||
document.getElementById("appointments-empty")!.style.display = "none";
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) {
|
||||
@@ -91,7 +90,7 @@ async function loadTermine() {
|
||||
return;
|
||||
}
|
||||
const data: Appointment[] = await resp.json();
|
||||
allAppointments = akteFilter === PERSONAL ? data.filter((x) => !x.project_id) : data;
|
||||
allAppointments = projectFilter === PERSONAL ? data.filter((x) => !x.project_id) : data;
|
||||
loadedOK = true;
|
||||
render();
|
||||
} catch {
|
||||
@@ -123,15 +122,15 @@ function esc(s: string): string {
|
||||
|
||||
function render() {
|
||||
if (!loadedOK) return;
|
||||
const tbody = document.getElementById("termine-body")!;
|
||||
const empty = document.getElementById("termine-empty")!;
|
||||
const emptyFiltered = document.getElementById("termine-empty-filtered")!;
|
||||
const tbody = document.getElementById("appointments-body")!;
|
||||
const empty = document.getElementById("appointments-empty")!;
|
||||
const emptyFiltered = document.getElementById("appointments-empty-filtered")!;
|
||||
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
|
||||
|
||||
if (allAppointments.length === 0) {
|
||||
tbody.innerHTML = "";
|
||||
tableWrap.style.display = "none";
|
||||
if (!typeFilter && !akteFilter && !fromFilter && !toFilter) {
|
||||
if (!typeFilter && !projectFilter && !fromFilter && !toFilter) {
|
||||
empty.style.display = "block";
|
||||
emptyFiltered.style.display = "none";
|
||||
} else {
|
||||
@@ -149,7 +148,7 @@ function render() {
|
||||
.map((tt) => {
|
||||
const typeLabel = tt.appointment_type ? t(`termine.type.${tt.appointment_type}`) || tt.appointment_type : "";
|
||||
const typeClass = tt.appointment_type ? `termin-type-${tt.appointment_type}` : "";
|
||||
const akteCell = tt.project_id
|
||||
const projectCell = tt.project_id
|
||||
? `<a class="akten-ref-link" href="/projects/${esc(tt.project_id)}">${esc(tt.project_reference ?? "")}</a>`
|
||||
+ `<span class="frist-project-title">${esc(tt.project_title ?? "")}</span>`
|
||||
: `<span class="termin-personal-tag" data-i18n="termine.personal">${esc(t("termine.personal"))}</span>`;
|
||||
@@ -157,7 +156,7 @@ function render() {
|
||||
<td class="frist-col-check"><span class="termin-dot ${typeClass}" /></td>
|
||||
<td class="frist-col-due">${esc(fmtDateTime(tt.start_at))}</td>
|
||||
<td class="frist-col-title">${esc(tt.title)}</td>
|
||||
<td class="frist-col-project">${akteCell}</td>
|
||||
<td class="frist-col-project">${projectCell}</td>
|
||||
<td>${esc(tt.location ?? "")}</td>
|
||||
<td><span class="termin-type-chip ${typeClass}">${esc(typeLabel)}</span></td>
|
||||
</tr>`;
|
||||
@@ -175,14 +174,14 @@ function render() {
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
const type = document.getElementById("termin-filter-type") as HTMLSelectElement;
|
||||
const project = document.getElementById("termin-filter-project") as HTMLSelectElement;
|
||||
const from = document.getElementById("termin-filter-from") as HTMLInputElement;
|
||||
const to = document.getElementById("termin-filter-to") as HTMLInputElement;
|
||||
const type = document.getElementById("appointment-filter-type") as HTMLSelectElement;
|
||||
const project = document.getElementById("appointment-filter-project") as HTMLSelectElement;
|
||||
const from = document.getElementById("appointment-filter-from") as HTMLInputElement;
|
||||
const to = document.getElementById("appointment-filter-to") as HTMLInputElement;
|
||||
|
||||
const params = urlParams();
|
||||
if (params.has("type")) typeFilter = params.get("type")!;
|
||||
if (params.has("project_id")) akteFilter = params.get("project_id")!;
|
||||
if (params.has("project_id")) projectFilter = params.get("project_id")!;
|
||||
if (params.has("from")) fromFilter = params.get("from")!;
|
||||
if (params.has("to")) toFilter = params.get("to")!;
|
||||
type.value = typeFilter;
|
||||
@@ -191,35 +190,35 @@ function initFilters() {
|
||||
|
||||
type.addEventListener("change", async () => {
|
||||
typeFilter = type.value;
|
||||
await Promise.all([loadTermine(), loadSummary()]);
|
||||
await Promise.all([loadAppointments(), loadSummary()]);
|
||||
});
|
||||
project.addEventListener("change", async () => {
|
||||
akteFilter = project.value;
|
||||
await Promise.all([loadTermine(), loadSummary()]);
|
||||
projectFilter = project.value;
|
||||
await Promise.all([loadAppointments(), loadSummary()]);
|
||||
});
|
||||
from.addEventListener("change", async () => {
|
||||
fromFilter = from.value;
|
||||
await loadTermine();
|
||||
await loadAppointments();
|
||||
});
|
||||
to.addEventListener("change", async () => {
|
||||
toFilter = to.value;
|
||||
await loadTermine();
|
||||
await loadAppointments();
|
||||
});
|
||||
}
|
||||
|
||||
function populateAkteFilter() {
|
||||
const sel = document.getElementById("termin-filter-project") as HTMLSelectElement;
|
||||
function populateProjectFilter() {
|
||||
const sel = document.getElementById("appointment-filter-project") as HTMLSelectElement;
|
||||
const options: string[] = [
|
||||
`<option value="">${esc(t("termine.filter.akte.all"))}</option>`,
|
||||
`<option value="${PERSONAL}">${esc(t("termine.filter.akte.personal"))}</option>`,
|
||||
];
|
||||
for (const a of allProjects) {
|
||||
options.push(
|
||||
`<option value="${esc(a.id)}">${esc(a.reference || "")} \u2014 ${esc(a.title)}</option>`,
|
||||
`<option value="${esc(a.id)}">${esc(a.reference || "")} — ${esc(a.title)}</option>`,
|
||||
);
|
||||
}
|
||||
sel.innerHTML = options.join("");
|
||||
if (akteFilter) sel.value = akteFilter;
|
||||
if (projectFilter) sel.value = projectFilter;
|
||||
}
|
||||
|
||||
function initSummaryCards() {
|
||||
@@ -250,9 +249,9 @@ function initSummaryCards() {
|
||||
}
|
||||
fromFilter = from;
|
||||
toFilter = to;
|
||||
(document.getElementById("termin-filter-from") as HTMLInputElement).value = from;
|
||||
(document.getElementById("termin-filter-to") as HTMLInputElement).value = to;
|
||||
await loadTermine();
|
||||
(document.getElementById("appointment-filter-from") as HTMLInputElement).value = from;
|
||||
(document.getElementById("appointment-filter-to") as HTMLInputElement).value = to;
|
||||
await loadAppointments();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -263,7 +262,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
initFilters();
|
||||
initSummaryCards();
|
||||
onLangChange(render);
|
||||
await loadAkten();
|
||||
populateAkteFilter();
|
||||
await Promise.all([loadTermine(), loadSummary()]);
|
||||
await loadProjects();
|
||||
populateProjectFilter();
|
||||
await Promise.all([loadAppointments(), loadSummary()]);
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ interface UpcomingDeadline {
|
||||
due_date: string;
|
||||
project_id: string;
|
||||
project_title: string;
|
||||
projekt_ref: string;
|
||||
project_reference: string;
|
||||
urgency: "overdue" | "today" | "urgent" | "soon";
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ interface UpcomingAppointment {
|
||||
type: string | null;
|
||||
project_id: string | null;
|
||||
project_title: string | null;
|
||||
projekt_ref: string | null;
|
||||
project_reference: string | null;
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
@@ -49,7 +49,7 @@ interface ActivityEntry {
|
||||
actor_name: string | null;
|
||||
project_id: string;
|
||||
project_title: string;
|
||||
projekt_ref: string;
|
||||
project_reference: string;
|
||||
action: string | null;
|
||||
details: string;
|
||||
description: string | null;
|
||||
@@ -160,10 +160,10 @@ function renderDeadlines(items: UpcomingDeadline[]): void {
|
||||
const urgencyClass = `dashboard-urgency-${d.urgency}`;
|
||||
const urgencyLabel = t(`dashboard.urgency.${d.urgency}`);
|
||||
return `<li class="dashboard-list-item">
|
||||
<a href="/projects/${esc(d.project_id)}/fristen" class="dashboard-list-link">
|
||||
<a href="/projects/${esc(d.project_id)}/deadlines" class="dashboard-list-link">
|
||||
<div class="dashboard-list-main">
|
||||
<span class="dashboard-list-title">${esc(d.title)}</span>
|
||||
<span class="dashboard-list-ref">${esc(d.projekt_ref)} · ${esc(d.project_title)}</span>
|
||||
<span class="dashboard-list-ref">${esc(d.project_reference)} · ${esc(d.project_title)}</span>
|
||||
</div>
|
||||
<div class="dashboard-list-meta">
|
||||
<span class="dashboard-urgency-badge ${urgencyClass}" title="${escAttr(urgencyLabel)}">${esc(formatRelative(d.due_date))}</span>
|
||||
@@ -189,16 +189,16 @@ function renderAppointments(items: UpcomingAppointment[]): void {
|
||||
const dot = a.type
|
||||
? `<span class="dashboard-termin-dot dashboard-termin-${esc(a.type)}" aria-hidden="true"></span>`
|
||||
: `<span class="dashboard-termin-dot" aria-hidden="true"></span>`;
|
||||
const href = a.project_id ? `/projects/${esc(a.project_id)}/termine` : "#";
|
||||
const href = a.project_id ? `/projects/${esc(a.project_id)}/appointments` : "#";
|
||||
const tag = a.project_id ? "a" : "div";
|
||||
const akteLine = a.projekt_ref && a.project_title
|
||||
? `<span class="dashboard-list-ref">${esc(a.projekt_ref)} · ${esc(a.project_title)}</span>`
|
||||
const projectLine = a.project_reference && a.project_title
|
||||
? `<span class="dashboard-list-ref">${esc(a.project_reference)} · ${esc(a.project_title)}</span>`
|
||||
: "";
|
||||
return `<li class="dashboard-list-item">
|
||||
<${tag} href="${href}" class="dashboard-list-link">
|
||||
<div class="dashboard-list-main">
|
||||
<span class="dashboard-list-title">${dot}${esc(a.title)}</span>
|
||||
${akteLine}
|
||||
${projectLine}
|
||||
</div>
|
||||
<div class="dashboard-list-meta">
|
||||
<span class="dashboard-appt-time">${esc(formatDateTime(a.start_at))}</span>
|
||||
@@ -237,7 +237,7 @@ function renderActivity(items: ActivityEntry[]): void {
|
||||
<span class="dashboard-activity-body">
|
||||
<span class="dashboard-activity-actor">${esc(actor)}</span>
|
||||
<span class="dashboard-activity-action">${esc(actionLabel)}</span>
|
||||
<a href="/projects/${esc(e.project_id)}" class="dashboard-activity-project">${esc(e.projekt_ref)}</a>
|
||||
<a href="/projects/${esc(e.project_id)}" class="dashboard-activity-project">${esc(e.project_reference)}</a>
|
||||
${detail ? `<span class="dashboard-activity-details">${esc(detail)}</span>` : ""}
|
||||
</span>
|
||||
</li>`;
|
||||
|
||||
@@ -29,25 +29,24 @@ function urgencyClass(due: string, status: string): string {
|
||||
if (status === "completed") return "frist-urgency-done";
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const d = new Date(due + "T00:00:00");
|
||||
const d = new Date(due.slice(0, 10) + "T00:00:00");
|
||||
const diffDays = Math.floor((d.getTime() - today.getTime()) / 86400000);
|
||||
if (diffDays < 0) return "frist-urgency-overdue";
|
||||
if (diffDays <= 7) return "frist-urgency-soon";
|
||||
return "frist-urgency-later";
|
||||
}
|
||||
|
||||
async function loadFristen() {
|
||||
async function loadDeadlines() {
|
||||
try {
|
||||
// Load all (open + completed) — calendar shows everything for context.
|
||||
const resp = await fetch("/api/deadlines?status=all");
|
||||
if (resp.ok) allFristen = await resp.json();
|
||||
if (resp.ok) allDeadlines = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
function deadlinesForDate(iso: string): Deadline[] {
|
||||
return allFristen.filter((f) => f.due_date.slice(0, 10) === iso);
|
||||
return allDeadlines.filter((f) => f.due_date.slice(0, 10) === iso);
|
||||
}
|
||||
|
||||
function isoDate(year: number, month: number, day: number): string {
|
||||
@@ -59,10 +58,9 @@ function isoDate(year: number, month: number, day: number): string {
|
||||
function render() {
|
||||
document.getElementById("cal-month-label")!.textContent = fmtMonth(viewYear, viewMonth);
|
||||
|
||||
// First weekday of month (Mon=0..Sun=6)
|
||||
const firstDay = new Date(viewYear, viewMonth, 1);
|
||||
const jsWeekday = firstDay.getDay(); // Sun=0..Sat=6
|
||||
const offset = (jsWeekday + 6) % 7; // Mon=0..Sun=6
|
||||
const jsWeekday = firstDay.getDay();
|
||||
const offset = (jsWeekday + 6) % 7;
|
||||
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
||||
const today = new Date();
|
||||
const todayISO = isoDate(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
@@ -73,24 +71,24 @@ function render() {
|
||||
}
|
||||
for (let day = 1; day <= daysInMonth; day++) {
|
||||
const iso = isoDate(viewYear, viewMonth, day);
|
||||
const fristen = deadlinesForDate(iso);
|
||||
const items = deadlinesForDate(iso);
|
||||
const isToday = iso === todayISO;
|
||||
|
||||
const dots = fristen
|
||||
const dots = items
|
||||
.slice(0, 4)
|
||||
.map((f) => `<span class="frist-cal-dot ${urgencyClass(f.due_date, f.status)}"></span>`)
|
||||
.join("");
|
||||
const more = fristen.length > 4 ? `<span class="frist-cal-more">+${fristen.length - 4}</span>` : "";
|
||||
const more = items.length > 4 ? `<span class="frist-cal-more">+${items.length - 4}</span>` : "";
|
||||
|
||||
cells.push(
|
||||
`<div class="frist-cal-cell${isToday ? " frist-cal-today" : ""}${fristen.length > 0 ? " frist-cal-cell-has" : ""}" data-iso="${iso}">
|
||||
`<div class="frist-cal-cell${isToday ? " frist-cal-today" : ""}${items.length > 0 ? " frist-cal-cell-has" : ""}" data-iso="${iso}">
|
||||
<span class="frist-cal-day">${day}</span>
|
||||
<div class="frist-cal-dots">${dots}${more}</div>
|
||||
</div>`,
|
||||
);
|
||||
}
|
||||
|
||||
const grid = document.getElementById("frist-cal-grid")!;
|
||||
const grid = document.getElementById("deadline-cal-grid")!;
|
||||
grid.innerHTML = cells.join("");
|
||||
|
||||
grid.querySelectorAll<HTMLElement>(".frist-cal-cell-has").forEach((cell) => {
|
||||
@@ -99,17 +97,17 @@ function render() {
|
||||
|
||||
const monthStart = isoDate(viewYear, viewMonth, 1);
|
||||
const monthEnd = isoDate(viewYear, viewMonth, daysInMonth);
|
||||
const hasInMonth = allFristen.some((f) => {
|
||||
const hasInMonth = allDeadlines.some((f) => {
|
||||
const iso = f.due_date.slice(0, 10);
|
||||
return iso >= monthStart && iso <= monthEnd;
|
||||
});
|
||||
const empty = document.getElementById("frist-cal-empty")!;
|
||||
const empty = document.getElementById("deadline-cal-empty")!;
|
||||
empty.style.display = hasInMonth ? "none" : "";
|
||||
}
|
||||
|
||||
function openPopup(iso: string) {
|
||||
const fristen = deadlinesForDate(iso);
|
||||
if (fristen.length === 0) return;
|
||||
const items = deadlinesForDate(iso);
|
||||
if (items.length === 0) return;
|
||||
const popup = document.getElementById("cal-popup")!;
|
||||
const dateEl = document.getElementById("cal-popup-date")!;
|
||||
const list = document.getElementById("cal-popup-list")!;
|
||||
@@ -122,7 +120,7 @@ function openPopup(iso: string) {
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
list.innerHTML = fristen
|
||||
list.innerHTML = items
|
||||
.map((f) => {
|
||||
const cls = urgencyClass(f.due_date, f.status);
|
||||
return `<li class="frist-cal-popup-item">
|
||||
@@ -178,6 +176,6 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
initNav();
|
||||
initPopup();
|
||||
onLangChange(render);
|
||||
await loadFristen();
|
||||
await loadDeadlines();
|
||||
render();
|
||||
});
|
||||
|
||||
@@ -39,9 +39,9 @@ let project: Project | null = null;
|
||||
let rule: DeadlineRule | null = null;
|
||||
let me: Me | null = null;
|
||||
|
||||
function parseFristID(): string | null {
|
||||
function parseDeadlineID(): string | null {
|
||||
const parts = window.location.pathname.split("/").filter(Boolean);
|
||||
if (parts[0] !== "fristen" || !parts[1]) return null;
|
||||
if (parts[0] !== "deadlines" || !parts[1]) return null;
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
@@ -83,27 +83,27 @@ function urgencyClass(due: string, status: string): string {
|
||||
if (status === "completed") return "frist-urgency-done";
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const d = new Date(due + "T00:00:00");
|
||||
const d = new Date(due.slice(0, 10) + "T00:00:00");
|
||||
const diffDays = Math.floor((d.getTime() - today.getTime()) / 86400000);
|
||||
if (diffDays < 0) return "frist-urgency-overdue";
|
||||
if (diffDays <= 7) return "frist-urgency-soon";
|
||||
return "frist-urgency-later";
|
||||
}
|
||||
|
||||
async function loadFrist(id: string): Promise<boolean> {
|
||||
async function loadDeadline(id: string): Promise<boolean> {
|
||||
try {
|
||||
const resp = await fetch(`/api/deadlines/${id}`);
|
||||
if (!resp.ok) return false;
|
||||
frist = await resp.json();
|
||||
deadline = await resp.json();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAkte(akteID: string) {
|
||||
async function loadProject(projectID: string) {
|
||||
try {
|
||||
const resp = await fetch(`/api/projects/${akteID}`);
|
||||
const resp = await fetch(`/api/projects/${projectID}`);
|
||||
if (resp.ok) project = await resp.json();
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
@@ -131,58 +131,58 @@ async function loadMe() {
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!frist) return;
|
||||
(document.getElementById("frist-title-display") as HTMLElement).textContent = frist.title;
|
||||
(document.getElementById("frist-title-edit") as HTMLInputElement).value = frist.title;
|
||||
if (!deadline) return;
|
||||
(document.getElementById("deadline-title-display") as HTMLElement).textContent = deadline.title;
|
||||
(document.getElementById("deadline-title-edit") as HTMLInputElement).value = deadline.title;
|
||||
|
||||
const dueChip = document.getElementById("frist-due-chip")!;
|
||||
dueChip.className = `frist-due-chip ${urgencyClass(frist.due_date, frist.status)}`;
|
||||
dueChip.textContent = fmtDate(frist.due_date);
|
||||
(document.getElementById("frist-due-display") as HTMLElement).textContent = fmtDate(frist.due_date);
|
||||
(document.getElementById("frist-due-edit") as HTMLInputElement).value = frist.due_date.slice(0, 10);
|
||||
const dueChip = document.getElementById("deadline-due-chip")!;
|
||||
dueChip.className = `frist-due-chip ${urgencyClass(deadline.due_date, deadline.status)}`;
|
||||
dueChip.textContent = fmtDate(deadline.due_date);
|
||||
(document.getElementById("deadline-due-display") as HTMLElement).textContent = fmtDate(deadline.due_date);
|
||||
(document.getElementById("deadline-due-edit") as HTMLInputElement).value = deadline.due_date.slice(0, 10);
|
||||
|
||||
const statusChip = document.getElementById("frist-status-chip")!;
|
||||
statusChip.className = `akten-status-chip akten-status-${frist.status}`;
|
||||
statusChip.textContent = t(`fristen.status.${frist.status}`) || frist.status;
|
||||
const statusChip = document.getElementById("deadline-status-chip")!;
|
||||
statusChip.className = `akten-status-chip akten-status-${deadline.status}`;
|
||||
statusChip.textContent = t(`fristen.status.${deadline.status}`) || deadline.status;
|
||||
|
||||
const akteLink = document.getElementById("frist-project-link") as HTMLAnchorElement;
|
||||
const projectLink = document.getElementById("deadline-project-link") as HTMLAnchorElement;
|
||||
if (project) {
|
||||
akteLink.href = `/projects/${project.id}`;
|
||||
akteLink.textContent = `${project.reference || ""} \u2014 ${project.title}`;
|
||||
projectLink.href = `/projects/${project.id}`;
|
||||
projectLink.textContent = `${project.reference || ""} — ${project.title}`;
|
||||
} else {
|
||||
akteLink.href = `/projects/${frist.project_id}`;
|
||||
akteLink.textContent = "\u2014";
|
||||
projectLink.href = `/projects/${deadline.project_id}`;
|
||||
projectLink.textContent = "—";
|
||||
}
|
||||
|
||||
const ruleEl = document.getElementById("frist-rule-display")!;
|
||||
const ruleEl = document.getElementById("deadline-rule-display")!;
|
||||
if (rule) {
|
||||
const code = rule.rule_code || rule.code || "";
|
||||
ruleEl.textContent = code ? `${code} \u2014 ${rule.name}` : rule.name;
|
||||
ruleEl.textContent = code ? `${code} — ${rule.name}` : rule.name;
|
||||
} else {
|
||||
ruleEl.textContent = "\u2014";
|
||||
ruleEl.textContent = "—";
|
||||
}
|
||||
|
||||
(document.getElementById("frist-source-display") as HTMLElement).textContent =
|
||||
t(`fristen.source.${frist.source}`) || frist.source;
|
||||
(document.getElementById("deadline-source-display") as HTMLElement).textContent =
|
||||
t(`fristen.source.${deadline.source}`) || deadline.source;
|
||||
|
||||
(document.getElementById("frist-notes-display") as HTMLElement).textContent = frist.notes || "\u2014";
|
||||
(document.getElementById("frist-notes-edit") as HTMLTextAreaElement).value = frist.notes || "";
|
||||
(document.getElementById("deadline-notes-display") as HTMLElement).textContent = deadline.notes || "—";
|
||||
(document.getElementById("deadline-notes-edit") as HTMLTextAreaElement).value = deadline.notes || "";
|
||||
|
||||
(document.getElementById("frist-created-display") as HTMLElement).textContent = fmtDateTime(frist.created_at);
|
||||
(document.getElementById("deadline-created-display") as HTMLElement).textContent = fmtDateTime(deadline.created_at);
|
||||
|
||||
const completedLabel = document.getElementById("frist-completed-row-label")!;
|
||||
const completedDD = document.getElementById("frist-completed-display")!;
|
||||
if (frist.completed_at) {
|
||||
const completedLabel = document.getElementById("deadline-completed-row-label")!;
|
||||
const completedDD = document.getElementById("deadline-completed-display")!;
|
||||
if (deadline.completed_at) {
|
||||
completedLabel.style.display = "";
|
||||
completedDD.style.display = "";
|
||||
completedDD.textContent = fmtDateTime(frist.completed_at);
|
||||
completedDD.textContent = fmtDateTime(deadline.completed_at);
|
||||
} else {
|
||||
completedLabel.style.display = "none";
|
||||
completedDD.style.display = "none";
|
||||
}
|
||||
|
||||
const completeBtn = document.getElementById("frist-complete-btn") as HTMLButtonElement;
|
||||
if (frist.status === "completed") {
|
||||
const completeBtn = document.getElementById("deadline-complete-btn") as HTMLButtonElement;
|
||||
if (deadline.status === "completed") {
|
||||
completeBtn.disabled = true;
|
||||
completeBtn.textContent = t("fristen.detail.completed.already");
|
||||
} else {
|
||||
@@ -190,7 +190,7 @@ function render() {
|
||||
completeBtn.textContent = t("fristen.detail.complete");
|
||||
}
|
||||
|
||||
const deleteWrap = document.getElementById("frist-delete-wrap")!;
|
||||
const deleteWrap = document.getElementById("deadline-delete-wrap")!;
|
||||
if (me && (me.role === "partner" || me.role === "admin")) {
|
||||
deleteWrap.style.display = "";
|
||||
} else {
|
||||
@@ -199,14 +199,14 @@ function render() {
|
||||
}
|
||||
|
||||
function initEdit() {
|
||||
const titleDisplay = document.getElementById("frist-title-display")!;
|
||||
const titleEdit = document.getElementById("frist-title-edit") as HTMLInputElement;
|
||||
const dueDisplay = document.getElementById("frist-due-display")!;
|
||||
const dueEdit = document.getElementById("frist-due-edit") as HTMLInputElement;
|
||||
const notesDisplay = document.getElementById("frist-notes-display")!;
|
||||
const notesEdit = document.getElementById("frist-notes-edit") as HTMLTextAreaElement;
|
||||
const editBtn = document.getElementById("frist-edit-btn") as HTMLButtonElement;
|
||||
const saveBtn = document.getElementById("frist-save-btn") as HTMLButtonElement;
|
||||
const titleDisplay = document.getElementById("deadline-title-display")!;
|
||||
const titleEdit = document.getElementById("deadline-title-edit") as HTMLInputElement;
|
||||
const dueDisplay = document.getElementById("deadline-due-display")!;
|
||||
const dueEdit = document.getElementById("deadline-due-edit") as HTMLInputElement;
|
||||
const notesDisplay = document.getElementById("deadline-notes-display")!;
|
||||
const notesEdit = document.getElementById("deadline-notes-edit") as HTMLTextAreaElement;
|
||||
const editBtn = document.getElementById("deadline-edit-btn") as HTMLButtonElement;
|
||||
const saveBtn = document.getElementById("deadline-save-btn") as HTMLButtonElement;
|
||||
|
||||
function enterEdit() {
|
||||
titleDisplay.style.display = "none";
|
||||
@@ -234,20 +234,20 @@ function initEdit() {
|
||||
editBtn.addEventListener("click", enterEdit);
|
||||
|
||||
saveBtn.addEventListener("click", async () => {
|
||||
if (!frist) return;
|
||||
if (!deadline) return;
|
||||
const newTitle = titleEdit.value.trim();
|
||||
const newDue = dueEdit.value;
|
||||
const newNotes = notesEdit.value;
|
||||
if (!newTitle || !newDue) return;
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/deadlines/${frist.id}`, {
|
||||
const resp = await fetch(`/api/deadlines/${deadline.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ title: newTitle, due_date: newDue, notes: newNotes }),
|
||||
});
|
||||
if (resp.ok) {
|
||||
frist = await resp.json();
|
||||
deadline = await resp.json();
|
||||
render();
|
||||
}
|
||||
} finally {
|
||||
@@ -258,14 +258,14 @@ function initEdit() {
|
||||
}
|
||||
|
||||
function initComplete() {
|
||||
const btn = document.getElementById("frist-complete-btn") as HTMLButtonElement;
|
||||
const btn = document.getElementById("deadline-complete-btn") as HTMLButtonElement;
|
||||
btn.addEventListener("click", async () => {
|
||||
if (!frist || frist.status === "completed") return;
|
||||
if (!deadline || deadline.status === "completed") return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const resp = await fetch(`/api/deadlines/${frist.id}/complete`, { method: "PATCH" });
|
||||
const resp = await fetch(`/api/deadlines/${deadline.id}/complete`, { method: "PATCH" });
|
||||
if (resp.ok) {
|
||||
frist = await resp.json();
|
||||
deadline = await resp.json();
|
||||
render();
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
@@ -277,11 +277,11 @@ function initComplete() {
|
||||
}
|
||||
|
||||
function initDelete() {
|
||||
const btn = document.getElementById("frist-delete-btn")!;
|
||||
const modal = document.getElementById("frist-delete-modal")!;
|
||||
const close = document.getElementById("frist-delete-modal-close")!;
|
||||
const cancel = document.getElementById("frist-delete-modal-cancel")!;
|
||||
const confirmBtn = document.getElementById("frist-delete-modal-confirm") as HTMLButtonElement;
|
||||
const btn = document.getElementById("deadline-delete-btn")!;
|
||||
const modal = document.getElementById("deadline-delete-modal")!;
|
||||
const close = document.getElementById("deadline-delete-modal-close")!;
|
||||
const cancel = document.getElementById("deadline-delete-modal-cancel")!;
|
||||
const confirmBtn = document.getElementById("deadline-delete-modal-confirm") as HTMLButtonElement;
|
||||
|
||||
btn.addEventListener("click", () => {
|
||||
modal.style.display = "flex";
|
||||
@@ -295,11 +295,11 @@ function initDelete() {
|
||||
if (e.target === e.currentTarget) closeModal();
|
||||
});
|
||||
confirmBtn.addEventListener("click", async () => {
|
||||
if (!frist) return;
|
||||
if (!deadline) return;
|
||||
confirmBtn.disabled = true;
|
||||
const resp = await fetch(`/api/deadlines/${frist.id}`, { method: "DELETE" });
|
||||
const resp = await fetch(`/api/deadlines/${deadline.id}`, { method: "DELETE" });
|
||||
if (resp.ok) {
|
||||
const target = project ? `/projects/${project.id}/fristen` : "/deadlines";
|
||||
const target = project ? `/projects/${project.id}/deadlines` : "/deadlines";
|
||||
window.location.href = target;
|
||||
} else {
|
||||
confirmBtn.disabled = false;
|
||||
@@ -309,24 +309,24 @@ function initDelete() {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const id = parseFristID();
|
||||
const loading = document.getElementById("frist-loading")!;
|
||||
const notfound = document.getElementById("frist-notfound")!;
|
||||
const body = document.getElementById("frist-body")!;
|
||||
const id = parseDeadlineID();
|
||||
const loading = document.getElementById("deadline-loading")!;
|
||||
const notfound = document.getElementById("deadline-notfound")!;
|
||||
const body = document.getElementById("deadline-body")!;
|
||||
if (!id) {
|
||||
loading.style.display = "none";
|
||||
notfound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
await loadMe();
|
||||
const ok = await loadFrist(id);
|
||||
if (!ok || !frist) {
|
||||
const ok = await loadDeadline(id);
|
||||
if (!ok || !deadline) {
|
||||
loading.style.display = "none";
|
||||
notfound.style.display = "block";
|
||||
return;
|
||||
}
|
||||
await loadAkte(frist.project_id);
|
||||
if (frist.rule_id) await loadRule(frist.rule_id);
|
||||
await loadProject(deadline.project_id);
|
||||
if (deadline.rule_id) await loadRule(deadline.rule_id);
|
||||
|
||||
loading.style.display = "none";
|
||||
body.style.display = "";
|
||||
|
||||
@@ -24,14 +24,14 @@ function esc(s: string): string {
|
||||
}
|
||||
|
||||
function showError(msg: string) {
|
||||
const el = document.getElementById("frist-neu-msg")!;
|
||||
const el = document.getElementById("deadline-new-msg")!;
|
||||
el.textContent = msg;
|
||||
el.className = "form-msg form-msg-error";
|
||||
}
|
||||
|
||||
async function loadProjects() {
|
||||
const sel = document.getElementById("frist-project") as HTMLSelectElement;
|
||||
const hint = document.getElementById("frist-project-empty-hint")!;
|
||||
const sel = document.getElementById("deadline-project") as HTMLSelectElement;
|
||||
const hint = document.getElementById("deadline-project-empty-hint")!;
|
||||
try {
|
||||
const resp = await fetch("/api/projects");
|
||||
if (!resp.ok) return;
|
||||
@@ -59,7 +59,7 @@ async function loadProjects() {
|
||||
|
||||
async function loadRules() {
|
||||
// Optional: load rules so user can attach. We pull all rules; small set.
|
||||
const sel = document.getElementById("frist-rule") as HTMLSelectElement;
|
||||
const sel = document.getElementById("deadline-rule") as HTMLSelectElement;
|
||||
try {
|
||||
const resp = await fetch("/api/deadline-rules");
|
||||
if (!resp.ok) return;
|
||||
@@ -80,8 +80,8 @@ async function loadRules() {
|
||||
|
||||
function initBackLinks() {
|
||||
if (preselectedProjectID) {
|
||||
const back = document.getElementById("frist-neu-back") as HTMLAnchorElement;
|
||||
const cancel = document.getElementById("frist-neu-cancel") as HTMLAnchorElement;
|
||||
const back = document.getElementById("deadline-new-back") as HTMLAnchorElement;
|
||||
const cancel = document.getElementById("deadline-new-cancel") as HTMLAnchorElement;
|
||||
back.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
cancel.href = `/projects/${preselectedProjectID}/deadlines`;
|
||||
}
|
||||
@@ -89,14 +89,14 @@ function initBackLinks() {
|
||||
|
||||
async function submitForm(e: Event) {
|
||||
e.preventDefault();
|
||||
const submitBtn = document.querySelector<HTMLButtonElement>("#frist-neu-form button[type=submit]")!;
|
||||
const msg = document.getElementById("frist-neu-msg")!;
|
||||
const submitBtn = document.querySelector<HTMLButtonElement>("#deadline-new-form button[type=submit]")!;
|
||||
const msg = document.getElementById("deadline-new-msg")!;
|
||||
|
||||
const projectID = (document.getElementById("frist-project") as HTMLSelectElement).value;
|
||||
const title = (document.getElementById("frist-title") as HTMLInputElement).value.trim();
|
||||
const due = (document.getElementById("frist-due") as HTMLInputElement).value;
|
||||
const ruleID = (document.getElementById("frist-rule") as HTMLSelectElement).value;
|
||||
const notes = (document.getElementById("frist-notes") as HTMLTextAreaElement).value.trim();
|
||||
const projectID = (document.getElementById("deadline-project") as HTMLSelectElement).value;
|
||||
const title = (document.getElementById("deadline-title") as HTMLInputElement).value.trim();
|
||||
const due = (document.getElementById("deadline-due") as HTMLInputElement).value;
|
||||
const ruleID = (document.getElementById("deadline-rule") as HTMLSelectElement).value;
|
||||
const notes = (document.getElementById("deadline-notes") as HTMLTextAreaElement).value.trim();
|
||||
|
||||
if (!projectID || !title || !due) {
|
||||
showError(t("fristen.error.required"));
|
||||
@@ -156,9 +156,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
initSidebar();
|
||||
detectPreselect();
|
||||
initBackLinks();
|
||||
document.getElementById("frist-neu-form")!.addEventListener("submit", submitForm);
|
||||
document.getElementById("deadline-new-form")!.addEventListener("submit", submitForm);
|
||||
// Default due to today
|
||||
const dueInput = document.getElementById("frist-due") as HTMLInputElement;
|
||||
const dueInput = document.getElementById("deadline-due") as HTMLInputElement;
|
||||
if (!dueInput.value) dueInput.value = new Date().toISOString().split("T")[0];
|
||||
await Promise.all([loadProjects(), loadRules()]);
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ interface Deadline {
|
||||
rule_id?: string;
|
||||
project_reference: string;
|
||||
project_title: string;
|
||||
projekt_office: string;
|
||||
rule_code?: string;
|
||||
}
|
||||
|
||||
@@ -32,14 +31,14 @@ interface Summary {
|
||||
let allDeadlines: Deadline[] = [];
|
||||
let allProjects: Project[] = [];
|
||||
let statusFilter = "pending";
|
||||
let akteFilter = "";
|
||||
let projectFilter = "";
|
||||
let loadedOK = false;
|
||||
|
||||
function urlParams(): URLSearchParams {
|
||||
return new URLSearchParams(window.location.search);
|
||||
}
|
||||
|
||||
async function loadAkten() {
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const resp = await fetch("/api/projects");
|
||||
if (resp.ok) allProjects = await resp.json();
|
||||
@@ -50,8 +49,8 @@ async function loadAkten() {
|
||||
|
||||
async function loadSummary() {
|
||||
try {
|
||||
const url = akteFilter
|
||||
? `/api/deadlines/summary?project_id=${encodeURIComponent(akteFilter)}`
|
||||
const url = projectFilter
|
||||
? `/api/deadlines/summary?project_id=${encodeURIComponent(projectFilter)}`
|
||||
: `/api/deadlines/summary`;
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) return;
|
||||
@@ -70,18 +69,18 @@ function setCount(id: string, n: number) {
|
||||
if (el) el.textContent = String(n);
|
||||
}
|
||||
|
||||
async function loadFristen() {
|
||||
const unavailable = document.getElementById("fristen-unavailable")!;
|
||||
async function loadDeadlines() {
|
||||
const unavailable = document.getElementById("deadlines-unavailable")!;
|
||||
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (statusFilter) params.set("status", statusFilter);
|
||||
if (akteFilter) params.set("project_id", akteFilter);
|
||||
if (projectFilter) params.set("project_id", projectFilter);
|
||||
const resp = await fetch(`/api/deadlines?${params.toString()}`);
|
||||
if (resp.status === 503) {
|
||||
unavailable.style.display = "block";
|
||||
tableWrap.style.display = "none";
|
||||
document.getElementById("fristen-empty")!.style.display = "none";
|
||||
document.getElementById("deadlines-empty")!.style.display = "none";
|
||||
return;
|
||||
}
|
||||
if (!resp.ok) {
|
||||
@@ -89,7 +88,7 @@ async function loadFristen() {
|
||||
tableWrap.style.display = "none";
|
||||
return;
|
||||
}
|
||||
allFristen = await resp.json();
|
||||
allDeadlines = await resp.json();
|
||||
loadedOK = true;
|
||||
render();
|
||||
} catch {
|
||||
@@ -102,7 +101,7 @@ function urgencyClass(due: string, status: string): string {
|
||||
if (status === "completed") return "frist-urgency-done";
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const d = new Date(due + "T00:00:00");
|
||||
const d = new Date(due.slice(0, 10) + "T00:00:00");
|
||||
const diffDays = Math.floor((d.getTime() - today.getTime()) / 86400000);
|
||||
if (diffDays < 0) return "frist-urgency-overdue";
|
||||
if (diffDays <= 7) return "frist-urgency-soon";
|
||||
@@ -111,7 +110,7 @@ function urgencyClass(due: string, status: string): string {
|
||||
|
||||
function fmtDate(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso + "T00:00:00");
|
||||
const d = new Date(iso.length === 10 ? iso + "T00:00:00" : iso);
|
||||
return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
@@ -130,15 +129,15 @@ function esc(s: string): string {
|
||||
|
||||
function render() {
|
||||
if (!loadedOK) return;
|
||||
const tbody = document.getElementById("fristen-body")!;
|
||||
const empty = document.getElementById("fristen-empty")!;
|
||||
const emptyFiltered = document.getElementById("fristen-empty-filtered")!;
|
||||
const tbody = document.getElementById("deadlines-body")!;
|
||||
const empty = document.getElementById("deadlines-empty")!;
|
||||
const emptyFiltered = document.getElementById("deadlines-empty-filtered")!;
|
||||
const tableWrap = document.querySelector<HTMLElement>(".akten-table-wrap")!;
|
||||
|
||||
if (allFristen.length === 0) {
|
||||
if (allDeadlines.length === 0) {
|
||||
tbody.innerHTML = "";
|
||||
tableWrap.style.display = "none";
|
||||
if (statusFilter === "all" && !akteFilter) {
|
||||
if (statusFilter === "all" && !projectFilter) {
|
||||
empty.style.display = "block";
|
||||
emptyFiltered.style.display = "none";
|
||||
} else {
|
||||
@@ -152,7 +151,7 @@ function render() {
|
||||
empty.style.display = "none";
|
||||
emptyFiltered.style.display = "none";
|
||||
|
||||
tbody.innerHTML = allFristen
|
||||
tbody.innerHTML = allDeadlines
|
||||
.map((f) => {
|
||||
const urgency = urgencyClass(f.due_date, f.status);
|
||||
const statusLabel = t(`fristen.status.${f.status}`) || f.status;
|
||||
@@ -180,7 +179,6 @@ function render() {
|
||||
tbody.querySelectorAll<HTMLTableRowElement>(".frist-row").forEach((row) => {
|
||||
const id = row.dataset.id!;
|
||||
row.addEventListener("click", (e) => {
|
||||
// Don't navigate if clicking the checkbox or a link
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.closest(".frist-complete-cb") || target.closest("a")) return;
|
||||
window.location.href = `/deadlines/${id}`;
|
||||
@@ -193,7 +191,7 @@ function render() {
|
||||
try {
|
||||
const resp = await fetch(`/api/deadlines/${id}/complete`, { method: "PATCH" });
|
||||
if (resp.ok) {
|
||||
await Promise.all([loadFristen(), loadSummary()]);
|
||||
await Promise.all([loadDeadlines(), loadSummary()]);
|
||||
} else {
|
||||
cb.checked = false;
|
||||
cb.disabled = false;
|
||||
@@ -208,38 +206,36 @@ function render() {
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
const status = document.getElementById("frist-filter-status") as HTMLSelectElement;
|
||||
const project = document.getElementById("frist-filter-project") as HTMLSelectElement;
|
||||
const status = document.getElementById("deadline-filter-status") as HTMLSelectElement;
|
||||
const project = document.getElementById("deadline-filter-project") as HTMLSelectElement;
|
||||
|
||||
// Pre-fill from URL
|
||||
const params = urlParams();
|
||||
if (params.has("status")) statusFilter = params.get("status")!;
|
||||
if (params.has("project_id")) akteFilter = params.get("project_id")!;
|
||||
if (params.has("project_id")) projectFilter = params.get("project_id")!;
|
||||
status.value = statusFilter;
|
||||
|
||||
status.addEventListener("change", async () => {
|
||||
statusFilter = status.value;
|
||||
await Promise.all([loadFristen(), loadSummary()]);
|
||||
await Promise.all([loadDeadlines(), loadSummary()]);
|
||||
});
|
||||
project.addEventListener("change", async () => {
|
||||
akteFilter = project.value;
|
||||
await Promise.all([loadFristen(), loadSummary()]);
|
||||
projectFilter = project.value;
|
||||
await Promise.all([loadDeadlines(), loadSummary()]);
|
||||
});
|
||||
}
|
||||
|
||||
function populateAkteFilter() {
|
||||
const sel = document.getElementById("frist-filter-project") as HTMLSelectElement;
|
||||
// Keep the first "all" option, then append sorted Akten.
|
||||
function populateProjectFilter() {
|
||||
const sel = document.getElementById("deadline-filter-project") as HTMLSelectElement;
|
||||
const options: string[] = [
|
||||
`<option value="" data-i18n="fristen.filter.akte.all">${esc(t("fristen.filter.akte.all"))}</option>`,
|
||||
];
|
||||
for (const a of allProjects) {
|
||||
options.push(
|
||||
`<option value="${esc(a.id)}">${esc(a.reference || "")} \u2014 ${esc(a.title)}</option>`,
|
||||
`<option value="${esc(a.id)}">${esc(a.reference || "")} — ${esc(a.title)}</option>`,
|
||||
);
|
||||
}
|
||||
sel.innerHTML = options.join("");
|
||||
if (akteFilter) sel.value = akteFilter;
|
||||
if (projectFilter) sel.value = projectFilter;
|
||||
}
|
||||
|
||||
function initSummaryCards() {
|
||||
@@ -247,8 +243,8 @@ function initSummaryCards() {
|
||||
card.addEventListener("click", async () => {
|
||||
const newStatus = card.dataset.status!;
|
||||
statusFilter = newStatus;
|
||||
(document.getElementById("frist-filter-status") as HTMLSelectElement).value = newStatus;
|
||||
await Promise.all([loadFristen(), loadSummary()]);
|
||||
(document.getElementById("deadline-filter-status") as HTMLSelectElement).value = newStatus;
|
||||
await Promise.all([loadDeadlines(), loadSummary()]);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -259,7 +255,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
initFilters();
|
||||
initSummaryCards();
|
||||
onLangChange(render);
|
||||
await loadAkten();
|
||||
populateAkteFilter();
|
||||
await Promise.all([loadFristen(), loadSummary()]);
|
||||
await loadProjects();
|
||||
populateProjectFilter();
|
||||
await Promise.all([loadDeadlines(), loadSummary()]);
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ export function renderDeadlinesCalendar(): string {
|
||||
<button type="button" id="cal-today" className="btn-secondary btn-small" data-i18n="fristen.kalender.today">Heute</button>
|
||||
</div>
|
||||
|
||||
<div className="frist-calendar" id="frist-calendar">
|
||||
<div className="frist-calendar" id="deadline-calendar">
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.mon">Mo</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.tue">Di</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.wed">Mi</div>
|
||||
@@ -47,10 +47,10 @@ export function renderDeadlinesCalendar(): string {
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.fri">Fr</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.sat">Sa</div>
|
||||
<div className="frist-cal-weekday" data-i18n="cal.day.sun">So</div>
|
||||
<div id="frist-cal-grid" className="frist-cal-grid" />
|
||||
<div id="deadline-cal-grid" className="frist-cal-grid" />
|
||||
</div>
|
||||
|
||||
<p className="akten-events-empty" id="frist-cal-empty" style="display:none" data-i18n="fristen.kalender.empty">
|
||||
<p className="akten-events-empty" id="deadline-cal-empty" style="display:none" data-i18n="fristen.kalender.empty">
|
||||
Keine Fristen im ausgewählten Zeitraum.
|
||||
</p>
|
||||
|
||||
|
||||
@@ -19,37 +19,37 @@ export function renderDeadlinesDetail(): string {
|
||||
<div className="container">
|
||||
<a href="/deadlines" className="akten-back-link" data-i18n="fristen.detail.back">← Zurück zur Fristenübersicht</a>
|
||||
|
||||
<div id="frist-loading" className="akten-loading">
|
||||
<div id="deadline-loading" className="akten-loading">
|
||||
<p data-i18n="fristen.detail.loading">Lädt…</p>
|
||||
</div>
|
||||
|
||||
<div id="frist-notfound" className="akten-empty" style="display:none">
|
||||
<div id="deadline-notfound" className="akten-empty" style="display:none">
|
||||
<p data-i18n="fristen.detail.notfound">Frist nicht gefunden oder keine Berechtigung.</p>
|
||||
</div>
|
||||
|
||||
<div id="frist-body" style="display:none">
|
||||
<div id="deadline-body" style="display:none">
|
||||
<header className="akten-detail-header">
|
||||
<div className="akten-detail-title-row">
|
||||
<div className="akten-detail-title-col">
|
||||
<h1 id="frist-title-display" />
|
||||
<input type="text" id="frist-title-edit" className="akten-title-input" style="display:none" />
|
||||
<h1 id="deadline-title-display" />
|
||||
<input type="text" id="deadline-title-edit" className="akten-title-input" style="display:none" />
|
||||
<div className="akten-detail-meta">
|
||||
<span id="frist-due-chip" className="frist-due-chip" />
|
||||
<span id="frist-status-chip" className="akten-status-chip" />
|
||||
<a id="frist-project-link" className="akten-ref" href="#" />
|
||||
<span id="deadline-due-chip" className="frist-due-chip" />
|
||||
<span id="deadline-status-chip" className="akten-status-chip" />
|
||||
<a id="deadline-project-link" className="akten-ref" href="#" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="akten-detail-actions">
|
||||
<button id="frist-complete-btn" type="button" className="btn-primary btn-cta-lime btn-small" data-i18n="fristen.detail.complete">
|
||||
<button id="deadline-complete-btn" type="button" className="btn-primary btn-cta-lime btn-small" data-i18n="fristen.detail.complete">
|
||||
Als erledigt markieren
|
||||
</button>
|
||||
<button id="frist-edit-btn" className="btn-icon" type="button" aria-label="Bearbeiten" data-i18n-title="fristen.detail.edit" title="Bearbeiten">
|
||||
<button id="deadline-edit-btn" className="btn-icon" type="button" aria-label="Bearbeiten" data-i18n-title="fristen.detail.edit" title="Bearbeiten">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button id="frist-save-btn" className="btn-primary btn-cta-lime" type="button" style="display:none" data-i18n="fristen.detail.save">
|
||||
<button id="deadline-save-btn" className="btn-primary btn-cta-lime" type="button" style="display:none" data-i18n="fristen.detail.save">
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
@@ -60,56 +60,56 @@ export function renderDeadlinesDetail(): string {
|
||||
<dl className="frist-detail-list">
|
||||
<dt data-i18n="fristen.detail.due">Fälligkeitsdatum</dt>
|
||||
<dd>
|
||||
<span id="frist-due-display" />
|
||||
<input type="date" id="frist-due-edit" style="display:none" />
|
||||
<span id="deadline-due-display" />
|
||||
<input type="date" id="deadline-due-edit" style="display:none" />
|
||||
</dd>
|
||||
|
||||
<dt data-i18n="fristen.detail.rule">Regel</dt>
|
||||
<dd id="frist-rule-display">—</dd>
|
||||
<dd id="deadline-rule-display">—</dd>
|
||||
|
||||
<dt data-i18n="fristen.detail.source">Quelle</dt>
|
||||
<dd id="frist-source-display" />
|
||||
<dd id="deadline-source-display" />
|
||||
|
||||
<dt data-i18n="fristen.detail.notes">Notizen</dt>
|
||||
<dd>
|
||||
<span id="frist-notes-display" />
|
||||
<textarea id="frist-notes-edit" rows={3} style="display:none" />
|
||||
<span id="deadline-notes-display" />
|
||||
<textarea id="deadline-notes-edit" rows={3} style="display:none" />
|
||||
</dd>
|
||||
|
||||
<dt data-i18n="fristen.detail.created">Angelegt</dt>
|
||||
<dd id="frist-created-display" />
|
||||
<dd id="deadline-created-display" />
|
||||
|
||||
<dt id="frist-completed-row-label" data-i18n="fristen.detail.completed" style="display:none">
|
||||
<dt id="deadline-completed-row-label" data-i18n="fristen.detail.completed" style="display:none">
|
||||
Erledigt am
|
||||
</dt>
|
||||
<dd id="frist-completed-display" style="display:none" />
|
||||
<dd id="deadline-completed-display" style="display:none" />
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section className="frist-notes-section">
|
||||
<h2 className="frist-section-heading" data-i18n="notizen.section.title">Notizen</h2>
|
||||
<div id="notes-container" className="notiz-container" data-parent-type="frist" />
|
||||
<div id="notes-container" className="notiz-container" data-parent-type="deadline" />
|
||||
</section>
|
||||
|
||||
<div className="akten-detail-footer" id="frist-delete-wrap" style="display:none">
|
||||
<button id="frist-delete-btn" className="btn-danger" type="button" data-i18n="fristen.detail.delete">
|
||||
<div className="akten-detail-footer" id="deadline-delete-wrap" style="display:none">
|
||||
<button id="deadline-delete-btn" className="btn-danger" type="button" data-i18n="fristen.detail.delete">
|
||||
Frist löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-overlay" id="frist-delete-modal" style="display:none">
|
||||
<div className="modal-overlay" id="deadline-delete-modal" style="display:none">
|
||||
<div className="modal-card">
|
||||
<div className="modal-header">
|
||||
<h2 data-i18n="fristen.detail.delete.confirm.title">Frist wirklich löschen?</h2>
|
||||
<button className="modal-close" id="frist-delete-modal-close" type="button">×</button>
|
||||
<button className="modal-close" id="deadline-delete-modal-close" type="button">×</button>
|
||||
</div>
|
||||
<p data-i18n="fristen.detail.delete.confirm.body">
|
||||
Die Frist wird endgültig entfernt. Der Eintrag im Verlauf der Akte bleibt erhalten.
|
||||
</p>
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn-cancel" id="frist-delete-modal-cancel" data-i18n="fristen.detail.delete.confirm.cancel">Abbrechen</button>
|
||||
<button type="button" className="btn-danger" id="frist-delete-modal-confirm" data-i18n="fristen.detail.delete.confirm.ok">Löschen</button>
|
||||
<button type="button" className="btn-cancel" id="deadline-delete-modal-cancel" data-i18n="fristen.detail.delete.confirm.cancel">Abbrechen</button>
|
||||
<button type="button" className="btn-danger" id="deadline-delete-modal-confirm" data-i18n="fristen.detail.delete.confirm.ok">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,29 +18,29 @@ export function renderDeadlinesNew(): string {
|
||||
<section className="tool-page">
|
||||
<div className="container container-narrow">
|
||||
<div className="tool-header">
|
||||
<a href="/deadlines" className="akten-back-link" id="frist-neu-back" data-i18n="fristen.neu.back">← Zurück zur Übersicht</a>
|
||||
<a href="/deadlines" className="akten-back-link" id="deadline-new-back" data-i18n="fristen.neu.back">← Zurück zur Übersicht</a>
|
||||
<h1 data-i18n="fristen.neu.heading">Neue Frist anlegen</h1>
|
||||
<p className="tool-subtitle" data-i18n="fristen.neu.subtitle">
|
||||
Eine persistente Frist an einer Akte. Sichtbar für alle Personen, die die Akte sehen können.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form id="frist-neu-form" className="akten-form" autocomplete="off">
|
||||
<form id="deadline-new-form" className="akten-form" autocomplete="off">
|
||||
<div className="form-field">
|
||||
<label htmlFor="frist-project" data-i18n="fristen.field.akte">Akte</label>
|
||||
<select id="frist-project" required>
|
||||
<label htmlFor="deadline-project" data-i18n="fristen.field.akte">Akte</label>
|
||||
<select id="deadline-project" required>
|
||||
<option value="" disabled selected data-i18n="fristen.field.akte.choose">Bitte wählen…</option>
|
||||
</select>
|
||||
<p className="form-hint" id="frist-project-empty-hint" style="display:none" data-i18n="fristen.field.akte.empty">
|
||||
<p className="form-hint" id="deadline-project-empty-hint" style="display:none" data-i18n="fristen.field.akte.empty">
|
||||
Sie haben noch keine Akte. Bitte zuerst eine Akte anlegen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="frist-title" data-i18n="fristen.field.title">Titel</label>
|
||||
<label htmlFor="deadline-title" data-i18n="fristen.field.title">Titel</label>
|
||||
<input
|
||||
type="text"
|
||||
id="frist-title"
|
||||
id="deadline-title"
|
||||
required
|
||||
placeholder="z.B. Klageerwiderung einreichen"
|
||||
data-i18n-placeholder="fristen.field.title.placeholder"
|
||||
@@ -49,27 +49,27 @@ export function renderDeadlinesNew(): string {
|
||||
|
||||
<div className="form-field-row">
|
||||
<div className="form-field">
|
||||
<label htmlFor="frist-due" data-i18n="fristen.field.due">Fälligkeitsdatum</label>
|
||||
<input type="date" id="frist-due" required />
|
||||
<label htmlFor="deadline-due" data-i18n="fristen.field.due">Fälligkeitsdatum</label>
|
||||
<input type="date" id="deadline-due" required />
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="frist-rule" data-i18n="fristen.field.rule">Regel (optional)</label>
|
||||
<select id="frist-rule">
|
||||
<label htmlFor="deadline-rule" data-i18n="fristen.field.rule">Regel (optional)</label>
|
||||
<select id="deadline-rule">
|
||||
<option value="" data-i18n="fristen.field.rule.none">Keine Regel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-field">
|
||||
<label htmlFor="frist-notes" data-i18n="fristen.field.notes">Notizen (optional)</label>
|
||||
<textarea id="frist-notes" rows={3} placeholder="Hinweise, Verweise, nächste Schritte…" data-i18n-placeholder="fristen.field.notes.placeholder" />
|
||||
<label htmlFor="deadline-notes" data-i18n="fristen.field.notes">Notizen (optional)</label>
|
||||
<textarea id="deadline-notes" rows={3} placeholder="Hinweise, Verweise, nächste Schritte…" data-i18n-placeholder="fristen.field.notes.placeholder" />
|
||||
</div>
|
||||
|
||||
<p className="form-msg" id="frist-neu-msg" />
|
||||
<p className="form-msg" id="deadline-new-msg" />
|
||||
|
||||
<div className="form-actions">
|
||||
<a href="/deadlines" id="frist-neu-cancel" className="btn-cancel" data-i18n="fristen.neu.cancel">Abbrechen</a>
|
||||
<a href="/deadlines" id="deadline-new-cancel" className="btn-cancel" data-i18n="fristen.neu.cancel">Abbrechen</a>
|
||||
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="fristen.neu.submit">Frist anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function renderDeadlines(): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="frist-summary-cards" id="frist-summary">
|
||||
<div className="frist-summary-cards" id="deadlines-summary">
|
||||
<button type="button" className="frist-summary-card frist-card-overdue" data-status="overdue">
|
||||
<span className="frist-summary-dot" />
|
||||
<span className="frist-summary-count" id="sum-overdue">0</span>
|
||||
@@ -61,8 +61,8 @@ export function renderDeadlines(): string {
|
||||
|
||||
<div className="akten-controls">
|
||||
<div className="akten-filter-row">
|
||||
<label className="akten-filter-label" htmlFor="frist-filter-status" data-i18n="fristen.filter.status">Status</label>
|
||||
<select id="frist-filter-status" className="akten-select">
|
||||
<label className="akten-filter-label" htmlFor="deadline-filter-status" data-i18n="fristen.filter.status">Status</label>
|
||||
<select id="deadline-filter-status" className="akten-select">
|
||||
<option value="all" data-i18n="fristen.filter.all">Alle offenen & erledigten</option>
|
||||
<option value="pending" data-i18n="fristen.filter.pending">Alle offenen</option>
|
||||
<option value="overdue" data-i18n="fristen.filter.overdue">Überfällig</option>
|
||||
@@ -71,21 +71,21 @@ export function renderDeadlines(): string {
|
||||
<option value="completed" data-i18n="fristen.filter.completed">Erledigt</option>
|
||||
</select>
|
||||
|
||||
<label className="akten-filter-label" htmlFor="frist-filter-project" data-i18n="fristen.filter.akte">Akte</label>
|
||||
<select id="frist-filter-project" className="akten-select">
|
||||
<label className="akten-filter-label" htmlFor="deadline-filter-project" data-i18n="fristen.filter.akte">Akte</label>
|
||||
<select id="deadline-filter-project" className="akten-select">
|
||||
<option value="" data-i18n="fristen.filter.akte.all">Alle Akten</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fristen-unavailable" className="akten-unavailable" style="display:none">
|
||||
<div id="deadlines-unavailable" className="akten-unavailable" style="display:none">
|
||||
<p data-i18n="fristen.unavailable">
|
||||
Fristenverwaltung zurzeit nicht verfügbar — bitte Administrator kontaktieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="akten-table-wrap">
|
||||
<table className="akten-table fristen-table" id="fristen-table">
|
||||
<table className="akten-table fristen-table" id="deadlines-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
@@ -96,11 +96,11 @@ export function renderDeadlines(): string {
|
||||
<th data-i18n="fristen.col.status">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="fristen-body" />
|
||||
<tbody id="deadlines-body" />
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="akten-empty" id="fristen-empty" style="display:none">
|
||||
<div className="akten-empty" id="deadlines-empty" style="display:none">
|
||||
<h2 data-i18n="fristen.empty.title">Keine Fristen vorhanden</h2>
|
||||
<p data-i18n="fristen.empty.hint">
|
||||
Sobald Fristen angelegt oder aus dem Fristenrechner übernommen werden, erscheinen sie hier.
|
||||
@@ -108,7 +108,7 @@ export function renderDeadlines(): string {
|
||||
<a href="/deadlines/new" className="btn-primary btn-cta-lime" data-i18n="fristen.list.new">Neue Frist</a>
|
||||
</div>
|
||||
|
||||
<div className="akten-empty akten-empty-filtered" id="fristen-empty-filtered" style="display:none">
|
||||
<div className="akten-empty akten-empty-filtered" id="deadlines-empty-filtered" style="display:none">
|
||||
<p data-i18n="fristen.empty.filtered">Keine Fristen mit diesen Filtern.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ type UpcomingDeadline struct {
|
||||
DueDate string `json:"due_date" db:"due_date"`
|
||||
ProjectID uuid.UUID `json:"project_id" db:"project_id"`
|
||||
ProjectTitle string `json:"project_title" db:"project_title"`
|
||||
ProjectRef string `json:"projekt_ref" db:"projekt_ref"`
|
||||
ProjectRef string `json:"project_reference" db:"project_reference"`
|
||||
Urgency string `json:"urgency"`
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ type UpcomingAppointment struct {
|
||||
Type *string `json:"type" db:"appointment_type"`
|
||||
ProjectID *uuid.UUID `json:"project_id" db:"project_id"`
|
||||
ProjectTitle *string `json:"project_title" db:"project_title"`
|
||||
ProjectRef *string `json:"projekt_ref" db:"projekt_ref"`
|
||||
ProjectRef *string `json:"project_reference" db:"project_reference"`
|
||||
}
|
||||
|
||||
// ActivityEntry is one row in the "Letzte Aktivität" feed.
|
||||
@@ -91,7 +91,7 @@ type ActivityEntry struct {
|
||||
ActorName *string `json:"actor_name" db:"actor_name"`
|
||||
ProjectID uuid.UUID `json:"project_id" db:"project_id"`
|
||||
ProjectTitle string `json:"project_title" db:"project_title"`
|
||||
ProjectRef string `json:"projekt_ref" db:"projekt_ref"`
|
||||
ProjectRef string `json:"project_reference" db:"project_reference"`
|
||||
Action *string `json:"action" db:"action"`
|
||||
Details string `json:"details" db:"details"`
|
||||
Description *string `json:"description" db:"description"`
|
||||
@@ -202,7 +202,7 @@ SELECT f.id,
|
||||
to_char(f.due_date, 'YYYY-MM-DD') AS due_date,
|
||||
p.id AS project_id,
|
||||
p.title AS project_title,
|
||||
COALESCE(p.reference, '') AS projekt_ref
|
||||
COALESCE(p.reference, '') AS project_reference
|
||||
FROM paliad.deadlines f
|
||||
JOIN paliad.projects p ON p.id = f.project_id
|
||||
WHERE f.status = 'pending'
|
||||
@@ -231,7 +231,7 @@ SELECT t.id,
|
||||
t.appointment_type,
|
||||
t.project_id,
|
||||
p.title AS project_title,
|
||||
COALESCE(p.reference, NULL) AS projekt_ref
|
||||
COALESCE(p.reference, NULL) AS project_reference
|
||||
FROM paliad.appointments t
|
||||
LEFT JOIN paliad.projects p ON p.id = t.project_id
|
||||
WHERE t.start_at >= $3
|
||||
@@ -260,7 +260,7 @@ SELECT COALESCE(e.event_date, e.created_at) AS timestamp,
|
||||
u.display_name AS actor_name,
|
||||
e.project_id,
|
||||
p.title AS project_title,
|
||||
COALESCE(p.reference, '') AS projekt_ref,
|
||||
COALESCE(p.reference, '') AS project_reference,
|
||||
e.event_type AS action,
|
||||
e.title AS details,
|
||||
e.description
|
||||
|
||||
Reference in New Issue
Block a user