Files
breakpilot-compliance/admin-compliance/app/sdk/vendor-compliance/controls/page.tsx
Benjamin Admin 215b95adfa
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard).
SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest.
Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt.

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

288 lines
9.3 KiB
TypeScript

'use client'
import { useState, useMemo } from 'react'
import {
useVendorCompliance,
CONTROLS_LIBRARY,
getControlDomainMeta,
getControlsGroupedByDomain,
ControlDomain,
ControlStatus,
} from '@/lib/sdk/vendor-compliance'
export default function ControlsPage() {
const { controlInstances, vendors, isLoading } = useVendorCompliance()
const [selectedDomain, setSelectedDomain] = useState<ControlDomain | 'ALL'>('ALL')
const [showOnlyRequired, setShowOnlyRequired] = useState(false)
const groupedControls = useMemo(() => getControlsGroupedByDomain(), [])
const filteredControls = useMemo(() => {
let controls = [...CONTROLS_LIBRARY]
if (selectedDomain !== 'ALL') {
controls = controls.filter((c) => c.domain === selectedDomain)
}
if (showOnlyRequired) {
controls = controls.filter((c) => c.isRequired)
}
return controls
}, [selectedDomain, showOnlyRequired])
const controlStats = useMemo(() => {
const stats = {
total: CONTROLS_LIBRARY.length,
required: CONTROLS_LIBRARY.filter((c) => c.isRequired).length,
passed: 0,
partial: 0,
failed: 0,
notAssessed: 0,
}
// Count by status across all instances
for (const instance of controlInstances) {
switch (instance.status) {
case 'PASS':
stats.passed++
break
case 'PARTIAL':
stats.partial++
break
case 'FAIL':
stats.failed++
break
default:
stats.notAssessed++
}
}
return stats
}, [controlInstances])
const getControlStatus = (controlId: string, vendorId: string): ControlStatus | null => {
const instance = controlInstances.find(
(ci) => ci.controlId === controlId && ci.entityId === vendorId && ci.entityType === 'VENDOR'
)
return instance?.status ?? null
}
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Control-Katalog
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Standardkontrollen für Vendor- und Verarbeitungs-Compliance
</p>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<StatCard
label="Gesamt"
value={controlStats.total}
color="gray"
/>
<StatCard
label="Pflicht"
value={controlStats.required}
color="blue"
/>
<StatCard
label="Bestanden"
value={controlStats.passed}
color="green"
/>
<StatCard
label="Teilweise"
value={controlStats.partial}
color="yellow"
/>
<StatCard
label="Fehlgeschlagen"
value={controlStats.failed}
color="red"
/>
</div>
{/* Filters */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="flex flex-wrap items-center gap-4">
<div>
<label htmlFor="domain" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Domain
</label>
<select
id="domain"
className="block w-48 pl-3 pr-10 py-2 border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
value={selectedDomain}
onChange={(e) => setSelectedDomain(e.target.value as ControlDomain | 'ALL')}
>
<option value="ALL">Alle Domains</option>
{Array.from(groupedControls.keys()).map((domain) => (
<option key={domain} value={domain}>
{getControlDomainMeta(domain).de}
</option>
))}
</select>
</div>
<div className="flex items-center">
<input
id="required"
type="checkbox"
checked={showOnlyRequired}
onChange={(e) => setShowOnlyRequired(e.target.checked)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label htmlFor="required" className="ml-2 block text-sm text-gray-900 dark:text-white">
Nur Pflichtkontrollen
</label>
</div>
</div>
</div>
{/* Controls by Domain */}
{selectedDomain === 'ALL' ? (
// Show grouped by domain
Array.from(groupedControls.entries()).map(([domain, controls]) => {
const filteredDomainControls = showOnlyRequired
? controls.filter((c) => c.isRequired)
: controls
if (filteredDomainControls.length === 0) return null
return (
<div key={domain} className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{getControlDomainMeta(domain).de}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{filteredDomainControls.length} Kontrollen
</p>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredDomainControls.map((control) => (
<ControlRow key={control.id} control={control} />
))}
</div>
</div>
)
})
) : (
// Show flat list
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{getControlDomainMeta(selectedDomain).de}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{filteredControls.length} Kontrollen
</p>
</div>
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{filteredControls.map((control) => (
<ControlRow key={control.id} control={control} />
))}
</div>
</div>
)}
</div>
)
}
function StatCard({
label,
value,
color,
}: {
label: string
value: number
color: 'gray' | 'blue' | 'green' | 'yellow' | 'red'
}) {
const colors = {
gray: 'bg-gray-50 dark:bg-gray-700/50',
blue: 'bg-blue-50 dark:bg-blue-900/20',
green: 'bg-green-50 dark:bg-green-900/20',
yellow: 'bg-yellow-50 dark:bg-yellow-900/20',
red: 'bg-red-50 dark:bg-red-900/20',
}
return (
<div className={`${colors[color]} rounded-lg p-4`}>
<p className="text-sm text-gray-500 dark:text-gray-400">{label}</p>
<p className="mt-1 text-2xl font-bold text-gray-900 dark:text-white">{value}</p>
</div>
)
}
function ControlRow({ control }: { control: typeof CONTROLS_LIBRARY[0] }) {
return (
<div className="px-6 py-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-mono text-gray-500 dark:text-gray-400">
{control.id}
</span>
{control.isRequired && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
Pflicht
</span>
)}
</div>
<h3 className="mt-1 text-sm font-medium text-gray-900 dark:text-white">
{control.title.de}
</h3>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
{control.description.de}
</p>
<div className="mt-2 flex flex-wrap gap-2">
{control.requirements.map((req, idx) => (
<span
key={idx}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
>
{req}
</span>
))}
</div>
</div>
<div className="ml-4 text-right">
<p className="text-xs text-gray-500 dark:text-gray-400">
Prüfintervall
</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">
{control.defaultFrequency === 'QUARTERLY'
? 'Vierteljährlich'
: control.defaultFrequency === 'SEMI_ANNUAL'
? 'Halbjährlich'
: control.defaultFrequency === 'ANNUAL'
? 'Jährlich'
: 'Alle 2 Jahre'}
</p>
</div>
</div>
<div className="mt-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded">
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Pass-Kriterium:</p>
<p className="text-sm text-gray-700 dark:text-gray-300">
{control.passCriteria.de}
</p>
</div>
</div>
)
}