'use client' /** * CookieFindings — bereitet die Library-Befunde bearbeitbar auf, statt als * Fließtext-Liste. Zwei Sichten (Umschalter): * - Nach Fehlertyp: je Typ eine Maßnahme + betroffene Cookies + Ticket-Text * (= eine Ticket-Einheit). Getrennt in FINDINGS (zu beheben) und HINWEISE * (neutral, gegen DSE zu prüfen: Drittland, EU-Alternative). * - Matrix: Zeilen = Cookies, Spalten = Fehlertypen, Markierung wo nachzubessern * ist (ein Cookie, alle Probleme auf einen Blick). */ import React, { useMemo, useState } from 'react' import type { CookieFinding } from './CookieLibraryPanel' const TYPE_LABEL: Record = { tracker_as_necessary: 'Tracker als „notwendig" deklariert', missing_purpose: 'Zweck fehlt', excessive_lifetime: 'Speicherdauer zu lang', vague_duration: 'Speicherdauer nicht konkret', missing_retention: 'Keine Speicherdauer/Löschfrist', storage_transparency: 'Speichertyp nicht transparent', third_country: 'Drittland-Transfer', eu_alternative: 'EU-Alternative verfügbar', } const TYPE_MEASURE: Record = { tracker_as_necessary: 'Als einwilligungspflichtig einstufen (§ 25 Abs. 1 TDDDG).', missing_purpose: 'Zweck je Cookie ergänzen (Art. 13 DSGVO).', vague_duration: 'Konkrete Speicherdauer oder Löschkriterium angeben (Art. 5 Abs. 1 lit. e).', missing_retention: 'Speicherdauer/Löschfrist je Verarbeiter festlegen (Art. 5 Abs. 1 lit. e).', excessive_lifetime: 'Speicherdauer auf das Erforderliche reduzieren (Art. 5 Abs. 1 lit. e).', storage_transparency: 'Speichertyp + -dauer je Objekt transparent ausweisen (§ 25 TDDDG).', third_country: 'Geeignete Garantien je Verarbeiter prüfen (SCC Art. 46 / Art. 49).', eu_alternative: 'EU-Alternative prüfen (kommerziell, kein Drittland-Transfer).', } const TYPE_ORDER = [ 'tracker_as_necessary', 'missing_purpose', 'vague_duration', 'missing_retention', 'excessive_lifetime', 'storage_transparency', 'third_country', 'eu_alternative', ] const SEV_ORDER: Record = { HIGH: 0, MEDIUM: 1, LOW: 2 } const SEV_COLOR: Record = { HIGH: 'bg-red-100 text-red-700', MEDIUM: 'bg-amber-100 text-amber-700', LOW: 'bg-blue-100 text-blue-700', } interface Group { type: string; items: CookieFinding[]; severity: string } function groupByType(findings: CookieFinding[]): Group[] { const m = new Map() for (const f of findings) { if (!m.has(f.type)) m.set(f.type, []) m.get(f.type)!.push(f) } const groups = [...m.entries()].map(([type, items]) => ({ type, items, severity: items.reduce( (s, f) => (SEV_ORDER[f.severity] ?? 3) < (SEV_ORDER[s] ?? 3) ? f.severity : s, 'LOW'), })) groups.sort((a, b) => (TYPE_ORDER.indexOf(a.type) + 99) % 100 - (TYPE_ORDER.indexOf(b.type) + 99) % 100) return groups } function cookieLabel(f: CookieFinding): string { const v = f.vendor && f.vendor !== '—' ? ` (${f.vendor})` : '' const d = f.declared ? ` — ${f.declared}` : '' return `${f.cookie}${v}${d}` } function ticketText(g: Group): string { return [ `${TYPE_LABEL[g.type] || g.type} — ${g.items.length} betroffen`, `Maßnahme: ${TYPE_MEASURE[g.type] || ''}`, '', ...g.items.map(f => `- ${cookieLabel(f)}`), ].join('\n') } function GroupCard({ g }: { g: Group }) { const [open, setOpen] = useState(false) const [copied, setCopied] = useState(false) const copy = () => { navigator.clipboard?.writeText(ticketText(g)).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500) }).catch(() => {}) } return (
{open && (
Maßnahme: {TYPE_MEASURE[g.type] || '—'}
{g.items.map((f, i) => ( ))}
{f.cookie} {f.vendor} {f.declared || ''}
)}
) } function Section({ title, hint, groups }: { title: string; hint?: string; groups: Group[] }) { if (!groups.length) return null return (
{title} {hint && {hint}}
{groups.map(g => )}
) } function Matrix({ findings }: { findings: CookieFinding[] }) { const { rows, cols } = useMemo(() => { const colSet = new Set(findings.map(f => f.type)) const cols = TYPE_ORDER.filter(t => colSet.has(t)) const rowMap = new Map }>() for (const f of findings) { const key = `${f.cookie}@@${f.vendor}` if (!rowMap.has(key)) rowMap.set(key, { label: f.cookie, vendor: f.vendor, hits: {} }) rowMap.get(key)!.hits[f.type] = (f.kind === 'hinweis') ? '⚠' : '✗' } return { rows: [...rowMap.values()], cols } }, [findings]) return (
{cols.map(c => ( ))} {rows.map((r, i) => ( {cols.map(c => ( ))} ))}
Cookie {(TYPE_LABEL[c] || c).split(' ')[0]}
{r.label} {r.vendor && r.vendor !== '—' && · {r.vendor}} {r.hits[c] || '·'}
✗ = Handlung nötig · ⚠ = Hinweis (zu prüfen) · Spalte = Fehlertyp (Tooltip)
) } export function CookieFindings({ findings }: { findings: CookieFinding[] }) { const [mode, setMode] = useState<'type' | 'matrix'>('type') const real = findings.filter(f => (f.kind ?? 'finding') !== 'hinweis') const hints = findings.filter(f => (f.kind ?? 'finding') === 'hinweis') if (!findings.length) { return
Keine Abweichungen gegen die Library.
} const btn = (m: 'type' | 'matrix', label: string) => ( ) return (
{findings.length} Befund{findings.length !== 1 ? 'e' : ''} {real.length} zu beheben · {hints.length} Hinweise
{btn('type', 'Nach Fehlertyp')} {btn('matrix', 'Matrix')}
{mode === 'matrix' ? ( ) : (
)}
) }