feat(260929-if2): Erinnerung zusaetzlich per E-Mail, Doku und Aenderungsliste

- E-Mail-Planer: Anspruch vor dem Senden (genau eine Mail je Faelligkeit, hoechstens 3 Versuche), Systemlesen nur fuer die Kandidatenabfrage
- MailService.sendReminderEmail (Berliner Zeit, nur Text), GET /reminders/email-status, Haken im Formular mit Erklaerung
- Zugriffsklassifikation und Erlaubnisliste fuer forSystem nachgezogen, Aenderungsliste und Anwenderanleitung

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 14:10:06 +02:00
parent 709b41a007
commit 6879c756f2
21 changed files with 1038 additions and 21 deletions
@@ -7,6 +7,7 @@ import { requestBrowserPermissionOnce } from '@/lib/reminder-notify';
import { defaultNewReminderInputs, isoToLocalInputs, localInputsToIso } from '@/lib/reminder-time';
import {
type Reminder,
type ReminderEmailStatus,
ReminderRequestError,
createReminder,
updateReminder,
@@ -15,6 +16,8 @@ import {
interface ReminderFormModalProps {
/** `null` = neue Erinnerung anlegen, sonst diese (noch nicht faellige) bearbeiten. */
reminder: Reminder | null;
/** Ergebnis von `GET /reminders/email-status`; `null` = unbekannt oder fehlgeschlagen (gilt als nicht moeglich). */
emailStatus: ReminderEmailStatus | null;
onClose: () => void;
/** Nach erfolgreichem Speichern (die Kachel laedt neu und meldet die Aenderung). */
onSaved: () => void;
@@ -42,7 +45,13 @@ const DESCRIPTION_MAX = 2000;
* eine Nutzerhandlung, die der Browser fuer die Frage akzeptiert. Nie beim
* Laden der Seite.
*/
export function ReminderFormModal({ reminder, onClose, onSaved, onStale }: ReminderFormModalProps) {
export function ReminderFormModal({
reminder,
emailStatus,
onClose,
onSaved,
onStale,
}: ReminderFormModalProps) {
const t = useTranslations('widgets');
const initial = useRef(
reminder ? isoToLocalInputs(reminder.dueAt) : defaultNewReminderInputs(new Date()),
@@ -51,6 +60,7 @@ export function ReminderFormModal({ reminder, onClose, onSaved, onStale }: Remin
const [time, setTime] = useState(initial.time);
const [title, setTitle] = useState(reminder?.title ?? '');
const [description, setDescription] = useState(reminder?.description ?? '');
const [emailEnabled, setEmailEnabled] = useState(reminder?.emailEnabled ?? false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const titleRef = useRef<HTMLInputElement>(null);
@@ -84,7 +94,16 @@ export function ReminderFormModal({ reminder, onClose, onSaved, onStale }: Remin
setSaving(true);
try {
const input = { title: trimmedTitle, description: description.trim(), dueAt };
// `emailEnabled` nur senden, wenn es sich aendert oder beim Anlegen `true`
// ist: ein Server, der E-Mail nicht mehr anbietet, lehnt `true` mit 400
// ab — eine unveraenderte Alt-Einstellung darf das Speichern nicht kippen.
const emailChanged = reminder ? emailEnabled !== reminder.emailEnabled : emailEnabled;
const input = {
title: trimmedTitle,
description: description.trim(),
dueAt,
...(emailChanged ? { emailEnabled } : {}),
};
if (reminder) {
await updateReminder(reminder.id, input);
} else {
@@ -105,6 +124,17 @@ export function ReminderFormModal({ reminder, onClose, onSaved, onStale }: Remin
}
};
// E-Mail moeglich: Versand eingerichtet UND Adresse im Konto. Unbekannt gilt
// als nicht moeglich. Ein bereits angehaktes Feld bleibt bedienbar, damit man
// es wieder abwaehlen kann.
const emailAvailable = emailStatus?.smtpConfigured === true && emailStatus.hasEmail;
const emailHint =
emailStatus === null || emailAvailable
? null
: !emailStatus.smtpConfigured
? t('reminder.emailNoSmtp')
: t('reminder.emailNoAddress');
if (typeof document === 'undefined') return null;
return createPortal(
@@ -175,6 +205,24 @@ export function ReminderFormModal({ reminder, onClose, onSaved, onStale }: Remin
/>
</div>
<div className="space-y-1">
<label className="flex items-center gap-2 text-sm text-foreground">
<input
type="checkbox"
checked={emailEnabled}
disabled={!emailAvailable && !emailEnabled}
onChange={(e) => setEmailEnabled(e.target.checked)}
aria-describedby={emailHint ? 'reminder-email-hint' : undefined}
/>
{t('reminder.emailLabel')}
</label>
{emailHint && (
<p id="reminder-email-hint" className="text-xs text-muted-foreground">
{emailHint}
</p>
)}
</div>
{error && (
<div
role="alert"