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>
461 lines
25 KiB
TypeScript
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>
|
|
)
|
|
}
|