- obligations: unused vendors state/fetch, unreachable filter==='ai' path - tom: unused vendorControlsLoading state, unused bulkUpdateTOMs import - loeschfristen: unused BASELINE_TEMPLATES imports, sdk hook, managingLegalHolds state - vvt: unused apiGetCompleteness/apiGetLibrary, 7 unused VVTLib* interfaces - vendor-compliance: 11 unused context methods, 6 unused selector hooks, ContractUploadData type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
478 lines
18 KiB
TypeScript
478 lines
18 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState, useCallback, useMemo, useEffect } from 'react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { useSDK } from '@/lib/sdk'
|
||
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
|
||
import { useTOMGenerator } from '@/lib/sdk/tom-generator/context'
|
||
import { DerivedTOM } from '@/lib/sdk/tom-generator/types'
|
||
import { TOMOverviewTab, TOMEditorTab, TOMGapExportTab, TOMDocumentTab } from '@/components/sdk/tom-dashboard'
|
||
import { runTOMComplianceCheck, type TOMComplianceCheckResult } from '@/lib/sdk/tom-compliance'
|
||
|
||
// =============================================================================
|
||
// TYPES
|
||
// =============================================================================
|
||
|
||
type Tab = 'uebersicht' | 'editor' | 'generator' | 'gap-export' | 'tom-dokument'
|
||
|
||
interface TabDefinition {
|
||
key: Tab
|
||
label: string
|
||
}
|
||
|
||
const TABS: TabDefinition[] = [
|
||
{ key: 'uebersicht', label: 'Uebersicht' },
|
||
{ key: 'editor', label: 'Detail-Editor' },
|
||
{ key: 'generator', label: 'Generator' },
|
||
{ key: 'gap-export', label: 'Gap-Analyse & Export' },
|
||
{ key: 'tom-dokument', label: 'TOM-Dokument' },
|
||
]
|
||
|
||
// =============================================================================
|
||
// PAGE COMPONENT
|
||
// =============================================================================
|
||
|
||
export default function TOMPage() {
|
||
const router = useRouter()
|
||
const sdk = useSDK()
|
||
const { state, dispatch, runGapAnalysis } = useTOMGenerator()
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Local state
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const [tab, setTab] = useState<Tab>('uebersicht')
|
||
const [selectedTOMId, setSelectedTOMId] = useState<string | null>(null)
|
||
const [complianceResult, setComplianceResult] = useState<TOMComplianceCheckResult | null>(null)
|
||
const [vendorControls, setVendorControls] = useState<Array<{
|
||
vendorId: string
|
||
vendorName: string
|
||
controlId: string
|
||
controlName: string
|
||
domain: string
|
||
status: string
|
||
lastTestedAt?: string
|
||
}>>([])
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Compliance check (auto-run when derivedTOMs change)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
useEffect(() => {
|
||
if (state?.derivedTOMs && Array.isArray(state.derivedTOMs) && state.derivedTOMs.length > 0) {
|
||
setComplianceResult(runTOMComplianceCheck(state))
|
||
}
|
||
}, [state?.derivedTOMs])
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Vendor controls cross-reference (fetch when overview tab is active)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
useEffect(() => {
|
||
if (tab !== 'uebersicht') return
|
||
Promise.all([
|
||
fetch('/api/sdk/v1/vendor-compliance/control-instances?limit=500').then(r => r.ok ? r.json() : null),
|
||
fetch('/api/sdk/v1/vendor-compliance/vendors?limit=500').then(r => r.ok ? r.json() : null),
|
||
]).then(([ciData, vendorData]) => {
|
||
const instances = ciData?.data?.items || []
|
||
const vendors = vendorData?.data?.items || []
|
||
const vendorMap = new Map<string, string>()
|
||
for (const v of vendors) {
|
||
vendorMap.set(v.id, v.name)
|
||
}
|
||
// Filter for TOM-domain controls
|
||
const tomControls = instances
|
||
.filter((ci: any) => ci.domain === 'TOM' || ci.controlId?.startsWith('VND-TOM'))
|
||
.map((ci: any) => ({
|
||
vendorId: ci.vendorId || ci.vendor_id,
|
||
vendorName: vendorMap.get(ci.vendorId || ci.vendor_id) || 'Unbekannt',
|
||
controlId: ci.controlId || ci.control_id,
|
||
controlName: ci.controlName || ci.control_name || ci.controlId || ci.control_id,
|
||
domain: ci.domain || 'TOM',
|
||
status: ci.status || 'UNKNOWN',
|
||
lastTestedAt: ci.lastTestedAt || ci.last_tested_at,
|
||
}))
|
||
setVendorControls(tomControls)
|
||
}).catch(() => {})
|
||
}, [tab])
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Computed / memoised values
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const tomCount = useMemo(() => {
|
||
if (!state?.derivedTOMs) return 0
|
||
return Array.isArray(state.derivedTOMs)
|
||
? state.derivedTOMs.length
|
||
: Object.keys(state.derivedTOMs).length
|
||
}, [state?.derivedTOMs])
|
||
|
||
const lastModifiedFormatted = useMemo(() => {
|
||
if (!state?.metadata?.lastModified) return null
|
||
try {
|
||
const date = new Date(state.metadata.lastModified)
|
||
return date.toLocaleDateString('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
} catch {
|
||
return null
|
||
}
|
||
}, [state?.metadata?.lastModified])
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Handlers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const handleSelectTOM = useCallback((tomId: string) => {
|
||
setSelectedTOMId(tomId)
|
||
setTab('editor')
|
||
}, [])
|
||
|
||
const handleUpdateTOM = useCallback(
|
||
(tomId: string, updates: Partial<DerivedTOM>) => {
|
||
dispatch({
|
||
type: 'UPDATE_DERIVED_TOM',
|
||
payload: { id: tomId, data: updates },
|
||
})
|
||
},
|
||
[dispatch],
|
||
)
|
||
|
||
const handleStartGenerator = useCallback(() => {
|
||
router.push('/sdk/tom-generator')
|
||
}, [router])
|
||
|
||
const handleBackToOverview = useCallback(() => {
|
||
setSelectedTOMId(null)
|
||
setTab('uebersicht')
|
||
}, [])
|
||
|
||
const handleRunGapAnalysis = useCallback(() => {
|
||
if (typeof runGapAnalysis === 'function') {
|
||
runGapAnalysis()
|
||
}
|
||
}, [runGapAnalysis])
|
||
|
||
const handleTabChange = useCallback((newTab: Tab) => {
|
||
setTab(newTab)
|
||
}, [])
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Render helpers
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderTabBar = () => (
|
||
<div className="flex flex-wrap gap-2">
|
||
{TABS.map((t) => {
|
||
const isActive = tab === t.key
|
||
return (
|
||
<button
|
||
key={t.key}
|
||
onClick={() => handleTabChange(t.key)}
|
||
className={`
|
||
rounded-lg px-4 py-2 text-sm font-medium transition-colors
|
||
${
|
||
isActive
|
||
? 'bg-purple-600 text-white shadow-sm'
|
||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||
}
|
||
`}
|
||
>
|
||
{t.label}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tab 1 – Uebersicht
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderUebersicht = () => (
|
||
<TOMOverviewTab
|
||
state={state}
|
||
onSelectTOM={handleSelectTOM}
|
||
onStartGenerator={handleStartGenerator}
|
||
/>
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tab 2 – Detail-Editor
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderEditor = () => (
|
||
<TOMEditorTab
|
||
state={state}
|
||
selectedTOMId={selectedTOMId}
|
||
onUpdateTOM={handleUpdateTOM}
|
||
onBack={handleBackToOverview}
|
||
/>
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tab 3 – Generator
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderGenerator = () => (
|
||
<div className="space-y-6">
|
||
{/* Info card */}
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||
<div className="flex items-start gap-4">
|
||
{/* Icon */}
|
||
<div className="flex-shrink-0 w-12 h-12 rounded-lg bg-purple-100 flex items-center justify-center">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
className="h-6 w-6 text-purple-600"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
strokeWidth={2}
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
|
||
<div className="flex-1">
|
||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||
TOM Generator – 6-Schritte-Assistent
|
||
</h3>
|
||
<p className="text-gray-600 leading-relaxed mb-4">
|
||
Der TOM Generator fuehrt Sie in 6 Schritten durch die systematische
|
||
Ableitung Ihrer technischen und organisatorischen Massnahmen. Sie
|
||
beantworten gezielte Fragen zu Ihrem Unternehmen, Ihrer
|
||
IT-Infrastruktur und Ihren Verarbeitungstaetigkeiten. Daraus werden
|
||
passende TOMs automatisch abgeleitet und priorisiert.
|
||
</p>
|
||
|
||
<div className="bg-purple-50 rounded-lg p-4 mb-4">
|
||
<h4 className="text-sm font-semibold text-purple-800 mb-2">
|
||
Die 6 Schritte im Ueberblick:
|
||
</h4>
|
||
<ol className="list-decimal list-inside text-sm text-purple-700 space-y-1">
|
||
<li>Unternehmenskontext erfassen</li>
|
||
<li>IT-Infrastruktur beschreiben</li>
|
||
<li>Verarbeitungstaetigkeiten zuordnen</li>
|
||
<li>Risikobewertung durchfuehren</li>
|
||
<li>TOM-Ableitung und Priorisierung</li>
|
||
<li>Ergebnis pruefen und uebernehmen</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<button
|
||
onClick={handleStartGenerator}
|
||
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-6 py-3 text-sm font-medium transition-colors shadow-sm"
|
||
>
|
||
TOM Generator starten
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick stats – only rendered when derivedTOMs exist */}
|
||
{tomCount > 0 && (
|
||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||
<h3 className="text-base font-semibold text-gray-900 mb-4">
|
||
Aktueller Stand
|
||
</h3>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{/* TOM count */}
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-sm text-gray-500 mb-1">Abgeleitete TOMs</p>
|
||
<p className="text-2xl font-bold text-gray-900">{tomCount}</p>
|
||
</div>
|
||
|
||
{/* Last generated date */}
|
||
{lastModifiedFormatted && (
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-sm text-gray-500 mb-1">Zuletzt generiert</p>
|
||
<p className="text-lg font-semibold text-gray-900">
|
||
{lastModifiedFormatted}
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Status */}
|
||
<div className="bg-gray-50 rounded-lg p-4">
|
||
<p className="text-sm text-gray-500 mb-1">Status</p>
|
||
<div className="flex items-center gap-2">
|
||
<span className="w-2.5 h-2.5 rounded-full bg-green-500" />
|
||
<p className="text-sm font-medium text-gray-900">
|
||
TOMs vorhanden
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-4 pt-4 border-t border-gray-100">
|
||
<p className="text-xs text-gray-400">
|
||
Sie koennen den Generator jederzeit erneut ausfuehren, um Ihre
|
||
TOMs zu aktualisieren oder zu erweitern.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Empty state when no TOMs exist yet */}
|
||
{tomCount === 0 && (
|
||
<div className="bg-white rounded-xl border border-dashed border-gray-300 p-8 text-center">
|
||
<div className="mx-auto w-16 h-16 rounded-full bg-gray-100 flex items-center justify-center mb-4">
|
||
<svg
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
className="h-8 w-8 text-gray-400"
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
strokeWidth={1.5}
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
<h4 className="text-base font-medium text-gray-700 mb-1">
|
||
Noch keine TOMs vorhanden
|
||
</h4>
|
||
<p className="text-sm text-gray-500 mb-4">
|
||
Starten Sie den Generator, um Ihre ersten technischen und
|
||
organisatorischen Massnahmen abzuleiten.
|
||
</p>
|
||
<button
|
||
onClick={handleStartGenerator}
|
||
className="bg-purple-600 text-white hover:bg-purple-700 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
||
>
|
||
Jetzt starten
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tab 4 – Gap-Analyse & Export
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderGapExport = () => (
|
||
<TOMGapExportTab
|
||
state={state}
|
||
onRunGapAnalysis={handleRunGapAnalysis}
|
||
/>
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tab 5 – TOM-Dokument
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderTOMDokument = () => (
|
||
<TOMDocumentTab
|
||
state={state}
|
||
complianceResult={complianceResult}
|
||
/>
|
||
)
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tab content router
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const renderActiveTab = () => {
|
||
switch (tab) {
|
||
case 'uebersicht':
|
||
return renderUebersicht()
|
||
case 'editor':
|
||
return renderEditor()
|
||
case 'generator':
|
||
return renderGenerator()
|
||
case 'gap-export':
|
||
return renderGapExport()
|
||
case 'tom-dokument':
|
||
return renderTOMDokument()
|
||
default:
|
||
return renderUebersicht()
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Main render
|
||
// ---------------------------------------------------------------------------
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Step header */}
|
||
<StepHeader stepId="tom" {...STEP_EXPLANATIONS['tom']} />
|
||
|
||
{/* Tab bar */}
|
||
<div className="bg-white rounded-xl border border-gray-200 p-3">
|
||
{renderTabBar()}
|
||
</div>
|
||
|
||
{/* Active tab content */}
|
||
<div>{renderActiveTab()}</div>
|
||
|
||
{/* Vendor-Controls cross-reference (only on overview tab) */}
|
||
{tab === 'uebersicht' && vendorControls.length > 0 && (
|
||
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div>
|
||
<h3 className="text-base font-semibold text-gray-900">Auftragsverarbeiter-Controls (Art. 28)</h3>
|
||
<p className="text-sm text-gray-500 mt-0.5">TOM-relevante Controls aus dem Vendor Register</p>
|
||
</div>
|
||
<a href="/sdk/vendor-compliance" className="text-sm text-purple-600 hover:text-purple-700 font-medium">
|
||
Zum Vendor Register →
|
||
</a>
|
||
</div>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead>
|
||
<tr className="border-b border-gray-200">
|
||
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Vendor</th>
|
||
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Control</th>
|
||
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Status</th>
|
||
<th className="text-left py-2 px-3 text-xs font-medium text-gray-500 uppercase">Letzte Pruefung</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-100">
|
||
{vendorControls.map((vc, i) => (
|
||
<tr key={`${vc.vendorId}-${vc.controlId}-${i}`} className="hover:bg-gray-50">
|
||
<td className="py-2.5 px-3 font-medium text-gray-900">{vc.vendorName}</td>
|
||
<td className="py-2.5 px-3">
|
||
<span className="font-mono text-xs text-gray-500">{vc.controlId}</span>
|
||
<span className="ml-2 text-gray-700">{vc.controlName !== vc.controlId ? vc.controlName : ''}</span>
|
||
</td>
|
||
<td className="py-2.5 px-3">
|
||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||
vc.status === 'PASS' ? 'bg-green-100 text-green-700' :
|
||
vc.status === 'PARTIAL' ? 'bg-yellow-100 text-yellow-700' :
|
||
vc.status === 'FAIL' ? 'bg-red-100 text-red-700' :
|
||
'bg-gray-100 text-gray-600'
|
||
}`}>
|
||
{vc.status === 'PASS' ? 'Bestanden' :
|
||
vc.status === 'PARTIAL' ? 'Teilweise' :
|
||
vc.status === 'FAIL' ? 'Nicht bestanden' :
|
||
vc.status}
|
||
</span>
|
||
</td>
|
||
<td className="py-2.5 px-3 text-gray-500">
|
||
{vc.lastTestedAt ? new Date(vc.lastTestedAt).toLocaleDateString('de-DE') : '\u2014'}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|