syncova-policies/components/policy-detail.tsx
Weapie 265ca8ac38
All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 5m15s
Improve accessibility and access controls; verify production and database recovery
2026-09-14 11:44:41 +02:00

160 lines
5.9 KiB
TypeScript

"use client"
import {
AlertTriangle,
Briefcase,
Calendar,
Contact,
Database,
ClipboardCheck,
Cpu,
Gavel,
Globe,
Layers,
Scale,
OctagonAlert,
RotateCcw,
ShieldCheck,
Target,
Timer,
UserCheck,
Users,
type LucideIcon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
import {
Dialog,
DialogBody,
DialogContent,
DialogHeader,
DialogDescription,
DialogTitle,
} from "@/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { MarkdownRenderer } from "@/components/markdown-renderer"
import { PrintPolicy } from "@/components/print-policy"
import { PrivacyPolicy } from "@/types/privacy-policy"
import { formatDate } from "@/lib/format"
interface PolicyDetailProps {
policy: PrivacyPolicy | null
open: boolean
onOpenChange: (open: boolean) => void
}
interface Section {
icon: LucideIcon
label: string
content?: string | null
}
function SectionBlock({ icon: Icon, label, content }: Section) {
if (!content) return null
return (
<section className="space-y-2">
<h3 className="flex items-center gap-2 text-sm font-semibold">
<Icon className="size-4 text-brand" />
{label}
</h3>
<div className="rounded-lg border bg-muted/30 p-4">
<MarkdownRenderer content={content} />
</div>
</section>
)
}
export function PolicyDetail({ policy, open, onOpenChange }: PolicyDetailProps) {
const responsibleSections: Section[] = policy
? [
{ icon: Contact, label: "Verantwortlicher im Sinne der DSGVO", content: policy.responsible },
{ icon: ShieldCheck, label: "Kontaktdaten der/des Datenschutzbeauftragten", content: policy.contactDPO },
]
: []
const processingSections: Section[] = policy
? [
{ icon: Briefcase, label: "Bezeichnung der Verarbeitungstätigkeit", content: policy.processingDescription },
{ icon: Target, label: "Zweck der Datenerhebung", content: policy.processingPurposes },
{ icon: Gavel, label: "Rechtsgrundlage der Verarbeitung", content: policy.legalBasis },
{ icon: Scale, label: "Berechtigte Interessen", content: policy.legitimateInterests },
{ icon: Layers, label: "Kategorien der verarbeiteten Daten", content: policy.dataCategories },
{ icon: Users, label: "Geplante Empfänger der Daten", content: policy.recipients },
{ icon: Globe, label: "Übermittlung in Drittländer", content: policy.thirdCountryTransfer },
{ icon: Timer, label: "Speicherdauer", content: policy.storageDuration },
{ icon: Database, label: "Herkunft der Daten", content: policy.dataSource },
{ icon: Cpu, label: "Automatisierte Entscheidungsfindung", content: policy.automatedDecision },
]
: []
const rightsSections: Section[] = policy
? [
{ icon: UserCheck, label: "Betroffenen-Rechte", content: policy.dataSubjectsRights },
{ icon: AlertTriangle, label: "Beschwerderecht bei der Aufsichtsbehörde", content: policy.complaintRight },
{ icon: RotateCcw, label: "Widerrufsrecht bei Einwilligung", content: policy.withdrawalRight },
{ icon: OctagonAlert, label: "Widerspruchsrecht (Art. 21 DSGVO)", content: policy.objectionRight },
{ icon: ClipboardCheck, label: "Ihre Pflicht zur Bereitstellung der Daten", content: policy.provisionObligation },
{ icon: OctagonAlert, label: "Folgen, wenn Sie die Daten nicht angeben", content: policy.provisionConsequences },
]
: []
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-h-[calc(100dvh-2rem)] sm:max-w-3xl">
{policy && (
<>
<DialogHeader>
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0 space-y-2">
<DialogTitle>{policy.title}</DialogTitle>
<DialogDescription>Vorschau der Datenschutzerklärung</DialogDescription>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground">
<span className="flex items-center gap-1.5">
<Calendar className="size-4" />
Erstellt am {formatDate(policy.createdAt)}
</span>
{policy.updatedAt !== policy.createdAt && (
<span>Aktualisiert am {formatDate(policy.updatedAt)}</span>
)}
<Badge variant="brand">DSGVO</Badge>
</div>
</div>
<PrintPolicy policy={policy} size="sm" className="mr-8" />
</div>
</DialogHeader>
<Tabs defaultValue="responsible" className="min-h-0 flex-1">
<TabsList className="w-full">
<TabsTrigger value="responsible">Verantwortlich</TabsTrigger>
<TabsTrigger value="processing">Verarbeitung</TabsTrigger>
<TabsTrigger value="rights">Ihre Rechte</TabsTrigger>
</TabsList>
<DialogBody>
<TabsContent value="responsible" className="space-y-5 py-1">
{responsibleSections.map((section) => (
<SectionBlock key={section.label} {...section} />
))}
</TabsContent>
<TabsContent value="processing" className="space-y-5 py-1">
{processingSections.map((section) => (
<SectionBlock key={section.label} {...section} />
))}
</TabsContent>
<TabsContent value="rights" className="space-y-5 py-1">
{rightsSections.map((section) => (
<SectionBlock key={section.label} {...section} />
))}
</TabsContent>
</DialogBody>
</Tabs>
</>
)}
</DialogContent>
</Dialog>
)
}