feat(260929-if2): faellige Erinnerungen erledigen, spaeter erinnern, bearbeiten und loeschen

- API: Aendern (409 wenn faellig), Spaeter erinnern (409 wenn nicht faellig, setzt E-Mail-Spur zurueck), Loeschen; fremde Kennungen 404
- Kachel: faellige Zeilen hervorgehoben mit Erledigt und drei Spaeter-Optionen, kuenftige mit Bearbeiten und Loeschen
- Zeit-Hilfen (morgen zur gleichen Uhrzeit), Zugriffsklassifikation nachgemessen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 13:59:43 +02:00
parent 325c5ddbf2
commit 709b41a007
16 changed files with 946 additions and 57 deletions
@@ -4,13 +4,22 @@ import { useTranslations } from 'next-intl';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { requestBrowserPermissionOnce } from '@/lib/reminder-notify';
import { defaultNewReminderInputs, localInputsToIso } from '@/lib/reminder-time';
import { ReminderRequestError, createReminder } from '@/lib/reminders-api';
import { defaultNewReminderInputs, isoToLocalInputs, localInputsToIso } from '@/lib/reminder-time';
import {
type Reminder,
ReminderRequestError,
createReminder,
updateReminder,
} from '@/lib/reminders-api';
interface ReminderFormModalProps {
/** `null` = neue Erinnerung anlegen, sonst diese (noch nicht faellige) bearbeiten. */
reminder: Reminder | null;
onClose: () => void;
/** Nach erfolgreichem Speichern (die Kachel laedt neu und meldet die Aenderung). */
onSaved: () => void;
/** Der Server meldet, dass die Erinnerung inzwischen faellig ist (409) — die Kachel laedt neu. */
onStale?: () => void;
}
const TITLE_MAX = 200;
@@ -25,18 +34,23 @@ const DESCRIPTION_MAX = 2000;
* macht sich fuer `position: fixed` zum Bezugsrahmen (Vorbild
* `picture-frame-lightbox.tsx`). Dialog-Aufbau wie `custom-module-form-modal`.
*
* ERLAUBNIS (D-04): `requestBrowserPermissionOnce()` laeuft als ERSTES im
* Absende-Handler, synchron und noch vor jedem `await` — so bleibt der Aufruf
* BEARBEITEN (`reminder` gesetzt): nur fuer eine noch nicht faellige Erinnerung;
* eine inzwischen faellige lehnt der Server mit 409 ab (`alreadyDue`).
*
* ERLAUBNIS (D-04): `requestBrowserPermissionOnce()` laeuft beim ANLEGEN als
* ERSTES im Absende-Handler, synchron und noch vor jedem `await` — so bleibt der Aufruf
* eine Nutzerhandlung, die der Browser fuer die Frage akzeptiert. Nie beim
* Laden der Seite.
*/
export function ReminderFormModal({ onClose, onSaved }: ReminderFormModalProps) {
export function ReminderFormModal({ reminder, onClose, onSaved, onStale }: ReminderFormModalProps) {
const t = useTranslations('widgets');
const initial = useRef(defaultNewReminderInputs(new Date())).current;
const initial = useRef(
reminder ? isoToLocalInputs(reminder.dueAt) : defaultNewReminderInputs(new Date()),
).current;
const [date, setDate] = useState(initial.date);
const [time, setTime] = useState(initial.time);
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [title, setTitle] = useState(reminder?.title ?? '');
const [description, setDescription] = useState(reminder?.description ?? '');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const titleRef = useRef<HTMLInputElement>(null);
@@ -53,7 +67,7 @@ export function ReminderFormModal({ onClose, onSaved }: ReminderFormModalProps)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Als Erstes und synchron: Nutzerhandlung fuer die Browser-Erlaubnis (D-04).
requestBrowserPermissionOnce();
if (!reminder) requestBrowserPermissionOnce();
setError(null);
const trimmedTitle = title.trim();
@@ -70,15 +84,22 @@ export function ReminderFormModal({ onClose, onSaved }: ReminderFormModalProps)
setSaving(true);
try {
await createReminder({ title: trimmedTitle, description: description.trim(), dueAt });
const input = { title: trimmedTitle, description: description.trim(), dueAt };
if (reminder) {
await updateReminder(reminder.id, input);
} else {
await createReminder(input);
}
onSaved();
onClose();
} catch (err) {
setError(
err instanceof ReminderRequestError && err.status === 409
? t('reminder.limitReached')
: t('reminder.saveError'),
);
if (err instanceof ReminderRequestError && err.status === 409) {
// Anlegen: Grenze erreicht; Bearbeiten: inzwischen faellig.
setError(reminder ? t('reminder.alreadyDue') : t('reminder.limitReached'));
if (reminder) onStale?.();
} else {
setError(t('reminder.saveError'));
}
} finally {
setSaving(false);
}
@@ -95,7 +116,7 @@ export function ReminderFormModal({ onClose, onSaved }: ReminderFormModalProps)
className="w-full max-w-md rounded-lg bg-card p-6 shadow-lg dark:border dark:border-border"
>
<h2 id="reminder-form-title" className="mb-4 text-lg font-semibold text-foreground">
{t('reminder.formTitle')}
{reminder ? t('reminder.editTitle') : t('reminder.formTitle')}
</h2>
<form onSubmit={handleSubmit} noValidate className="space-y-4">
<div className="flex gap-3">