Files
breakpilot-compliance/admin-compliance/app/sdk/loeschfristen/_components/EditorSections.tsx
Sharang Parnerkar 6c883fb12e refactor(admin): split loeschfristen + dsb-portal page.tsx into colocated components
Split two oversized page files into _components/ directories following
Next.js 15 conventions and the 500-LOC hard cap:

- loeschfristen/page.tsx (2322 LOC -> 412 LOC orchestrator + 6 components)
- dsb-portal/page.tsx (2068 LOC -> 135 LOC orchestrator + 9 components)

All component files stay under 500 lines. Build verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 18:51:16 +02:00

461 lines
25 KiB
TypeScript

'use client'
import React from 'react'
import {
LoeschfristPolicy, LegalHold, StorageLocation,
RETENTION_DRIVER_META, RetentionDriverType, DeletionMethodType,
DELETION_METHOD_LABELS, STATUS_LABELS,
STORAGE_LOCATION_LABELS, StorageLocationType, PolicyStatus,
ReviewInterval, DeletionTriggerLevel, RetentionUnit,
LegalHoldStatus, REVIEW_INTERVAL_LABELS,
} from '@/lib/sdk/loeschfristen-types'
import { TagInput } from './TagInput'
import { renderTriggerBadge } from './UebersichtTab'
// ---------------------------------------------------------------------------
// Shared type
// ---------------------------------------------------------------------------
export type SetFn = <K extends keyof LoeschfristPolicy>(key: K, val: LoeschfristPolicy[K]) => void
// ---------------------------------------------------------------------------
// Sektion 1: Datenobjekt
// ---------------------------------------------------------------------------
export function DataObjectSection({ policy, set }: { policy: LoeschfristPolicy; set: SetFn }) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">1. Datenobjekt</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name des Datenobjekts *</label>
<input type="text" value={policy.dataObjectName} onChange={(e) => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung</label>
<textarea value={policy.description} onChange={(e) => set('description', e.target.value)} rows={3}
placeholder="Beschreibung des Datenobjekts und seiner Verarbeitung..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Betroffene Personengruppen</label>
<TagInput value={policy.affectedGroups} onChange={(v) => set('affectedGroups', v)}
placeholder="z.B. Bewerber, Mitarbeiter... (Enter zum Hinzufuegen)" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Datenkategorien</label>
<TagInput value={policy.dataCategories} onChange={(v) => set('dataCategories', v)}
placeholder="z.B. Stammdaten, Kontaktdaten... (Enter zum Hinzufuegen)" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Primaerer Verarbeitungszweck</label>
<textarea value={policy.primaryPurpose} onChange={(e) => set('primaryPurpose', e.target.value)} rows={2}
placeholder="Welchem Zweck dient die Verarbeitung dieser Daten?"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Sektion 2: 3-stufige Loeschlogik
// ---------------------------------------------------------------------------
export function DeletionLogicSection({
policy, pid, set, updateLegalHoldItem, addLegalHold, removeLegalHold,
}: {
policy: LoeschfristPolicy; pid: string; set: SetFn
updateLegalHoldItem: (idx: number, updater: (h: LegalHold) => LegalHold) => void
addLegalHold: (policyId: string) => void
removeLegalHold: (policyId: string, idx: number) => void
}) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">2. 3-stufige Loeschlogik</h3>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Loeschausloeser (Trigger-Stufe)</label>
<div className="space-y-2">
{(['PURPOSE_END', 'RETENTION_DRIVER', 'LEGAL_HOLD'] as DeletionTriggerLevel[]).map((trigger) => (
<label key={trigger} className="flex items-start gap-3 p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer">
<input type="radio" name={`trigger-${pid}`} checked={policy.deletionTrigger === trigger}
onChange={() => set('deletionTrigger', trigger)} className="mt-0.5 text-purple-600 focus:ring-purple-500" />
<div>
<div className="flex items-center gap-2">{renderTriggerBadge(trigger)}</div>
<p className="text-xs text-gray-500 mt-1">
{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'}
</p>
</div>
</label>
))}
</div>
</div>
{policy.deletionTrigger === 'RETENTION_DRIVER' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungstreiber</label>
<select value={policy.retentionDriver}
onChange={(e) => {
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">
<option value="">Bitte waehlen...</option>
{Object.entries(RETENTION_DRIVER_META).map(([key, meta]) => (
<option key={key} value={key}>{meta.label}</option>
))}
</select>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsdauer</label>
<input type="number" min={0} value={policy.retentionDuration}
onChange={(e) => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Einheit</label>
<select value={policy.retentionUnit} onChange={(e) => 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">
<option value="DAYS">Tage</option>
<option value="MONTHS">Monate</option>
<option value="YEARS">Jahre</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung der Aufbewahrungspflicht</label>
<input type="text" value={policy.retentionDescription} onChange={(e) => 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Startereignis (Fristbeginn)</label>
<input type="text" value={policy.startEvent} onChange={(e) => 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" />
</div>
{/* Legal Holds */}
<div className="border-t border-gray-200 pt-4">
<div className="flex items-center justify-between mb-3">
<h4 className="text-sm font-semibold text-gray-800">Legal Holds</h4>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={policy.hasActiveLegalHold}
onChange={(e) => set('hasActiveLegalHold', e.target.checked)}
className="text-purple-600 focus:ring-purple-500 rounded" />
Aktiver Legal Hold
</label>
</div>
{policy.legalHolds.length > 0 && (
<div className="overflow-x-auto mb-3">
<table className="w-full text-sm border border-gray-200 rounded-lg">
<thead>
<tr className="bg-gray-50">
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Bezeichnung</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Grund</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Status</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Erstellt am</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Aktion</th>
</tr>
</thead>
<tbody>
{policy.legalHolds.map((hold, idx) => (
<tr key={idx} className="border-t border-gray-100">
<td className="px-3 py-2">
<input type="text" value={hold.name}
onChange={(e) => updateLegalHoldItem(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" />
</td>
<td className="px-3 py-2">
<input type="text" value={hold.reason}
onChange={(e) => updateLegalHoldItem(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" />
</td>
<td className="px-3 py-2">
<select value={hold.status}
onChange={(e) => updateLegalHoldItem(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">
<option value="ACTIVE">Aktiv</option>
<option value="RELEASED">Aufgehoben</option>
<option value="EXPIRED">Abgelaufen</option>
</select>
</td>
<td className="px-3 py-2">
<input type="date" value={hold.createdAt}
onChange={(e) => updateLegalHoldItem(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" />
</td>
<td className="px-3 py-2">
<button onClick={() => removeLegalHold(pid, idx)}
className="text-red-500 hover:text-red-700 text-sm font-medium">Entfernen</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<button onClick={() => addLegalHold(pid)} className="text-sm text-purple-600 hover:text-purple-800 font-medium">
+ Legal Hold hinzufuegen
</button>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Sektion 3: Speicherorte & Loeschmethode
// ---------------------------------------------------------------------------
export function StorageSection({
policy, pid, set, updateStorageLocationItem, addStorageLocation, removeStorageLocation,
}: {
policy: LoeschfristPolicy; pid: string; set: SetFn
updateStorageLocationItem: (idx: number, updater: (s: StorageLocation) => StorageLocation) => void
addStorageLocation: (policyId: string) => void
removeStorageLocation: (policyId: string, idx: number) => void
}) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">3. Speicherorte & Loeschmethode</h3>
{policy.storageLocations.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full text-sm border border-gray-200 rounded-lg">
<thead>
<tr className="bg-gray-50">
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Name</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Typ</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Backup</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Anbieter</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Loeschfaehig</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Aktion</th>
</tr>
</thead>
<tbody>
{policy.storageLocations.map((loc, idx) => (
<tr key={idx} className="border-t border-gray-100">
<td className="px-3 py-2">
<input type="text" value={loc.name}
onChange={(e) => updateStorageLocationItem(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" />
</td>
<td className="px-3 py-2">
<select value={loc.type}
onChange={(e) => updateStorageLocationItem(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]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</td>
<td className="px-3 py-2 text-center">
<input type="checkbox" checked={loc.isBackup}
onChange={(e) => updateStorageLocationItem(idx, (s) => ({ ...s, isBackup: e.target.checked }))}
className="text-purple-600 focus:ring-purple-500 rounded" />
</td>
<td className="px-3 py-2">
<input type="text" value={loc.provider}
onChange={(e) => updateStorageLocationItem(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" />
</td>
<td className="px-3 py-2 text-center">
<input type="checkbox" checked={loc.deletionCapable}
onChange={(e) => updateStorageLocationItem(idx, (s) => ({ ...s, deletionCapable: e.target.checked }))}
className="text-purple-600 focus:ring-purple-500 rounded" />
</td>
<td className="px-3 py-2">
<button onClick={() => removeStorageLocation(pid, idx)}
className="text-red-500 hover:text-red-700 text-sm font-medium">Entfernen</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
<button onClick={() => addStorageLocation(pid)} className="text-sm text-purple-600 hover:text-purple-800 font-medium">
+ Speicherort hinzufuegen
</button>
<div className="border-t border-gray-200 pt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Loeschmethode</label>
<select value={policy.deletionMethod} onChange={(e) => 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]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Details zur Loeschmethode</label>
<textarea value={policy.deletionMethodDetail} onChange={(e) => set('deletionMethodDetail', e.target.value)} rows={2}
placeholder="Weitere Details zum Loeschverfahren..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
</div>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Sektion 4: Verantwortlichkeit
// ---------------------------------------------------------------------------
export function ResponsibilitySection({ policy, set }: { policy: LoeschfristPolicy; set: SetFn }) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">4. Verantwortlichkeit</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortliche Rolle</label>
<input type="text" value={policy.responsibleRole} onChange={(e) => set('responsibleRole', e.target.value)}
placeholder="z.B. Datenschutzbeauftragter"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verantwortliche Person</label>
<input type="text" value={policy.responsiblePerson} onChange={(e) => set('responsiblePerson', e.target.value)}
placeholder="Name der verantwortlichen Person"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Freigabeprozess</label>
<textarea value={policy.releaseProcess} onChange={(e) => set('releaseProcess', e.target.value)} rows={3}
placeholder="Beschreibung des Freigabeprozesses fuer Loeschungen..."
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500" />
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Sektion 5: VVT-Verknuepfung
// ---------------------------------------------------------------------------
export function VVTLinkSection({
policy, pid, vvtActivities, updatePolicy,
}: {
policy: LoeschfristPolicy; pid: string; vvtActivities: any[]
updatePolicy: (id: string, updater: (p: LoeschfristPolicy) => LoeschfristPolicy) => void
}) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">5. VVT-Verknuepfung</h3>
{vvtActivities.length > 0 ? (
<div>
<p className="text-sm text-gray-500 mb-3">
Verknuepfen Sie diese Loeschfrist mit einer Verarbeitungstaetigkeit aus Ihrem VVT.
</p>
<div className="space-y-2">
{policy.linkedVvtIds && policy.linkedVvtIds.length > 0 && (
<div className="mb-3">
<label className="block text-xs font-medium text-gray-500 mb-1">Verknuepfte Taetigkeiten:</label>
<div className="flex flex-wrap gap-1">
{policy.linkedVvtIds.map((vvtId: string) => {
const activity = vvtActivities.find((a: any) => a.id === vvtId)
return (
<span key={vvtId} className="inline-flex items-center gap-1 bg-blue-100 text-blue-800 text-xs font-medium px-2 py-0.5 rounded-full">
{activity?.name || vvtId}
<button type="button"
onClick={() => updatePolicy(pid, (p) => ({
...p, linkedVvtIds: (p.linkedVvtIds || []).filter((id: string) => id !== vvtId),
}))}
className="text-blue-600 hover:text-blue-900">x</button>
</span>
)
})}
</div>
</div>
)}
<select
onChange={(e) => {
const val = e.target.value
if (val && !(policy.linkedVvtIds || []).includes(val)) {
updatePolicy(pid, (p) => ({ ...p, linkedVvtIds: [...(p.linkedVvtIds || []), 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">
<option value="">Verarbeitungstaetigkeit verknuepfen...</option>
{vvtActivities
.filter((a: any) => !(policy.linkedVvtIds || []).includes(a.id))
.map((a: any) => (<option key={a.id} value={a.id}>{a.name || a.id}</option>))}
</select>
</div>
</div>
) : (
<p className="text-sm text-gray-400">
Kein VVT gefunden. Erstellen Sie zuerst ein Verarbeitungsverzeichnis, um hier Verknuepfungen herstellen zu koennen.
</p>
)}
</div>
)
}
// ---------------------------------------------------------------------------
// Sektion 6: Review-Einstellungen
// ---------------------------------------------------------------------------
export function ReviewSection({ policy, set }: { policy: LoeschfristPolicy; set: SetFn }) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<h3 className="text-lg font-semibold text-gray-900">6. Review-Einstellungen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Status</label>
<select value={policy.status} onChange={(e) => 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]) => (<option key={key} value={key}>{label}</option>))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Pruefintervall</label>
<select value={policy.reviewInterval} onChange={(e) => 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]) => (<option key={key} value={key}>{label}</option>))}
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Letzte Pruefung</label>
<input type="date" value={policy.lastReviewDate} onChange={(e) => set('lastReviewDate', 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" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Naechste Pruefung</label>
<input type="date" value={policy.nextReviewDate} onChange={(e) => set('nextReviewDate', 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" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Tags</label>
<TagInput value={policy.tags} onChange={(v) => set('tags', v)} placeholder="Tags hinzufuegen (Enter zum Bestaetigen)" />
</div>
</div>
)
}