feat(260929-if2): Erinnerungen anlegen und zur Faelligkeit benachrichtigen (Tracer)

- Reminder-Tabelle mit Zeilenschutz (Mandant+Benutzer, Systemlesen fuer den E-Mail-Planer), API reminders (Liste, Anlegen)
- Kachel "Erinnerungen", globaler Melder im Portalrahmen (Browser und Desktop, je Faelligkeit einmal)
- Desktop: Laufzeit-Berechtigung fuer Benachrichtigungen nur fuer die gespeicherte Server-Adresse
- Zugriffsklassifikation nachgemessen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 13:54:07 +02:00
parent cd1f8f6cda
commit 325c5ddbf2
31 changed files with 1969 additions and 11 deletions
+62
View File
@@ -0,0 +1,62 @@
/**
* Erinnerungen — API-Klient (quick-260929-if2). Spiegelt den
* `RemindersController` der API. Alle Aufrufe mit `credentials: 'include'`
* (Cookie-Anmeldung), Muster `favorites-api.ts`.
*/
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
/** Ereignis nach jeder erfolgreichen Aenderung — der globale Melder laedt daraufhin neu. */
export const REMINDERS_CHANGED_EVENT = 'tessera:reminders-changed';
/** Eine Erinnerung; Zeitangaben als ISO-Zeichenketten (so liefert sie die API). */
export interface Reminder {
id: string;
title: string;
description: string;
dueAt: string;
emailEnabled: boolean;
createdAt: string;
updatedAt: string;
}
/** Ein abgewiesener Aufruf; `status` ist der HTTP-Status (401, 404, 409 ...). */
export class ReminderRequestError extends Error {
public readonly status: number;
constructor(status: number) {
super(`Reminder request failed (${status})`);
this.name = 'ReminderRequestError';
this.status = status;
}
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_URL}${path}`, { credentials: 'include', ...init });
if (!res.ok) throw new ReminderRequestError(res.status);
return res.json() as Promise<T>;
}
function jsonInit(method: string, body: unknown): RequestInit {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
};
}
/** Die eigenen Erinnerungen, die naechste Faelligkeit zuerst. */
export function listReminders(): Promise<Reminder[]> {
return request<Reminder[]>('/reminders');
}
export interface ReminderInput {
title: string;
description?: string;
/** ISO-8601-Zeitpunkt. */
dueAt: string;
}
export function createReminder(input: ReminderInput): Promise<Reminder> {
return request<Reminder>('/reminders', jsonInit('POST', input));
}