Initial commit: breakpilot-compliance - Compliance SDK Platform

Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,264 @@
'use client'
import React from 'react'
import { useCompliance } from '../provider'
import { ComplianceScore } from './ComplianceScore'
import { RiskMatrix } from './RiskMatrix'
export interface ComplianceDashboardProps {
showScore?: boolean
showRisks?: boolean
showProgress?: boolean
showObligations?: boolean
className?: string
style?: React.CSSProperties
}
export function ComplianceDashboard({
showScore = true,
showRisks = true,
showProgress = true,
showObligations = true,
className,
style,
}: ComplianceDashboardProps) {
const { state, compliance, completionPercentage, phase1Completion, phase2Completion } =
useCompliance()
const score = compliance.calculateComplianceScore()
const criticalRisks = compliance.getCriticalRisks()
const upcomingObligations = compliance.getUpcomingObligations()
const overdueObligations = compliance.getOverdueObligations()
const containerStyle: React.CSSProperties = {
fontFamily: 'system-ui, -apple-system, sans-serif',
...style,
}
const cardStyle: React.CSSProperties = {
backgroundColor: '#fff',
borderRadius: '8px',
padding: '20px',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
marginBottom: '20px',
}
const gridStyle: React.CSSProperties = {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '20px',
marginBottom: '20px',
}
const statStyle: React.CSSProperties = {
...cardStyle,
textAlign: 'center',
}
const statValueStyle: React.CSSProperties = {
fontSize: '36px',
fontWeight: 700,
margin: '10px 0',
}
const statLabelStyle: React.CSSProperties = {
fontSize: '14px',
color: '#666',
}
return (
<div style={containerStyle} className={className}>
<h1 style={{ margin: '0 0 20px' }}>Compliance Dashboard</h1>
{/* Stats Grid */}
<div style={gridStyle}>
{showScore && (
<div style={statStyle}>
<div style={statLabelStyle}>Compliance Score</div>
<div style={{ ...statValueStyle, color: score.overall >= 70 ? '#16a34a' : '#dc2626' }}>
{score.overall}%
</div>
<div style={statLabelStyle}>
Trend:{' '}
{score.trend === 'UP' ? '↑ Steigend' : score.trend === 'DOWN' ? '↓ Fallend' : '→ Stabil'}
</div>
</div>
)}
{showProgress && (
<>
<div style={statStyle}>
<div style={statLabelStyle}>Gesamtfortschritt</div>
<div style={statValueStyle}>{completionPercentage}%</div>
<div style={statLabelStyle}>
{state.completedSteps.length} von 19 Schritten abgeschlossen
</div>
</div>
<div style={statStyle}>
<div style={statLabelStyle}>Phase 1: Assessment</div>
<div style={statValueStyle}>{phase1Completion}%</div>
<div style={{ height: '8px', backgroundColor: '#e5e5e5', borderRadius: '4px' }}>
<div
style={{
height: '100%',
width: `${phase1Completion}%`,
backgroundColor: '#3b82f6',
borderRadius: '4px',
}}
/>
</div>
</div>
<div style={statStyle}>
<div style={statLabelStyle}>Phase 2: Dokumentation</div>
<div style={statValueStyle}>{phase2Completion}%</div>
<div style={{ height: '8px', backgroundColor: '#e5e5e5', borderRadius: '4px' }}>
<div
style={{
height: '100%',
width: `${phase2Completion}%`,
backgroundColor: '#8b5cf6',
borderRadius: '4px',
}}
/>
</div>
</div>
</>
)}
</div>
{/* Main Content Grid */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
{/* Score Breakdown */}
{showScore && (
<div style={cardStyle}>
<h3 style={{ margin: '0 0 15px' }}>Score nach Regulierung</h3>
{Object.entries(score.byRegulation).map(([reg, value]) => (
<div
key={reg}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '10px 0',
borderBottom: '1px solid #eee',
}}
>
<span>{reg}</span>
<span style={{ fontWeight: 600, color: value >= 70 ? '#16a34a' : '#dc2626' }}>
{value}%
</span>
</div>
))}
</div>
)}
{/* Obligations */}
{showObligations && (
<div style={cardStyle}>
<h3 style={{ margin: '0 0 15px' }}>Anstehende Pflichten</h3>
{overdueObligations.length > 0 && (
<div
style={{
padding: '10px',
backgroundColor: '#fef2f2',
borderRadius: '4px',
marginBottom: '10px',
}}
>
<strong style={{ color: '#dc2626' }}>
{overdueObligations.length} überfällige Pflichten!
</strong>
</div>
)}
{upcomingObligations.length === 0 && overdueObligations.length === 0 ? (
<p style={{ color: '#666' }}>Keine anstehenden Pflichten in den nächsten 30 Tagen.</p>
) : (
<div>
{[...overdueObligations, ...upcomingObligations].slice(0, 5).map(o => (
<div
key={o.id}
style={{
padding: '10px',
borderBottom: '1px solid #eee',
}}
>
<div style={{ fontWeight: 500 }}>{o.title}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{o.regulationCode} Art. {o.article}
</div>
{o.deadline && (
<div
style={{
fontSize: '12px',
color: new Date(o.deadline) < new Date() ? '#dc2626' : '#666',
}}
>
Frist: {new Date(o.deadline).toLocaleDateString('de-DE')}
</div>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
{/* Risk Matrix */}
{showRisks && state.risks.length > 0 && (
<div style={cardStyle}>
<h3 style={{ margin: '0 0 15px' }}>Risikomatrix</h3>
<RiskMatrix risks={state.risks} />
{criticalRisks.length > 0 && (
<div
style={{
marginTop: '15px',
padding: '10px',
backgroundColor: '#fef2f2',
borderRadius: '4px',
}}
>
<strong style={{ color: '#dc2626' }}>
{criticalRisks.length} kritische/hohe Risiken ohne Mitigation
</strong>
</div>
)}
</div>
)}
{/* Controls Summary */}
<div style={cardStyle}>
<h3 style={{ margin: '0 0 15px' }}>Controls Übersicht</h3>
<div style={gridStyle}>
<div>
<div style={{ fontSize: '24px', fontWeight: 700 }}>{state.controls.length}</div>
<div style={{ color: '#666' }}>Gesamt</div>
</div>
<div>
<div style={{ fontSize: '24px', fontWeight: 700, color: '#16a34a' }}>
{state.controls.filter(c => c.implementationStatus === 'IMPLEMENTED').length}
</div>
<div style={{ color: '#666' }}>Implementiert</div>
</div>
<div>
<div style={{ fontSize: '24px', fontWeight: 700, color: '#f59e0b' }}>
{state.controls.filter(c => c.implementationStatus === 'PARTIAL').length}
</div>
<div style={{ color: '#666' }}>Teilweise</div>
</div>
<div>
<div style={{ fontSize: '24px', fontWeight: 700, color: '#dc2626' }}>
{state.controls.filter(c => c.implementationStatus === 'NOT_IMPLEMENTED').length}
</div>
<div style={{ color: '#666' }}>Offen</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,111 @@
'use client'
import React from 'react'
export interface ComplianceScoreProps {
score: number
size?: 'small' | 'medium' | 'large'
showLabel?: boolean
className?: string
style?: React.CSSProperties
}
export function ComplianceScore({
score,
size = 'medium',
showLabel = true,
className,
style,
}: ComplianceScoreProps) {
const sizes = {
small: { width: 80, strokeWidth: 6, fontSize: 18 },
medium: { width: 120, strokeWidth: 8, fontSize: 24 },
large: { width: 160, strokeWidth: 10, fontSize: 32 },
}
const { width, strokeWidth, fontSize } = sizes[size]
const radius = (width - strokeWidth) / 2
const circumference = radius * 2 * Math.PI
const offset = circumference - (score / 100) * circumference
const getColor = (score: number): string => {
if (score >= 80) return '#16a34a' // Green
if (score >= 60) return '#f59e0b' // Yellow
if (score >= 40) return '#f97316' // Orange
return '#dc2626' // Red
}
const color = getColor(score)
return (
<div
style={{
display: 'inline-flex',
flexDirection: 'column',
alignItems: 'center',
...style,
}}
className={className}
>
<svg
width={width}
height={width}
style={{ transform: 'rotate(-90deg)' }}
>
{/* Background circle */}
<circle
cx={width / 2}
cy={width / 2}
r={radius}
fill="none"
stroke="#e5e5e5"
strokeWidth={strokeWidth}
/>
{/* Progress circle */}
<circle
cx={width / 2}
cy={width / 2}
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
style={{
transition: 'stroke-dashoffset 0.5s ease-in-out',
}}
/>
{/* Score text */}
<text
x="50%"
y="50%"
dominantBaseline="middle"
textAnchor="middle"
style={{
transform: 'rotate(90deg)',
transformOrigin: 'center',
fontSize: `${fontSize}px`,
fontWeight: 700,
fill: color,
}}
>
{score}%
</text>
</svg>
{showLabel && (
<div
style={{
marginTop: '8px',
fontSize: '14px',
color: '#666',
textAlign: 'center',
}}
>
Compliance Score
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,213 @@
'use client'
import React, { useState, useCallback } from 'react'
import { useCompliance } from '../provider'
import type { ConsentPurpose, CookieBannerPosition, CookieBannerTheme } from '@breakpilot/compliance-sdk-types'
export interface ConsentBannerProps {
position?: CookieBannerPosition
theme?: CookieBannerTheme
onConsentChange?: (consents: Record<ConsentPurpose, boolean>) => void
privacyPolicyUrl?: string
imprintUrl?: string
className?: string
style?: React.CSSProperties
}
export function ConsentBanner({
position = 'BOTTOM',
theme = 'LIGHT',
onConsentChange,
privacyPolicyUrl = '/privacy',
imprintUrl = '/imprint',
className,
style,
}: ConsentBannerProps) {
const { state, dispatch } = useCompliance()
const [showSettings, setShowSettings] = useState(false)
const [consents, setConsents] = useState<Record<ConsentPurpose, boolean>>({
ESSENTIAL: true,
FUNCTIONAL: false,
ANALYTICS: false,
MARKETING: false,
PERSONALIZATION: false,
THIRD_PARTY: false,
})
const config = state.cookieBanner
const handleAcceptAll = useCallback(() => {
const allConsents: Record<ConsentPurpose, boolean> = {
ESSENTIAL: true,
FUNCTIONAL: true,
ANALYTICS: true,
MARKETING: true,
PERSONALIZATION: true,
THIRD_PARTY: true,
}
setConsents(allConsents)
onConsentChange?.(allConsents)
// Would save to backend here
}, [onConsentChange])
const handleRejectAll = useCallback(() => {
const minimalConsents: Record<ConsentPurpose, boolean> = {
ESSENTIAL: true,
FUNCTIONAL: false,
ANALYTICS: false,
MARKETING: false,
PERSONALIZATION: false,
THIRD_PARTY: false,
}
setConsents(minimalConsents)
onConsentChange?.(minimalConsents)
}, [onConsentChange])
const handleSaveSettings = useCallback(() => {
onConsentChange?.(consents)
setShowSettings(false)
}, [consents, onConsentChange])
const handleConsentToggle = useCallback((purpose: ConsentPurpose) => {
if (purpose === 'ESSENTIAL') return // Cannot disable essential
setConsents(prev => ({ ...prev, [purpose]: !prev[purpose] }))
}, [])
const positionStyles: Record<CookieBannerPosition, React.CSSProperties> = {
TOP: { top: 0, left: 0, right: 0 },
BOTTOM: { bottom: 0, left: 0, right: 0 },
CENTER: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' },
}
const themeStyles: Record<CookieBannerTheme, React.CSSProperties> = {
LIGHT: { backgroundColor: '#ffffff', color: '#1a1a1a' },
DARK: { backgroundColor: '#1a1a1a', color: '#ffffff' },
CUSTOM: config?.customColors
? {
backgroundColor: config.customColors.background,
color: config.customColors.text,
}
: {},
}
const bannerStyle: React.CSSProperties = {
position: 'fixed',
zIndex: 9999,
padding: '20px',
boxShadow: '0 -2px 10px rgba(0, 0, 0, 0.1)',
fontFamily: 'system-ui, -apple-system, sans-serif',
...positionStyles[position],
...themeStyles[theme],
...style,
}
const buttonBaseStyle: React.CSSProperties = {
padding: '10px 20px',
borderRadius: '4px',
border: 'none',
cursor: 'pointer',
fontWeight: 500,
marginRight: '10px',
}
const primaryButtonStyle: React.CSSProperties = {
...buttonBaseStyle,
backgroundColor: theme === 'DARK' ? '#ffffff' : '#1a1a1a',
color: theme === 'DARK' ? '#1a1a1a' : '#ffffff',
}
const secondaryButtonStyle: React.CSSProperties = {
...buttonBaseStyle,
backgroundColor: 'transparent',
border: `1px solid ${theme === 'DARK' ? '#ffffff' : '#1a1a1a'}`,
color: theme === 'DARK' ? '#ffffff' : '#1a1a1a',
}
if (showSettings) {
return (
<div style={bannerStyle} className={className}>
<h3 style={{ margin: '0 0 15px', fontSize: '18px' }}>
{config?.texts?.settings || 'Cookie-Einstellungen'}
</h3>
<div style={{ marginBottom: '20px' }}>
{Object.entries({
ESSENTIAL: { name: 'Notwendig', description: 'Erforderlich für die Grundfunktionen' },
FUNCTIONAL: { name: 'Funktional', description: 'Verbesserte Funktionen' },
ANALYTICS: { name: 'Analyse', description: 'Nutzungsstatistiken' },
MARKETING: { name: 'Marketing', description: 'Personalisierte Werbung' },
}).map(([key, { name, description }]) => (
<div
key={key}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '10px 0',
borderBottom: `1px solid ${theme === 'DARK' ? '#333' : '#eee'}`,
}}
>
<div>
<div style={{ fontWeight: 500 }}>{name}</div>
<div style={{ fontSize: '12px', opacity: 0.7 }}>{description}</div>
</div>
<label style={{ cursor: key === 'ESSENTIAL' ? 'not-allowed' : 'pointer' }}>
<input
type="checkbox"
checked={consents[key as ConsentPurpose]}
onChange={() => handleConsentToggle(key as ConsentPurpose)}
disabled={key === 'ESSENTIAL'}
style={{ width: '20px', height: '20px' }}
/>
</label>
</div>
))}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
<button style={primaryButtonStyle} onClick={handleSaveSettings}>
{config?.texts?.save || 'Einstellungen speichern'}
</button>
<button style={secondaryButtonStyle} onClick={() => setShowSettings(false)}>
Zurück
</button>
</div>
</div>
)
}
return (
<div style={bannerStyle} className={className}>
<div style={{ marginBottom: '15px' }}>
<h3 style={{ margin: '0 0 10px', fontSize: '18px' }}>
{config?.texts?.title || 'Cookie-Einwilligung'}
</h3>
<p style={{ margin: 0, fontSize: '14px', opacity: 0.8 }}>
{config?.texts?.description ||
'Wir verwenden Cookies, um Ihre Erfahrung zu verbessern. Weitere Informationen finden Sie in unserer Datenschutzerklärung.'}
</p>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', alignItems: 'center' }}>
<button style={primaryButtonStyle} onClick={handleAcceptAll}>
{config?.texts?.acceptAll || 'Alle akzeptieren'}
</button>
<button style={secondaryButtonStyle} onClick={handleRejectAll}>
{config?.texts?.rejectAll || 'Nur notwendige'}
</button>
<button style={secondaryButtonStyle} onClick={() => setShowSettings(true)}>
{config?.texts?.settings || 'Einstellungen'}
</button>
<div style={{ marginLeft: 'auto', fontSize: '12px' }}>
<a href={privacyPolicyUrl} style={{ marginRight: '15px', color: 'inherit' }}>
Datenschutz
</a>
<a href={imprintUrl} style={{ color: 'inherit' }}>
Impressum
</a>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,240 @@
'use client'
import React, { useState, useCallback } from 'react'
import { useCompliance } from '../provider'
import type { DSRRequestType } from '@breakpilot/compliance-sdk-types'
export interface DSRPortalProps {
onSubmit?: (type: DSRRequestType, email: string, name: string) => void
className?: string
style?: React.CSSProperties
}
const DSR_TYPES: Record<DSRRequestType, { name: string; description: string }> = {
ACCESS: {
name: 'Auskunft (Art. 15)',
description: 'Welche Daten haben Sie über mich gespeichert?',
},
RECTIFICATION: {
name: 'Berichtigung (Art. 16)',
description: 'Korrigieren Sie falsche Daten über mich.',
},
ERASURE: {
name: 'Löschung (Art. 17)',
description: 'Löschen Sie alle meine personenbezogenen Daten.',
},
PORTABILITY: {
name: 'Datenübertragbarkeit (Art. 20)',
description: 'Exportieren Sie meine Daten in einem maschinenlesbaren Format.',
},
RESTRICTION: {
name: 'Einschränkung (Art. 18)',
description: 'Schränken Sie die Verarbeitung meiner Daten ein.',
},
OBJECTION: {
name: 'Widerspruch (Art. 21)',
description: 'Ich widerspreche der Verarbeitung meiner Daten.',
},
}
export function DSRPortal({ onSubmit, className, style }: DSRPortalProps) {
const { dsgvo } = useCompliance()
const [selectedType, setSelectedType] = useState<DSRRequestType | null>(null)
const [email, setEmail] = useState('')
const [name, setName] = useState('')
const [additionalInfo, setAdditionalInfo] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitted, setSubmitted] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault()
if (!selectedType || !email || !name) return
setIsSubmitting(true)
setError(null)
try {
await dsgvo.submitDSR(selectedType, email, name)
onSubmit?.(selectedType, email, name)
setSubmitted(true)
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten')
} finally {
setIsSubmitting(false)
}
},
[selectedType, email, name, dsgvo, onSubmit]
)
const containerStyle: React.CSSProperties = {
fontFamily: 'system-ui, -apple-system, sans-serif',
maxWidth: '600px',
margin: '0 auto',
padding: '20px',
...style,
}
const inputStyle: React.CSSProperties = {
width: '100%',
padding: '12px',
fontSize: '14px',
border: '1px solid #ddd',
borderRadius: '4px',
marginBottom: '15px',
boxSizing: 'border-box',
}
const buttonStyle: React.CSSProperties = {
padding: '12px 24px',
fontSize: '16px',
fontWeight: 500,
color: '#fff',
backgroundColor: '#1a1a1a',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
width: '100%',
}
if (submitted) {
return (
<div style={containerStyle} className={className}>
<div
style={{
textAlign: 'center',
padding: '40px 20px',
backgroundColor: '#f0fdf4',
borderRadius: '8px',
}}
>
<div style={{ fontSize: '48px', marginBottom: '20px' }}></div>
<h2 style={{ margin: '0 0 10px', color: '#166534' }}>Anfrage eingereicht</h2>
<p style={{ margin: 0, color: '#166534' }}>
Wir werden Ihre Anfrage innerhalb von 30 Tagen bearbeiten. Sie erhalten eine
Bestätigung per E-Mail an {email}.
</p>
</div>
</div>
)
}
return (
<div style={containerStyle} className={className}>
<h2 style={{ margin: '0 0 10px' }}>Betroffenenrechte-Portal</h2>
<p style={{ margin: '0 0 20px', color: '#666' }}>
Hier können Sie Ihre Rechte gemäß DSGVO wahrnehmen. Wählen Sie die gewünschte Anfrage und
füllen Sie das Formular aus.
</p>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '10px' }}>
Art der Anfrage *
</label>
<div style={{ display: 'grid', gap: '10px' }}>
{Object.entries(DSR_TYPES).map(([type, { name, description }]) => (
<label
key={type}
style={{
display: 'flex',
padding: '15px',
border: `2px solid ${selectedType === type ? '#1a1a1a' : '#ddd'}`,
borderRadius: '8px',
cursor: 'pointer',
backgroundColor: selectedType === type ? '#f5f5f5' : '#fff',
}}
>
<input
type="radio"
name="dsrType"
value={type}
checked={selectedType === type}
onChange={() => setSelectedType(type as DSRRequestType)}
style={{ marginRight: '15px' }}
/>
<div>
<div style={{ fontWeight: 500 }}>{name}</div>
<div style={{ fontSize: '13px', color: '#666' }}>{description}</div>
</div>
</label>
))}
</div>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '5px' }}>
Ihr Name *
</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
required
placeholder="Max Mustermann"
style={inputStyle}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '5px' }}>
E-Mail-Adresse *
</label>
<input
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
placeholder="max@example.com"
style={inputStyle}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', fontWeight: 500, marginBottom: '5px' }}>
Zusätzliche Informationen (optional)
</label>
<textarea
value={additionalInfo}
onChange={e => setAdditionalInfo(e.target.value)}
placeholder="Weitere Details zu Ihrer Anfrage..."
rows={4}
style={{ ...inputStyle, resize: 'vertical' }}
/>
</div>
{error && (
<div
style={{
padding: '12px',
backgroundColor: '#fef2f2',
color: '#dc2626',
borderRadius: '4px',
marginBottom: '15px',
}}
>
{error}
</div>
)}
<button
type="submit"
disabled={!selectedType || !email || !name || isSubmitting}
style={{
...buttonStyle,
opacity: !selectedType || !email || !name || isSubmitting ? 0.5 : 1,
cursor: !selectedType || !email || !name || isSubmitting ? 'not-allowed' : 'pointer',
}}
>
{isSubmitting ? 'Wird gesendet...' : 'Anfrage einreichen'}
</button>
</form>
<p style={{ marginTop: '20px', fontSize: '12px', color: '#666' }}>
Ihre Anfrage wird gemäß Art. 12 DSGVO innerhalb von einem Monat bearbeitet. In komplexen
Fällen kann diese Frist um weitere zwei Monate verlängert werden.
</p>
</div>
)
}

View File

@@ -0,0 +1,193 @@
'use client'
import React from 'react'
import type { Risk, RiskLikelihood, RiskImpact } from '@breakpilot/compliance-sdk-types'
export interface RiskMatrixProps {
risks: Risk[]
onRiskClick?: (risk: Risk) => void
className?: string
style?: React.CSSProperties
}
export function RiskMatrix({ risks, onRiskClick, className, style }: RiskMatrixProps) {
const getColor = (likelihood: number, impact: number): string => {
const score = likelihood * impact
if (score >= 20) return '#dc2626' // Critical - Red
if (score >= 12) return '#f97316' // High - Orange
if (score >= 6) return '#f59e0b' // Medium - Yellow
return '#16a34a' // Low - Green
}
const getCellRisks = (likelihood: RiskLikelihood, impact: RiskImpact): Risk[] => {
return risks.filter(r => r.likelihood === likelihood && r.impact === impact)
}
const containerStyle: React.CSSProperties = {
fontFamily: 'system-ui, -apple-system, sans-serif',
...style,
}
const gridStyle: React.CSSProperties = {
display: 'grid',
gridTemplateColumns: '60px repeat(5, 1fr)',
gridTemplateRows: '30px repeat(5, 60px)',
gap: '2px',
backgroundColor: '#e5e5e5',
padding: '2px',
borderRadius: '8px',
}
const cellStyle: React.CSSProperties = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
position: 'relative',
}
const headerStyle: React.CSSProperties = {
...cellStyle,
fontWeight: 600,
fontSize: '12px',
color: '#666',
}
const likelihoodLabels = ['Sehr niedrig', 'Niedrig', 'Mittel', 'Hoch', 'Sehr hoch']
const impactLabels = ['1', '2', '3', '4', '5']
return (
<div style={containerStyle} className={className}>
<div style={{ display: 'flex', marginBottom: '10px' }}>
<div style={{ flex: 1 }}>
<span style={{ fontWeight: 600 }}>Risikomatrix</span>
<span style={{ marginLeft: '15px', color: '#666', fontSize: '14px' }}>
{risks.length} Risiken
</span>
</div>
<div style={{ display: 'flex', gap: '10px', fontSize: '12px' }}>
<span>
<span
style={{
display: 'inline-block',
width: '12px',
height: '12px',
backgroundColor: '#16a34a',
borderRadius: '2px',
marginRight: '4px',
}}
/>
Niedrig
</span>
<span>
<span
style={{
display: 'inline-block',
width: '12px',
height: '12px',
backgroundColor: '#f59e0b',
borderRadius: '2px',
marginRight: '4px',
}}
/>
Mittel
</span>
<span>
<span
style={{
display: 'inline-block',
width: '12px',
height: '12px',
backgroundColor: '#f97316',
borderRadius: '2px',
marginRight: '4px',
}}
/>
Hoch
</span>
<span>
<span
style={{
display: 'inline-block',
width: '12px',
height: '12px',
backgroundColor: '#dc2626',
borderRadius: '2px',
marginRight: '4px',
}}
/>
Kritisch
</span>
</div>
</div>
<div style={gridStyle}>
{/* Header row */}
<div style={headerStyle} />
{impactLabels.map((label, i) => (
<div key={`impact-${i}`} style={headerStyle}>
Auswirkung {label}
</div>
))}
{/* Data rows (reversed so high likelihood is at top) */}
{[5, 4, 3, 2, 1].map(likelihood => (
<React.Fragment key={`row-${likelihood}`}>
<div style={headerStyle}>{likelihoodLabels[likelihood - 1]}</div>
{[1, 2, 3, 4, 5].map(impact => {
const cellRisks = getCellRisks(likelihood as RiskLikelihood, impact as RiskImpact)
return (
<div
key={`cell-${likelihood}-${impact}`}
style={{
...cellStyle,
backgroundColor: getColor(likelihood, impact),
opacity: cellRisks.length > 0 ? 1 : 0.3,
cursor: cellRisks.length > 0 ? 'pointer' : 'default',
}}
onClick={() => {
if (cellRisks.length > 0 && onRiskClick) {
onRiskClick(cellRisks[0])
}
}}
>
{cellRisks.length > 0 && (
<div
style={{
width: '24px',
height: '24px',
borderRadius: '50%',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
fontSize: '12px',
color: '#1a1a1a',
}}
>
{cellRisks.length}
</div>
)}
</div>
)
})}
</React.Fragment>
))}
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '10px',
fontSize: '12px',
color: '#666',
}}
>
<span>Wahrscheinlichkeit </span>
<span>Auswirkung </span>
</div>
</div>
)
}