refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
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
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
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>
This commit is contained in:
287
admin-compliance/app/sdk/vendor-compliance/controls/page.tsx
Normal file
287
admin-compliance/app/sdk/vendor-compliance/controls/page.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user