EditorSections.tsx (524 LOC) split into EditorSections.tsx (267 LOC) and EditorSectionsB.tsx (279 LOC). DeletionLogicSection and StorageSection moved to B; SetFn type canonical in B. EditorSections re-exports both so all existing imports from EditorTab.tsx remain valid unchanged. SDKPipelineSidebar (193), SourcesTab (311), ScopeDecisionTab (127), ComplianceAdvisorWidget (265) were already under the 500-LOC hard cap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
268 lines
14 KiB
TypeScript
268 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import {
|
|
LoeschfristPolicy,
|
|
STATUS_LABELS,
|
|
PolicyStatus,
|
|
ReviewInterval, REVIEW_INTERVAL_LABELS,
|
|
} from '@/lib/sdk/loeschfristen-types'
|
|
import { TagInput } from './TagInput'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared type (defined in EditorSectionsB to avoid circular imports)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type { SetFn } from './EditorSectionsB'
|
|
import type { SetFn } from './EditorSectionsB'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Re-exports from EditorSectionsB (keeps existing import paths working)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export { DeletionLogicSection, StorageSection } from './EditorSectionsB'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 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.linkedVVTActivityIds && policy.linkedVVTActivityIds.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.linkedVVTActivityIds.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, linkedVVTActivityIds: (p.linkedVVTActivityIds || []).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.linkedVVTActivityIds || []).includes(val)) {
|
|
updatePolicy(pid, (p) => ({ ...p, linkedVVTActivityIds: [...(p.linkedVVTActivityIds || []), 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.linkedVVTActivityIds || []).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 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 (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">5b. Verknuepfte Auftragsverarbeiter</h3>
|
|
{vendorList.length > 0 ? (
|
|
<div>
|
|
<p className="text-sm text-gray-500 mb-3">
|
|
Verknuepfen Sie diese Loeschfrist mit relevanten Auftragsverarbeitern.
|
|
</p>
|
|
<div className="space-y-2">
|
|
{policy.linkedVendorIds && policy.linkedVendorIds.length > 0 && (
|
|
<div className="mb-3">
|
|
<label className="block text-xs font-medium text-gray-500 mb-1">Verknuepfte Auftragsverarbeiter:</label>
|
|
<div className="flex flex-wrap gap-1">
|
|
{policy.linkedVendorIds.map((vendorId: string) => {
|
|
const vendor = vendorList.find((v) => v.id === vendorId)
|
|
return (
|
|
<span key={vendorId} className="inline-flex items-center gap-1 bg-orange-100 text-orange-800 text-xs font-medium px-2 py-0.5 rounded-full">
|
|
{vendor?.name || vendorId}
|
|
<button type="button"
|
|
onClick={() => updatePolicy(pid, (p) => ({
|
|
...p, linkedVendorIds: (p.linkedVendorIds || []).filter((id: string) => id !== vendorId),
|
|
}))}
|
|
className="text-orange-600 hover:text-orange-900">x</button>
|
|
</span>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<select
|
|
onChange={(e) => {
|
|
const val = e.target.value
|
|
if (val && !(policy.linkedVendorIds || []).includes(val)) {
|
|
updatePolicy(pid, (p) => ({ ...p, linkedVendorIds: [...(p.linkedVendorIds || []), 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="">Auftragsverarbeiter verknuepfen...</option>
|
|
{vendorList
|
|
.filter((v) => !(policy.linkedVendorIds || []).includes(v.id))
|
|
.map((v) => (<option key={v.id} value={v.id}>{v.name || v.id}</option>))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-400">
|
|
Keine Auftragsverarbeiter gefunden. Erstellen Sie zuerst Auftragsverarbeiter im Vendor-Compliance-Modul, 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>
|
|
)
|
|
}
|