'use client' import React, { useState, useEffect, useCallback, useMemo } from 'react' import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader' import { LoeschfristPolicy, createEmptyLegalHold, createEmptyStorageLocation, isPolicyOverdue, getActiveLegalHolds, } from '@/lib/sdk/loeschfristen-types' import { PROFILING_STEPS, ProfilingAnswer, isStepComplete, getProfilingProgress, generatePoliciesFromProfile, } from '@/lib/sdk/loeschfristen-profiling' 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 // --------------------------------------------------------------------------- type Tab = 'uebersicht' | 'editor' | 'generator' | 'export' | 'loeschkonzept' 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' }, ] // --------------------------------------------------------------------------- // Main Page // --------------------------------------------------------------------------- export default function LoeschfristenPage() { // ---- 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([]) // -------------------------------------------------------------------------- // 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() }, []) 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(() => { try { const raw = localStorage.getItem('bp_vvt') if (raw) { const parsed = JSON.parse(raw) if (Array.isArray(parsed)) setVvtActivities(parsed) } } catch { /* ignore */ } }) }, [tab, editingId]) 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(() => {}) }, []) useEffect(() => { try { const raw = localStorage.getItem('bp_loeschkonzept_revisions') if (raw) { const parsed = JSON.parse(raw) if (Array.isArray(parsed)) setRevisions(parsed) } } catch { /* ignore */ } 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 } } } catch { /* ignore */ } 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 state // -------------------------------------------------------------------------- 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) { created.push(apiToPolicy(await res.json())) } else { created.push(p) } } 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(() => { setComplianceResult(runComplianceCheck(policies)) }, [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 updated = apiToPolicy(await res.json()) 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]) 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]) const handleAddRevision = useCallback(() => { const newRev: LoeschkonzeptRevision = { version: orgHeader.loeschkonzeptVersion, date: new Date().toISOString().split('T')[0], author: orgHeader.dpoName || orgHeader.responsiblePerson || '', changes: '', } const updated = [...revisions, newRev] setRevisions(updated) localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated)) }, [orgHeader, revisions]) 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]) const handleRemoveRevision = useCallback((index: number) => { const updated = revisions.filter((_, i) => i !== index) setRevisions(updated) localStorage.setItem('bp_loeschkonzept_revisions', JSON.stringify(updated)) }, [revisions]) // -------------------------------------------------------------------------- // Render // -------------------------------------------------------------------------- if (!loaded) { return
Lade Loeschfristen...
} return (
{/* Tab bar */}
{TAB_CONFIG.map((t) => ( ))}
{/* Tab content */} {tab === 'uebersicht' && ( setTab(t as Tab)} setEditingId={setEditingId} createNewPolicy={createNewPolicy} /> )} {tab === 'editor' && ( setTab(t as Tab)} updatePolicy={updatePolicy} createNewPolicy={createNewPolicy} deletePolicy={deletePolicy} addLegalHold={addLegalHold} removeLegalHold={removeLegalHold} addStorageLocation={addStorageLocation} removeStorageLocation={removeStorageLocation} handleSaveAndClose={handleSaveAndClose} /> )} {tab === 'generator' && ( )} {tab === 'export' && ( setTab(t as Tab)} /> )} {tab === 'loeschkonzept' && ( )}
) }