L6 Benachrichtigungen & Audit: Glocke, Posteingang, Nutzer-Einstellungen, Mailkonfiguration
- 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>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user