Merge: deadlines/{id} notfound + Invalid Date list (t-paliad-039)

This commit is contained in:
m
2026-04-26 01:32:09 +02:00
18 changed files with 371 additions and 381 deletions

View File

@@ -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&auml;hlten Zeitraum.
</p>

View File

@@ -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">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<div id="termin-loading" className="akten-loading" data-i18n="termine.detail.loading">L&auml;dt&hellip;</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&auml;dt&hellip;</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&ouml;schen</button>
<button type="button" id="appointment-delete-btn" className="btn-danger" data-i18n="termine.detail.delete">Termin l&ouml;schen</button>
<button type="submit" className="btn-primary btn-cta-lime" data-i18n="termine.detail.save">&Auml;nderungen speichern</button>
</div>
</form>

View File

@@ -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">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<a href="/appointments" className="akten-back-link" id="appointment-new-back" data-i18n="termine.neu.back">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<h1 data-i18n="termine.neu.heading">Neuer Termin</h1>
<p className="tool-subtitle" data-i18n="termine.neu.subtitle">
Pers&ouml;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&uuml;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&ouml;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&uuml;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&uuml;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&auml;chste Schritte&hellip;" 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&auml;chste Schritte&hellip;" 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>

View File

@@ -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 &amp; pers&ouml;nlich</option>
<option value="__personal__" data-i18n="termine.filter.akte.personal">Nur pers&ouml;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&uuml;gbar &mdash; 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>

View File

@@ -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();
});

View File

@@ -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) {

View File

@@ -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);
});

View File

@@ -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()]);
});

View File

@@ -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)} &middot; ${esc(d.project_title)}</span>
<span class="dashboard-list-ref">${esc(d.project_reference)} &middot; ${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)} &middot; ${esc(a.project_title)}</span>`
const projectLine = a.project_reference && a.project_title
? `<span class="dashboard-list-ref">${esc(a.project_reference)} &middot; ${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>`;

View File

@@ -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();
});

View File

@@ -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 = "";

View File

@@ -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()]);
});

View File

@@ -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()]);
});

View File

@@ -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&auml;hlten Zeitraum.
</p>

View File

@@ -19,37 +19,37 @@ export function renderDeadlinesDetail(): string {
<div className="container">
<a href="/deadlines" className="akten-back-link" data-i18n="fristen.detail.back">&larr; Zur&uuml;ck zur Fristen&uuml;bersicht</a>
<div id="frist-loading" className="akten-loading">
<div id="deadline-loading" className="akten-loading">
<p data-i18n="fristen.detail.loading">L&auml;dt&hellip;</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&auml;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">&mdash;</dd>
<dd id="deadline-rule-display">&mdash;</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&ouml;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&ouml;schen?</h2>
<button className="modal-close" id="frist-delete-modal-close" type="button">&times;</button>
<button className="modal-close" id="deadline-delete-modal-close" type="button">&times;</button>
</div>
<p data-i18n="fristen.detail.delete.confirm.body">
Die Frist wird endg&uuml;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&ouml;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&ouml;schen</button>
</div>
</div>
</div>

View File

@@ -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">&larr; Zur&uuml;ck zur &Uuml;bersicht</a>
<a href="/deadlines" className="akten-back-link" id="deadline-new-back" data-i18n="fristen.neu.back">&larr; Zur&uuml;ck zur &Uuml;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&uuml;r alle Personen, die die Akte sehen k&ouml;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&auml;hlen&hellip;</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&auml;lligkeitsdatum</label>
<input type="date" id="frist-due" required />
<label htmlFor="deadline-due" data-i18n="fristen.field.due">F&auml;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&auml;chste Schritte&hellip;" 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&auml;chste Schritte&hellip;" 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>

View File

@@ -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 &amp; erledigten</option>
<option value="pending" data-i18n="fristen.filter.pending">Alle offenen</option>
<option value="overdue" data-i18n="fristen.filter.overdue">&Uuml;berf&auml;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&uuml;gbar &mdash; 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 &uuml;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>

View File

@@ -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