- {policy.linkedVvtIds && policy.linkedVvtIds.length > 0 && (
+ {policy.linkedVVTActivityIds && policy.linkedVVTActivityIds.length > 0 && (
Verknuepfte Taetigkeiten:
- {policy.linkedVvtIds.map((vvtId: string) => {
+ {policy.linkedVVTActivityIds.map((vvtId: string) => {
const activity = vvtActivities.find((a: any) => a.id === vvtId)
return (
{activity?.name || vvtId}
updatePolicy(pid, (p) => ({
- ...p, linkedVvtIds: (p.linkedVvtIds || []).filter((id: string) => id !== vvtId),
+ ...p, linkedVVTActivityIds: (p.linkedVVTActivityIds || []).filter((id: string) => id !== vvtId),
}))}
className="text-blue-600 hover:text-blue-900">x
@@ -393,15 +393,15 @@ export function VVTLinkSection({
{
const val = e.target.value
- if (val && !(policy.linkedVvtIds || []).includes(val)) {
- updatePolicy(pid, (p) => ({ ...p, linkedVvtIds: [...(p.linkedVvtIds || []), val] }))
+ if (val && !(policy.linkedVVTActivityIds || []).includes(val)) {
+ updatePolicy(pid, (p) => ({ ...p, linkedVVTActivityIds: [...(p.linkedVVTActivityIds || []), val] }))
}
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-purple-500">
Verarbeitungstaetigkeit verknuepfen...
{vvtActivities
- .filter((a: any) => !(policy.linkedVvtIds || []).includes(a.id))
+ .filter((a: any) => !(policy.linkedVVTActivityIds || []).includes(a.id))
.map((a: any) => ({a.name || a.id} ))}
@@ -415,6 +415,70 @@ export function VVTLinkSection({
)
}
+// ---------------------------------------------------------------------------
+// Sektion 5b: Auftragsverarbeiter-Verknuepfung
+// ---------------------------------------------------------------------------
+
+export function VendorLinkSection({
+ policy, pid, vendorList, updatePolicy,
+}: {
+ policy: LoeschfristPolicy; pid: string; vendorList: Array<{id: string; name: string}>
+ updatePolicy: (id: string, updater: (p: LoeschfristPolicy) => LoeschfristPolicy) => void
+}) {
+ return (
+
+
5b. Verknuepfte Auftragsverarbeiter
+ {vendorList.length > 0 ? (
+
+
+ Verknuepfen Sie diese Loeschfrist mit relevanten Auftragsverarbeitern.
+
+
+ {policy.linkedVendorIds && policy.linkedVendorIds.length > 0 && (
+
+
Verknuepfte Auftragsverarbeiter:
+
+ {policy.linkedVendorIds.map((vendorId: string) => {
+ const vendor = vendorList.find((v) => v.id === vendorId)
+ return (
+
+ {vendor?.name || vendorId}
+ updatePolicy(pid, (p) => ({
+ ...p, linkedVendorIds: (p.linkedVendorIds || []).filter((id: string) => id !== vendorId),
+ }))}
+ className="text-orange-600 hover:text-orange-900">x
+
+ )
+ })}
+
+
+ )}
+
{
+ const val = e.target.value
+ if (val && !(policy.linkedVendorIds || []).includes(val)) {
+ updatePolicy(pid, (p) => ({ ...p, linkedVendorIds: [...(p.linkedVendorIds || []), val] }))
+ }
+ 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-purple-500">
+ Auftragsverarbeiter verknuepfen...
+ {vendorList
+ .filter((v) => !(policy.linkedVendorIds || []).includes(v.id))
+ .map((v) => ({v.name || v.id} ))}
+
+
+
+ ) : (
+
+ Keine Auftragsverarbeiter gefunden. Erstellen Sie zuerst Auftragsverarbeiter im Vendor-Compliance-Modul, um hier Verknuepfungen herstellen zu koennen.
+
+ )}
+
+ )
+}
+
// ---------------------------------------------------------------------------
// Sektion 6: Review-Einstellungen
// ---------------------------------------------------------------------------
diff --git a/admin-compliance/app/sdk/loeschfristen/_components/EditorTab.tsx b/admin-compliance/app/sdk/loeschfristen/_components/EditorTab.tsx
index d3191e3..5515d7d 100644
--- a/admin-compliance/app/sdk/loeschfristen/_components/EditorTab.tsx
+++ b/admin-compliance/app/sdk/loeschfristen/_components/EditorTab.tsx
@@ -7,7 +7,7 @@ import {
import { renderStatusBadge } from './UebersichtTab'
import {
DataObjectSection, DeletionLogicSection, StorageSection,
- ResponsibilitySection, VVTLinkSection, ReviewSection,
+ ResponsibilitySection, VVTLinkSection, VendorLinkSection, ReviewSection,
} from './EditorSections'
// ---------------------------------------------------------------------------
@@ -19,6 +19,7 @@ interface EditorTabProps {
editingId: string | null
editingPolicy: LoeschfristPolicy | null
vvtActivities: any[]
+ vendorList: Array<{id: string; name: string}>
saving: boolean
setEditingId: (id: string | null) => void
setTab: (tab: 'uebersicht' | 'editor' | 'generator' | 'export') => void
@@ -79,7 +80,7 @@ function EditorNoSelection({
// ---------------------------------------------------------------------------
function EditorForm({
- policy, vvtActivities, saving, setEditingId, setTab,
+ policy, vvtActivities, vendorList, saving, setEditingId, setTab,
updatePolicy, deletePolicy, addLegalHold, removeLegalHold,
addStorageLocation, removeStorageLocation, handleSaveAndClose,
}: Omit
& {
@@ -127,6 +128,7 @@ function EditorForm({
addStorageLocation={addStorageLocation} removeStorageLocation={removeStorageLocation} />
+
{/* Action buttons */}
diff --git a/admin-compliance/app/sdk/loeschfristen/_components/LoeschkonzeptTab.tsx b/admin-compliance/app/sdk/loeschfristen/_components/LoeschkonzeptTab.tsx
new file mode 100644
index 0000000..1806173
--- /dev/null
+++ b/admin-compliance/app/sdk/loeschfristen/_components/LoeschkonzeptTab.tsx
@@ -0,0 +1,257 @@
+'use client'
+
+import React from 'react'
+import { LoeschfristPolicy } from '@/lib/sdk/loeschfristen-types'
+import { ComplianceCheckResult } from '@/lib/sdk/loeschfristen-compliance'
+import {
+ buildLoeschkonzeptHtml,
+ type LoeschkonzeptOrgHeader,
+ type LoeschkonzeptRevision,
+} from '@/lib/sdk/loeschfristen-document'
+import { downloadFile } from '@/lib/sdk/loeschfristen-export'
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+
+interface LoeschkonzeptTabProps {
+ policies: LoeschfristPolicy[]
+ orgHeader: LoeschkonzeptOrgHeader
+ revisions: LoeschkonzeptRevision[]
+ complianceResult: ComplianceCheckResult | null
+ vvtActivities: any[]
+ onOrgHeaderChange: (field: keyof LoeschkonzeptOrgHeader, value: string | string[]) => void
+ onAddRevision: () => void
+ onUpdateRevision: (index: number, field: keyof LoeschkonzeptRevision, value: string) => void
+ onRemoveRevision: (index: number) => void
+}
+
+// ---------------------------------------------------------------------------
+// Component
+// ---------------------------------------------------------------------------
+
+export function LoeschkonzeptTab({
+ policies,
+ orgHeader,
+ revisions,
+ complianceResult,
+ vvtActivities,
+ onOrgHeaderChange,
+ onAddRevision,
+ onUpdateRevision,
+ onRemoveRevision,
+}: LoeschkonzeptTabProps) {
+ const activePolicies = policies.filter(p => p.status !== 'ARCHIVED')
+
+ 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')
+ }
+
+ return (
+
+ {/* Action bar */}
+
+
+
+
+ Loeschkonzept (Art. 5/17/30 DSGVO)
+
+
+ Druckfertiges Loeschkonzept mit Deckblatt, Loeschregeln, VVT-Verknuepfung und Compliance-Status.
+
+
+
+
+
+ HTML herunterladen
+
+
+
+ Als PDF drucken
+
+
+
+
+ {activePolicies.length === 0 && (
+
+ Keine aktiven Policies vorhanden. Erstellen Sie mindestens eine Policy, um das Loeschkonzept zu generieren.
+
+ )}
+
+
+ {/* Org Header Form */}
+
+
Organisationsdaten (Deckblatt)
+
+
+ Organisation
+ onOrgHeaderChange('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" />
+
+
+ Branche
+ onOrgHeaderChange('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" />
+
+
+ Datenschutzbeauftragter
+ onOrgHeaderChange('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" />
+
+
+ DSB-Kontakt
+ onOrgHeaderChange('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" />
+
+
+ Verantwortlicher (Art. 4 Nr. 7)
+ onOrgHeaderChange('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" />
+
+
+ Mitarbeiter
+ onOrgHeaderChange('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" />
+
+
+ Version
+ onOrgHeaderChange('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" />
+
+
+ Pruefintervall
+ onOrgHeaderChange('reviewInterval', 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">
+ Vierteljaehrlich
+ Halbjaehrlich
+ Jaehrlich
+
+
+
+ Letzte Pruefung
+ onOrgHeaderChange('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" />
+
+
+ Naechste Pruefung
+ onOrgHeaderChange('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
+
+ + Revision hinzufuegen
+
+
+ {revisions.length === 0 ? (
+
+ Noch keine Revisionen. Die Erstversion wird automatisch im Dokument eingefuegt.
+
+ ) : (
+
+ {revisions.map((rev, idx) => (
+
+ ))}
+
+ )}
+
+
+ {/* Document Preview */}
+
+
Dokument-Vorschau
+
+
+
Loeschkonzept
+
gemaess Art. 5/17/30 DSGVO
+
+ {orgHeader.organizationName || Organisation nicht angegeben }
+
+
+ Version {orgHeader.loeschkonzeptVersion} | {new Date().toLocaleDateString('de-DE')}
+
+
+
+
12 Sektionen
+
+
1. Ziel und Zweck
7. Auftragsverarbeiter
+
2. Geltungsbereich
8. Legal Hold Verfahren
+
3. Grundprinzipien
9. Verantwortlichkeiten
+
4. Loeschregeln-Uebersicht
10. Pruef-/Revisionszyklus
+
5. Detaillierte Loeschregeln
11. Compliance-Status
+
6. VVT-Verknuepfung
12. Aenderungshistorie
+
+
+
+ {activePolicies.length} Loeschregeln
+ {policies.filter(p => p.linkedVVTActivityIds.length > 0).length} VVT-Verknuepfungen
+ {policies.filter(p => p.linkedVendorIds.length > 0).length} Vendor-Verknuepfungen
+ {revisions.length} Revisionen
+ {complianceResult && (
+ Compliance-Score: = 75 ? 'text-green-600' : complianceResult.score >= 50 ? 'text-yellow-600' : 'text-red-600'}>{complianceResult.score}/100
+ )}
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/loeschfristen/_components/api.ts b/admin-compliance/app/sdk/loeschfristen/_components/api.ts
new file mode 100644
index 0000000..0eb1355
--- /dev/null
+++ b/admin-compliance/app/sdk/loeschfristen/_components/api.ts
@@ -0,0 +1,74 @@
+import { LoeschfristPolicy, createEmptyPolicy } from '@/lib/sdk/loeschfristen-types'
+
+export const LOESCHFRISTEN_API = '/api/sdk/v1/compliance/loeschfristen'
+
+export function apiToPolicy(raw: any): LoeschfristPolicy {
+ const base = createEmptyPolicy()
+ return {
+ ...base,
+ id: raw.id,
+ policyId: raw.policy_id || base.policyId,
+ dataObjectName: raw.data_object_name || '',
+ description: raw.description || '',
+ affectedGroups: raw.affected_groups || [],
+ dataCategories: raw.data_categories || [],
+ primaryPurpose: raw.primary_purpose || '',
+ deletionTrigger: raw.deletion_trigger || 'PURPOSE_END',
+ retentionDriver: raw.retention_driver || null,
+ retentionDriverDetail: raw.retention_driver_detail || '',
+ retentionDuration: raw.retention_duration ?? null,
+ retentionUnit: raw.retention_unit || null,
+ retentionDescription: raw.retention_description || '',
+ startEvent: raw.start_event || '',
+ hasActiveLegalHold: raw.has_active_legal_hold || false,
+ legalHolds: raw.legal_holds || [],
+ storageLocations: raw.storage_locations || [],
+ deletionMethod: raw.deletion_method || 'MANUAL_REVIEW_DELETE',
+ deletionMethodDetail: raw.deletion_method_detail || '',
+ responsibleRole: raw.responsible_role || '',
+ responsiblePerson: raw.responsible_person || '',
+ releaseProcess: raw.release_process || '',
+ linkedVVTActivityIds: raw.linked_vvt_activity_ids || [],
+ linkedVendorIds: raw.linked_vendor_ids || [],
+ status: raw.status || 'DRAFT',
+ lastReviewDate: raw.last_review_date || base.lastReviewDate,
+ nextReviewDate: raw.next_review_date || base.nextReviewDate,
+ reviewInterval: raw.review_interval || 'ANNUAL',
+ tags: raw.tags || [],
+ createdAt: raw.created_at || base.createdAt,
+ updatedAt: raw.updated_at || base.updatedAt,
+ }
+}
+
+export function policyToPayload(p: LoeschfristPolicy): any {
+ return {
+ policy_id: p.policyId,
+ data_object_name: p.dataObjectName,
+ description: p.description,
+ affected_groups: p.affectedGroups,
+ data_categories: p.dataCategories,
+ primary_purpose: p.primaryPurpose,
+ deletion_trigger: p.deletionTrigger,
+ retention_driver: p.retentionDriver || null,
+ retention_driver_detail: p.retentionDriverDetail,
+ retention_duration: p.retentionDuration || null,
+ retention_unit: p.retentionUnit || null,
+ retention_description: p.retentionDescription,
+ start_event: p.startEvent,
+ has_active_legal_hold: p.hasActiveLegalHold,
+ legal_holds: p.legalHolds,
+ storage_locations: p.storageLocations,
+ deletion_method: p.deletionMethod,
+ deletion_method_detail: p.deletionMethodDetail,
+ responsible_role: p.responsibleRole,
+ responsible_person: p.responsiblePerson,
+ release_process: p.releaseProcess,
+ linked_vvt_activity_ids: p.linkedVVTActivityIds,
+ linked_vendor_ids: p.linkedVendorIds,
+ status: p.status,
+ last_review_date: p.lastReviewDate || null,
+ next_review_date: p.nextReviewDate || null,
+ review_interval: p.reviewInterval,
+ tags: p.tags,
+ }
+}
diff --git a/admin-compliance/app/sdk/loeschfristen/page.tsx b/admin-compliance/app/sdk/loeschfristen/page.tsx
index 0442258..dc82dcf 100644
--- a/admin-compliance/app/sdk/loeschfristen/page.tsx
+++ b/admin-compliance/app/sdk/loeschfristen/page.tsx
@@ -1,30 +1,29 @@
'use client'
import React, { useState, useEffect, useCallback, useMemo } from 'react'
-import { useRouter } from 'next/navigation'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import {
LoeschfristPolicy,
- createEmptyPolicy, createEmptyLegalHold, createEmptyStorageLocation,
+ createEmptyLegalHold, createEmptyStorageLocation,
isPolicyOverdue, getActiveLegalHolds,
} from '@/lib/sdk/loeschfristen-types'
import {
- PROFILING_STEPS, ProfilingAnswer, ProfilingStep,
+ PROFILING_STEPS, ProfilingAnswer,
isStepComplete, getProfilingProgress, generatePoliciesFromProfile,
} from '@/lib/sdk/loeschfristen-profiling'
-import {
- runComplianceCheck, ComplianceCheckResult, ComplianceIssue,
-} from '@/lib/sdk/loeschfristen-compliance'
-import {
- exportPoliciesAsJSON, exportPoliciesAsCSV,
- generateComplianceSummary, downloadFile,
-} from '@/lib/sdk/loeschfristen-export'
+import { runComplianceCheck, ComplianceCheckResult } from '@/lib/sdk/loeschfristen-compliance'
import {
buildLoeschkonzeptHtml,
type LoeschkonzeptOrgHeader,
type LoeschkonzeptRevision,
createDefaultLoeschkonzeptOrgHeader,
} from '@/lib/sdk/loeschfristen-document'
+import { LOESCHFRISTEN_API, apiToPolicy, policyToPayload } from './_components/api'
+import { UebersichtTab } from './_components/UebersichtTab'
+import { EditorTab } from './_components/EditorTab'
+import { GeneratorTab } from './_components/GeneratorTab'
+import { ExportTab } from './_components/ExportTab'
+import { LoeschkonzeptTab } from './_components/LoeschkonzeptTab'
// ---------------------------------------------------------------------------
// Types
@@ -37,90 +36,14 @@ const TAB_CONFIG: { key: Tab; label: string }[] = [
{ key: 'editor', label: 'Editor' },
{ key: 'generator', label: 'Generator' },
{ key: 'export', label: 'Export & Compliance' },
+ { key: 'loeschkonzept', label: 'Loeschkonzept' },
]
-// ---------------------------------------------------------------------------
-// API helpers
-// ---------------------------------------------------------------------------
-
-const LOESCHFRISTEN_API = '/api/sdk/v1/compliance/loeschfristen'
-
-function apiToPolicy(raw: any): LoeschfristPolicy {
- const base = createEmptyPolicy()
- return {
- ...base,
- id: raw.id,
- policyId: raw.policy_id || base.policyId,
- dataObjectName: raw.data_object_name || '',
- description: raw.description || '',
- affectedGroups: raw.affected_groups || [],
- dataCategories: raw.data_categories || [],
- primaryPurpose: raw.primary_purpose || '',
- deletionTrigger: raw.deletion_trigger || 'PURPOSE_END',
- retentionDriver: raw.retention_driver || null,
- retentionDriverDetail: raw.retention_driver_detail || '',
- retentionDuration: raw.retention_duration ?? null,
- retentionUnit: raw.retention_unit || null,
- retentionDescription: raw.retention_description || '',
- startEvent: raw.start_event || '',
- hasActiveLegalHold: raw.has_active_legal_hold || false,
- legalHolds: raw.legal_holds || [],
- storageLocations: raw.storage_locations || [],
- deletionMethod: raw.deletion_method || 'MANUAL_REVIEW_DELETE',
- deletionMethodDetail: raw.deletion_method_detail || '',
- responsibleRole: raw.responsible_role || '',
- responsiblePerson: raw.responsible_person || '',
- releaseProcess: raw.release_process || '',
- linkedVVTActivityIds: raw.linked_vvt_activity_ids || [],
- status: raw.status || 'DRAFT',
- lastReviewDate: raw.last_review_date || base.lastReviewDate,
- nextReviewDate: raw.next_review_date || base.nextReviewDate,
- reviewInterval: raw.review_interval || 'ANNUAL',
- tags: raw.tags || [],
- createdAt: raw.created_at || base.createdAt,
- updatedAt: raw.updated_at || base.updatedAt,
- }
-}
-
-function policyToPayload(p: LoeschfristPolicy): any {
- return {
- policy_id: p.policyId,
- data_object_name: p.dataObjectName,
- description: p.description,
- affected_groups: p.affectedGroups,
- data_categories: p.dataCategories,
- primary_purpose: p.primaryPurpose,
- deletion_trigger: p.deletionTrigger,
- retention_driver: p.retentionDriver || null,
- retention_driver_detail: p.retentionDriverDetail,
- retention_duration: p.retentionDuration || null,
- retention_unit: p.retentionUnit || null,
- retention_description: p.retentionDescription,
- start_event: p.startEvent,
- has_active_legal_hold: p.hasActiveLegalHold,
- legal_holds: p.legalHolds,
- storage_locations: p.storageLocations,
- deletion_method: p.deletionMethod,
- deletion_method_detail: p.deletionMethodDetail,
- responsible_role: p.responsibleRole,
- responsible_person: p.responsiblePerson,
- release_process: p.releaseProcess,
- linked_vvt_activity_ids: p.linkedVVTActivityIds,
- status: p.status,
- last_review_date: p.lastReviewDate || null,
- next_review_date: p.nextReviewDate || null,
- review_interval: p.reviewInterval,
- tags: p.tags,
- }
-}
-
// ---------------------------------------------------------------------------
// Main Page
// ---------------------------------------------------------------------------
export default function LoeschfristenPage() {
- const router = useRouter()
-
// ---- Core state ----
const [tab, setTab] = useState('uebersicht')
const [policies, setPolicies] = useState([])
@@ -153,100 +76,26 @@ export default function LoeschfristenPage() {
const [revisions, setRevisions] = useState([])
// --------------------------------------------------------------------------
- // Persistence (API-backed)
+ // Data loading
// --------------------------------------------------------------------------
useEffect(() => {
+ async function loadPolicies() {
+ try {
+ const res = await fetch(`${LOESCHFRISTEN_API}?limit=500`)
+ if (res.ok) {
+ const data = await res.json()
+ const fetched = Array.isArray(data.policies) ? data.policies.map(apiToPolicy) : []
+ setPolicies(fetched)
+ }
+ } catch (e) {
+ console.error('Failed to load Loeschfristen from API:', e)
+ }
+ setLoaded(true)
+ }
loadPolicies()
}, [])
- async function loadPolicies() {
- try {
- const res = await fetch(`${LOESCHFRISTEN_API}?limit=500`)
- if (res.ok) {
- const data = await res.json()
- const fetched = Array.isArray(data.policies) ? data.policies.map(apiToPolicy) : []
- setPolicies(fetched)
- }
- } catch (e) {
- console.error('Failed to load Loeschfristen from API:', e)
- }
- setLoaded(true)
- }
-
- function apiToPolicy(raw: any): LoeschfristPolicy {
- // Map snake_case API response to camelCase LoeschfristPolicy
- const base = createEmptyPolicy()
- return {
- ...base,
- id: raw.id, // DB UUID — used for API calls
- policyId: raw.policy_id || base.policyId, // Display ID like "LF-2026-001"
- dataObjectName: raw.data_object_name || '',
- description: raw.description || '',
- affectedGroups: raw.affected_groups || [],
- dataCategories: raw.data_categories || [],
- primaryPurpose: raw.primary_purpose || '',
- deletionTrigger: raw.deletion_trigger || 'PURPOSE_END',
- retentionDriver: raw.retention_driver || null,
- retentionDriverDetail: raw.retention_driver_detail || '',
- retentionDuration: raw.retention_duration ?? null,
- retentionUnit: raw.retention_unit || null,
- retentionDescription: raw.retention_description || '',
- startEvent: raw.start_event || '',
- hasActiveLegalHold: raw.has_active_legal_hold || false,
- legalHolds: raw.legal_holds || [],
- storageLocations: raw.storage_locations || [],
- deletionMethod: raw.deletion_method || 'MANUAL_REVIEW_DELETE',
- deletionMethodDetail: raw.deletion_method_detail || '',
- responsibleRole: raw.responsible_role || '',
- responsiblePerson: raw.responsible_person || '',
- releaseProcess: raw.release_process || '',
- linkedVVTActivityIds: raw.linked_vvt_activity_ids || [],
- linkedVendorIds: raw.linked_vendor_ids || [],
- status: raw.status || 'DRAFT',
- lastReviewDate: raw.last_review_date || base.lastReviewDate,
- nextReviewDate: raw.next_review_date || base.nextReviewDate,
- reviewInterval: raw.review_interval || 'ANNUAL',
- tags: raw.tags || [],
- createdAt: raw.created_at || base.createdAt,
- updatedAt: raw.updated_at || base.updatedAt,
- }
- }
-
- function policyToPayload(p: LoeschfristPolicy): any {
- return {
- policy_id: p.policyId,
- data_object_name: p.dataObjectName,
- description: p.description,
- affected_groups: p.affectedGroups,
- data_categories: p.dataCategories,
- primary_purpose: p.primaryPurpose,
- deletion_trigger: p.deletionTrigger,
- retention_driver: p.retentionDriver || null,
- retention_driver_detail: p.retentionDriverDetail,
- retention_duration: p.retentionDuration || null,
- retention_unit: p.retentionUnit || null,
- retention_description: p.retentionDescription,
- start_event: p.startEvent,
- has_active_legal_hold: p.hasActiveLegalHold,
- legal_holds: p.legalHolds,
- storage_locations: p.storageLocations,
- deletion_method: p.deletionMethod,
- deletion_method_detail: p.deletionMethodDetail,
- responsible_role: p.responsibleRole,
- responsible_person: p.responsiblePerson,
- release_process: p.releaseProcess,
- linked_vvt_activity_ids: p.linkedVVTActivityIds,
- linked_vendor_ids: p.linkedVendorIds,
- status: p.status,
- last_review_date: p.lastReviewDate || null,
- next_review_date: p.nextReviewDate || null,
- review_interval: p.reviewInterval,
- tags: p.tags,
- }
- }
-
- // Load VVT activities from API
useEffect(() => {
fetch('/api/sdk/v1/compliance/vvt?limit=200')
.then(res => res.ok ? res.json() : null)
@@ -264,7 +113,6 @@ export default function LoeschfristenPage() {
})
}, [tab, editingId])
- // Load vendor list from API
useEffect(() => {
fetch('/api/sdk/v1/vendor-compliance/vendors?limit=500')
.then(r => r.ok ? r.json() : null)
@@ -275,9 +123,7 @@ export default function LoeschfristenPage() {
.catch(() => {})
}, [])
- // 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) {
@@ -286,19 +132,17 @@ export default function LoeschfristenPage() {
}
} 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
+ return
}
}
} catch { /* ignore */ }
- // Fallback: fetch from VVT organization API
fetch('/api/sdk/v1/compliance/vvt/organization')
.then(res => res.ok ? res.json() : null)
.then(data => {
@@ -318,7 +162,7 @@ export default function LoeschfristenPage() {
}, [])
// --------------------------------------------------------------------------
- // Derived
+ // Derived state
// --------------------------------------------------------------------------
const editingPolicy = useMemo(
@@ -482,1905 +326,13 @@ export default function LoeschfristenPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingPolicy])
- // --------------------------------------------------------------------------
- // Render
- // --------------------------------------------------------------------------
-
- const TAB_CONFIG: { key: Tab; label: string }[] = [
- { key: 'uebersicht', label: 'Uebersicht' },
- { key: 'editor', label: 'Editor' },
- { key: 'generator', label: 'Generator' },
- { key: 'export', label: 'Export & Compliance' },
- { key: 'loeschkonzept', label: 'Loeschkonzept' },
- ]
-
- // --------------------------------------------------------------------------
- // Render helpers
- // --------------------------------------------------------------------------
-
- const renderStatusBadge = (status: PolicyStatus) => {
- const colors = STATUS_COLORS[status] ?? 'bg-gray-100 text-gray-800'
- const label = STATUS_LABELS[status] ?? status
- return (
-
- {label}
-
- )
- }
-
- const renderTriggerBadge = (trigger: DeletionTriggerLevel) => {
- const colors = TRIGGER_COLORS[trigger] ?? 'bg-gray-100 text-gray-800'
- const label = TRIGGER_LABELS[trigger] ?? trigger
- return (
-
- {label}
-
- )
- }
-
- // ==========================================================================
- // TAB 1: Uebersicht
- // ==========================================================================
-
- const renderUebersicht = () => (
-
- {/* Stats bar */}
-
- {[
- { label: 'Gesamt', value: stats.total, color: 'text-gray-900' },
- { label: 'Aktiv', value: stats.active, color: 'text-green-600' },
- { label: 'Entwurf', value: stats.draft, color: 'text-yellow-600' },
- {
- label: 'Pruefung faellig',
- value: stats.overdue,
- color: 'text-red-600',
- },
- {
- label: 'Legal Holds aktiv',
- value: stats.legalHolds,
- color: 'text-orange-600',
- },
- ].map((s) => (
-
-
{s.value}
-
{s.label}
-
- ))}
-
-
- {/* Search & filters */}
-
-
setSearchQuery(e.target.value)}
- placeholder="Suche nach Name, ID oder Beschreibung..."
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- />
-
- Status:
- {[
- { key: 'all', label: 'Alle' },
- { key: 'active', label: 'Aktiv' },
- { key: 'draft', label: 'Entwurf' },
- { key: 'review', label: 'Pruefung noetig' },
- ].map((f) => (
- setFilter(f.key)}
- className={`px-3 py-1 rounded-lg text-sm font-medium transition ${
- filter === f.key
- ? 'bg-purple-600 text-white'
- : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
- }`}
- >
- {f.label}
-
- ))}
-
- Aufbewahrungstreiber:
-
- setDriverFilter(e.target.value)}
- className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- >
- Alle
- {Object.entries(RETENTION_DRIVER_META).map(([key, meta]) => (
-
- {meta.label}
-
- ))}
-
-
-
-
- {/* Policy cards or empty state */}
- {filteredPolicies.length === 0 && policies.length === 0 ? (
-
-
📋
-
- Noch keine Loeschfristen angelegt
-
-
- Starten Sie den Generator, um auf Basis Ihres Unternehmensprofils
- automatisch passende Loeschfristen zu erstellen, oder legen Sie
- manuell eine neue Loeschfrist an.
-
-
- setTab('generator')}
- className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition"
- >
- Generator starten
-
-
- Neue Loeschfrist
-
-
-
- ) : filteredPolicies.length === 0 ? (
-
-
- Keine Loeschfristen entsprechen den aktuellen Filtern.
-
-
- ) : (
-
- {filteredPolicies.map((p) => {
- const trigger = getEffectiveDeletionTrigger(p)
- const activeHolds = getActiveLegalHolds(p)
- const overdue = isPolicyOverdue(p)
- return (
-
- {activeHolds.length > 0 && (
-
- ⚠
-
- )}
-
- {p.policyId}
-
-
- {p.dataObjectName || 'Ohne Bezeichnung'}
-
-
- {renderTriggerBadge(trigger)}
-
- {formatRetentionDuration(p)}
-
- {renderStatusBadge(p.status)}
- {overdue && (
-
- Pruefung faellig
-
- )}
-
- {p.description && (
-
- {p.description}
-
- )}
-
{
- setEditingId(p.policyId)
- setTab('editor')
- }}
- className="text-sm text-purple-600 hover:text-purple-800 font-medium"
- >
- Bearbeiten →
-
-
- )
- })}
-
- )}
-
- {/* Floating action button */}
- {policies.length > 0 && (
-
-
- + Neue Loeschfrist
-
-
- )}
-
- )
-
- // ==========================================================================
- // TAB 2: Editor
- // ==========================================================================
-
- const renderEditorNoSelection = () => (
-
-
- Loeschfrist zum Bearbeiten waehlen
-
- {policies.length === 0 ? (
-
- Noch keine Loeschfristen vorhanden.{' '}
-
- Neue Loeschfrist anlegen
-
-
- ) : (
-
- {policies.map((p) => (
-
setEditingId(p.policyId)}
- className="w-full text-left px-4 py-3 border border-gray-200 rounded-lg hover:bg-gray-50 transition flex items-center justify-between"
- >
-
-
- {p.policyId}
-
-
- {p.dataObjectName || 'Ohne Bezeichnung'}
-
-
- {renderStatusBadge(p.status)}
-
- ))}
-
- + Neue Loeschfrist anlegen
-
-
- )}
-
- )
-
- const renderEditorForm = (policy: LoeschfristPolicy) => {
- const pid = policy.policyId
-
- const set = (
- key: K,
- val: LoeschfristPolicy[K],
- ) => {
- updatePolicy(pid, (p) => ({ ...p, [key]: val }))
- }
-
- const updateLegalHold = (
- idx: number,
- updater: (h: LegalHold) => LegalHold,
- ) => {
- updatePolicy(pid, (p) => ({
- ...p,
- legalHolds: p.legalHolds.map((h, i) => (i === idx ? updater(h) : h)),
- }))
- }
-
- const updateStorageLocation = (
- idx: number,
- updater: (s: StorageLocation) => StorageLocation,
- ) => {
- updatePolicy(pid, (p) => ({
- ...p,
- storageLocations: p.storageLocations.map((s, i) =>
- i === idx ? updater(s) : s,
- ),
- }))
- }
-
- return (
-
- {/* Header with back button */}
-
-
- setEditingId(null)}
- className="text-gray-400 hover:text-gray-600 transition"
- >
- ← Zurueck
-
-
- {policy.dataObjectName || 'Neue Loeschfrist'}
-
-
- {policy.policyId}
-
-
- {renderStatusBadge(policy.status)}
-
-
- {/* Sektion 1: Datenobjekt */}
-
-
- 1. Datenobjekt
-
-
-
-
- Name des Datenobjekts *
-
- set('dataObjectName', e.target.value)}
- placeholder="z.B. Bewerbungsunterlagen"
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- />
-
-
-
-
- Beschreibung
-
-
-
-
-
- Betroffene Personengruppen
-
- set('affectedGroups', v)}
- placeholder="z.B. Bewerber, Mitarbeiter... (Enter zum Hinzufuegen)"
- />
-
-
-
-
- Datenkategorien
-
- set('dataCategories', v)}
- placeholder="z.B. Stammdaten, Kontaktdaten... (Enter zum Hinzufuegen)"
- />
-
-
-
-
- Primaerer Verarbeitungszweck
-
-
-
-
- {/* Sektion 2: 3-stufige Loeschlogik */}
-
-
- 2. 3-stufige Loeschlogik
-
-
-
-
- Loeschausloeser (Trigger-Stufe)
-
-
- {(
- ['PURPOSE_END', 'RETENTION_DRIVER', 'LEGAL_HOLD'] as DeletionTriggerLevel[]
- ).map((trigger) => (
-
- set('deletionTrigger', trigger)}
- className="mt-0.5 text-purple-600 focus:ring-purple-500"
- />
-
-
- {renderTriggerBadge(trigger)}
-
-
- {trigger === 'PURPOSE_END' &&
- 'Loeschung nach Wegfall des Verarbeitungszwecks'}
- {trigger === 'RETENTION_DRIVER' &&
- 'Loeschung nach Ablauf gesetzlicher oder vertraglicher Aufbewahrungsfrist'}
- {trigger === 'LEGAL_HOLD' &&
- 'Loeschung durch aktiven Legal Hold blockiert'}
-
-
-
- ))}
-
-
-
- {/* Retention driver selection */}
- {policy.deletionTrigger === 'RETENTION_DRIVER' && (
-
-
- Aufbewahrungstreiber
-
- {
- const driver = e.target.value as RetentionDriverType
- const meta =
- RETENTION_DRIVER_META[driver]
- set('retentionDriver', driver)
- if (meta) {
- set('retentionDuration', meta.defaultDuration)
- set('retentionUnit', meta.defaultUnit as RetentionUnit)
- set('retentionDescription', meta.description)
- }
- }}
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- >
- Bitte waehlen...
- {Object.entries(RETENTION_DRIVER_META).map(([key, meta]) => (
-
- {meta.label}
-
- ))}
-
-
- )}
-
-
-
-
- Aufbewahrungsdauer
-
-
- set('retentionDuration', parseInt(e.target.value) || 0)
- }
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- />
-
-
-
- Einheit
-
-
- set('retentionUnit', e.target.value as RetentionUnit)
- }
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- >
- Tage
- Monate
- Jahre
-
-
-
-
-
-
- Beschreibung der Aufbewahrungspflicht
-
- set('retentionDescription', e.target.value)}
- placeholder="z.B. Handelsrechtliche Aufbewahrungspflicht gem. HGB"
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- />
-
-
-
-
- Startereignis (Fristbeginn)
-
- set('startEvent', e.target.value)}
- placeholder="z.B. Ende des Geschaeftsjahres, Vertragsende..."
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- />
-
-
- {/* Legal Holds */}
-
-
-
- Legal Holds
-
-
-
- set('hasActiveLegalHold', e.target.checked)
- }
- className="text-purple-600 focus:ring-purple-500 rounded"
- />
- Aktiver Legal Hold
-
-
-
- {policy.legalHolds.length > 0 && (
-
-
-
-
-
- Bezeichnung
-
-
- Grund
-
-
- Status
-
-
- Erstellt am
-
-
- Aktion
-
-
-
-
- {policy.legalHolds.map((hold, idx) => (
-
-
-
- updateLegalHold(idx, (h) => ({
- ...h,
- name: e.target.value,
- }))
- }
- placeholder="Bezeichnung"
- className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- />
-
-
-
- updateLegalHold(idx, (h) => ({
- ...h,
- reason: e.target.value,
- }))
- }
- placeholder="Grund"
- className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- />
-
-
-
- updateLegalHold(idx, (h) => ({
- ...h,
- status: e.target.value as LegalHoldStatus,
- }))
- }
- className="px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- >
- Aktiv
- Aufgehoben
- Abgelaufen
-
-
-
-
- updateLegalHold(idx, (h) => ({
- ...h,
- createdAt: e.target.value,
- }))
- }
- className="px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- />
-
-
- removeLegalHold(pid, idx)}
- className="text-red-500 hover:text-red-700 text-sm font-medium"
- >
- Entfernen
-
-
-
- ))}
-
-
-
- )}
-
-
addLegalHold(pid)}
- className="text-sm text-purple-600 hover:text-purple-800 font-medium"
- >
- + Legal Hold hinzufuegen
-
-
-
-
- {/* Sektion 3: Speicherorte & Loeschmethode */}
-
-
- 3. Speicherorte & Loeschmethode
-
-
- {policy.storageLocations.length > 0 && (
-
-
-
-
-
- Name
-
-
- Typ
-
-
- Backup
-
-
- Anbieter
-
-
- Loeschfaehig
-
-
- Aktion
-
-
-
-
- {policy.storageLocations.map((loc, idx) => (
-
-
-
- updateStorageLocation(idx, (s) => ({
- ...s,
- name: e.target.value,
- }))
- }
- placeholder="Name"
- className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- />
-
-
-
- updateStorageLocation(idx, (s) => ({
- ...s,
- type: e.target.value as StorageLocationType,
- }))
- }
- className="px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- >
- {Object.entries(STORAGE_LOCATION_LABELS).map(
- ([key, label]) => (
-
- {label}
-
- ),
- )}
-
-
-
-
- updateStorageLocation(idx, (s) => ({
- ...s,
- isBackup: e.target.checked,
- }))
- }
- className="text-purple-600 focus:ring-purple-500 rounded"
- />
-
-
-
- updateStorageLocation(idx, (s) => ({
- ...s,
- provider: e.target.value,
- }))
- }
- placeholder="Anbieter"
- className="w-full px-2 py-1 border border-gray-200 rounded text-sm focus:ring-1 focus:ring-purple-500"
- />
-
-
-
- updateStorageLocation(idx, (s) => ({
- ...s,
- deletionCapable: e.target.checked,
- }))
- }
- className="text-purple-600 focus:ring-purple-500 rounded"
- />
-
-
- removeStorageLocation(pid, idx)}
- className="text-red-500 hover:text-red-700 text-sm font-medium"
- >
- Entfernen
-
-
-
- ))}
-
-
-
- )}
-
-
addStorageLocation(pid)}
- className="text-sm text-purple-600 hover:text-purple-800 font-medium"
- >
- + Speicherort hinzufuegen
-
-
-
-
-
- Loeschmethode
-
-
- set('deletionMethod', e.target.value as DeletionMethodType)
- }
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- >
- {Object.entries(DELETION_METHOD_LABELS).map(([key, label]) => (
-
- {label}
-
- ))}
-
-
-
-
- Details zur Loeschmethode
-
-
-
-
-
- {/* Sektion 4: Verantwortlichkeit */}
-
-
- 4. Verantwortlichkeit
-
-
-
-
-
-
- Freigabeprozess
-
-
-
-
- {/* Sektion 5: VVT-Verknuepfung */}
-
-
- 5. VVT-Verknuepfung
-
-
- {vvtActivities.length > 0 ? (
-
-
- Verknuepfen Sie diese Loeschfrist mit einer
- Verarbeitungstaetigkeit aus Ihrem VVT.
-
-
- {policy.linkedVVTActivityIds && policy.linkedVVTActivityIds.length > 0 && (
-
-
- Verknuepfte Taetigkeiten:
-
-
- {policy.linkedVVTActivityIds.map((vvtId: string) => {
- const activity = vvtActivities.find(
- (a: any) => a.id === vvtId,
- )
- return (
-
- {activity?.name || vvtId}
-
- updatePolicy(pid, (p) => ({
- ...p,
- linkedVVTActivityIds: (
- p.linkedVVTActivityIds || []
- ).filter((id: string) => id !== vvtId),
- }))
- }
- className="text-blue-600 hover:text-blue-900"
- >
- x
-
-
- )
- })}
-
-
- )}
-
{
- const val = e.target.value
- if (
- val &&
- !(policy.linkedVVTActivityIds || []).includes(val)
- ) {
- updatePolicy(pid, (p) => ({
- ...p,
- linkedVVTActivityIds: [...(p.linkedVVTActivityIds || []), val],
- }))
- }
- 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-purple-500"
- >
-
- Verarbeitungstaetigkeit verknuepfen...
-
- {vvtActivities
- .filter(
- (a: any) =>
- !(policy.linkedVVTActivityIds || []).includes(a.id),
- )
- .map((a: any) => (
-
- {a.name || a.id}
-
- ))}
-
-
-
- ) : (
-
- Kein VVT gefunden. Erstellen Sie zuerst ein
- Verarbeitungsverzeichnis, um hier Verknuepfungen herstellen zu
- koennen.
-
- )}
-
-
- {/* Sektion 5b: Auftragsverarbeiter-Verknuepfung */}
-
-
- 5b. Verknuepfte Auftragsverarbeiter
-
-
- {vendorList.length > 0 ? (
-
-
- Verknuepfen Sie diese Loeschfrist mit relevanten Auftragsverarbeitern.
-
-
- {policy.linkedVendorIds && policy.linkedVendorIds.length > 0 && (
-
-
- Verknuepfte Auftragsverarbeiter:
-
-
- {policy.linkedVendorIds.map((vendorId: string) => {
- const vendor = vendorList.find(
- (v) => v.id === vendorId,
- )
- return (
-
- {vendor?.name || vendorId}
-
- updatePolicy(pid, (p) => ({
- ...p,
- linkedVendorIds: (
- p.linkedVendorIds || []
- ).filter((id: string) => id !== vendorId),
- }))
- }
- className="text-orange-600 hover:text-orange-900"
- >
- x
-
-
- )
- })}
-
-
- )}
-
{
- const val = e.target.value
- if (
- val &&
- !(policy.linkedVendorIds || []).includes(val)
- ) {
- updatePolicy(pid, (p) => ({
- ...p,
- linkedVendorIds: [...(p.linkedVendorIds || []), val],
- }))
- }
- 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-purple-500"
- >
-
- Auftragsverarbeiter verknuepfen...
-
- {vendorList
- .filter(
- (v) =>
- !(policy.linkedVendorIds || []).includes(v.id),
- )
- .map((v) => (
-
- {v.name || v.id}
-
- ))}
-
-
-
- ) : (
-
- Keine Auftragsverarbeiter gefunden. Erstellen Sie zuerst
- Auftragsverarbeiter im Vendor-Compliance-Modul, um hier Verknuepfungen
- herstellen zu koennen.
-
- )}
-
-
- {/* Sektion 6: Review-Einstellungen */}
-
-
- 6. Review-Einstellungen
-
-
-
-
-
- Status
-
-
- set('status', e.target.value as PolicyStatus)
- }
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- >
- {Object.entries(STATUS_LABELS).map(([key, label]) => (
-
- {label}
-
- ))}
-
-
-
-
- Pruefintervall
-
-
- set('reviewInterval', e.target.value as ReviewInterval)
- }
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- >
- {Object.entries(REVIEW_INTERVAL_LABELS).map(
- ([key, label]) => (
-
- {label}
-
- ),
- )}
-
-
-
-
-
-
-
-
- Tags
-
- set('tags', v)}
- placeholder="Tags hinzufuegen (Enter zum Bestaetigen)"
- />
-
-
-
- {/* Action buttons */}
-
-
{
- if (
- confirm(
- 'Moechten Sie diese Loeschfrist wirklich loeschen?',
- )
- ) {
- deletePolicy(pid)
- setTab('uebersicht')
- }
- }}
- className="text-red-600 hover:text-red-800 font-medium text-sm"
- >
- Loeschfrist loeschen
-
-
- {
- setEditingId(null)
- setTab('uebersicht')
- }}
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
- >
- Zurueck zur Uebersicht
-
-
- {saving ? 'Speichern...' : 'Speichern & Schliessen'}
-
-
-
-
- )
- }
-
- const renderEditor = () => {
- if (!editingId || !editingPolicy) {
- return renderEditorNoSelection()
- }
- return renderEditorForm(editingPolicy)
- }
-
- // ==========================================================================
- // TAB 3: Generator
- // ==========================================================================
-
- const renderGenerator = () => {
- const totalSteps = PROFILING_STEPS.length
- const progress = getProfilingProgress(profilingAnswers)
- const allComplete = PROFILING_STEPS.every((step, idx) =>
- isStepComplete(step, profilingAnswers.filter((a) => a.stepIndex === idx)),
- )
-
- // If we have generated policies, show the preview
- if (generatedPolicies.length > 0) {
- return (
-
-
-
- Generierte Loeschfristen
-
-
- Auf Basis Ihres Profils wurden {generatedPolicies.length}{' '}
- Loeschfristen generiert. Waehlen Sie die relevanten aus und
- uebernehmen Sie sie.
-
-
-
-
- setSelectedGenerated(
- new Set(generatedPolicies.map((p) => p.policyId)),
- )
- }
- className="text-sm text-purple-600 hover:text-purple-800 font-medium"
- >
- Alle auswaehlen
-
- setSelectedGenerated(new Set())}
- className="text-sm text-gray-500 hover:text-gray-700 font-medium"
- >
- Alle abwaehlen
-
-
-
-
- {generatedPolicies.map((gp) => {
- const selected = selectedGenerated.has(gp.policyId)
- return (
-
- {
- const next = new Set(selectedGenerated)
- if (e.target.checked) next.add(gp.policyId)
- else next.delete(gp.policyId)
- setSelectedGenerated(next)
- }}
- className="mt-1 text-purple-600 focus:ring-purple-500 rounded"
- />
-
-
-
- {gp.dataObjectName}
-
-
- {gp.policyId}
-
-
-
- {gp.description}
-
-
- {renderTriggerBadge(
- getEffectiveDeletionTrigger(gp),
- )}
-
- {formatRetentionDuration(gp)}
-
- {gp.retentionDriver && (
-
- {RETENTION_DRIVER_META[gp.retentionDriver]
- ?.label || gp.retentionDriver}
-
- )}
-
-
-
- )
- })}
-
-
-
-
-
{
- setGeneratedPolicies([])
- setSelectedGenerated(new Set())
- }}
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
- >
- Zurueck zum Profiling
-
-
- adoptGeneratedPolicies(false)}
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
- >
- Alle uebernehmen ({generatedPolicies.length})
-
- adoptGeneratedPolicies(true)}
- disabled={selectedGenerated.size === 0}
- className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Ausgewaehlte uebernehmen ({selectedGenerated.size})
-
-
-
-
- )
- }
-
- // Profiling wizard
- const currentStep: ProfilingStep | undefined = PROFILING_STEPS[profilingStep]
-
- return (
-
- {/* Progress bar */}
-
-
-
- Profiling-Assistent
-
-
- Schritt {profilingStep + 1} von {totalSteps}
-
-
-
-
- {PROFILING_STEPS.map((step, idx) => (
- setProfilingStep(idx)}
- className={`text-xs font-medium transition ${
- idx === profilingStep
- ? 'text-purple-600'
- : idx < profilingStep
- ? 'text-green-600'
- : 'text-gray-400'
- }`}
- >
- {step.title}
-
- ))}
-
-
-
- {/* Current step questions */}
- {currentStep && (
-
-
-
- {currentStep.title}
-
- {currentStep.description && (
-
- {currentStep.description}
-
- )}
-
-
- {currentStep.questions.map((question) => {
- const currentAnswer = profilingAnswers.find(
- (a) =>
- a.stepIndex === profilingStep &&
- a.questionId === question.id,
- )
-
- return (
-
-
- {question.label}
- {question.helpText && (
-
- {question.helpText}
-
- )}
-
-
- {/* Boolean */}
- {question.type === 'boolean' && (
-
- {[
- { val: true, label: 'Ja' },
- { val: false, label: 'Nein' },
- ].map((opt) => (
-
- handleProfilingAnswer(
- profilingStep,
- question.id,
- opt.val,
- )
- }
- className={`px-4 py-2 rounded-lg text-sm font-medium transition ${
- currentAnswer?.value === opt.val
- ? 'bg-purple-600 text-white'
- : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
- }`}
- >
- {opt.label}
-
- ))}
-
- )}
-
- {/* Single select */}
- {question.type === 'single' && question.options && (
-
- {question.options.map((opt) => (
-
-
- handleProfilingAnswer(
- profilingStep,
- question.id,
- opt.value,
- )
- }
- className="text-purple-600 focus:ring-purple-500"
- />
-
-
- {opt.label}
-
- {opt.description && (
-
- {opt.description}
-
- )}
-
-
- ))}
-
- )}
-
- {/* Multi select */}
- {question.type === 'multi' && question.options && (
-
- {question.options.map((opt) => {
- const selectedValues: string[] =
- currentAnswer?.value || []
- const isSelected = selectedValues.includes(opt.value)
- return (
-
- {
- let next: string[]
- if (e.target.checked) {
- next = [...selectedValues, opt.value]
- } else {
- next = selectedValues.filter(
- (v) => v !== opt.value,
- )
- }
- handleProfilingAnswer(
- profilingStep,
- question.id,
- next,
- )
- }}
- className="text-purple-600 focus:ring-purple-500 rounded"
- />
-
-
- {opt.label}
-
- {opt.description && (
-
- {opt.description}
-
- )}
-
-
- )
- })}
-
- )}
-
- {/* Number input */}
- {question.type === 'number' && (
-
- handleProfilingAnswer(
- profilingStep,
- question.id,
- e.target.value ? parseInt(e.target.value) : '',
- )
- }
- min={0}
- placeholder="Bitte Zahl eingeben"
- className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
- />
- )}
-
- )
- })}
-
- )}
-
- {/* Navigation */}
-
- setProfilingStep((s) => Math.max(0, s - 1))}
- disabled={profilingStep === 0}
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Zurueck
-
-
- {profilingStep < totalSteps - 1 ? (
-
- setProfilingStep((s) => Math.min(totalSteps - 1, s + 1))
- }
- className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 font-medium transition"
- >
- Weiter
-
- ) : (
-
- Loeschfristen generieren
-
- )}
-
-
- )
- }
-
- // ==========================================================================
- // TAB 4: Export & Compliance
- // ==========================================================================
-
- const renderExport = () => {
- const allLegalHolds = policies.flatMap((p) =>
- p.legalHolds.map((h) => ({
- ...h,
- policyId: p.policyId,
- policyName: p.dataObjectName,
- })),
- )
- const activeLegalHolds = allLegalHolds.filter(
- (h) => h.status === 'ACTIVE',
- )
-
- return (
-
- {/* Compliance Check */}
-
-
-
- Compliance-Check
-
-
- Analyse starten
-
-
-
- {policies.length === 0 && (
-
- Erstellen Sie zuerst Loeschfristen, um eine Compliance-Analyse
- durchzufuehren.
-
- )}
-
- {complianceResult && (
-
- {/* Score */}
-
-
= 75
- ? 'text-green-600'
- : complianceResult.score >= 50
- ? 'text-yellow-600'
- : 'text-red-600'
- }`}
- >
- {complianceResult.score}
-
-
-
- Compliance-Score
-
-
- {complianceResult.score >= 75
- ? 'Guter Zustand - wenige Optimierungen noetig'
- : complianceResult.score >= 50
- ? 'Verbesserungsbedarf - wichtige Punkte offen'
- : 'Kritisch - dringender Handlungsbedarf'}
-
-
-
-
- {/* Issues grouped by severity */}
- {(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const).map(
- (severity) => {
- const issues = complianceResult.issues.filter(
- (i) => i.severity === severity,
- )
- if (issues.length === 0) return null
-
- const severityConfig = {
- CRITICAL: {
- label: 'Kritisch',
- bg: 'bg-red-50',
- border: 'border-red-200',
- text: 'text-red-800',
- badge: 'bg-red-100 text-red-800',
- },
- HIGH: {
- label: 'Hoch',
- bg: 'bg-orange-50',
- border: 'border-orange-200',
- text: 'text-orange-800',
- badge: 'bg-orange-100 text-orange-800',
- },
- MEDIUM: {
- label: 'Mittel',
- bg: 'bg-yellow-50',
- border: 'border-yellow-200',
- text: 'text-yellow-800',
- badge: 'bg-yellow-100 text-yellow-800',
- },
- LOW: {
- label: 'Niedrig',
- bg: 'bg-blue-50',
- border: 'border-blue-200',
- text: 'text-blue-800',
- badge: 'bg-blue-100 text-blue-800',
- },
- }[severity]
-
- return (
-
-
-
- {severityConfig.label}
-
-
- {issues.length}{' '}
- {issues.length === 1 ? 'Problem' : 'Probleme'}
-
-
-
- {issues.map((issue, idx) => (
-
-
- {issue.title}
-
-
- {issue.description}
-
- {issue.recommendation && (
-
- Empfehlung: {issue.recommendation}
-
- )}
- {issue.affectedPolicyId && (
-
{
- setEditingId(issue.affectedPolicyId!)
- setTab('editor')
- }}
- className="text-xs text-purple-600 hover:text-purple-800 font-medium mt-1"
- >
- Zur Loeschfrist: {issue.affectedPolicyId}
-
- )}
-
- ))}
-
-
- )
- },
- )}
-
- {complianceResult.issues.length === 0 && (
-
-
- Keine Compliance-Probleme gefunden
-
-
- Alle Loeschfristen entsprechen den Anforderungen.
-
-
- )}
-
- )}
-
-
- {/* Legal Hold Management */}
-
-
- Legal Hold Verwaltung
-
-
- {allLegalHolds.length === 0 ? (
-
- Keine Legal Holds vorhanden.
-
- ) : (
-
-
-
- Gesamt: {' '}
-
- {allLegalHolds.length}
-
-
-
- Aktiv: {' '}
-
- {activeLegalHolds.length}
-
-
-
-
-
-
-
-
-
- Loeschfrist
-
-
- Bezeichnung
-
-
- Grund
-
-
- Status
-
-
- Erstellt
-
-
-
-
- {allLegalHolds.map((hold, idx) => (
-
-
- {
- setEditingId(hold.policyId)
- setTab('editor')
- }}
- className="text-purple-600 hover:text-purple-800 font-medium text-xs"
- >
- {hold.policyName || hold.policyId}
-
-
-
- {hold.name || '-'}
-
-
- {hold.reason || '-'}
-
-
-
- {hold.status === 'ACTIVE'
- ? 'Aktiv'
- : hold.status === 'RELEASED'
- ? 'Aufgehoben'
- : 'Abgelaufen'}
-
-
-
- {hold.createdAt || '-'}
-
-
- ))}
-
-
-
-
- )}
-
-
- {/* Export */}
-
-
- Datenexport
-
-
- Exportieren Sie Ihre Loeschfristen und den Compliance-Status in
- verschiedenen Formaten.
-
-
- {policies.length === 0 ? (
-
- Erstellen Sie zuerst Loeschfristen, um Exporte zu generieren.
-
- ) : (
-
-
- downloadFile(
- exportPoliciesAsJSON(policies),
- 'loeschfristen-export.json',
- 'application/json',
- )
- }
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
- >
- JSON Export
-
-
- downloadFile(
- exportPoliciesAsCSV(policies),
- 'loeschfristen-export.csv',
- 'text/csv;charset=utf-8',
- )
- }
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
- >
- CSV Export
-
-
- downloadFile(
- generateComplianceSummary(policies),
- 'compliance-bericht.md',
- 'text/markdown',
- )
- }
- className="bg-gray-100 text-gray-700 hover:bg-gray-200 rounded-lg px-4 py-2 font-medium transition"
- >
- Compliance-Bericht
-
-
- )}
-
-
- )
- }
-
- // ==========================================================================
- // Tab 5: Loeschkonzept Document
- // ==========================================================================
-
- function handleOrgHeaderChange(field: keyof LoeschkonzeptOrgHeader, value: string | string[]) {
+ const handleOrgHeaderChange = useCallback((field: keyof LoeschkonzeptOrgHeader, value: string | string[]) => {
const updated = { ...orgHeader, [field]: value }
setOrgHeader(updated)
localStorage.setItem('bp_loeschkonzept_orgheader', JSON.stringify(updated))
- }
+ }, [orgHeader])
- function handleAddRevision() {
+ const handleAddRevision = useCallback(() => {
const newRev: LoeschkonzeptRevision = {
version: orgHeader.loeschkonzeptVersion,
date: new Date().toISOString().split('T')[0],
@@ -2390,299 +342,23 @@ export default function LoeschfristenPage() {
const updated = [...revisions, newRev]
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
- }
+ }, [orgHeader, revisions])
- function handleUpdateRevision(index: number, field: keyof LoeschkonzeptRevision, value: string) {
+ const handleUpdateRevision = useCallback((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))
- }
+ }, [revisions])
- function handleRemoveRevision(index: number) {
+ const handleRemoveRevision = useCallback((index: number) => {
const updated = revisions.filter((_, i) => i !== index)
setRevisions(updated)
localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated))
- }
+ }, [revisions])
- 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.
-
-
-
-
-
- HTML herunterladen
-
-
-
- Als PDF drucken
-
-
-
-
- {activePolicies.length === 0 && (
-
- Keine aktiven Policies vorhanden. Erstellen Sie mindestens eine Policy, um das Loeschkonzept zu generieren.
-
- )}
-
-
- {/* Org Header Form */}
-
-
Organisationsdaten (Deckblatt)
-
-
- Organisation
- 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"
- />
-
-
- Branche
- 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"
- />
-
-
- Datenschutzbeauftragter
- 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"
- />
-
-
- DSB-Kontakt
- 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"
- />
-
-
- Verantwortlicher (Art. 4 Nr. 7)
- 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"
- />
-
-
- Mitarbeiter
- 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"
- />
-
-
- Version
- 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"
- />
-
-
- Pruefintervall
- handleOrgHeaderChange('reviewInterval', 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"
- >
- Vierteljaehrlich
- Halbjaehrlich
- Jaehrlich
-
-
-
- Letzte Pruefung
- 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"
- />
-
-
- Naechste Pruefung
- 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
-
- + Revision hinzufuegen
-
-
- {revisions.length === 0 ? (
-
- Noch keine Revisionen. Die Erstversion wird automatisch im Dokument eingefuegt.
-
- ) : (
-
- {revisions.map((rev, idx) => (
-
- ))}
-
- )}
-
-
- {/* 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 */}
-
-
12 Sektionen
-
-
1. Ziel und Zweck
-
7. Auftragsverarbeiter
-
2. Geltungsbereich
-
8. Legal Hold Verfahren
-
3. Grundprinzipien
-
9. Verantwortlichkeiten
-
4. Loeschregeln-Uebersicht
-
10. Pruef-/Revisionszyklus
-
5. Detaillierte Loeschregeln
-
11. Compliance-Status
-
6. VVT-Verknuepfung
-
12. Aenderungshistorie
-
-
-
- {/* Stats */}
-
- {activePolicies.length} Loeschregeln
- {policies.filter(p => p.linkedVVTActivityIds.length > 0).length} VVT-Verknuepfungen
- {policies.filter(p => p.linkedVendorIds.length > 0).length} Vendor-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
- // ==========================================================================
+ // --------------------------------------------------------------------------
+ // Render
+ // --------------------------------------------------------------------------
if (!loaded) {
return Lade Loeschfristen...
@@ -2710,11 +386,82 @@ export default function LoeschfristenPage() {
{/* Tab content */}
- {tab === 'uebersicht' && renderUebersicht()}
- {tab === 'editor' && renderEditor()}
- {tab === 'generator' && renderGenerator()}
- {tab === 'export' && renderExport()}
- {tab === 'loeschkonzept' && renderLoeschkonzept()}
+ {tab === 'uebersicht' && (
+