This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/components/sdk/tom-dashboard/TOMEditorTab.tsx
BreakPilot Dev 870302a82b feat(admin-v2): Major SDK/Compliance overhaul and new modules
SDK modules added/enhanced:
- compliance-hub, compliance-scope, consent-management, notfallplan
- audit-report, workflow, source-policy, dsms
- advisory-board documentation section
- TOM dashboard components, TOM generator SDM mapping
- DSFA: mitigation library, risk catalog, threshold analysis, source attribution
- VVT: baseline catalog, profiling engine, types
- Loeschfristen: baseline catalog, compliance engine, export, profiling, types
- Compliance scope: engine, profiling, golden tests, types

Existing SDK pages updated:
- dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality
- SDKSidebar, StepHeader — new navigation items and layout
- SDK layout, context, types — expanded type system

Other admin-v2 changes:
- AI agents page, RAG pipeline DSFA integration
- GridOverlay component updates
- Companion feature (development + education)
- Compliance advisor SOUL definition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 00:01:04 +01:00

377 lines
16 KiB
TypeScript

'use client'
import { useMemo, useState, useEffect } from 'react'
import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
interface TOMEditorTabProps {
state: TOMGeneratorState
selectedTOMId: string | null
onUpdateTOM: (tomId: string, updates: Partial<DerivedTOM>) => void
onBack: () => void
}
const STATUS_OPTIONS: { value: DerivedTOM['implementationStatus']; label: string; className: string }[] = [
{ value: 'IMPLEMENTED', label: 'Implementiert', className: 'border-green-300 bg-green-50 text-green-700' },
{ value: 'PARTIAL', label: 'Teilweise implementiert', className: 'border-yellow-300 bg-yellow-50 text-yellow-700' },
{ value: 'NOT_IMPLEMENTED', label: 'Nicht implementiert', className: 'border-red-300 bg-red-50 text-red-700' },
]
const TYPE_BADGES: Record<string, { label: string; className: string }> = {
TECHNICAL: { label: 'Technisch', className: 'bg-blue-100 text-blue-700' },
ORGANIZATIONAL: { label: 'Organisatorisch', className: 'bg-indigo-100 text-indigo-700' },
}
interface VVTActivity {
id: string
name?: string
title?: string
structuredToms?: { category?: string }[]
}
export function TOMEditorTab({ state, selectedTOMId, onUpdateTOM, onBack }: TOMEditorTabProps) {
const tom = useMemo(() => {
if (!selectedTOMId) return null
return state.derivedTOMs.find(t => t.id === selectedTOMId) || null
}, [state.derivedTOMs, selectedTOMId])
const control = useMemo(() => {
if (!tom) return null
return getControlById(tom.controlId)
}, [tom])
const [implementationStatus, setImplementationStatus] = useState<DerivedTOM['implementationStatus']>('NOT_IMPLEMENTED')
const [responsiblePerson, setResponsiblePerson] = useState('')
const [implementationDate, setImplementationDate] = useState('')
const [notes, setNotes] = useState('')
const [linkedEvidence, setLinkedEvidence] = useState<string[]>([])
const [selectedEvidenceId, setSelectedEvidenceId] = useState('')
useEffect(() => {
if (tom) {
setImplementationStatus(tom.implementationStatus)
setResponsiblePerson(tom.responsiblePerson || '')
setImplementationDate(tom.implementationDate ? new Date(tom.implementationDate).toISOString().slice(0, 10) : '')
setNotes(tom.aiGeneratedDescription || '')
setLinkedEvidence(tom.linkedEvidence || [])
}
}, [tom])
const vvtActivities = useMemo(() => {
if (!control) return []
try {
const raw = localStorage.getItem('bp_vvt')
if (!raw) return []
const activities: VVTActivity[] = JSON.parse(raw)
return activities.filter(a =>
a.structuredToms?.some(t => t.category === control.category)
)
} catch {
return []
}
}, [control])
const availableDocuments = useMemo(() => {
return (state.documents || []).filter(
doc => !linkedEvidence.includes(doc.id)
)
}, [state.documents, linkedEvidence])
const linkedDocuments = useMemo(() => {
return linkedEvidence
.map(id => (state.documents || []).find(d => d.id === id))
.filter(Boolean)
}, [state.documents, linkedEvidence])
const evidenceGaps = useMemo(() => {
if (!control?.evidenceRequirements) return []
return control.evidenceRequirements.map(req => {
const hasMatch = (state.documents || []).some(doc =>
linkedEvidence.includes(doc.id) &&
(doc.filename?.toLowerCase().includes(req.toLowerCase()) ||
doc.documentType?.toLowerCase().includes(req.toLowerCase()))
)
return { requirement: req, fulfilled: hasMatch }
})
}, [control, state.documents, linkedEvidence])
const handleSave = () => {
if (!tom) return
onUpdateTOM(tom.id, {
implementationStatus,
responsiblePerson: responsiblePerson || null,
implementationDate: implementationDate ? new Date(implementationDate) : null,
aiGeneratedDescription: notes || null,
linkedEvidence,
})
}
const handleAddEvidence = () => {
if (!selectedEvidenceId) return
setLinkedEvidence(prev => [...prev, selectedEvidenceId])
setSelectedEvidenceId('')
}
const handleRemoveEvidence = (docId: string) => {
setLinkedEvidence(prev => prev.filter(id => id !== docId))
}
if (!selectedTOMId || !tom) {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="text-gray-400 mb-4">
<svg className="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-700 mb-2">Keine TOM ausgewaehlt</h3>
<p className="text-gray-500">Waehlen Sie eine TOM aus der Uebersicht, um sie zu bearbeiten.</p>
</div>
)
}
const typeBadge = TYPE_BADGES[control?.type || 'TECHNICAL'] || TYPE_BADGES.TECHNICAL
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<button
onClick={onBack}
className="text-sm text-purple-600 hover:text-purple-800 font-medium flex items-center gap-1"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
<button
onClick={handleSave}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
>
Aenderungen speichern
</button>
</div>
{/* TOM Header Card */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start gap-3 mb-3">
<span className="text-xs font-mono bg-gray-100 text-gray-600 px-2 py-1 rounded">{control?.code || tom.controlId}</span>
<span className={`text-xs px-2 py-1 rounded-full font-medium ${typeBadge.className}`}>
{typeBadge.label}
</span>
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded-full font-medium">
{control?.category || 'Unbekannt'}
</span>
</div>
<h2 className="text-xl font-bold text-gray-900 mb-2">{control?.name?.de || tom.controlId}</h2>
{control?.description?.de && (
<p className="text-sm text-gray-600 leading-relaxed">{control.description.de}</p>
)}
</div>
{/* Implementation Status */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">Implementierungsstatus</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{STATUS_OPTIONS.map(opt => (
<label
key={opt.value}
className={`flex items-center gap-3 border rounded-lg p-3 cursor-pointer transition-all ${
implementationStatus === opt.value
? opt.className + ' ring-2 ring-offset-1 ring-current'
: 'border-gray-200 bg-white text-gray-600 hover:bg-gray-50'
}`}
>
<input
type="radio"
name="implementationStatus"
value={opt.value}
checked={implementationStatus === opt.value}
onChange={() => setImplementationStatus(opt.value)}
className="sr-only"
/>
<div className={`w-4 h-4 rounded-full border-2 flex items-center justify-center ${
implementationStatus === opt.value ? 'border-current' : 'border-gray-300'
}`}>
{implementationStatus === opt.value && (
<div className="w-2 h-2 rounded-full bg-current" />
)}
</div>
<span className="text-sm font-medium">{opt.label}</span>
</label>
))}
</div>
</div>
{/* Responsible Person */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">Verantwortliche Person</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Umgesetzt von</label>
<input
type="text"
value={responsiblePerson}
onChange={e => setResponsiblePerson(e.target.value)}
placeholder="Name der verantwortlichen Person"
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-600 mb-1">Umsetzungsdatum</label>
<input
type="date"
value={implementationDate}
onChange={e => setImplementationDate(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
/>
</div>
</div>
</div>
{/* Notes */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">Anmerkungen</h3>
<textarea
value={notes}
onChange={e => setNotes(e.target.value)}
rows={4}
placeholder="Anmerkungen zur Umsetzung, Besonderheiten, etc."
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-y"
/>
</div>
{/* Evidence Section */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">Nachweisdokumente</h3>
{linkedDocuments.length > 0 ? (
<div className="space-y-2 mb-4">
{linkedDocuments.map(doc => doc && (
<div key={doc.id} className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
<div className="flex items-center gap-2">
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span className="text-sm text-gray-700">{doc.originalName || doc.filename || doc.id}</span>
</div>
<button
onClick={() => handleRemoveEvidence(doc.id)}
className="text-red-500 hover:text-red-700 text-xs font-medium"
>
Entfernen
</button>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-400 mb-4">Keine Nachweisdokumente verknuepft.</p>
)}
{availableDocuments.length > 0 && (
<div className="flex items-end gap-2">
<div className="flex-1">
<label className="block text-xs font-medium text-gray-600 mb-1">Dokument hinzufuegen</label>
<select
value={selectedEvidenceId}
onChange={e => setSelectedEvidenceId(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="">Dokument auswaehlen...</option>
{availableDocuments.map(doc => (
<option key={doc.id} value={doc.id}>{doc.originalName || doc.filename || doc.id}</option>
))}
</select>
</div>
<button
onClick={handleAddEvidence}
disabled={!selectedEvidenceId}
className="bg-purple-600 text-white hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg px-4 py-2 text-sm font-medium transition-colors"
>
Hinzufuegen
</button>
</div>
)}
</div>
{/* Evidence Gaps */}
{evidenceGaps.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">Nachweis-Anforderungen</h3>
<div className="space-y-2">
{evidenceGaps.map((gap, idx) => (
<div key={idx} className="flex items-center gap-3">
<div className={`w-5 h-5 rounded flex items-center justify-center flex-shrink-0 ${
gap.fulfilled ? 'bg-green-100 text-green-600' : 'bg-red-50 text-red-400'
}`}>
{gap.fulfilled ? (
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
) : (
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
)}
</div>
<span className={`text-sm ${gap.fulfilled ? 'text-gray-700' : 'text-gray-500'}`}>
{gap.requirement}
</span>
</div>
))}
</div>
</div>
)}
{/* VVT Cross-References */}
{vvtActivities.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">VVT-Querverweise</h3>
<div className="space-y-2">
{vvtActivities.map(activity => (
<div key={activity.id} className="flex items-center gap-2 bg-purple-50 rounded-lg px-3 py-2">
<svg className="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
<span className="text-sm text-purple-700">{activity.name || activity.title || activity.id}</span>
</div>
))}
</div>
</div>
)}
{/* Framework Mappings */}
{control?.mappings && control.mappings.length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="text-sm font-semibold text-gray-700 mb-4">Framework-Zuordnungen</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{control.mappings.map((mapping, idx) => (
<div key={idx} className="flex items-center gap-2 bg-gray-50 rounded-lg px-3 py-2">
<span className="text-xs font-semibold text-gray-500 uppercase">{mapping.framework}</span>
<span className="text-sm text-gray-700">{mapping.reference}</span>
</div>
))}
</div>
</div>
)}
{/* Bottom Save */}
<div className="flex items-center justify-between pt-2">
<button
onClick={onBack}
className="text-sm text-gray-500 hover:text-gray-700 font-medium"
>
Zurueck zur Uebersicht
</button>
<button
onClick={handleSave}
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-6 py-2.5 font-medium transition-colors"
>
Aenderungen speichern
</button>
</div>
</div>
)
}