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:
12
breakpilot-compliance-sdk/packages/react/src/components.ts
Normal file
12
breakpilot-compliance-sdk/packages/react/src/components.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* React Components
|
||||
*
|
||||
* Pre-built UI components for common compliance functionality
|
||||
*/
|
||||
|
||||
// Re-export all components
|
||||
export { ConsentBanner, type ConsentBannerProps } from './components/ConsentBanner'
|
||||
export { DSRPortal, type DSRPortalProps } from './components/DSRPortal'
|
||||
export { ComplianceDashboard, type ComplianceDashboardProps } from './components/ComplianceDashboard'
|
||||
export { ComplianceScore, type ComplianceScoreProps } from './components/ComplianceScore'
|
||||
export { RiskMatrix, type RiskMatrixProps } from './components/RiskMatrix'
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
474
breakpilot-compliance-sdk/packages/react/src/hooks.ts
Normal file
474
breakpilot-compliance-sdk/packages/react/src/hooks.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useMemo, useCallback } from 'react'
|
||||
import { ComplianceContext, type ComplianceContextValue } from './provider'
|
||||
import type {
|
||||
SDKState,
|
||||
SDKAction,
|
||||
Control,
|
||||
Evidence,
|
||||
Risk,
|
||||
Requirement,
|
||||
Obligation,
|
||||
TOM,
|
||||
ProcessingActivity,
|
||||
ConsentPurpose,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// MAIN HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useCompliance(): ComplianceContextValue {
|
||||
const context = useContext(ComplianceContext)
|
||||
if (!context) {
|
||||
throw new Error('useCompliance must be used within ComplianceProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// STATE HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useComplianceState(): SDKState {
|
||||
const { state } = useCompliance()
|
||||
return state
|
||||
}
|
||||
|
||||
export function useComplianceDispatch(): React.Dispatch<SDKAction> {
|
||||
const { dispatch } = useCompliance()
|
||||
return dispatch
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DSGVO HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useDSGVO() {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// DSR
|
||||
dsrRequests: state.dsrRequests,
|
||||
dsrConfig: state.dsrConfig,
|
||||
submitDSR: dsgvo.submitDSR.bind(dsgvo),
|
||||
|
||||
// Consent
|
||||
consents: state.consents,
|
||||
hasConsent: dsgvo.hasConsent.bind(dsgvo),
|
||||
getConsentsByUserId: dsgvo.getConsentsByUserId.bind(dsgvo),
|
||||
|
||||
// VVT
|
||||
processingActivities: state.vvt,
|
||||
getProcessingActivityById: dsgvo.getProcessingActivityById.bind(dsgvo),
|
||||
|
||||
// DSFA
|
||||
dsfa: state.dsfa,
|
||||
isDSFARequired: dsgvo.isDSFARequired.bind(dsgvo),
|
||||
|
||||
// TOMs
|
||||
toms: state.toms,
|
||||
getTOMsByCategory: dsgvo.getTOMsByCategory.bind(dsgvo),
|
||||
getTOMScore: dsgvo.getTOMScore.bind(dsgvo),
|
||||
|
||||
// Retention
|
||||
retentionPolicies: state.retentionPolicies,
|
||||
getUpcomingDeletions: dsgvo.getUpcomingDeletions.bind(dsgvo),
|
||||
|
||||
// Cookie Banner
|
||||
cookieBanner: state.cookieBanner,
|
||||
generateCookieBannerCode: dsgvo.generateCookieBannerCode.bind(dsgvo),
|
||||
}),
|
||||
[dsgvo, state]
|
||||
)
|
||||
}
|
||||
|
||||
export function useConsent(userId: string) {
|
||||
const { dsgvo, state } = useCompliance()
|
||||
|
||||
return useMemo(() => {
|
||||
const userConsents = state.consents.filter(c => c.userId === userId)
|
||||
|
||||
return {
|
||||
consents: userConsents,
|
||||
hasConsent: (purpose: ConsentPurpose) => dsgvo.hasConsent(userId, purpose),
|
||||
hasAnalyticsConsent: dsgvo.hasConsent(userId, 'ANALYTICS'),
|
||||
hasMarketingConsent: dsgvo.hasConsent(userId, 'MARKETING'),
|
||||
hasFunctionalConsent: dsgvo.hasConsent(userId, 'FUNCTIONAL'),
|
||||
}
|
||||
}, [dsgvo, state.consents, userId])
|
||||
}
|
||||
|
||||
export function useDSR() {
|
||||
const { dsgvo, state, dispatch } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
requests: state.dsrRequests,
|
||||
config: state.dsrConfig,
|
||||
submitRequest: dsgvo.submitDSR.bind(dsgvo),
|
||||
pendingRequests: state.dsrRequests.filter(r => r.status !== 'COMPLETED' && r.status !== 'REJECTED'),
|
||||
overdueRequests: state.dsrRequests.filter(r => {
|
||||
if (r.status === 'COMPLETED' || r.status === 'REJECTED') return false
|
||||
return new Date(r.dueDate) < new Date()
|
||||
}),
|
||||
}),
|
||||
[dsgvo, state.dsrRequests, state.dsrConfig]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPLIANCE HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useComplianceModule() {
|
||||
const { compliance, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// Controls
|
||||
controls: state.controls,
|
||||
getControlById: compliance.getControlById.bind(compliance),
|
||||
getControlsByDomain: compliance.getControlsByDomain.bind(compliance),
|
||||
getControlsByStatus: compliance.getControlsByStatus.bind(compliance),
|
||||
controlComplianceRate: compliance.getControlComplianceRate(),
|
||||
|
||||
// Evidence
|
||||
evidence: state.evidence,
|
||||
getEvidenceByControlId: compliance.getEvidenceByControlId.bind(compliance),
|
||||
expiringEvidence: compliance.getExpiringEvidence(),
|
||||
|
||||
// Requirements
|
||||
requirements: state.requirements,
|
||||
getRequirementsByRegulation: compliance.getRequirementsByRegulation.bind(compliance),
|
||||
requirementComplianceRate: compliance.getRequirementComplianceRate(),
|
||||
|
||||
// Obligations
|
||||
obligations: state.obligations,
|
||||
upcomingObligations: compliance.getUpcomingObligations(),
|
||||
overdueObligations: compliance.getOverdueObligations(),
|
||||
|
||||
// AI Act
|
||||
aiActClassification: state.aiActClassification,
|
||||
aiActRiskCategory: compliance.getAIActRiskCategory(),
|
||||
isHighRiskAI: compliance.isHighRiskAI(),
|
||||
|
||||
// Score
|
||||
complianceScore: compliance.calculateComplianceScore(),
|
||||
|
||||
// Risks
|
||||
risks: state.risks,
|
||||
criticalRisks: compliance.getCriticalRisks(),
|
||||
averageRiskScore: compliance.getAverageRiskScore(),
|
||||
}),
|
||||
[compliance, state]
|
||||
)
|
||||
}
|
||||
|
||||
export function useControls() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addControl = useCallback(
|
||||
(control: Control) => {
|
||||
dispatch({ type: 'ADD_CONTROL', payload: control })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateControl = useCallback(
|
||||
(id: string, data: Partial<Control>) => {
|
||||
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
controls: state.controls,
|
||||
addControl,
|
||||
updateControl,
|
||||
getById: compliance.getControlById.bind(compliance),
|
||||
getByDomain: compliance.getControlsByDomain.bind(compliance),
|
||||
getByStatus: compliance.getControlsByStatus.bind(compliance),
|
||||
implementedCount: state.controls.filter(c => c.implementationStatus === 'IMPLEMENTED').length,
|
||||
totalCount: state.controls.length,
|
||||
complianceRate: compliance.getControlComplianceRate(),
|
||||
}),
|
||||
[state.controls, compliance, addControl, updateControl]
|
||||
)
|
||||
}
|
||||
|
||||
export function useEvidence() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addEvidence = useCallback(
|
||||
(evidence: Evidence) => {
|
||||
dispatch({ type: 'ADD_EVIDENCE', payload: evidence })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateEvidence = useCallback(
|
||||
(id: string, data: Partial<Evidence>) => {
|
||||
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteEvidence = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
evidence: state.evidence,
|
||||
addEvidence,
|
||||
updateEvidence,
|
||||
deleteEvidence,
|
||||
getByControlId: compliance.getEvidenceByControlId.bind(compliance),
|
||||
expiringEvidence: compliance.getExpiringEvidence(),
|
||||
activeCount: state.evidence.filter(e => e.status === 'ACTIVE').length,
|
||||
totalCount: state.evidence.length,
|
||||
}),
|
||||
[state.evidence, compliance, addEvidence, updateEvidence, deleteEvidence]
|
||||
)
|
||||
}
|
||||
|
||||
export function useRisks() {
|
||||
const { compliance, state, dispatch } = useCompliance()
|
||||
|
||||
const addRisk = useCallback(
|
||||
(risk: Risk) => {
|
||||
dispatch({ type: 'ADD_RISK', payload: risk })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const updateRisk = useCallback(
|
||||
(id: string, data: Partial<Risk>) => {
|
||||
dispatch({ type: 'UPDATE_RISK', payload: { id, data } })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
const deleteRisk = useCallback(
|
||||
(id: string) => {
|
||||
dispatch({ type: 'DELETE_RISK', payload: id })
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
risks: state.risks,
|
||||
addRisk,
|
||||
updateRisk,
|
||||
deleteRisk,
|
||||
criticalRisks: compliance.getCriticalRisks(),
|
||||
getByStatus: compliance.getRisksByStatus.bind(compliance),
|
||||
getBySeverity: compliance.getRisksBySeverity.bind(compliance),
|
||||
averageScore: compliance.getAverageRiskScore(),
|
||||
}),
|
||||
[state.risks, compliance, addRisk, updateRisk, deleteRisk]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RAG HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useRAG() {
|
||||
const { rag } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
search: rag.search.bind(rag),
|
||||
searchByRegulation: rag.searchByRegulation.bind(rag),
|
||||
searchByArticle: rag.searchByArticle.bind(rag),
|
||||
ask: rag.ask.bind(rag),
|
||||
askAboutRegulation: rag.askAboutRegulation.bind(rag),
|
||||
explainArticle: rag.explainArticle.bind(rag),
|
||||
checkCompliance: rag.checkCompliance.bind(rag),
|
||||
getQuickAnswer: rag.getQuickAnswer.bind(rag),
|
||||
findRelevantArticles: rag.findRelevantArticles.bind(rag),
|
||||
availableRegulations: rag.getAvailableRegulations(),
|
||||
chatHistory: rag.getChatHistory(),
|
||||
clearChatHistory: rag.clearChatHistory.bind(rag),
|
||||
startNewSession: rag.startNewSession.bind(rag),
|
||||
}),
|
||||
[rag]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SECURITY HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSecurity() {
|
||||
const { security, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
// SBOM
|
||||
sbom: state.sbom,
|
||||
components: security.getComponents(),
|
||||
vulnerableComponents: security.getVulnerableComponents(),
|
||||
licenseSummary: security.getLicenseSummary(),
|
||||
|
||||
// Issues
|
||||
issues: state.securityIssues,
|
||||
openIssues: security.getOpenIssues(),
|
||||
criticalIssues: security.getCriticalIssues(),
|
||||
getIssuesBySeverity: security.getIssuesBySeverity.bind(security),
|
||||
getIssuesByTool: security.getIssuesByTool.bind(security),
|
||||
|
||||
// Backlog
|
||||
backlog: state.securityBacklog,
|
||||
overdueBacklogItems: security.getOverdueBacklogItems(),
|
||||
|
||||
// Scanning
|
||||
startScan: security.startScan.bind(security),
|
||||
getScanResult: security.getScanResult.bind(security),
|
||||
lastScanResult: security.getLastScanResult(),
|
||||
|
||||
// Summary
|
||||
summary: security.getSecuritySummary(),
|
||||
securityScore: security.getSecurityScore(),
|
||||
availableTools: security.getAvailableTools(),
|
||||
}),
|
||||
[security, state]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// NAVIGATION HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSDKNavigation() {
|
||||
const {
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
state,
|
||||
} = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
currentStep,
|
||||
currentPhase: state.currentPhase,
|
||||
completedSteps: state.completedSteps,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
}),
|
||||
[
|
||||
currentStep,
|
||||
state.currentPhase,
|
||||
state.completedSteps,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SYNC HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useSync() {
|
||||
const { syncState, forceSyncToServer, isOnline, saveState, loadState } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
status: syncState.status,
|
||||
lastSyncedAt: syncState.lastSyncedAt,
|
||||
pendingChanges: syncState.pendingChanges,
|
||||
error: syncState.error,
|
||||
isOnline,
|
||||
isSyncing: syncState.status === 'syncing',
|
||||
hasConflict: syncState.status === 'conflict',
|
||||
forceSyncToServer,
|
||||
saveState,
|
||||
loadState,
|
||||
}),
|
||||
[syncState, isOnline, forceSyncToServer, saveState, loadState]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CHECKPOINT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useCheckpoints() {
|
||||
const { validateCheckpoint, overrideCheckpoint, getCheckpointStatus, state } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
checkpoints: state.checkpoints,
|
||||
validateCheckpoint,
|
||||
overrideCheckpoint,
|
||||
getCheckpointStatus,
|
||||
passedCheckpoints: Object.values(state.checkpoints).filter(c => c.passed).length,
|
||||
totalCheckpoints: Object.keys(state.checkpoints).length,
|
||||
}),
|
||||
[state.checkpoints, validateCheckpoint, overrideCheckpoint, getCheckpointStatus]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT HOOKS
|
||||
// =============================================================================
|
||||
|
||||
export function useExport() {
|
||||
const { exportState } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
exportJSON: () => exportState('json'),
|
||||
exportPDF: () => exportState('pdf'),
|
||||
exportZIP: () => exportState('zip'),
|
||||
exportState,
|
||||
}),
|
||||
[exportState]
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMAND BAR HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useCommandBar() {
|
||||
const { isCommandBarOpen, setCommandBarOpen } = useCompliance()
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isOpen: isCommandBarOpen,
|
||||
open: () => setCommandBarOpen(true),
|
||||
close: () => setCommandBarOpen(false),
|
||||
toggle: () => setCommandBarOpen(!isCommandBarOpen),
|
||||
}),
|
||||
[isCommandBarOpen, setCommandBarOpen]
|
||||
)
|
||||
}
|
||||
14
breakpilot-compliance-sdk/packages/react/src/index.ts
Normal file
14
breakpilot-compliance-sdk/packages/react/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @breakpilot/compliance-sdk-react
|
||||
*
|
||||
* React components and hooks for BreakPilot Compliance SDK
|
||||
*/
|
||||
|
||||
// Provider
|
||||
export { ComplianceProvider, ComplianceContext, type ComplianceProviderProps } from './provider'
|
||||
|
||||
// Hooks
|
||||
export * from './hooks'
|
||||
|
||||
// Components
|
||||
export * from './components'
|
||||
539
breakpilot-compliance-sdk/packages/react/src/provider.tsx
Normal file
539
breakpilot-compliance-sdk/packages/react/src/provider.tsx
Normal file
@@ -0,0 +1,539 @@
|
||||
'use client'
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
ComplianceClient,
|
||||
sdkReducer,
|
||||
initialState,
|
||||
StateSyncManager,
|
||||
createStateSyncManager,
|
||||
createDSGVOModule,
|
||||
createComplianceModule,
|
||||
createRAGModule,
|
||||
createSecurityModule,
|
||||
type DSGVOModule,
|
||||
type ComplianceModule,
|
||||
type RAGModule,
|
||||
type SecurityModule,
|
||||
} from '@breakpilot/compliance-sdk-core'
|
||||
import type {
|
||||
SDKState,
|
||||
SDKAction,
|
||||
SDKStep,
|
||||
CheckpointStatus,
|
||||
SyncState,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
UserPreferences,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
import {
|
||||
getStepById,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
getCompletionPercentage,
|
||||
getPhaseCompletionPercentage,
|
||||
} from '@breakpilot/compliance-sdk-types'
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface ComplianceContextValue {
|
||||
// State
|
||||
state: SDKState
|
||||
dispatch: React.Dispatch<SDKAction>
|
||||
|
||||
// Client
|
||||
client: ComplianceClient
|
||||
|
||||
// Modules
|
||||
dsgvo: DSGVOModule
|
||||
compliance: ComplianceModule
|
||||
rag: RAGModule
|
||||
security: SecurityModule
|
||||
|
||||
// Navigation
|
||||
currentStep: SDKStep | undefined
|
||||
goToStep: (stepId: string) => void
|
||||
goToNextStep: () => void
|
||||
goToPreviousStep: () => void
|
||||
canGoNext: boolean
|
||||
canGoPrevious: boolean
|
||||
|
||||
// Progress
|
||||
completionPercentage: number
|
||||
phase1Completion: number
|
||||
phase2Completion: number
|
||||
|
||||
// Checkpoints
|
||||
validateCheckpoint: (checkpointId: string) => Promise<CheckpointStatus>
|
||||
overrideCheckpoint: (checkpointId: string, reason: string) => Promise<void>
|
||||
getCheckpointStatus: (checkpointId: string) => CheckpointStatus | undefined
|
||||
|
||||
// State Updates
|
||||
updateUseCase: (id: string, data: Partial<UseCaseAssessment>) => void
|
||||
addRisk: (risk: Risk) => void
|
||||
updateControl: (id: string, data: Partial<Control>) => void
|
||||
|
||||
// Persistence
|
||||
saveState: () => Promise<void>
|
||||
loadState: () => Promise<void>
|
||||
resetState: () => void
|
||||
|
||||
// Sync
|
||||
syncState: SyncState
|
||||
forceSyncToServer: () => Promise<void>
|
||||
isOnline: boolean
|
||||
|
||||
// Export
|
||||
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
|
||||
|
||||
// Command Bar
|
||||
isCommandBarOpen: boolean
|
||||
setCommandBarOpen: (open: boolean) => void
|
||||
|
||||
// Status
|
||||
isInitialized: boolean
|
||||
isLoading: boolean
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export const ComplianceContext = createContext<ComplianceContextValue | null>(null)
|
||||
|
||||
// =============================================================================
|
||||
// PROVIDER PROPS
|
||||
// =============================================================================
|
||||
|
||||
export interface ComplianceProviderProps {
|
||||
children: React.ReactNode
|
||||
apiEndpoint: string
|
||||
apiKey?: string
|
||||
tenantId: string
|
||||
userId?: string
|
||||
enableBackendSync?: boolean
|
||||
onNavigate?: (url: string) => void
|
||||
onError?: (error: Error) => void
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROVIDER
|
||||
// =============================================================================
|
||||
|
||||
const SDK_STORAGE_KEY = 'breakpilot-compliance-sdk-state'
|
||||
|
||||
export function ComplianceProvider({
|
||||
children,
|
||||
apiEndpoint,
|
||||
apiKey,
|
||||
tenantId,
|
||||
userId = 'default',
|
||||
enableBackendSync = true,
|
||||
onNavigate,
|
||||
onError,
|
||||
}: ComplianceProviderProps) {
|
||||
const [state, dispatch] = useReducer(sdkReducer, {
|
||||
...initialState,
|
||||
tenantId,
|
||||
userId,
|
||||
})
|
||||
const [isCommandBarOpen, setCommandBarOpen] = useState(false)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [syncState, setSyncState] = useState<SyncState>({
|
||||
status: 'idle',
|
||||
lastSyncedAt: null,
|
||||
localVersion: 0,
|
||||
serverVersion: 0,
|
||||
pendingChanges: 0,
|
||||
error: null,
|
||||
})
|
||||
const [isOnline, setIsOnline] = useState(true)
|
||||
|
||||
// Refs
|
||||
const clientRef = useRef<ComplianceClient | null>(null)
|
||||
const syncManagerRef = useRef<StateSyncManager | null>(null)
|
||||
|
||||
// Initialize client
|
||||
if (!clientRef.current) {
|
||||
clientRef.current = new ComplianceClient({
|
||||
apiEndpoint,
|
||||
apiKey,
|
||||
tenantId,
|
||||
onError: err => {
|
||||
setError(err)
|
||||
onError?.(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const client = clientRef.current
|
||||
|
||||
// Modules
|
||||
const dsgvo = useMemo(
|
||||
() => createDSGVOModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
const compliance = useMemo(
|
||||
() => createComplianceModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
const rag = useMemo(() => createRAGModule(client), [client])
|
||||
const security = useMemo(
|
||||
() => createSecurityModule(client, () => state),
|
||||
[client, state]
|
||||
)
|
||||
|
||||
// Initialize sync manager
|
||||
useEffect(() => {
|
||||
if (enableBackendSync && typeof window !== 'undefined') {
|
||||
syncManagerRef.current = createStateSyncManager(
|
||||
client,
|
||||
tenantId,
|
||||
{ debounceMs: 2000, maxRetries: 3 },
|
||||
{
|
||||
onSyncStart: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
||||
},
|
||||
onSyncComplete: syncedState => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'idle',
|
||||
lastSyncedAt: new Date(),
|
||||
pendingChanges: 0,
|
||||
}))
|
||||
if (new Date(syncedState.lastModified) > new Date(state.lastModified)) {
|
||||
dispatch({ type: 'SET_STATE', payload: syncedState })
|
||||
}
|
||||
},
|
||||
onSyncError: err => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'error',
|
||||
error: err.message,
|
||||
}))
|
||||
setError(err)
|
||||
},
|
||||
onConflict: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'conflict' }))
|
||||
},
|
||||
onOffline: () => {
|
||||
setIsOnline(false)
|
||||
setSyncState(prev => ({ ...prev, status: 'offline' }))
|
||||
},
|
||||
onOnline: () => {
|
||||
setIsOnline(true)
|
||||
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
syncManagerRef.current?.destroy()
|
||||
}
|
||||
}, [enableBackendSync, tenantId, client])
|
||||
|
||||
// Load initial state
|
||||
useEffect(() => {
|
||||
const loadInitialState = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Load from localStorage first
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
if (parsed.lastModified) {
|
||||
parsed.lastModified = new Date(parsed.lastModified)
|
||||
}
|
||||
dispatch({ type: 'SET_STATE', payload: parsed })
|
||||
}
|
||||
}
|
||||
|
||||
// Then load from server if enabled
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
onError?.(err as Error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}
|
||||
|
||||
loadInitialState()
|
||||
}, [tenantId, enableBackendSync])
|
||||
|
||||
// Auto-save
|
||||
useEffect(() => {
|
||||
if (!isInitialized || !state.preferences.autoSave) return
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
syncManagerRef.current.queueSync(state)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to save state:', err)
|
||||
}
|
||||
}, 1000)
|
||||
|
||||
return () => clearTimeout(saveTimeout)
|
||||
}, [state, tenantId, isInitialized, enableBackendSync])
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setCommandBarOpen(prev => !prev)
|
||||
}
|
||||
if (e.key === 'Escape' && isCommandBarOpen) {
|
||||
setCommandBarOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [isCommandBarOpen])
|
||||
|
||||
// Navigation
|
||||
const currentStep = useMemo(() => getStepById(state.currentStep), [state.currentStep])
|
||||
|
||||
const goToStep = useCallback(
|
||||
(stepId: string) => {
|
||||
const step = getStepById(stepId)
|
||||
if (step) {
|
||||
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
||||
onNavigate?.(step.url)
|
||||
}
|
||||
},
|
||||
[onNavigate]
|
||||
)
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
const nextStep = getNextStep(state.currentStep)
|
||||
if (nextStep) {
|
||||
goToStep(nextStep.id)
|
||||
}
|
||||
}, [state.currentStep, goToStep])
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
const prevStep = getPreviousStep(state.currentStep)
|
||||
if (prevStep) {
|
||||
goToStep(prevStep.id)
|
||||
}
|
||||
}, [state.currentStep, goToStep])
|
||||
|
||||
const canGoNext = useMemo(() => getNextStep(state.currentStep) !== undefined, [state.currentStep])
|
||||
const canGoPrevious = useMemo(
|
||||
() => getPreviousStep(state.currentStep) !== undefined,
|
||||
[state.currentStep]
|
||||
)
|
||||
|
||||
// Progress
|
||||
const completionPercentage = useMemo(() => getCompletionPercentage(state), [state])
|
||||
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
|
||||
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
|
||||
|
||||
// Checkpoints
|
||||
const validateCheckpoint = useCallback(
|
||||
async (checkpointId: string): Promise<CheckpointStatus> => {
|
||||
if (enableBackendSync) {
|
||||
try {
|
||||
const result = await client.validateCheckpoint(checkpointId, state)
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId: result.checkpointId,
|
||||
passed: result.passed,
|
||||
validatedAt: new Date(result.validatedAt),
|
||||
validatedBy: result.validatedBy,
|
||||
errors: result.errors,
|
||||
warnings: result.warnings,
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
} catch {
|
||||
// Fall through to local validation
|
||||
}
|
||||
}
|
||||
|
||||
// Local validation
|
||||
const status: CheckpointStatus = {
|
||||
checkpointId,
|
||||
passed: true,
|
||||
validatedAt: new Date(),
|
||||
validatedBy: 'SYSTEM',
|
||||
errors: [],
|
||||
warnings: [],
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
},
|
||||
[state, enableBackendSync, client]
|
||||
)
|
||||
|
||||
const overrideCheckpoint = useCallback(
|
||||
async (checkpointId: string, reason: string): Promise<void> => {
|
||||
const existingStatus = state.checkpoints[checkpointId]
|
||||
const overriddenStatus: CheckpointStatus = {
|
||||
...existingStatus,
|
||||
checkpointId,
|
||||
passed: true,
|
||||
overrideReason: reason,
|
||||
overriddenBy: state.userId,
|
||||
overriddenAt: new Date(),
|
||||
errors: [],
|
||||
warnings: existingStatus?.warnings || [],
|
||||
}
|
||||
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overriddenStatus } })
|
||||
},
|
||||
[state.checkpoints, state.userId]
|
||||
)
|
||||
|
||||
const getCheckpointStatus = useCallback(
|
||||
(checkpointId: string): CheckpointStatus | undefined => {
|
||||
return state.checkpoints[checkpointId]
|
||||
},
|
||||
[state.checkpoints]
|
||||
)
|
||||
|
||||
// State Updates
|
||||
const updateUseCase = useCallback((id: string, data: Partial<UseCaseAssessment>) => {
|
||||
dispatch({ type: 'UPDATE_USE_CASE', payload: { id, data } })
|
||||
}, [])
|
||||
|
||||
const addRisk = useCallback((risk: Risk) => {
|
||||
dispatch({ type: 'ADD_RISK', payload: risk })
|
||||
}, [])
|
||||
|
||||
const updateControl = useCallback((id: string, data: Partial<Control>) => {
|
||||
dispatch({ type: 'UPDATE_CONTROL', payload: { id, data } })
|
||||
}, [])
|
||||
|
||||
// Persistence
|
||||
const saveState = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
||||
}
|
||||
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forceSync(state)
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
throw err
|
||||
}
|
||||
}, [state, tenantId, enableBackendSync])
|
||||
|
||||
const loadState = useCallback(async (): Promise<void> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
const serverState = await syncManagerRef.current.loadFromServer()
|
||||
if (serverState) {
|
||||
dispatch({ type: 'SET_STATE', payload: serverState })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
if (stored) {
|
||||
dispatch({ type: 'SET_STATE', payload: JSON.parse(stored) })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err as Error)
|
||||
throw err
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [tenantId, enableBackendSync])
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
dispatch({ type: 'RESET_STATE' })
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
||||
}
|
||||
}, [tenantId])
|
||||
|
||||
// Sync
|
||||
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forceSync(state)
|
||||
}
|
||||
}, [state, enableBackendSync])
|
||||
|
||||
// Export
|
||||
const exportState = useCallback(
|
||||
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
||||
if (format === 'json') {
|
||||
return new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
}
|
||||
return client.exportState(format)
|
||||
},
|
||||
[state, client]
|
||||
)
|
||||
|
||||
const value: ComplianceContextValue = {
|
||||
state,
|
||||
dispatch,
|
||||
client,
|
||||
dsgvo,
|
||||
compliance,
|
||||
rag,
|
||||
security,
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
completionPercentage,
|
||||
phase1Completion,
|
||||
phase2Completion,
|
||||
validateCheckpoint,
|
||||
overrideCheckpoint,
|
||||
getCheckpointStatus,
|
||||
updateUseCase,
|
||||
addRisk,
|
||||
updateControl,
|
||||
saveState,
|
||||
loadState,
|
||||
resetState,
|
||||
syncState,
|
||||
forceSyncToServer,
|
||||
isOnline,
|
||||
exportState,
|
||||
isCommandBarOpen,
|
||||
setCommandBarOpen,
|
||||
isInitialized,
|
||||
isLoading,
|
||||
error,
|
||||
}
|
||||
|
||||
return <ComplianceContext.Provider value={value}>{children}</ComplianceContext.Provider>
|
||||
}
|
||||
Reference in New Issue
Block a user