'use client' import { useMemo, useState, useEffect, useCallback } from 'react' import { DerivedTOM, TOMGeneratorState } from '@/lib/sdk/tom-generator/types' import { getControlById } from '@/lib/sdk/tom-generator/controls/loader' interface CanonicalMapping { id: string canonical_control_code: string canonical_title: string | null canonical_severity: string | null canonical_objective: string | null mapping_type: string } interface TOMEditorTabProps { state: TOMGeneratorState selectedTOMId: string | null onUpdateTOM: (tomId: string, updates: Partial) => 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 = { 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('NOT_IMPLEMENTED') const [responsiblePerson, setResponsiblePerson] = useState('') const [implementationDate, setImplementationDate] = useState('') const [notes, setNotes] = useState('') const [linkedEvidence, setLinkedEvidence] = useState([]) const [selectedEvidenceId, setSelectedEvidenceId] = useState('') const [canonicalMappings, setCanonicalMappings] = useState([]) const [showCanonical, setShowCanonical] = useState(false) // Load canonical controls for this TOM's category useEffect(() => { if (!control?.category) { setCanonicalMappings([]); return } fetch(`/api/sdk/v1/compliance/tom-mappings/by-tom/${encodeURIComponent(control.category)}`) .then(r => r.ok ? r.json() : null) .then(data => { if (data?.mappings) setCanonicalMappings(data.mappings) }) .catch(() => setCanonicalMappings([])) }, [control?.category]) 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 (

Keine TOM ausgewaehlt

Waehlen Sie eine TOM aus der Uebersicht, um sie zu bearbeiten.

) } const typeBadge = TYPE_BADGES[control?.type || 'TECHNICAL'] || TYPE_BADGES.TECHNICAL return (
{/* Header */}
{/* TOM Header Card */}
{control?.code || tom.controlId} {typeBadge.label} {control?.category || 'Unbekannt'}

{control?.name?.de || tom.controlId}

{control?.description?.de && (

{control.description.de}

)}
{/* Implementation Status */}

Implementierungsstatus

{STATUS_OPTIONS.map(opt => (
{/* Responsible Person */}

Verantwortliche Person

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" />
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" />
{/* Notes */}

Anmerkungen