'use client' /** * VVT — Verarbeitungsverzeichnis (Art. 30 DSGVO) * * 3 Tabs: * 1. Verzeichnis (Uebersicht + "Aus Scope generieren") * 2. Verarbeitung bearbeiten (Detail-Editor) * 3. Export & Compliance */ import { useState, useEffect, useCallback } from 'react' import { useSDK } from '@/lib/sdk' import StepHeader, { STEP_EXPLANATIONS } from '@/components/sdk/StepHeader/StepHeader' import { DATA_SUBJECT_CATEGORY_META, PERSONAL_DATA_CATEGORY_META, LEGAL_BASIS_META, TRANSFER_MECHANISM_META, ART9_CATEGORIES, BUSINESS_FUNCTION_LABELS, STATUS_LABELS, STATUS_COLORS, PROTECTION_LEVEL_LABELS, DEPLOYMENT_LABELS, REVIEW_INTERVAL_LABELS, createEmptyActivity, createDefaultOrgHeader, generateVVTId, isSpecialCategory, } from '@/lib/sdk/vvt-types' import type { VVTActivity, VVTOrganizationHeader, BusinessFunction } from '@/lib/sdk/vvt-types' import { generateActivities, prefillFromScopeAnswers, } from '@/lib/sdk/vvt-profiling' // ============================================================================= // CONSTANTS // ============================================================================= type Tab = 'verzeichnis' | 'editor' | 'export' // ============================================================================= // API CLIENT // ============================================================================= const VVT_API_BASE = '/api/sdk/v1/compliance/vvt' function activityFromApi(raw: any): VVTActivity { return { id: raw.id, vvtId: raw.vvt_id, name: raw.name || '', description: raw.description || '', purposes: raw.purposes || [], legalBases: raw.legal_bases || [], dataSubjectCategories: raw.data_subject_categories || [], personalDataCategories: raw.personal_data_categories || [], recipientCategories: raw.recipient_categories || [], thirdCountryTransfers: raw.third_country_transfers || [], retentionPeriod: raw.retention_period || { description: '' }, tomDescription: raw.tom_description || '', businessFunction: raw.business_function || 'other', systems: raw.systems || [], deploymentModel: raw.deployment_model || 'cloud', dataSources: raw.data_sources || [], dataFlows: raw.data_flows || [], protectionLevel: raw.protection_level || 'MEDIUM', dpiaRequired: raw.dpia_required || false, structuredToms: raw.structured_toms || { accessControl: [], confidentiality: [], integrity: [], availability: [], separation: [] }, status: raw.status || 'DRAFT', responsible: raw.responsible || '', owner: raw.owner || '', createdAt: raw.created_at || new Date().toISOString(), updatedAt: raw.updated_at || raw.created_at || new Date().toISOString(), } } function activityToApi(act: VVTActivity): Record { return { vvt_id: act.vvtId, name: act.name, description: act.description, purposes: act.purposes, legal_bases: act.legalBases, data_subject_categories: act.dataSubjectCategories, personal_data_categories: act.personalDataCategories, recipient_categories: act.recipientCategories, third_country_transfers: act.thirdCountryTransfers, retention_period: act.retentionPeriod, tom_description: act.tomDescription, business_function: act.businessFunction, systems: act.systems, deployment_model: act.deploymentModel, data_sources: act.dataSources, data_flows: act.dataFlows, protection_level: act.protectionLevel, dpia_required: act.dpiaRequired, structured_toms: act.structuredToms, status: act.status, responsible: act.responsible, owner: act.owner, } } function orgHeaderFromApi(raw: any): VVTOrganizationHeader { return { organizationName: raw.organization_name || '', industry: raw.industry || '', locations: raw.locations || [], employeeCount: raw.employee_count || 0, dpoName: raw.dpo_name || '', dpoContact: raw.dpo_contact || '', vvtVersion: raw.vvt_version || '1.0', lastReviewDate: raw.last_review_date || '', nextReviewDate: raw.next_review_date || '', reviewInterval: raw.review_interval || 'annual', } } function orgHeaderToApi(org: VVTOrganizationHeader): Record { return { organization_name: org.organizationName, industry: org.industry, locations: org.locations, employee_count: org.employeeCount, dpo_name: org.dpoName, dpo_contact: org.dpoContact, vvt_version: org.vvtVersion, last_review_date: org.lastReviewDate || null, next_review_date: org.nextReviewDate || null, review_interval: org.reviewInterval, } } async function apiListActivities(): Promise { const res = await fetch(`${VVT_API_BASE}/activities`) if (!res.ok) throw new Error(`GET activities failed: ${res.status}`) const data = await res.json() return data.map(activityFromApi) } async function apiGetOrganization(): Promise { const res = await fetch(`${VVT_API_BASE}/organization`) if (!res.ok) return null const data = await res.json() if (!data) return null return orgHeaderFromApi(data) } async function apiCreateActivity(act: VVTActivity): Promise { const res = await fetch(`${VVT_API_BASE}/activities`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(activityToApi(act)), }) if (!res.ok) throw new Error(`POST activity failed: ${res.status}`) return activityFromApi(await res.json()) } async function apiUpdateActivity(id: string, act: VVTActivity): Promise { const res = await fetch(`${VVT_API_BASE}/activities/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(activityToApi(act)), }) if (!res.ok) throw new Error(`PUT activity failed: ${res.status}`) return activityFromApi(await res.json()) } async function apiDeleteActivity(id: string): Promise { const res = await fetch(`${VVT_API_BASE}/activities/${id}`, { method: 'DELETE' }) if (!res.ok) throw new Error(`DELETE activity failed: ${res.status}`) } async function apiUpsertOrganization(org: VVTOrganizationHeader): Promise { const res = await fetch(`${VVT_API_BASE}/organization`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(orgHeaderToApi(org)), }) if (!res.ok) throw new Error(`PUT organization failed: ${res.status}`) return orgHeaderFromApi(await res.json()) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function VVTPage() { const { state } = useSDK() const [tab, setTab] = useState('verzeichnis') const [activities, setActivities] = useState([]) const [orgHeader, setOrgHeader] = useState(createDefaultOrgHeader()) const [editingId, setEditingId] = useState(null) const [filter, setFilter] = useState('all') const [searchQuery, setSearchQuery] = useState('') const [sortBy, setSortBy] = useState<'name' | 'date' | 'status'>('name') const [isLoading, setIsLoading] = useState(true) const [apiError, setApiError] = useState(null) // Load activities + org header from API useEffect(() => { async function loadFromApi() { setIsLoading(true) setApiError(null) try { const [acts, org] = await Promise.all([ apiListActivities(), apiGetOrganization(), ]) setActivities(acts) if (org) setOrgHeader(org) } catch (err) { setApiError('Fehler beim Laden der VVT-Daten. Bitte Verbindung prüfen.') console.error('VVT API load error:', err) } finally { setIsLoading(false) } } loadFromApi() }, []) // Computed stats const activeCount = activities.filter(a => a.status === 'APPROVED').length const draftCount = activities.filter(a => a.status === 'DRAFT').length const thirdCountryCount = activities.filter(a => a.thirdCountryTransfers.length > 0).length const art9Count = activities.filter(a => a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c))).length // Filtered & sorted activities const filteredActivities = activities .filter(a => { const matchesFilter = filter === 'all' || a.status === filter || (filter === 'thirdcountry' && a.thirdCountryTransfers.length > 0) || (filter === 'art9' && a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c))) const matchesSearch = searchQuery === '' || a.name.toLowerCase().includes(searchQuery.toLowerCase()) || a.description.toLowerCase().includes(searchQuery.toLowerCase()) || a.vvtId.toLowerCase().includes(searchQuery.toLowerCase()) return matchesFilter && matchesSearch }) .sort((a, b) => { if (sortBy === 'name') return a.name.localeCompare(b.name) if (sortBy === 'date') return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() return a.status.localeCompare(b.status) }) const editingActivity = editingId ? activities.find(a => a.id === editingId) : null const stepInfo = STEP_EXPLANATIONS['vvt'] // Tab buttons const tabs: { id: Tab; label: string; count?: number }[] = [ { id: 'verzeichnis', label: 'Verzeichnis', count: activities.length }, { id: 'editor', label: 'Verarbeitung bearbeiten' }, { id: 'export', label: 'Export & Compliance' }, ] if (isLoading) { return (
) } return (
{apiError && (
{apiError}
)} {/* Tab Navigation */}
{tabs.map(t => ( ))}
{/* Tab Content */} {tab === 'verzeichnis' && ( { setEditingId(id); setTab('editor') }} onNew={async () => { const vvtId = generateVVTId(activities.map(a => a.vvtId)) const newAct = createEmptyActivity(vvtId) try { const created = await apiCreateActivity(newAct) setActivities(prev => [...prev, created]) setEditingId(created.id) setTab('editor') } catch (err) { setApiError('Fehler beim Anlegen der Verarbeitung.') console.error(err) } }} onDelete={async (id) => { try { await apiDeleteActivity(id) setActivities(prev => prev.filter(a => a.id !== id)) } catch (err) { setApiError('Fehler beim Löschen der Verarbeitung.') console.error(err) } }} onAdoptGenerated={async (newActivities) => { const created: VVTActivity[] = [] for (const act of newActivities) { try { const saved = await apiCreateActivity(act) created.push(saved) } catch (err) { console.error('Failed to create activity from scope:', err) } } if (created.length > 0) setActivities(prev => [...prev, ...created]) }} /> )} {tab === 'editor' && ( { try { const saved = await apiUpdateActivity(updated.id, updated) setActivities(prev => prev.map(a => a.id === saved.id ? saved : a)) } catch (err) { setApiError('Fehler beim Speichern der Verarbeitung.') console.error(err) } }} onBack={() => setTab('verzeichnis')} onSelectActivity={(id) => setEditingId(id)} /> )} {tab === 'export' && ( { try { const saved = await apiUpsertOrganization(org) setOrgHeader(saved) } catch (err) { setApiError('Fehler beim Speichern der Organisationsdaten.') console.error(err) } }} /> )}
) } // ============================================================================= // TAB 1: VERZEICHNIS // ============================================================================= function TabVerzeichnis({ activities, allActivities, activeCount, draftCount, thirdCountryCount, art9Count, filter, setFilter, searchQuery, setSearchQuery, sortBy, setSortBy, scopeAnswers, onEdit, onNew, onDelete, onAdoptGenerated, }: { activities: VVTActivity[] allActivities: VVTActivity[] activeCount: number draftCount: number thirdCountryCount: number art9Count: number filter: string setFilter: (f: string) => void searchQuery: string setSearchQuery: (q: string) => void sortBy: string setSortBy: (s: 'name' | 'date' | 'status') => void scopeAnswers?: import('@/lib/sdk/compliance-scope-types').ScopeProfilingAnswer[] onEdit: (id: string) => void onNew: () => void onDelete: (id: string) => void onAdoptGenerated: (activities: VVTActivity[]) => void }) { const [scopePreview, setScopePreview] = useState(null) const [isGenerating, setIsGenerating] = useState(false) const handleGenerateFromScope = useCallback(() => { if (!scopeAnswers) return setIsGenerating(true) try { const profilingAnswers = prefillFromScopeAnswers(scopeAnswers) const result = generateActivities(profilingAnswers) setScopePreview(result.generatedActivities) } finally { setIsGenerating(false) } }, [scopeAnswers]) const handleAdoptPreview = useCallback(() => { if (!scopePreview) return onAdoptGenerated(scopePreview) setScopePreview(null) }, [scopePreview, onAdoptGenerated]) // Preview mode for generated activities if (scopePreview) { return (

Generierte Verarbeitungen

Basierend auf Ihrer Scope-Analyse wurden {scopePreview.length} Verarbeitungstaetigkeiten generiert. Sie koennen einzelne Eintraege abwaehlen, bevor Sie diese uebernehmen.

{scopePreview.map((a, i) => (
{ if (!e.target.checked) { setScopePreview(scopePreview.filter((_, j) => j !== i)) } }} />
{a.vvtId} {a.name} {BUSINESS_FUNCTION_LABELS[a.businessFunction]}

{a.description}

))}
) } return (
{/* Scope Generate Button */} {scopeAnswers && scopeAnswers.length > 0 && (

Aus Scope-Analyse generieren

Erstellen Sie automatisch Verarbeitungstaetigkeiten basierend auf Ihren Scope-Profiling-Antworten.

)} {/* Stats */}
{/* Search + Filter + New */}
setSearchQuery(e.target.value)} placeholder="VVT-ID, Name oder Beschreibung suchen..." className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" />
{[ { key: 'all', label: 'Alle' }, { key: 'DRAFT', label: 'Entwurf' }, { key: 'REVIEW', label: 'Pruefung' }, { key: 'APPROVED', label: 'Genehmigt' }, { key: 'thirdcountry', label: 'Drittland' }, { key: 'art9', label: 'Art. 9' }, ].map(f => ( ))}
{/* Activity Cards */}
{activities.map(activity => ( ))}
{activities.length === 0 && (

Keine Verarbeitungen gefunden

Erstellen Sie eine neue Verarbeitung manuell oder generieren Sie Eintraege automatisch aus Ihrer Scope-Analyse.

)}
) } function StatCard({ label, value, color }: { label: string; value: number; color: string }) { const borderColors: Record = { gray: 'border-gray-200', green: 'border-green-200', yellow: 'border-yellow-200', orange: 'border-orange-200', red: 'border-red-200', } const textColors: Record = { gray: 'text-gray-600', green: 'text-green-600', yellow: 'text-yellow-600', orange: 'text-orange-600', red: 'text-red-600', } return (
{label}
{value}
) } function ActivityCard({ activity, onEdit, onDelete }: { activity: VVTActivity; onEdit: (id: string) => void; onDelete: (id: string) => void }) { const hasArt9 = activity.personalDataCategories.some(c => ART9_CATEGORIES.includes(c)) const hasThirdCountry = activity.thirdCountryTransfers.length > 0 return (
{activity.vvtId} {STATUS_LABELS[activity.status] || activity.status} {hasArt9 && ( Art. 9 )} {hasThirdCountry && ( Drittland )} {activity.dpiaRequired && ( DSFA )}

{activity.name || '(Ohne Namen)'}

{activity.description && (

{activity.description}

)}
{BUSINESS_FUNCTION_LABELS[activity.businessFunction]} {activity.responsible || 'Kein Verantwortlicher'} Aktualisiert: {new Date(activity.updatedAt).toLocaleDateString('de-DE')}
) } // ============================================================================= // TAB 2: EDITOR // ============================================================================= function TabEditor({ activity, activities, onSave, onBack, onSelectActivity, }: { activity: VVTActivity | null | undefined activities: VVTActivity[] onSave: (updated: VVTActivity) => void onBack: () => void onSelectActivity: (id: string) => void }) { const [local, setLocal] = useState(null) const [showAdvanced, setShowAdvanced] = useState(false) useEffect(() => { setLocal(activity ? { ...activity } : null) }, [activity]) if (!local) { return (

Keine Verarbeitung ausgewaehlt

Waehlen Sie eine Verarbeitung aus dem Verzeichnis oder erstellen Sie eine neue.

{activities.length > 0 && (

Verarbeitungen zum Bearbeiten:

{activities.map(a => ( ))}
)}
) } const update = (patch: Partial) => setLocal(prev => prev ? { ...prev, ...patch } : prev) const handleSave = () => { if (local) onSave(local) } return (
{/* Header */}
{local.vvtId}

{local.name || 'Neue Verarbeitung'}

{/* Form */}
{/* Bezeichnung + Beschreibung */} update({ name: e.target.value })} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent" placeholder="z.B. Mitarbeiterverwaltung" />