- Glocke im Backoffice-Header (ungelesen-Zähler, letzte 10, alle gelesen), mobil einbindbar über variant="mobile". - /notifications mit Filter gelesen/ungelesen/Art, Öffnen markiert gelesen (nur relative Links), Pagination. - /account Abschnitt Benachrichtigungen: E-Mail-Opt-out je Typ, Notdienst Pflicht. - /settings/email (tenant:manage): Absendername, Antwortadresse, Empfänger Notdienst und Abrechnung; Validierung gegen Header-Injection, max. 20 Adressen, Audit. - Actions unter actions/notifications mit moduleGuard + guard; Navigation ergänzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
58 lines
2.2 KiB
TypeScript
58 lines
2.2 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { saveNotificationPreferences, type PreferencesState } from "@/server/actions/notifications/preferences";
|
|
|
|
export type PreferenceItem = { type: string; label: string; email: boolean; mandatory: boolean };
|
|
|
|
/** E-mail opt-out per notification type (in-app notifications are always on). */
|
|
export function NotificationPreferencesForm({
|
|
items,
|
|
labels,
|
|
}: {
|
|
items: PreferenceItem[];
|
|
labels: { email: string; mandatory: string; save: string; saved: string; error: string };
|
|
}) {
|
|
const [state, action, pending] = useActionState(saveNotificationPreferences, { status: "idle" } as PreferencesState);
|
|
|
|
return (
|
|
<form action={action} className="space-y-3">
|
|
<ul className="divide-y rounded-lg border">
|
|
{items.map((item) => {
|
|
const id = `pref-${item.type}`;
|
|
return (
|
|
<li key={item.type} className="flex min-h-11 items-center justify-between gap-3 px-3 py-1.5">
|
|
<label htmlFor={id} className="flex-1 text-[13px]">
|
|
{item.label}
|
|
{item.mandatory && <span className="block text-[11.5px] text-muted-foreground">{item.mandatory ? labels.mandatory : null}</span>}
|
|
</label>
|
|
<span className="flex items-center gap-2 text-[12px] text-muted-foreground">
|
|
{labels.email}
|
|
<input
|
|
id={id}
|
|
type="checkbox"
|
|
name="emailOn"
|
|
value={item.type}
|
|
defaultChecked={item.email}
|
|
disabled={item.mandatory}
|
|
className="size-5 accent-[var(--primary)]"
|
|
/>
|
|
</span>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
<div className="flex items-center gap-3">
|
|
<Button type="submit" disabled={pending} className="min-h-11">
|
|
{labels.save}
|
|
</Button>
|
|
<p role="status" className="text-[12.5px]">
|
|
{state.status === "saved" && <span className="text-[var(--ok)]">✓ {labels.saved}</span>}
|
|
{state.status === "error" && <span className="text-[var(--risk)]">⚠ {labels.error}</span>}
|
|
</p>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|