Files
breakpilot-compliance/admin-compliance/app/sdk/vendor-compliance/transfers/page.tsx
T
Benjamin Admin ef8eead513 feat: Adequacy decisions, DPF check, customer guidance for transfers
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>
2026-05-01 12:57:54 +02:00

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>
)
}