"use client" import { useEffect, useState } from "react" import { AlertCircle } from "lucide-react" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { MarkdownInput } from "@/components/markdown-input" import { updatePolicyDefaults, usePolicyDefaults } from "@/hooks/use-policy-defaults" import { ApiError } from "@/lib/api" import { DEFAULTED_FIELDS, DEFAULTED_FIELD_LABELS, DefaultedField, REQUIRED_DEFAULTED_FIELDS, } from "@/lib/policy-defaults" import { PolicyDefaults } from "@/types/privacy-policy" /** Eingabewerte des Formulars – wie PolicyDefaults, aber ohne `updatedAt`. */ type DefaultsFormData = Omit const emptyForm: DefaultsFormData = { responsible: "", contactDPO: "", dataSubjectsRights: "", complaintRight: "", withdrawalRight: "", objectionRight: "", thirdCountryTransfer: "", automatedDecision: "", provisionObligation: "", provisionConsequences: "", } type TabId = "base" | "rights" | "processing" /** Aufteilung der Bausteine auf die Reiter des Dialogs. */ const tabFields: Record = { base: ["responsible", "contactDPO"], processing: ["thirdCountryTransfer", "automatedDecision"], rights: [ "dataSubjectsRights", "complaintRight", "withdrawalRight", "objectionRight", "provisionObligation", "provisionConsequences", ], } /** Zeilenhöhe der Textfelder – die Rechtetexte sind deutlich länger. */ const fieldRows: Partial> = { dataSubjectsRights: 12, complaintRight: 8, withdrawalRight: 6, objectionRight: 8, thirdCountryTransfer: 6, automatedDecision: 6, provisionObligation: 4, provisionConsequences: 4, } interface PolicyDefaultsFormProps { open: boolean onOpenChange: (open: boolean) => void } /** * Pflegt die Standardtexte, die für alle Erklärungen gelten. Eine einzelne * Erklärung kann jeden dieser Bausteine im Sonderfall überschreiben. */ export function PolicyDefaultsForm({ open, onOpenChange }: PolicyDefaultsFormProps) { const { defaults, isLoading } = usePolicyDefaults() const [formData, setFormData] = useState(emptyForm) const [errors, setErrors] = useState([]) const [invalidFields, setInvalidFields] = useState>(new Set()) const [isSubmitting, setIsSubmitting] = useState(false) const [tab, setTab] = useState("base") // Beim Öffnen den gespeicherten Stand laden useEffect(() => { if (!open || isLoading) return setErrors([]) setInvalidFields(new Set()) setTab("base") // Über die Feldliste statt Feld für Feld: Ein neuer Baustein im Schema // landet damit automatisch auch im Formular. setFormData( Object.fromEntries( DEFAULTED_FIELDS.map((key) => [key, defaults[key]]) ) as DefaultsFormData ) // `defaults` bewusst nicht als Abhängigkeit: sonst würden Eingaben beim // Hintergrund-Refresh von SWR überschrieben. // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, isLoading]) /** Setzt ein Feld und entfernt dessen Fehlermarkierung. */ const set = (key: DefaultedField, value: string) => { setFormData((prev) => ({ ...prev, [key]: value })) setInvalidFields((prev) => { if (!prev.has(key)) return prev const next = new Set(prev) next.delete(key) return next }) } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() // Pflicht-Bausteine dürfen zentral nicht leer sein const missing = REQUIRED_DEFAULTED_FIELDS.filter( (field) => !formData[field].trim() ) if (missing.length > 0) { setErrors(missing.map((field) => `${DEFAULTED_FIELD_LABELS[field]} ist erforderlich`)) setInvalidFields(new Set(missing)) const reiter = (Object.keys(tabFields) as TabId[]).find((id) => tabFields[id].includes(missing[0]) ) setTab(reiter ?? "base") return } setErrors([]) setInvalidFields(new Set()) setIsSubmitting(true) try { await updatePolicyDefaults(formData) toast.success("Standardtexte gespeichert") onOpenChange(false) } catch (error) { const details = error instanceof ApiError && error.details.length > 0 ? error.details : [error instanceof Error ? error.message : "Fehler beim Speichern"] setErrors(details) } finally { setIsSubmitting(false) } } /** Rendert ein Baustein-Feld mit Beschriftung und Markdown-Vorschau. */ const field = (name: DefaultedField) => ( set(name, value)} rows={fieldRows[name] ?? 6} required={REQUIRED_DEFAULTED_FIELDS.includes(name)} invalid={invalidFields.has(name)} /> ) return ( Standardtexte Diese Texte gelten für alle Datenschutzerklärungen, die keinen eigenen Text hinterlegt haben. Eine Änderung wirkt sich sofort auf alle diese Erklärungen aus.
setTab(value as TabId)} className="min-h-0 flex-1 gap-3" > Stammdaten Verarbeitung Ihre Rechte {errors.length > 0 && (
    {errors.map((error, index) => (
  • {error}
  • ))}
)} {tabFields.base.map(field)}

Auf beides muss ausdrücklich hingewiesen werden – auch dann, wenn es nicht stattfindet. Ein klares „findet nicht statt“ ist die richtige Antwort, kein leeres Feld.

{tabFields.processing.map(field)}
{tabFields.rights.slice(0, 2).map(field)}
{tabFields.rights.slice(2).map(field)}
) }