"use client" import { useMemo, useState } from "react" import { ArrowDown, ArrowUp, Eye, EyeOff, FileText, FileType2, Scale, LogOut, MoreHorizontal, Pencil, Plus, Search, TriangleAlert, Trash2, Users, X, } from "lucide-react" import { signOut, useSession } from "next-auth/react" import { toast } from "sonner" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { Avatar, AvatarFallback } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Skeleton } from "@/components/ui/skeleton" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" import { Brand } from "@/components/brand" import { PolicyDetail } from "@/components/policy-detail" import { PolicyDefaultsForm } from "@/components/policy-defaults-form" import { PolicyForm } from "@/components/policy-form" import { SitePagesForm } from "@/components/site-pages-form" import { ThemeToggle } from "@/components/theme-toggle" import { UserManagement } from "@/components/user-management" import { deletePolicy, usePrivacyPolicies } from "@/hooks/use-privacy-policies" import { usePolicyDefaults } from "@/hooks/use-policy-defaults" import { describeMissing, missingRequiredFields } from "@/lib/policy-completeness" import { formatDateTime, initials } from "@/lib/format" import { compareByTitle } from "@/lib/policy-sort" import { PrivacyPolicy } from "@/types/privacy-policy" type SortKey = "title" | "createdAt" | "updatedAt" export default function AdminDashboard() { const { data: session, status } = useSession() const { policies, isLoading, error } = usePrivacyPolicies() const { defaults } = usePolicyDefaults() const [searchTerm, setSearchTerm] = useState("") // Vorgabe: alphabetisch nach Titel. Die Spaltenköpfe bleiben umschaltbar. const [sortBy, setSortBy] = useState("title") const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") const [isFormOpen, setIsFormOpen] = useState(false) const [editingPolicy, setEditingPolicy] = useState(null) const [previewPolicy, setPreviewPolicy] = useState(null) const [isPreviewOpen, setIsPreviewOpen] = useState(false) const [policyToDelete, setPolicyToDelete] = useState(null) const [isDeleting, setIsDeleting] = useState(false) const [isUserManagementOpen, setIsUserManagementOpen] = useState(false) const [isDefaultsOpen, setIsDefaultsOpen] = useState(false) const [isSitePagesOpen, setIsSitePagesOpen] = useState(false) const visiblePolicies = useMemo(() => { const term = searchTerm.trim().toLowerCase() return policies .filter( (policy) => !term || policy.title.toLowerCase().includes(term) || policy.responsible.toLowerCase().includes(term) ) .sort((a, b) => { if (sortBy === "title") { return sortOrder === "asc" ? compareByTitle(a, b) : compareByTitle(b, a) } const aDate = new Date(a[sortBy]).getTime() const bDate = new Date(b[sortBy]).getTime() return sortOrder === "asc" ? aDate - bDate : bDate - aDate }) }, [policies, searchTerm, sortBy, sortOrder]) const toggleSort = (key: SortKey) => { if (sortBy === key) { setSortOrder(sortOrder === "asc" ? "desc" : "asc") } else { setSortBy(key) setSortOrder(key === "title" ? "asc" : "desc") } } const handleAddNew = () => { setEditingPolicy(null) setIsFormOpen(true) } const handlePreview = (policy: PrivacyPolicy) => { setPreviewPolicy(policy) setIsPreviewOpen(true) } const handleEdit = (policy: PrivacyPolicy) => { setEditingPolicy(policy) setIsFormOpen(true) } const handleDelete = async () => { if (!policyToDelete) return setIsDeleting(true) try { await deletePolicy(policyToDelete.id) toast.success("Datenschutzerklärung gelöscht") setPolicyToDelete(null) } catch (err) { toast.error(err instanceof Error ? err.message : "Fehler beim Löschen") } finally { setIsDeleting(false) } } const publicCount = policies.filter((policy) => policy.isPublic).length /** * Fehlende Pflichtangaben nach Art. 13/14 DSGVO je Erklärung. * * Die API liefert die Bausteine bereits mit eingesetztem Standardtext – was * hier als fehlend gilt, fehlt also tatsächlich und nicht nur im Datensatz. */ const missingByPolicy = useMemo(() => { const map = new Map() for (const policy of policies) { const missing = missingRequiredFields( policy as unknown as Record, defaults ) if (missing.length > 0) map.set(policy.id, missing.map(describeMissing)) } return map }, [policies, defaults]) const incompleteCount = missingByPolicy.size const SortableHead = ({ column, children, className, }: { column: SortKey children: React.ReactNode className?: string }) => ( ) if (status === "loading") { return (
) } if (status === "unauthenticated") { return (
Nicht angemeldet – Weiterleitung…
) } return (
Admin
Öffentliche Ansicht Standardtexte Benutzer {session?.user?.name || "Angemeldet"} {session?.user?.email} setIsDefaultsOpen(true)}> Standardtexte pflegen setIsSitePagesOpen(true)}> Rechtliche Seiten setIsUserManagementOpen(true)}> Benutzer verwalten signOut({ callbackUrl: "/auth/signin" })} > Abmelden

Datenschutzerklärungen

{policies.length} gesamt · {publicCount} öffentlich ·{" "} {policies.length - publicCount} privat {incompleteCount > 0 && ( <> {" · "} {incompleteCount} unvollständig )}

setSearchTerm(e.target.value)} className="px-9" /> {searchTerm && ( )}
{error ? (
Fehler beim Laden der Daten: {error.message}
) : isLoading ? (
{Array.from({ length: 5 }).map((_, index) => ( ))}
) : visiblePolicies.length === 0 ? (

{searchTerm ? "Keine Treffer" : "Noch keine Datenschutzerklärungen"}

{searchTerm ? "Passen Sie den Suchbegriff an." : "Legen Sie die erste DSGVO-Erklärung an."}

{!searchTerm && ( )}
) : (
Titel Verantwortlicher Status Erstellt Geändert {visiblePolicies.map((policy) => ( {policy.responsible.replace(/[*_#`]/g, "").split("\n")[0]}
{policy.isPublic ? ( Öffentlich ) : ( Privat )} {missingByPolicy.has(policy.id) && ( {missingByPolicy.get(policy.id)!.length}

Fehlende Pflichtangaben

    {missingByPolicy.get(policy.id)!.map((text) => (
  • · {text}
  • ))}
)}
{formatDateTime(policy.createdAt)} {formatDateTime(policy.updatedAt)} handlePreview(policy)}> Vorschau handleEdit(policy)}> Bearbeiten setPolicyToDelete(policy)} > Löschen
))}
)}
setIsUserManagementOpen(false)} /> !open && setPolicyToDelete(null)} > Datenschutzerklärung löschen? „{policyToDelete?.title}“ wird unwiderruflich entfernt. Diese Aktion kann nicht rückgängig gemacht werden. Abbrechen { e.preventDefault() handleDelete() }} disabled={isDeleting} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {isDeleting ? "Löschen…" : "Löschen"}
) }