Files
breakpilot-compliance/admin-compliance/app/sdk/vendor-compliance/transfers/page.tsx
T
Benjamin Admin a28db8f8f0 fix(admin): resolve all 266 TypeScript errors, enable strict build
Eliminate the pre-existing TS errors that were masked by
next.config.js `typescript.ignoreBuildErrors: true`, then turn the flag
OFF so the compiler is a real safety net for future changes. `next build`
and `tsc --noEmit` now pass with 0 errors.

The errors were not cosmetic — several exposed real latent bugs hidden by
the flag, e.g. the drafting-engine ConstraintEnforcer read non-existent
fields (`t.rule.dsfaRequired`, `d.required`, `r.title`), so its DSFA hard
gate and risk-flag checks were silently no-ops; scopeDefaults read
snake_case CompanyProfile fields that never matched the camelCase type
(generator defaults never populated). Both fixed by aligning code to the
current types.

Highlights:
- Vitest globals: add vitest-globals.d.ts (config already had globals:true)
  so the test files type-check; exclude Playwright specs from vitest.
- Add a minimal ambient `pg` module declaration (no @types/pg installed).
- Fix Next 15 route handlers to await Promise params.
- Reconcile drifted types across loeschfristen, compliance-scope, document-
  generator, drafting-engine, vendor-compliance, agent and more.

Pre-existing (NOT caused here, proven by stashing the diff): 3 vitest
logic tests still fail — getNextStep (2) and buildDocumentScope priority (1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 00:42:44 +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 unknown as Record<string, unknown>).processingLocations as Array<{
country: string; isEU: boolean; isAdequate: boolean
}> || []
const mechanisms = (vendor as unknown as Record<string, unknown>).transferMechanisms as string[] || []
// Check if vendor has any SCC contract
const vendorContracts = (contracts || []).filter(
(c) => (c as unknown as Record<string, unknown>).vendorId === vendor.id
)
const hasSCC = vendorContracts.some(
(c) => (c as unknown as Record<string, unknown>).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>
)
}