diff --git a/admin-compliance/app/sdk/loeschfristen/page.tsx b/admin-compliance/app/sdk/loeschfristen/page.tsx index 4d903dc..3e82ce9 100644 --- a/admin-compliance/app/sdk/loeschfristen/page.tsx +++ b/admin-compliance/app/sdk/loeschfristen/page.tsx @@ -27,12 +27,18 @@ import { exportPoliciesAsJSON, exportPoliciesAsCSV, generateComplianceSummary, downloadFile, } from '@/lib/sdk/loeschfristen-export' +import { + buildLoeschkonzeptHtml, + type LoeschkonzeptOrgHeader, + type LoeschkonzeptRevision, + createDefaultLoeschkonzeptOrgHeader, +} from '@/lib/sdk/loeschfristen-document' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- -type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' +type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' | 'loeschkonzept' // --------------------------------------------------------------------------- // Helper: TagInput @@ -130,6 +136,10 @@ export default function LoeschfristenPage() { // ---- VVT data ---- const [vvtActivities, setVvtActivities] = useState([]) + // ---- Loeschkonzept document state ---- + const [orgHeader, setOrgHeader] = useState(createDefaultLoeschkonzeptOrgHeader()) + const [revisions, setRevisions] = useState([]) + // -------------------------------------------------------------------------- // Persistence (API-backed) // -------------------------------------------------------------------------- @@ -247,6 +257,48 @@ export default function LoeschfristenPage() { }) }, [tab, editingId]) + // Load Loeschkonzept org header from VVT organization data + revisions from localStorage + useEffect(() => { + // Load revisions from localStorage + try { + const raw = localStorage.getItem('bp_loeschkonzept_revisions') + if (raw) { + const parsed = JSON.parse(raw) + if (Array.isArray(parsed)) setRevisions(parsed) + } + } catch { /* ignore */ } + + // Load org header from localStorage (user overrides) + try { + const raw = localStorage.getItem('bp_loeschkonzept_orgheader') + if (raw) { + const parsed = JSON.parse(raw) + if (parsed && typeof parsed === 'object') { + setOrgHeader(prev => ({ ...prev, ...parsed })) + return // User has saved org header, skip VVT fetch + } + } + } catch { /* ignore */ } + + // Fallback: fetch from VVT organization API + fetch('/api/sdk/v1/compliance/vvt/organization') + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data) { + setOrgHeader(prev => ({ + ...prev, + organizationName: data.organization_name || data.organizationName || prev.organizationName, + industry: data.industry || prev.industry, + dpoName: data.dpo_name || data.dpoName || prev.dpoName, + dpoContact: data.dpo_contact || data.dpoContact || prev.dpoContact, + responsiblePerson: data.responsible_person || data.responsiblePerson || prev.responsiblePerson, + employeeCount: data.employee_count || data.employeeCount || prev.employeeCount, + })) + } + }) + .catch(() => { /* ignore */ }) + }, []) + // -------------------------------------------------------------------------- // Derived // -------------------------------------------------------------------------- @@ -489,6 +541,7 @@ export default function LoeschfristenPage() { { key: 'editor', label: 'Editor' }, { key: 'generator', label: 'Generator' }, { key: 'export', label: 'Export & Compliance' }, + { key: 'loeschkonzept', label: 'Loeschkonzept' }, ] // -------------------------------------------------------------------------- @@ -2278,6 +2331,314 @@ export default function LoeschfristenPage() { ) } + // ========================================================================== + // Tab 5: Loeschkonzept Document + // ========================================================================== + + function handleOrgHeaderChange(field: keyof LoeschkonzeptOrgHeader, value: string | string[]) { + const updated = { ...orgHeader, [field]: value } + setOrgHeader(updated) + localStorage.setItem('bp_loeschkonzept_orgheader', JSON.stringify(updated)) + } + + function handleAddRevision() { + const newRev: LoeschkonzeptRevision = { + version: orgHeader.loeschkonzeptVersion, + date: new Date().toISOString().split('T')[0], + author: orgHeader.dpoName || orgHeader.responsiblePerson || '', + changes: '', + } + const updated = [...revisions, newRev] + setRevisions(updated) + localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated)) + } + + function handleUpdateRevision(index: number, field: keyof LoeschkonzeptRevision, value: string) { + const updated = revisions.map((r, i) => i === index ? { ...r, [field]: value } : r) + setRevisions(updated) + localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated)) + } + + function handleRemoveRevision(index: number) { + const updated = revisions.filter((_, i) => i !== index) + setRevisions(updated) + localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated)) + } + + function handlePrintLoeschkonzept() { + const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions) + const printWindow = window.open('', '_blank') + if (printWindow) { + printWindow.document.write(htmlContent) + printWindow.document.close() + printWindow.focus() + setTimeout(() => printWindow.print(), 300) + } + } + + function handleDownloadLoeschkonzeptHtml() { + const htmlContent = buildLoeschkonzeptHtml(policies, orgHeader, vvtActivities, complianceResult, revisions) + downloadFile(htmlContent, `loeschkonzept-${new Date().toISOString().split('T')[0]}.html`, 'text/html;charset=utf-8') + } + + function renderLoeschkonzept() { + const activePolicies = policies.filter(p => p.status !== 'ARCHIVED') + + return ( +
+ {/* Action bar */} +
+
+
+

+ Loeschkonzept (Art. 5/17/30 DSGVO) +

+

+ Druckfertiges Loeschkonzept mit Deckblatt, Loeschregeln, VVT-Verknuepfung und Compliance-Status. +

+
+
+ + +
+
+ + {activePolicies.length === 0 && ( +
+ Keine aktiven Policies vorhanden. Erstellen Sie mindestens eine Policy, um das Loeschkonzept zu generieren. +
+ )} +
+ + {/* Org Header Form */} +
+

Organisationsdaten (Deckblatt)

+
+
+ + handleOrgHeaderChange('organizationName', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="Name der Organisation" + /> +
+
+ + handleOrgHeaderChange('industry', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="z.B. IT / Software" + /> +
+
+ + handleOrgHeaderChange('dpoName', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="Name des DSB" + /> +
+
+ + handleOrgHeaderChange('dpoContact', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="E-Mail oder Telefon" + /> +
+
+ + handleOrgHeaderChange('responsiblePerson', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="Name des Verantwortlichen" + /> +
+
+ + handleOrgHeaderChange('employeeCount', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="z.B. 50-249" + /> +
+
+ + handleOrgHeaderChange('loeschkonzeptVersion', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + placeholder="1.0" + /> +
+
+ + +
+
+ + handleOrgHeaderChange('lastReviewDate', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> +
+
+ + handleOrgHeaderChange('nextReviewDate', e.target.value)} + className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500" + /> +
+
+
+ + {/* Revisions */} +
+
+

Aenderungshistorie

+ +
+ {revisions.length === 0 ? ( +

+ Noch keine Revisionen. Die Erstversion wird automatisch im Dokument eingefuegt. +

+ ) : ( +
+ {revisions.map((rev, idx) => ( +
+ handleUpdateRevision(idx, 'version', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + placeholder="1.1" + /> + handleUpdateRevision(idx, 'date', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + /> + handleUpdateRevision(idx, 'author', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + placeholder="Autor" + /> + handleUpdateRevision(idx, 'changes', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" + placeholder="Beschreibung der Aenderungen" + /> + +
+ ))} +
+ )} +
+ + {/* Document Preview */} +
+

Dokument-Vorschau

+
+ {/* Cover preview */} +
+
Loeschkonzept
+
gemaess Art. 5/17/30 DSGVO
+
+ {orgHeader.organizationName || Organisation nicht angegeben} +
+
+ Version {orgHeader.loeschkonzeptVersion} | {new Date().toLocaleDateString('de-DE')} +
+
+ + {/* Section list */} +
+
11 Sektionen
+
+
1. Ziel und Zweck
+
7. Legal Hold Verfahren
+
2. Geltungsbereich
+
8. Verantwortlichkeiten
+
3. Grundprinzipien
+
9. Pruef-/Revisionszyklus
+
4. Loeschregeln-Uebersicht
+
10. Compliance-Status
+
5. Detaillierte Loeschregeln
+
11. Aenderungshistorie
+
6. VVT-Verknuepfung
+
+
+ + {/* Stats */} +
+ {activePolicies.length} Loeschregeln + {policies.filter(p => p.linkedVVTActivityIds.length > 0).length} VVT-Verknuepfungen + {revisions.length} Revisionen + {complianceResult && ( + Compliance-Score: = 75 ? 'text-green-600' : complianceResult.score >= 50 ? 'text-yellow-600' : 'text-red-600'}>{complianceResult.score}/100 + )} +
+
+
+
+ ) + } + // ========================================================================== // Main render // ========================================================================== @@ -2317,6 +2678,7 @@ export default function LoeschfristenPage() { {tab === 'editor' && renderEditor()} {tab === 'generator' && renderGenerator()} {tab === 'export' && renderExport()} + {tab === 'loeschkonzept' && renderLoeschkonzept()} ) } diff --git a/admin-compliance/app/sdk/vvt/page.tsx b/admin-compliance/app/sdk/vvt/page.tsx index 5c04a2b..bce9ab7 100644 --- a/admin-compliance/app/sdk/vvt/page.tsx +++ b/admin-compliance/app/sdk/vvt/page.tsx @@ -39,7 +39,7 @@ import { // CONSTANTS // ============================================================================= -type Tab = 'verzeichnis' | 'editor' | 'export' +type Tab = 'verzeichnis' | 'editor' | 'export' | 'dokument' | 'processor' // ============================================================================= // API CLIENT @@ -74,6 +74,20 @@ function activityFromApi(raw: any): VVTActivity { owner: raw.owner || '', createdAt: raw.created_at || new Date().toISOString(), updatedAt: raw.updated_at || raw.created_at || new Date().toISOString(), + // Library refs + purposeRefs: raw.purpose_refs || undefined, + legalBasisRefs: raw.legal_basis_refs || undefined, + dataSubjectRefs: raw.data_subject_refs || undefined, + dataCategoryRefs: raw.data_category_refs || undefined, + recipientRefs: raw.recipient_refs || undefined, + retentionRuleRef: raw.retention_rule_ref || undefined, + transferMechanismRefs: raw.transfer_mechanism_refs || undefined, + tomRefs: raw.tom_refs || undefined, + linkedLoeschfristenIds: raw.linked_loeschfristen_ids || undefined, + linkedTomMeasureIds: raw.linked_tom_measure_ids || undefined, + sourceTemplateId: raw.source_template_id || undefined, + riskScore: raw.risk_score ?? undefined, + art30Completeness: raw.art30_completeness || undefined, } } @@ -101,6 +115,20 @@ function activityToApi(act: VVTActivity): Record { status: act.status, responsible: act.responsible, owner: act.owner, + // Library refs + purpose_refs: act.purposeRefs || null, + legal_basis_refs: act.legalBasisRefs || null, + data_subject_refs: act.dataSubjectRefs || null, + data_category_refs: act.dataCategoryRefs || null, + recipient_refs: act.recipientRefs || null, + retention_rule_ref: act.retentionRuleRef || null, + transfer_mechanism_refs: act.transferMechanismRefs || null, + tom_refs: act.tomRefs || null, + source_template_id: act.sourceTemplateId || null, + risk_score: act.riskScore ?? null, + linked_loeschfristen_ids: act.linkedLoeschfristenIds || null, + linked_tom_measure_ids: act.linkedTomMeasureIds || null, + art30_completeness: act.art30Completeness || null, } } @@ -184,6 +212,44 @@ async function apiUpsertOrganization(org: VVTOrganizationHeader): Promise { + const params = businessFunction ? `?business_function=${businessFunction}` : '' + const res = await fetch(`${VVT_API_BASE}/templates${params}`) + if (!res.ok) return [] + return res.json() +} + +async function apiInstantiateTemplate(templateId: string): Promise { + const res = await fetch(`${VVT_API_BASE}/templates/${templateId}/instantiate`, { method: 'POST' }) + if (!res.ok) throw new Error(`POST instantiate failed: ${res.status}`) + return activityFromApi(await res.json()) +} + +async function apiGetCompleteness(activityId: string): Promise<{ score: number; missing: string[]; warnings: string[] }> { + const res = await fetch(`${VVT_API_BASE}/activities/${activityId}/completeness`) + if (!res.ok) return { score: 0, missing: [], warnings: [] } + return res.json() +} + +interface LibraryItem { id: string; label_de: string; description_de?: string; [key: string]: any } + +async function apiGetLibrary(type: string): Promise { + const res = await fetch(`${VVT_API_BASE}/libraries/${type}`) + if (!res.ok) return [] + const data = await res.json() + return Array.isArray(data) ? data : [] +} + // ============================================================================= // MAIN PAGE // ============================================================================= @@ -252,6 +318,8 @@ export default function VVTPage() { const tabs: { id: Tab; label: string; count?: number }[] = [ { id: 'verzeichnis', label: 'Verzeichnis', count: activities.length }, { id: 'editor', label: 'Verarbeitung bearbeiten' }, + { id: 'dokument', label: 'VVT-Dokument' }, + { id: 'processor', label: 'Auftragsverarbeiter (Abs. 2)' }, { id: 'export', label: 'Export & Compliance' }, ] @@ -350,6 +418,17 @@ export default function VVTPage() { } if (created.length > 0) setActivities(prev => [...prev, ...created]) }} + onNewFromTemplate={async (templateId) => { + try { + const created = await apiInstantiateTemplate(templateId) + setActivities(prev => [...prev, created]) + setEditingId(created.id) + setTab('editor') + } catch (err) { + setApiError('Fehler beim Erstellen aus Vorlage.') + console.error(err) + } + }} /> )} @@ -371,6 +450,14 @@ export default function VVTPage() { /> )} + {tab === 'dokument' && ( + + )} + + {tab === 'processor' && ( + + )} + {tab === 'export' && ( void onDelete: (id: string) => void onAdoptGenerated: (activities: VVTActivity[]) => void + onNewFromTemplate: (templateId: string) => void }) { const [scopePreview, setScopePreview] = useState(null) const [isGenerating, setIsGenerating] = useState(false) + const [showTemplatePicker, setShowTemplatePicker] = useState(false) + const [templates, setTemplates] = useState([]) + const [templateFilter, setTemplateFilter] = useState('all') + const [templatesLoading, setTemplatesLoading] = useState(false) const handleGenerateFromScope = useCallback(() => { if (!scopeAnswers) return @@ -557,6 +649,26 @@ function TabVerzeichnis({ + + +
+ {[ + { key: 'all', label: 'Alle' }, + { key: 'hr', label: 'Personal' }, + { key: 'finance', label: 'Finanzen' }, + { key: 'sales_crm', label: 'Vertrieb' }, + { key: 'marketing', label: 'Marketing' }, + { key: 'support', label: 'Support' }, + { key: 'it_operations', label: 'IT' }, + { key: 'other', label: 'Sonstiges' }, + ].map(f => ( + + ))} +
+ +
+ {templatesLoading ? ( +
+
+
+ ) : ( +
+ {templates + .filter(t => templateFilter === 'all' || t.business_function === templateFilter) + .map(t => ( + + ))} +
+ )} +
+ + + )} ) } @@ -629,6 +824,9 @@ function ActivityCard({ activity, onEdit, onDelete }: { activity: VVTActivity; o {activity.dpiaRequired && ( DSFA )} + {activity.sourceTemplateId && ( + Vorlage + )}

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

{activity.description && ( @@ -638,6 +836,11 @@ function ActivityCard({ activity, onEdit, onDelete }: { activity: VVTActivity; o {BUSINESS_FUNCTION_LABELS[activity.businessFunction]} {activity.responsible || 'Kein Verantwortlicher'} Aktualisiert: {new Date(activity.updatedAt).toLocaleDateString('de-DE')} + {activity.art30Completeness && ( + = 80 ? 'text-green-600' : activity.art30Completeness.score >= 50 ? 'text-yellow-600' : 'text-red-600'}`}> + Art. 30: {activity.art30Completeness.score}% + + )}
@@ -1188,6 +1391,35 @@ function TabExport({ )}
+ {/* Art. 30 Completeness per Activity */} + {activities.length > 0 && ( +
+

Art. 30 Vollstaendigkeit (Detail)

+
+ {activities.map(a => { + const c = a.art30Completeness + const score = c?.score ?? null + return ( +
+ {a.vvtId} + {a.name || '(Ohne Namen)'} + {score !== null ? ( + <> +
+
= 80 ? 'bg-green-500' : score >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`} style={{ width: `${score}%` }} /> +
+ = 80 ? 'text-green-600' : score >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>{score}% + + ) : ( + Nicht berechnet + )} +
+ ) + })} +
+
+ )} + {/* Organisation Header */}

VVT-Metadaten (Organisation)

@@ -1273,6 +1505,693 @@ function TabExport({ ) } +// ============================================================================= +// TAB 4: VVT-DOKUMENT (Druckbare Ansicht + PDF) +// ============================================================================= + +function TabDokument({ activities, orgHeader }: { activities: VVTActivity[]; orgHeader: VVTOrganizationHeader }) { + const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + + const resolveDataSubjects = (cats: string[]) => + cats.map(c => DATA_SUBJECT_CATEGORY_META[c as keyof typeof DATA_SUBJECT_CATEGORY_META]?.de || c).join(', ') + + const resolveDataCategories = (cats: string[]) => + cats.map(c => PERSONAL_DATA_CATEGORY_META[c as keyof typeof PERSONAL_DATA_CATEGORY_META]?.label?.de || c).join(', ') + + const resolveLegalBasis = (lb: { type: string; description?: string; reference?: string }) => { + const meta = LEGAL_BASIS_META[lb.type as keyof typeof LEGAL_BASIS_META] + const label = meta ? `${meta.label.de} (${meta.article})` : lb.type + return lb.reference ? `${label} — ${lb.reference}` : label + } + + const resolveTransferMechanism = (m: string) => { + const meta = TRANSFER_MECHANISM_META[m as keyof typeof TRANSFER_MECHANISM_META] + return meta?.de || m + } + + const buildDocumentHtml = () => { + const approvedActivities = activities.filter(a => a.status !== 'ARCHIVED') + + let html = ` + + + + + Verzeichnis von Verarbeitungstaetigkeiten — ${orgHeader.organizationName || 'Organisation'} + + + + + +
+

Verzeichnis von Verarbeitungstaetigkeiten

+
gemaess Art. 30 Abs. 1 DSGVO
+
+ ${orgHeader.organizationName || '(Organisation eintragen)'}
+ ${orgHeader.industry ? `Branche: ${orgHeader.industry}
` : ''} + ${orgHeader.employeeCount ? `Mitarbeiter: ${orgHeader.employeeCount}
` : ''} + ${orgHeader.locations && orgHeader.locations.length > 0 ? `Standorte: ${orgHeader.locations.join(', ')}
` : ''} + ${orgHeader.dpoName ? `
Datenschutzbeauftragter: ${orgHeader.dpoName}
` : ''} + ${orgHeader.dpoContact ? `Kontakt DSB: ${orgHeader.dpoContact}
` : ''} +
+ +
+ + +
+

Inhaltsverzeichnis

+

+ ${approvedActivities.length} Verarbeitungstaetigkeiten in ${[...new Set(approvedActivities.map(a => a.businessFunction))].length} Geschaeftsbereichen +

+ ${approvedActivities.map((a, i) => ` +
+ ${a.vvtId} ${a.name || '(Ohne Namen)'} + ${BUSINESS_FUNCTION_LABELS[a.businessFunction]} — ${STATUS_LABELS[a.status]} +
+ `).join('')} +
+ + +` + + for (const a of approvedActivities) { + const hasArt9 = a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c)) + const hasThirdCountry = a.thirdCountryTransfers.length > 0 + + html += ` +
+
+ ${a.vvtId} +

${a.name || '(Ohne Namen)'}

+ ${STATUS_LABELS[a.status]} + ${hasArt9 ? 'Art. 9' : ''} + ${a.dpiaRequired ? 'DSFA' : ''} + ${hasThirdCountry ? 'Drittland' : ''} +
+ + ${a.description ? `
Beschreibung
${a.description}
` : ''} + + + + + + + + + + + + + +
Pflichtfeld (Art. 30)Inhalt
Verantwortlicher${a.responsible || `nicht angegeben`}
Geschaeftsbereich${BUSINESS_FUNCTION_LABELS[a.businessFunction]}
Zwecke der Verarbeitung${a.purposes.length > 0 ? a.purposes.join('; ') : `nicht angegeben`}
Rechtsgrundlage(n)${a.legalBases.length > 0 ? a.legalBases.map(resolveLegalBasis).join('
') : `nicht angegeben`}
Kategorien betroffener Personen${a.dataSubjectCategories.length > 0 ? resolveDataSubjects(a.dataSubjectCategories) : `nicht angegeben`}
Kategorien personenbezogener Daten${a.personalDataCategories.length > 0 ? resolveDataCategories(a.personalDataCategories) : `nicht angegeben`}${hasArt9 ? '
Enthalt besondere Kategorien nach Art. 9 DSGVO' : ''}
Empfaengerkategorien${a.recipientCategories.length > 0 ? a.recipientCategories.map(r => `${r.name} (${r.type})`).join('; ') : `keine`}
Uebermittlung an Drittlaender${hasThirdCountry ? a.thirdCountryTransfers.map(t => `${t.country}: ${t.recipient} — ${resolveTransferMechanism(t.transferMechanism)}`).join('
') : 'Keine Drittlanduebermittlung'}
Loeschfristen${a.retentionPeriod.description || `nicht angegeben`}${a.retentionPeriod.legalBasis ? `
Rechtsgrundlage: ${a.retentionPeriod.legalBasis}` : ''}${a.retentionPeriod.deletionProcedure ? `
Verfahren: ${a.retentionPeriod.deletionProcedure}` : ''}
TOM (Art. 32 DSGVO)${a.tomDescription || `nicht beschrieben`}
+ + ${a.structuredToms && (a.structuredToms.accessControl.length > 0 || a.structuredToms.confidentiality.length > 0 || a.structuredToms.integrity.length > 0 || a.structuredToms.availability.length > 0 || a.structuredToms.separation.length > 0) ? ` +
+
Strukturierte TOMs
+ + + ${a.structuredToms.accessControl.length > 0 ? `` : ''} + ${a.structuredToms.confidentiality.length > 0 ? `` : ''} + ${a.structuredToms.integrity.length > 0 ? `` : ''} + ${a.structuredToms.availability.length > 0 ? `` : ''} + ${a.structuredToms.separation.length > 0 ? `` : ''} +
KategorieMassnahmen
Zugriffskontrolle${a.structuredToms.accessControl.join(', ')}
Vertraulichkeit${a.structuredToms.confidentiality.join(', ')}
Integritaet${a.structuredToms.integrity.join(', ')}
Verfuegbarkeit${a.structuredToms.availability.join(', ')}
Trennbarkeit${a.structuredToms.separation.join(', ')}
+
+ ` : ''} + +
+ Erstellt: ${new Date(a.createdAt).toLocaleDateString('de-DE')} | Aktualisiert: ${new Date(a.updatedAt).toLocaleDateString('de-DE')} + ${a.dpiaRequired ? ' | DSFA erforderlich' : ''} + | Schutzniveau: ${PROTECTION_LEVEL_LABELS[a.protectionLevel]} + | Deployment: ${DEPLOYMENT_LABELS[a.deploymentModel]} +
+
+` + } + + html += ` + + + + +` + + return html + } + + const handlePrintDocument = () => { + const htmlContent = buildDocumentHtml() + const printWindow = window.open('', '_blank') + if (printWindow) { + printWindow.document.write(htmlContent) + printWindow.document.close() + printWindow.focus() + setTimeout(() => printWindow.print(), 300) + } + } + + const handleDownloadHtml = () => { + const htmlContent = buildDocumentHtml() + const blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `vvt-dokument-${new Date().toISOString().split('T')[0]}.html` + a.click() + URL.revokeObjectURL(url) + } + + const nonArchivedActivities = activities.filter(a => a.status !== 'ARCHIVED') + + return ( +
+ {/* Actions */} +
+
+
+

VVT-Dokument (Art. 30 DSGVO)

+

+ Druckfertiges Verarbeitungsverzeichnis mit Deckblatt, Inhaltsverzeichnis und allen {nonArchivedActivities.length} Verarbeitungstaetigkeiten. +

+
+
+ + +
+
+ + {nonArchivedActivities.length === 0 && ( +
+ Keine Verarbeitungstaetigkeiten vorhanden. Erstellen Sie zuerst Eintraege im Tab "Verzeichnis". +
+ )} +
+ + {/* Preview */} + {nonArchivedActivities.length > 0 && ( +
+

Vorschau — Inhalt des Dokuments

+ + {/* Cover preview */} +
+
Deckblatt
+
Verzeichnis von Verarbeitungstaetigkeiten
+
gemaess Art. 30 Abs. 1 DSGVO
+
+ {orgHeader.organizationName || '(Organisation eintragen)'} + {orgHeader.dpoName && <>
DSB: {orgHeader.dpoName}} + {orgHeader.dpoContact && <> ({orgHeader.dpoContact})} +
+
+ Version {orgHeader.vvtVersion} | Stand: {today} +
+
+ + {/* Activity list preview */} +
+ {nonArchivedActivities.map((a) => { + const hasArt9 = a.personalDataCategories.some(c => ART9_CATEGORIES.includes(c)) + return ( +
+
+ {a.vvtId} + {a.name || '(Ohne Namen)'} + {STATUS_LABELS[a.status]} + {hasArt9 && Art. 9} +
+
+
Zweck: {a.purposes.join(', ') || '—'}
+
Rechtsgrundlage: {a.legalBases.map(lb => lb.type).join(', ') || '—'}
+
Betroffene: {a.dataSubjectCategories.length || 0} Kategorien
+
Datenkategorien: {a.personalDataCategories.length || 0}
+
Empfaenger: {a.recipientCategories.length || 0}
+
Loeschfrist: {a.retentionPeriod.description || '—'}
+
+
+ ) + })} +
+ +
+ Tipp: Klicken Sie auf "Als PDF drucken" fuer das vollstaendige, formatierte Dokument mit allen + Pflichtfeldern nach Art. 30 DSGVO — inklusive Deckblatt und Inhaltsverzeichnis. +
+
+ )} +
+ ) +} + +// ============================================================================= +// TAB 5: AUFTRAGSVERARBEITER (Art. 30 Abs. 2) +// ============================================================================= + +interface ProcessorRecord { + id: string + vvtId: string + controllerName: string + controllerContact: string + processingCategories: string[] + subProcessors: { name: string; purpose: string; country: string; isThirdCountry: boolean }[] + thirdCountryTransfers: { country: string; recipient: string; transferMechanism: string }[] + tomDescription: string + status: 'DRAFT' | 'REVIEW' | 'APPROVED' | 'ARCHIVED' + createdAt: string + updatedAt: string +} + +function createEmptyProcessorRecord(): ProcessorRecord { + const now = new Date().toISOString() + return { + id: crypto.randomUUID(), + vvtId: 'AVV-001', + controllerName: '', + controllerContact: '', + processingCategories: [], + subProcessors: [], + thirdCountryTransfers: [], + tomDescription: '', + status: 'DRAFT', + createdAt: now, + updatedAt: now, + } +} + +function TabProcessor({ orgHeader }: { orgHeader: VVTOrganizationHeader }) { + const [records, setRecords] = useState([]) + const [editingRecord, setEditingRecord] = useState(null) + + const handleAdd = () => { + const nextNum = records.length + 1 + const rec = createEmptyProcessorRecord() + rec.vvtId = `AVV-${String(nextNum).padStart(3, '0')}` + setRecords(prev => [...prev, rec]) + setEditingRecord(rec) + } + + const handleSave = (updated: ProcessorRecord) => { + updated.updatedAt = new Date().toISOString() + setRecords(prev => prev.map(r => r.id === updated.id ? updated : r)) + setEditingRecord(null) + } + + const handleDelete = (id: string) => { + setRecords(prev => prev.filter(r => r.id !== id)) + if (editingRecord?.id === id) setEditingRecord(null) + } + + const handlePrintProcessorDoc = () => { + const today = new Date().toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) + const activeRecords = records.filter(r => r.status !== 'ARCHIVED') + + let html = ` + + + + + Verzeichnis Auftragsverarbeiter — Art. 30 Abs. 2 DSGVO + + + +
+

Verzeichnis aller Verarbeitungstaetigkeiten

+
als Auftragsverarbeiter gemaess Art. 30 Abs. 2 DSGVO
+
+ ${orgHeader.organizationName || '(Organisation eintragen)'}
+ ${orgHeader.dpoName ? `Datenschutzbeauftragter: ${orgHeader.dpoName}
` : ''} + Stand: ${today} +
+
+` + + for (const r of activeRecords) { + html += ` +
+
+ ${r.vvtId} +

Auftragsverarbeitung fuer: ${r.controllerName || '(Verantwortlicher)'}

+
+ + + + + + + + +
Pflichtfeld (Art. 30 Abs. 2)Inhalt
Name/Kontaktdaten des Auftragsverarbeiters${orgHeader.organizationName}${orgHeader.dpoContact ? `
Kontakt: ${orgHeader.dpoContact}` : ''}
Name/Kontaktdaten des Verantwortlichen${r.controllerName || 'nicht angegeben'}${r.controllerContact ? `
Kontakt: ${r.controllerContact}` : ''}
Kategorien von Verarbeitungen${r.processingCategories.length > 0 ? r.processingCategories.join('; ') : 'nicht angegeben'}
Unterauftragsverarbeiter${r.subProcessors.length > 0 ? r.subProcessors.map(s => `${s.name} (${s.purpose}) — ${s.country}${s.isThirdCountry ? ' (Drittland)' : ''}`).join('
') : 'Keine'}
Uebermittlung an Drittlaender${r.thirdCountryTransfers.length > 0 ? r.thirdCountryTransfers.map(t => `${t.country}: ${t.recipient} — ${t.transferMechanism}`).join('
') : 'Keine Drittlanduebermittlung'}
TOM (Art. 32 DSGVO)${r.tomDescription || 'nicht beschrieben'}
+
+` + } + + html += ` + +` + + const printWindow = window.open('', '_blank') + if (printWindow) { + printWindow.document.write(html) + printWindow.document.close() + printWindow.focus() + setTimeout(() => printWindow.print(), 300) + } + } + + // Editor mode + if (editingRecord) { + const update = (patch: Partial) => setEditingRecord(prev => prev ? { ...prev, ...patch } : prev) + + return ( +
+
+
+ +
+ {editingRecord.vvtId} +

Auftragsverarbeitung bearbeiten

+
+
+
+ + +
+
+ +
+ +
+ + update({ controllerName: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="Firma des Auftraggebers" /> + + + update({ controllerContact: e.target.value })} + className="w-full px-3 py-2 border border-gray-300 rounded-lg" placeholder="E-Mail oder Adresse" /> + +
+
+ + + update({ processingCategories })} + placeholder="Verarbeitungskategorie eingeben und Enter druecken" + /> + + + +
+ {editingRecord.subProcessors.map((sp, i) => ( +
+ { const copy = [...editingRecord.subProcessors]; copy[i] = { ...copy[i], name: e.target.value }; update({ subProcessors: copy }) }} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Name" /> + { const copy = [...editingRecord.subProcessors]; copy[i] = { ...copy[i], purpose: e.target.value }; update({ subProcessors: copy }) }} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Zweck" /> + { const copy = [...editingRecord.subProcessors]; copy[i] = { ...copy[i], country: e.target.value }; update({ subProcessors: copy }) }} + className="w-24 px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Land" /> + + +
+ ))} + +
+
+ + +
+ {editingRecord.thirdCountryTransfers.map((tc, i) => ( +
+ { const copy = [...editingRecord.thirdCountryTransfers]; copy[i] = { ...copy[i], country: e.target.value }; update({ thirdCountryTransfers: copy }) }} + className="w-20 px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Land" /> + { const copy = [...editingRecord.thirdCountryTransfers]; copy[i] = { ...copy[i], recipient: e.target.value }; update({ thirdCountryTransfers: copy }) }} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm" placeholder="Empfaenger" /> + + +
+ ))} + +
+
+ + +