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:
323
admin-compliance/app/sdk/vendor-compliance/risks/page.tsx
Normal file
323
admin-compliance/app/sdk/vendor-compliance/risks/page.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
useVendorCompliance,
|
||||
getRiskLevelFromScore,
|
||||
getRiskLevelColor,
|
||||
generateRiskMatrix,
|
||||
SEVERITY_DEFINITIONS,
|
||||
countFindingsBySeverity,
|
||||
} from '@/lib/sdk/vendor-compliance'
|
||||
|
||||
export default function RisksPage() {
|
||||
const { vendors, findings, riskOverview, isLoading } = useVendorCompliance()
|
||||
|
||||
const riskMatrix = useMemo(() => generateRiskMatrix(), [])
|
||||
|
||||
const findingsBySeverity = useMemo(() => {
|
||||
return countFindingsBySeverity(findings)
|
||||
}, [findings])
|
||||
|
||||
const openFindings = useMemo(() => {
|
||||
return findings.filter((f) => f.status === 'OPEN' || f.status === 'IN_PROGRESS')
|
||||
}, [findings])
|
||||
|
||||
const highRiskVendors = useMemo(() => {
|
||||
return vendors.filter((v) => v.residualRiskScore >= 60)
|
||||
}, [vendors])
|
||||
|
||||
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">
|
||||
Risiko-Dashboard
|
||||
</h1>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Übersicht über Vendor-Risiken und offene Findings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Risk Overview Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<RiskCard
|
||||
title="Durchschn. Inherent Risk"
|
||||
value={Math.round(riskOverview.averageInherentRisk)}
|
||||
suffix="%"
|
||||
color="purple"
|
||||
/>
|
||||
<RiskCard
|
||||
title="Durchschn. Residual Risk"
|
||||
value={Math.round(riskOverview.averageResidualRisk)}
|
||||
suffix="%"
|
||||
color="blue"
|
||||
/>
|
||||
<RiskCard
|
||||
title="High-Risk Vendors"
|
||||
value={riskOverview.highRiskVendors}
|
||||
color="red"
|
||||
/>
|
||||
<RiskCard
|
||||
title="Kritische Findings"
|
||||
value={riskOverview.criticalFindings}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Risk Matrix */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Risikomatrix
|
||||
</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-xs text-gray-500 dark:text-gray-400 font-medium pb-2"></th>
|
||||
{[1, 2, 3, 4, 5].map((impact) => (
|
||||
<th
|
||||
key={impact}
|
||||
className="text-xs text-gray-500 dark:text-gray-400 font-medium pb-2 text-center"
|
||||
>
|
||||
{impact}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[5, 4, 3, 2, 1].map((likelihood) => (
|
||||
<tr key={likelihood}>
|
||||
<td className="text-xs text-gray-500 dark:text-gray-400 font-medium pr-2 text-right">
|
||||
{likelihood}
|
||||
</td>
|
||||
{[1, 2, 3, 4, 5].map((impact) => {
|
||||
const cell = riskMatrix[likelihood - 1]?.[impact - 1]
|
||||
const colors = cell ? getRiskLevelColor(cell.level) : { bg: '', text: '' }
|
||||
const vendorCount = vendors.filter((v) => {
|
||||
const vLikelihood = Math.ceil(v.residualRiskScore / 20)
|
||||
const vImpact = Math.ceil(v.inherentRiskScore / 20)
|
||||
return vLikelihood === likelihood && vImpact === impact
|
||||
}).length
|
||||
|
||||
return (
|
||||
<td key={impact} className="p-1">
|
||||
<div
|
||||
className={`${colors.bg} ${colors.text} rounded p-2 text-center min-w-[40px]`}
|
||||
>
|
||||
<span className="text-sm font-medium">
|
||||
{vendorCount > 0 ? vendorCount : '-'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="mt-4 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>Eintrittswahrscheinlichkeit ↑</span>
|
||||
<span>Auswirkung →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Findings by Severity */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Findings nach Schweregrad
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const).map((severity) => {
|
||||
const count = findingsBySeverity[severity] || 0
|
||||
const total = findings.length
|
||||
const percentage = total > 0 ? (count / total) * 100 : 0
|
||||
const def = SEVERITY_DEFINITIONS[severity]
|
||||
|
||||
return (
|
||||
<div key={severity}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{def.label.de}
|
||||
</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full ${
|
||||
severity === 'CRITICAL'
|
||||
? 'bg-red-500'
|
||||
: severity === 'HIGH'
|
||||
? 'bg-orange-500'
|
||||
: severity === 'MEDIUM'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-blue-500'
|
||||
}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{def.responseTime.de}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* High Risk Vendors */}
|
||||
<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">
|
||||
High-Risk Vendors
|
||||
</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{highRiskVendors.map((vendor) => {
|
||||
const riskLevel = getRiskLevelFromScore(vendor.residualRiskScore / 4)
|
||||
const colors = getRiskLevelColor(riskLevel)
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={vendor.id}
|
||||
href={`/sdk/vendor-compliance/vendors/${vendor.id}`}
|
||||
className="block px-6 py-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{vendor.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{vendor.serviceDescription}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
Residual Risk
|
||||
</p>
|
||||
<p className={`text-lg font-bold ${colors.text}`}>
|
||||
{vendor.residualRiskScore}%
|
||||
</p>
|
||||
</div>
|
||||
<span className={`${colors.bg} ${colors.text} px-3 py-1 rounded-full text-sm font-medium`}>
|
||||
{riskLevel}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{highRiskVendors.length === 0 && (
|
||||
<div className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
||||
Keine High-Risk Vendors vorhanden
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open Findings */}
|
||||
<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 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Offene Findings ({openFindings.length})
|
||||
</h2>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{openFindings.slice(0, 10).map((finding) => {
|
||||
const vendor = vendors.find((v) => v.id === finding.vendorId)
|
||||
const severityColors = {
|
||||
LOW: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
MEDIUM: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
HIGH: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
CRITICAL: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={finding.id} 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={`px-2 py-0.5 rounded-full text-xs font-medium ${severityColors[finding.severity]}`}>
|
||||
{finding.severity}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{finding.category}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{finding.title.de}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{finding.description.de}
|
||||
</p>
|
||||
{vendor && (
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Vendor: {vendor.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
href={`/sdk/vendor-compliance/contracts/${finding.contractId}`}
|
||||
className="text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 ml-4"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{openFindings.length === 0 && (
|
||||
<div className="px-6 py-12 text-center text-gray-500 dark:text-gray-400">
|
||||
Keine offenen Findings
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RiskCard({
|
||||
title,
|
||||
value,
|
||||
suffix,
|
||||
color,
|
||||
}: {
|
||||
title: string
|
||||
value: number
|
||||
suffix?: string
|
||||
color: 'purple' | 'blue' | 'red' | 'orange'
|
||||
}) {
|
||||
const colors = {
|
||||
purple: 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
|
||||
blue: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400',
|
||||
red: 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400',
|
||||
orange: 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${colors[color]} rounded-lg p-6`}>
|
||||
<p className="text-sm font-medium opacity-80">{title}</p>
|
||||
<p className="mt-2 text-3xl font-bold">
|
||||
{value}
|
||||
{suffix}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user