ef8eead513
New: adequacy-decisions.ts - Complete list of 15 countries with EU adequacy decisions (Art. 45) - EU/EEA country set (30 countries) - getTransferRequirement() — determines SCC/TIA/certification needs per country code with human-readable explanations - US special handling: DPF certification required, check URL included Updated: transfers/page.tsx - "Was muss ich tun?" explanation section with 3 options: 1. Adequacy decision (green) — no action needed 2. DPF certification (blue, US only) — check dataprivacyframework.gov 3. SCC + TIA required (amber) — link to Document Generator - Collapsible adequacy countries table (15 countries with restrictions) - Schrems II background explanation for customers - Customer guidance written for non-experts who never heard of TIA/SCC Updated: templateRecommendations.ts - SCC+TIA rules now consider DPF certification and adequacy status - us_dpf_only → SCC/TIA optional (not required) - adequate_only → SCC/TIA not recommended Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
341 lines
15 KiB
TypeScript
341 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo, useState } from 'react'
|
|
import { useVendorCompliance } from '@/lib/sdk/vendor-compliance'
|
|
import { getTransferRequirement, ADEQUACY_DECISIONS, type AdequacyDecision } from '@/lib/sdk/vendor-compliance/adequacy-decisions'
|
|
import Link from 'next/link'
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
interface TransferEntry {
|
|
vendorId: string
|
|
vendorName: string
|
|
country: string
|
|
isEU: boolean
|
|
isAdequate: boolean
|
|
mechanisms: string[]
|
|
hasSCC: boolean
|
|
hasTIA: boolean
|
|
status: 'green' | 'yellow' | 'red'
|
|
statusLabel: string
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
function getTransferStatus(
|
|
isEU: boolean,
|
|
isAdequate: boolean,
|
|
mechanisms: string[],
|
|
hasSCC: boolean,
|
|
): { status: 'green' | 'yellow' | 'red'; label: string } {
|
|
if (isEU) return { status: 'green', label: 'EU/EWR' }
|
|
if (isAdequate) return { status: 'green', label: 'Angemessenheitsbeschluss' }
|
|
if (hasSCC && mechanisms.length > 0) return { status: 'yellow', label: 'SCC vorhanden' }
|
|
if (mechanisms.length > 0) return { status: 'yellow', label: 'Mechanismus vorhanden' }
|
|
return { status: 'red', label: 'Kein Transfermechanismus' }
|
|
}
|
|
|
|
const statusColors = {
|
|
green: 'bg-green-100 text-green-800',
|
|
yellow: 'bg-amber-100 text-amber-800',
|
|
red: 'bg-red-100 text-red-800',
|
|
}
|
|
|
|
const statusDots = {
|
|
green: 'bg-green-500',
|
|
yellow: 'bg-amber-500',
|
|
red: 'bg-red-500',
|
|
}
|
|
|
|
// ============================================================================
|
|
// Component
|
|
// ============================================================================
|
|
|
|
export default function TransfersPage() {
|
|
const { vendors, contracts } = useVendorCompliance()
|
|
const [filter, setFilter] = useState<'all' | 'green' | 'yellow' | 'red'>('all')
|
|
|
|
// Build transfer entries from vendors with non-EU processing locations
|
|
const transfers = useMemo<TransferEntry[]>(() => {
|
|
if (!vendors) return []
|
|
|
|
const entries: TransferEntry[] = []
|
|
|
|
for (const vendor of vendors) {
|
|
const locations = (vendor as Record<string, unknown>).processingLocations as Array<{
|
|
country: string; isEU: boolean; isAdequate: boolean
|
|
}> || []
|
|
const mechanisms = (vendor as Record<string, unknown>).transferMechanisms as string[] || []
|
|
|
|
// Check if vendor has any SCC contract
|
|
const vendorContracts = (contracts || []).filter(
|
|
(c: Record<string, unknown>) => c.vendorId === vendor.id
|
|
)
|
|
const hasSCC = vendorContracts.some(
|
|
(c: Record<string, unknown>) => c.documentType === 'SCC'
|
|
)
|
|
|
|
for (const loc of locations) {
|
|
const { status, label } = getTransferStatus(loc.isEU, loc.isAdequate, mechanisms, hasSCC)
|
|
|
|
entries.push({
|
|
vendorId: vendor.id,
|
|
vendorName: vendor.name,
|
|
country: loc.country,
|
|
isEU: loc.isEU,
|
|
isAdequate: loc.isAdequate,
|
|
mechanisms,
|
|
hasSCC,
|
|
hasTIA: false, // TODO: Check if TIA document exists for this vendor
|
|
status,
|
|
statusLabel: label,
|
|
})
|
|
}
|
|
}
|
|
|
|
return entries
|
|
}, [vendors, contracts])
|
|
|
|
// Filter
|
|
const filtered = filter === 'all'
|
|
? transfers
|
|
: transfers.filter((t) => t.status === filter)
|
|
|
|
// Stats
|
|
const stats = useMemo(() => ({
|
|
total: transfers.length,
|
|
eu: transfers.filter((t) => t.isEU).length,
|
|
adequate: transfers.filter((t) => !t.isEU && t.isAdequate).length,
|
|
thirdCountry: transfers.filter((t) => !t.isEU && !t.isAdequate).length,
|
|
red: transfers.filter((t) => t.status === 'red').length,
|
|
}), [transfers])
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900">Drittlandtransfers</h2>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Uebersicht aller Datenuebermittlungen nach Verarbeitungsstandort. Art. 44-49 DSGVO.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
<div className="bg-white rounded-xl border border-gray-200 p-4">
|
|
<div className="text-xs text-gray-500 uppercase tracking-wide">Gesamt</div>
|
|
<div className="text-2xl font-bold text-gray-900 mt-1">{stats.total}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-green-200 p-4">
|
|
<div className="text-xs text-green-600 uppercase tracking-wide">EU/EWR + Adequat</div>
|
|
<div className="text-2xl font-bold text-green-700 mt-1">{stats.eu + stats.adequate}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-amber-200 p-4">
|
|
<div className="text-xs text-amber-600 uppercase tracking-wide">Drittland (mit Mechanismus)</div>
|
|
<div className="text-2xl font-bold text-amber-700 mt-1">{stats.thirdCountry - stats.red}</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-red-200 p-4">
|
|
<div className="text-xs text-red-600 uppercase tracking-wide">Handlungsbedarf</div>
|
|
<div className="text-2xl font-bold text-red-700 mt-1">{stats.red}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter */}
|
|
<div className="flex gap-2">
|
|
{(['all', 'green', 'yellow', 'red'] as const).map((f) => (
|
|
<button
|
|
key={f}
|
|
onClick={() => setFilter(f)}
|
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
filter === f
|
|
? 'bg-gray-900 text-white'
|
|
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
|
}`}
|
|
>
|
|
{f === 'all' && 'Alle'}
|
|
{f === 'green' && 'OK'}
|
|
{f === 'yellow' && 'Pruefen'}
|
|
{f === 'red' && 'Handlungsbedarf'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Status</th>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Vendor</th>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Land</th>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Mechanismus</th>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">SCC</th>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">TIA</th>
|
|
<th className="text-left px-4 py-3 font-medium text-gray-500">Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-100">
|
|
{filtered.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={7} className="px-4 py-8 text-center text-gray-400">
|
|
{transfers.length === 0
|
|
? 'Keine Vendors mit Verarbeitungsstandorten erfasst.'
|
|
: 'Keine Eintraege fuer den gewaehlten Filter.'}
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
filtered.map((t, i) => (
|
|
<tr key={`${t.vendorId}-${t.country}-${i}`} className="hover:bg-gray-50">
|
|
<td className="px-4 py-3">
|
|
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${statusColors[t.status]}`}>
|
|
<span className={`w-2 h-2 rounded-full ${statusDots[t.status]}`} />
|
|
{t.statusLabel}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 font-medium text-gray-900">
|
|
<Link
|
|
href={`/sdk/vendor-compliance/vendors?id=${t.vendorId}`}
|
|
className="hover:text-blue-600"
|
|
>
|
|
{t.vendorName}
|
|
</Link>
|
|
</td>
|
|
<td className="px-4 py-3 text-gray-600">
|
|
{t.country}
|
|
{t.isEU && <span className="ml-1 text-xs text-green-600">(EU)</span>}
|
|
</td>
|
|
<td className="px-4 py-3 text-gray-600">
|
|
{t.mechanisms.length > 0
|
|
? t.mechanisms.join(', ')
|
|
: <span className="text-red-500">Keiner</span>}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{t.hasSCC
|
|
? <span className="text-green-600">Vorhanden</span>
|
|
: <span className="text-gray-400">-</span>}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{t.hasTIA
|
|
? <span className="text-green-600">Vorhanden</span>
|
|
: !t.isEU && !t.isAdequate
|
|
? <span className="text-red-500">Fehlt</span>
|
|
: <span className="text-gray-400">-</span>}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{!t.isEU && !t.isAdequate && (
|
|
<Link
|
|
href="/sdk/document-generator"
|
|
className="text-xs text-blue-600 hover:text-blue-800 font-medium"
|
|
>
|
|
TIA erstellen
|
|
</Link>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* Explanation: What do I need to do? */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Was muss ich tun?</h3>
|
|
<p className="text-sm text-gray-600">
|
|
Wenn Ihr Unternehmen personenbezogene Daten an Empfaenger ausserhalb der EU/des EWR uebermittelt,
|
|
muessen Sie sicherstellen, dass ein angemessenes Datenschutzniveau besteht. Es gibt drei Wege:
|
|
</p>
|
|
|
|
<div className="grid md:grid-cols-3 gap-4">
|
|
{/* Option 1: Adequacy */}
|
|
<div className="border border-green-200 rounded-lg p-4 bg-green-50">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="w-3 h-3 rounded-full bg-green-500" />
|
|
<span className="font-medium text-green-800">Angemessenheitsbeschluss</span>
|
|
</div>
|
|
<p className="text-xs text-green-700">
|
|
Die EU-Kommission hat fuer bestimmte Laender festgestellt, dass ein angemessenes Datenschutzniveau
|
|
besteht. Fuer diese Laender sind <strong>keine SCC und kein TIA erforderlich</strong>.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Option 2: DPF */}
|
|
<div className="border border-blue-200 rounded-lg p-4 bg-blue-50">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="w-3 h-3 rounded-full bg-blue-500" />
|
|
<span className="font-medium text-blue-800">DPF-Zertifizierung (nur USA)</span>
|
|
</div>
|
|
<p className="text-xs text-blue-700">
|
|
US-Unternehmen koennen sich nach dem <strong>EU-US Data Privacy Framework</strong> zertifizieren.
|
|
Pruefen Sie unter{' '}
|
|
<a href="https://www.dataprivacyframework.gov/list" target="_blank" rel="noopener noreferrer" className="underline">
|
|
dataprivacyframework.gov
|
|
</a>{' '}
|
|
ob Ihr US-Dienstleister zertifiziert ist. Falls ja: <strong>keine SCC/TIA noetig</strong>.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Option 3: SCC + TIA */}
|
|
<div className="border border-amber-200 rounded-lg p-4 bg-amber-50">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span className="w-3 h-3 rounded-full bg-amber-500" />
|
|
<span className="font-medium text-amber-800">SCC + TIA erforderlich</span>
|
|
</div>
|
|
<p className="text-xs text-amber-700">
|
|
Fuer alle anderen Drittlaender muessen Sie <strong>EU-Standardvertragsklauseln (SCC)</strong> abschliessen
|
|
und ein <strong>Transfer Impact Assessment (TIA)</strong> durchfuehren. Beides finden Sie im{' '}
|
|
<Link href="/sdk/document-generator" className="underline">Document Generator</Link> unter "Drittlandtransfer".
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Adequacy countries list */}
|
|
<details className="bg-white rounded-xl border border-gray-200">
|
|
<summary className="px-6 py-4 cursor-pointer text-sm font-medium text-gray-700 hover:text-purple-600">
|
|
Laender mit Angemessenheitsbeschluss anzeigen ({ADEQUACY_DECISIONS.length} Laender)
|
|
</summary>
|
|
<div className="px-6 pb-4">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-gray-100">
|
|
<th className="text-left py-2 font-medium text-gray-500">Land</th>
|
|
<th className="text-left py-2 font-medium text-gray-500">Seit</th>
|
|
<th className="text-left py-2 font-medium text-gray-500">Einschraenkung</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-50">
|
|
{ADEQUACY_DECISIONS.map((d: AdequacyDecision) => (
|
|
<tr key={d.countryCode}>
|
|
<td className="py-2 text-gray-900">
|
|
{d.countryName}
|
|
{d.requiresCertification && (
|
|
<span className="ml-2 text-xs text-blue-600 font-medium">Zertifizierung erforderlich</span>
|
|
)}
|
|
</td>
|
|
<td className="py-2 text-gray-600">{d.since}</td>
|
|
<td className="py-2 text-gray-500 text-xs">
|
|
{d.restriction || d.expires || '—'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</details>
|
|
|
|
{/* Schrems II info */}
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 text-sm text-blue-800">
|
|
<strong>Hintergrund — EuGH Schrems II:</strong> Der EuGH hat 2020 das EU-US Privacy Shield fuer ungueltig erklaert
|
|
und klargestellt, dass bei Drittlandtransfers immer geprueft werden muss, ob die Gesetze des Empfaengerstaats
|
|
den Schutz der uebermittelten Daten beeintraechtigen (z.B. durch Massenueberwachung oder fehlende Rechtsbehelfe).
|
|
Das TIA dokumentiert genau diese Pruefung. Seit Juli 2023 gibt es mit dem EU-US Data Privacy Framework einen neuen
|
|
Angemessenheitsbeschluss fuer DPF-zertifizierte US-Unternehmen.
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|