diff --git a/admin-compliance/app/sdk/loeschfristen/_components/EditorSections.tsx b/admin-compliance/app/sdk/loeschfristen/_components/EditorSections.tsx index 77a53e7..7f2266b 100644 --- a/admin-compliance/app/sdk/loeschfristen/_components/EditorSections.tsx +++ b/admin-compliance/app/sdk/loeschfristen/_components/EditorSections.tsx @@ -370,18 +370,18 @@ export function VVTLinkSection({ Verknuepfen Sie diese Loeschfrist mit einer Verarbeitungstaetigkeit aus Ihrem VVT.

- {policy.linkedVvtIds && policy.linkedVvtIds.length > 0 && ( + {policy.linkedVVTActivityIds && policy.linkedVVTActivityIds.length > 0 && (
- {policy.linkedVvtIds.map((vvtId: string) => { + {policy.linkedVVTActivityIds.map((vvtId: string) => { const activity = vvtActivities.find((a: any) => a.id === vvtId) return ( {activity?.name || vvtId} @@ -393,15 +393,15 @@ export function VVTLinkSection({
@@ -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 && ( +
+ +
+ {policy.linkedVendorIds.map((vendorId: string) => { + const vendor = vendorList.find((v) => v.id === vendorId) + return ( + + {vendor?.name || vendorId} + + + ) + })} +
+
+ )} + +
+
+ ) : ( +

+ 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. +

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

Organisationsdaten (Deckblatt)

+
+
+ + 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" /> +
+
+ + 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" /> +
+
+ + 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" /> +
+
+ + 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" /> +
+
+ + 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" /> +
+
+ + 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" /> +
+
+ + 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" /> +
+
+ + +
+
+ + 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" /> +
+
+ + 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

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

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

+ ) : ( +
+ {revisions.map((rev, idx) => ( +
+ onUpdateRevision(idx, 'version', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" placeholder="1.1" /> + onUpdateRevision(idx, 'date', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" /> + onUpdateRevision(idx, 'author', e.target.value)} + className="rounded-lg border border-gray-300 px-2 py-1.5 text-xs" placeholder="Autor" /> + onUpdateRevision(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

+
+
+
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) => ( - - ))} - - Aufbewahrungstreiber: - - -
-
- - {/* 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. -

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

- )} - -
- ) - })} -
- )} - - {/* Floating action button */} - {policies.length > 0 && ( -
- -
- )} -
- ) - - // ========================================================================== - // TAB 2: Editor - // ========================================================================== - - const renderEditorNoSelection = () => ( -
-

- Loeschfrist zum Bearbeiten waehlen -

- {policies.length === 0 ? ( -

- Noch keine Loeschfristen vorhanden.{' '} - -

- ) : ( -
- {policies.map((p) => ( - - ))} - -
- )} -
- ) - - 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 */} -
-
- -

- {policy.dataObjectName || 'Neue Loeschfrist'} -

- - {policy.policyId} - -
- {renderStatusBadge(policy.status)} -
- - {/* Sektion 1: Datenobjekt */} -
-

- 1. Datenobjekt -

- -
- - 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" - /> -
- -
- -