'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, LegalHold, StorageLocation, RETENTION_DRIVER_META, RetentionDriverType, DeletionMethodType, DELETION_METHOD_LABELS, STATUS_LABELS, STATUS_COLORS, TRIGGER_LABELS, TRIGGER_COLORS, REVIEW_INTERVAL_LABELS, STORAGE_LOCATION_LABELS, StorageLocationType, PolicyStatus, ReviewInterval, DeletionTriggerLevel, RetentionUnit, LegalHoldStatus, createEmptyPolicy, createEmptyLegalHold, createEmptyStorageLocation, formatRetentionDuration, isPolicyOverdue, getActiveLegalHolds, getEffectiveDeletionTrigger, } from '@/lib/sdk/loeschfristen-types' import { PROFILING_STEPS, ProfilingAnswer, ProfilingStep, 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 { buildLoeschkonzeptHtml, type LoeschkonzeptOrgHeader, type LoeschkonzeptRevision, createDefaultLoeschkonzeptOrgHeader, } from '@/lib/sdk/loeschfristen-document' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' | 'loeschkonzept' // --------------------------------------------------------------------------- // Helper: TagInput // --------------------------------------------------------------------------- function TagInput({ value, onChange, placeholder, }: { value: string[] onChange: (v: string[]) => void placeholder?: string }) { const [input, setInput] = useState('') const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ',') { e.preventDefault() const trimmed = input.trim().replace(/,+$/, '').trim() if (trimmed && !value.includes(trimmed)) { onChange([...value, trimmed]) } setInput('') } } const remove = (idx: number) => { onChange(value.filter((_, i) => i !== idx)) } return (
{value.map((tag, idx) => ( {tag} ))}
setInput(e.target.value)} onKeyDown={handleKeyDown} placeholder={placeholder ?? 'Eingabe + Enter'} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm" />
) } // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- export default function LoeschfristenPage() { const router = useRouter() // ---- Core state ---- const [tab, setTab] = useState('uebersicht') const [policies, setPolicies] = useState([]) const [loaded, setLoaded] = useState(false) const [editingId, setEditingId] = useState(null) const [filter, setFilter] = useState('all') const [searchQuery, setSearchQuery] = useState('') const [driverFilter, setDriverFilter] = useState('all') // ---- Generator state ---- const [profilingStep, setProfilingStep] = useState(0) const [profilingAnswers, setProfilingAnswers] = useState([]) const [generatedPolicies, setGeneratedPolicies] = useState([]) const [selectedGenerated, setSelectedGenerated] = useState>(new Set()) // ---- Compliance state ---- const [complianceResult, setComplianceResult] = useState(null) // ---- Saving state ---- const [saving, setSaving] = useState(false) // ---- VVT data ---- const [vvtActivities, setVvtActivities] = useState([]) // ---- Vendor data ---- const [vendorList, setVendorList] = useState>([]) // ---- Loeschkonzept document state ---- const [orgHeader, setOrgHeader] = useState(createDefaultLoeschkonzeptOrgHeader()) const [revisions, setRevisions] = useState([]) // -------------------------------------------------------------------------- // Persistence (API-backed) // -------------------------------------------------------------------------- const LOESCHFRISTEN_API = '/api/sdk/v1/compliance/loeschfristen' useEffect(() => { 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) .then(data => { if (data && Array.isArray(data.activities)) { setVvtActivities(data.activities) } }) .catch(() => { // fallback: try localStorage try { const raw = localStorage.getItem('bp_vvt') if (raw) { const parsed = JSON.parse(raw) if (Array.isArray(parsed)) setVvtActivities(parsed) } } catch { /* ignore */ } }) }, [tab, editingId]) // Load vendor list from API useEffect(() => { fetch('/api/sdk/v1/vendor-compliance/vendors?limit=500') .then(r => r.ok ? r.json() : null) .then(data => { const items = data?.data?.items || [] setVendorList(items.map((v: any) => ({ id: v.id, name: v.name }))) }) .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) { 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 // -------------------------------------------------------------------------- const editingPolicy = useMemo( () => policies.find((p) => p.policyId === editingId) ?? null, [policies, editingId], ) const filteredPolicies = useMemo(() => { let result = [...policies] if (searchQuery.trim()) { const q = searchQuery.toLowerCase() result = result.filter( (p) => p.dataObjectName.toLowerCase().includes(q) || p.policyId.toLowerCase().includes(q) || p.description.toLowerCase().includes(q), ) } if (filter === 'active') result = result.filter((p) => p.status === 'ACTIVE') else if (filter === 'draft') result = result.filter((p) => p.status === 'DRAFT') else if (filter === 'review') result = result.filter((p) => isPolicyOverdue(p)) if (driverFilter !== 'all') result = result.filter((p) => p.retentionDriver === driverFilter) return result }, [policies, searchQuery, filter, driverFilter]) const stats = useMemo(() => { const total = policies.length const active = policies.filter((p) => p.status === 'ACTIVE').length const draft = policies.filter((p) => p.status === 'DRAFT').length const overdue = policies.filter((p) => isPolicyOverdue(p)).length const legalHolds = policies.reduce( (acc, p) => acc + getActiveLegalHolds(p).length, 0, ) return { total, active, draft, overdue, legalHolds } }, [policies]) // -------------------------------------------------------------------------- // Handlers // -------------------------------------------------------------------------- const updatePolicy = useCallback( (id: string, updater: (p: LoeschfristPolicy) => LoeschfristPolicy) => { setPolicies((prev) => prev.map((p) => (p.policyId === id ? updater(p) : p)), ) }, [], ) const createNewPolicy = useCallback(async () => { setSaving(true) try { const empty = createEmptyPolicy() const res = await fetch(LOESCHFRISTEN_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(policyToPayload(empty)), }) if (res.ok) { const raw = await res.json() const newP = apiToPolicy(raw) setPolicies((prev) => [...prev, newP]) setEditingId(newP.policyId) setTab('editor') } } catch (e) { console.error('Failed to create policy:', e) } finally { setSaving(false) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const deletePolicy = useCallback( async (policyId: string) => { const policy = policies.find((p) => p.policyId === policyId) if (!policy) return try { const res = await fetch(`${LOESCHFRISTEN_API}/${policy.id}`, { method: 'DELETE' }) if (res.ok || res.status === 204 || res.status === 404) { setPolicies((prev) => prev.filter((p) => p.policyId !== policyId)) if (editingId === policyId) setEditingId(null) } } catch (e) { console.error('Failed to delete policy:', e) } }, [editingId, policies], ) const addLegalHold = useCallback( (policyId: string) => { updatePolicy(policyId, (p) => ({ ...p, legalHolds: [...p.legalHolds, createEmptyLegalHold()], })) }, [updatePolicy], ) const removeLegalHold = useCallback( (policyId: string, idx: number) => { updatePolicy(policyId, (p) => ({ ...p, legalHolds: p.legalHolds.filter((_, i) => i !== idx), })) }, [updatePolicy], ) const addStorageLocation = useCallback( (policyId: string) => { updatePolicy(policyId, (p) => ({ ...p, storageLocations: [...p.storageLocations, createEmptyStorageLocation()], })) }, [updatePolicy], ) const removeStorageLocation = useCallback( (policyId: string, idx: number) => { updatePolicy(policyId, (p) => ({ ...p, storageLocations: p.storageLocations.filter((_, i) => i !== idx), })) }, [updatePolicy], ) const handleProfilingAnswer = useCallback( (stepIndex: number, questionId: string, value: any) => { setProfilingAnswers((prev) => { const existing = prev.findIndex( (a) => a.stepIndex === stepIndex && a.questionId === questionId, ) const answer: ProfilingAnswer = { stepIndex, questionId, value } if (existing >= 0) { const copy = [...prev] copy[existing] = answer return copy } return [...prev, answer] }) }, [], ) const handleGenerate = useCallback(() => { const generated = generatePoliciesFromProfile(profilingAnswers) setGeneratedPolicies(generated) setSelectedGenerated(new Set(generated.map((p) => p.policyId))) }, [profilingAnswers]) const adoptGeneratedPolicies = useCallback( async (onlySelected: boolean) => { const toAdopt = onlySelected ? generatedPolicies.filter((p) => selectedGenerated.has(p.policyId)) : generatedPolicies setSaving(true) try { const created: LoeschfristPolicy[] = [] for (const p of toAdopt) { try { const res = await fetch(LOESCHFRISTEN_API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(policyToPayload(p)), }) if (res.ok) { const raw = await res.json() created.push(apiToPolicy(raw)) } else { created.push(p) // fallback: keep generated policy in state } } catch { created.push(p) } } setPolicies((prev) => [...prev, ...created]) } finally { setSaving(false) } setGeneratedPolicies([]) setSelectedGenerated(new Set()) setProfilingStep(0) setProfilingAnswers([]) setTab('uebersicht') }, // eslint-disable-next-line react-hooks/exhaustive-deps [generatedPolicies, selectedGenerated], ) const runCompliance = useCallback(() => { const result = runComplianceCheck(policies) setComplianceResult(result) }, [policies]) const handleSaveAndClose = useCallback(async () => { if (!editingPolicy) { setEditingId(null) setTab('uebersicht') return } setSaving(true) try { const res = await fetch(`${LOESCHFRISTEN_API}/${editingPolicy.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(policyToPayload(editingPolicy)), }) if (res.ok) { const raw = await res.json() const updated = apiToPolicy(raw) setPolicies((prev) => prev.map((p) => (p.policyId === editingPolicy.policyId ? updated : p)), ) } } catch (e) { console.error('Failed to save policy:', e) } finally { setSaving(false) } setEditingId(null) setTab('uebersicht') // eslint-disable-next-line react-hooks/exhaustive-deps }, [editingPolicy]) // -------------------------------------------------------------------------- // Tab definitions // -------------------------------------------------------------------------- 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" />