SDK modules added/enhanced: - compliance-hub, compliance-scope, consent-management, notfallplan - audit-report, workflow, source-policy, dsms - advisory-board documentation section - TOM dashboard components, TOM generator SDM mapping - DSFA: mitigation library, risk catalog, threshold analysis, source attribution - VVT: baseline catalog, profiling engine, types - Loeschfristen: baseline catalog, compliance engine, export, profiling, types - Compliance scope: engine, profiling, golden tests, types Existing SDK pages updated: - dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality - SDKSidebar, StepHeader — new navigation items and layout - SDK layout, context, types — expanded type system Other admin-v2 changes: - AI agents page, RAG pipeline DSFA integration - GridOverlay component updates - Companion feature (development + education) - Compliance advisor SOUL definition Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1113 lines
32 KiB
TypeScript
1113 lines
32 KiB
TypeScript
'use client'
|
|
|
|
import React, { createContext, useContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react'
|
|
import { useRouter, usePathname } from 'next/navigation'
|
|
import {
|
|
SDKState,
|
|
SDKAction,
|
|
SDKStep,
|
|
CheckpointStatus,
|
|
UseCaseAssessment,
|
|
Risk,
|
|
Control,
|
|
UserPreferences,
|
|
CustomerType,
|
|
CompanyProfile,
|
|
ImportedDocument,
|
|
GapAnalysis,
|
|
SDKPackageId,
|
|
SDK_STEPS,
|
|
SDK_PACKAGES,
|
|
getStepById,
|
|
getStepByUrl,
|
|
getNextStep,
|
|
getPreviousStep,
|
|
getCompletionPercentage,
|
|
getPhaseCompletionPercentage,
|
|
getPackageCompletionPercentage,
|
|
getStepsForPackage,
|
|
} from './types'
|
|
import { exportToPDF, exportToZIP } from './export'
|
|
import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client'
|
|
import { StateSyncManager, createStateSyncManager, SyncState } from './sync'
|
|
import { generateDemoState, seedDemoData as seedDemoDataApi, clearDemoData as clearDemoDataApi } from './demo-data'
|
|
|
|
// =============================================================================
|
|
// INITIAL STATE
|
|
// =============================================================================
|
|
|
|
const initialPreferences: UserPreferences = {
|
|
language: 'de',
|
|
theme: 'light',
|
|
compactMode: false,
|
|
showHints: true,
|
|
autoSave: true,
|
|
autoValidate: true,
|
|
allowParallelWork: true, // Standard: Paralleles Arbeiten erlaubt
|
|
}
|
|
|
|
const initialState: SDKState = {
|
|
// Metadata
|
|
version: '1.0.0',
|
|
lastModified: new Date(),
|
|
|
|
// Tenant & User
|
|
tenantId: '',
|
|
userId: '',
|
|
subscription: 'PROFESSIONAL',
|
|
|
|
// Customer Type
|
|
customerType: null,
|
|
|
|
// Company Profile
|
|
companyProfile: null,
|
|
|
|
// Compliance Scope
|
|
complianceScope: null,
|
|
|
|
// Progress
|
|
currentPhase: 1,
|
|
currentStep: 'company-profile',
|
|
completedSteps: [],
|
|
checkpoints: {},
|
|
|
|
// Imported Documents (for existing customers)
|
|
importedDocuments: [],
|
|
gapAnalysis: null,
|
|
|
|
// Phase 1 Data
|
|
useCases: [],
|
|
activeUseCase: null,
|
|
screening: null,
|
|
modules: [],
|
|
requirements: [],
|
|
controls: [],
|
|
evidence: [],
|
|
checklist: [],
|
|
risks: [],
|
|
|
|
// Phase 2 Data
|
|
aiActClassification: null,
|
|
obligations: [],
|
|
dsfa: null,
|
|
toms: [],
|
|
retentionPolicies: [],
|
|
vvt: [],
|
|
documents: [],
|
|
cookieBanner: null,
|
|
consents: [],
|
|
dsrConfig: null,
|
|
escalationWorkflows: [],
|
|
|
|
// Security
|
|
sbom: null,
|
|
securityIssues: [],
|
|
securityBacklog: [],
|
|
|
|
// UI State
|
|
commandBarHistory: [],
|
|
recentSearches: [],
|
|
preferences: initialPreferences,
|
|
}
|
|
|
|
// =============================================================================
|
|
// EXTENDED ACTION TYPES
|
|
// =============================================================================
|
|
|
|
// Extended action type to include demo data loading
|
|
type ExtendedSDKAction =
|
|
| SDKAction
|
|
| { type: 'LOAD_DEMO_DATA'; payload: Partial<SDKState> }
|
|
|
|
// =============================================================================
|
|
// REDUCER
|
|
// =============================================================================
|
|
|
|
function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState {
|
|
const updateState = (updates: Partial<SDKState>): SDKState => ({
|
|
...state,
|
|
...updates,
|
|
lastModified: new Date(),
|
|
})
|
|
|
|
switch (action.type) {
|
|
case 'SET_STATE':
|
|
return updateState(action.payload)
|
|
|
|
case 'LOAD_DEMO_DATA':
|
|
// Load demo data while preserving user preferences
|
|
return {
|
|
...initialState,
|
|
...action.payload,
|
|
tenantId: state.tenantId,
|
|
userId: state.userId,
|
|
preferences: state.preferences,
|
|
lastModified: new Date(),
|
|
}
|
|
|
|
case 'SET_CURRENT_STEP': {
|
|
const step = getStepById(action.payload)
|
|
return updateState({
|
|
currentStep: action.payload,
|
|
currentPhase: step?.phase || state.currentPhase,
|
|
})
|
|
}
|
|
|
|
case 'COMPLETE_STEP':
|
|
if (state.completedSteps.includes(action.payload)) {
|
|
return state
|
|
}
|
|
return updateState({
|
|
completedSteps: [...state.completedSteps, action.payload],
|
|
})
|
|
|
|
case 'SET_CHECKPOINT_STATUS':
|
|
return updateState({
|
|
checkpoints: {
|
|
...state.checkpoints,
|
|
[action.payload.id]: action.payload.status,
|
|
},
|
|
})
|
|
|
|
case 'SET_CUSTOMER_TYPE':
|
|
return updateState({ customerType: action.payload })
|
|
|
|
case 'SET_COMPANY_PROFILE':
|
|
return updateState({ companyProfile: action.payload })
|
|
|
|
case 'UPDATE_COMPANY_PROFILE':
|
|
return updateState({
|
|
companyProfile: state.companyProfile
|
|
? { ...state.companyProfile, ...action.payload }
|
|
: null,
|
|
})
|
|
|
|
case 'SET_COMPLIANCE_SCOPE':
|
|
return updateState({ complianceScope: action.payload })
|
|
|
|
case 'UPDATE_COMPLIANCE_SCOPE':
|
|
return updateState({
|
|
complianceScope: state.complianceScope
|
|
? { ...state.complianceScope, ...action.payload }
|
|
: null,
|
|
})
|
|
|
|
case 'ADD_IMPORTED_DOCUMENT':
|
|
return updateState({
|
|
importedDocuments: [...state.importedDocuments, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_IMPORTED_DOCUMENT':
|
|
return updateState({
|
|
importedDocuments: state.importedDocuments.map(doc =>
|
|
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
|
|
),
|
|
})
|
|
|
|
case 'DELETE_IMPORTED_DOCUMENT':
|
|
return updateState({
|
|
importedDocuments: state.importedDocuments.filter(doc => doc.id !== action.payload),
|
|
})
|
|
|
|
case 'SET_GAP_ANALYSIS':
|
|
return updateState({ gapAnalysis: action.payload })
|
|
|
|
case 'ADD_USE_CASE':
|
|
return updateState({
|
|
useCases: [...state.useCases, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_USE_CASE':
|
|
return updateState({
|
|
useCases: state.useCases.map(uc =>
|
|
uc.id === action.payload.id ? { ...uc, ...action.payload.data } : uc
|
|
),
|
|
})
|
|
|
|
case 'DELETE_USE_CASE':
|
|
return updateState({
|
|
useCases: state.useCases.filter(uc => uc.id !== action.payload),
|
|
activeUseCase: state.activeUseCase === action.payload ? null : state.activeUseCase,
|
|
})
|
|
|
|
case 'SET_ACTIVE_USE_CASE':
|
|
return updateState({ activeUseCase: action.payload })
|
|
|
|
case 'SET_SCREENING':
|
|
return updateState({ screening: action.payload })
|
|
|
|
case 'ADD_MODULE':
|
|
return updateState({
|
|
modules: [...state.modules, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_MODULE':
|
|
return updateState({
|
|
modules: state.modules.map(m =>
|
|
m.id === action.payload.id ? { ...m, ...action.payload.data } : m
|
|
),
|
|
})
|
|
|
|
case 'ADD_REQUIREMENT':
|
|
return updateState({
|
|
requirements: [...state.requirements, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_REQUIREMENT':
|
|
return updateState({
|
|
requirements: state.requirements.map(r =>
|
|
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
|
|
),
|
|
})
|
|
|
|
case 'ADD_CONTROL':
|
|
return updateState({
|
|
controls: [...state.controls, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_CONTROL':
|
|
return updateState({
|
|
controls: state.controls.map(c =>
|
|
c.id === action.payload.id ? { ...c, ...action.payload.data } : c
|
|
),
|
|
})
|
|
|
|
case 'ADD_EVIDENCE':
|
|
return updateState({
|
|
evidence: [...state.evidence, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_EVIDENCE':
|
|
return updateState({
|
|
evidence: state.evidence.map(e =>
|
|
e.id === action.payload.id ? { ...e, ...action.payload.data } : e
|
|
),
|
|
})
|
|
|
|
case 'DELETE_EVIDENCE':
|
|
return updateState({
|
|
evidence: state.evidence.filter(e => e.id !== action.payload),
|
|
})
|
|
|
|
case 'ADD_RISK':
|
|
return updateState({
|
|
risks: [...state.risks, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_RISK':
|
|
return updateState({
|
|
risks: state.risks.map(r =>
|
|
r.id === action.payload.id ? { ...r, ...action.payload.data } : r
|
|
),
|
|
})
|
|
|
|
case 'DELETE_RISK':
|
|
return updateState({
|
|
risks: state.risks.filter(r => r.id !== action.payload),
|
|
})
|
|
|
|
case 'SET_AI_ACT_RESULT':
|
|
return updateState({ aiActClassification: action.payload })
|
|
|
|
case 'ADD_OBLIGATION':
|
|
return updateState({
|
|
obligations: [...state.obligations, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_OBLIGATION':
|
|
return updateState({
|
|
obligations: state.obligations.map(o =>
|
|
o.id === action.payload.id ? { ...o, ...action.payload.data } : o
|
|
),
|
|
})
|
|
|
|
case 'SET_DSFA':
|
|
return updateState({ dsfa: action.payload })
|
|
|
|
case 'ADD_TOM':
|
|
return updateState({
|
|
toms: [...state.toms, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_TOM':
|
|
return updateState({
|
|
toms: state.toms.map(t =>
|
|
t.id === action.payload.id ? { ...t, ...action.payload.data } : t
|
|
),
|
|
})
|
|
|
|
case 'ADD_RETENTION_POLICY':
|
|
return updateState({
|
|
retentionPolicies: [...state.retentionPolicies, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_RETENTION_POLICY':
|
|
return updateState({
|
|
retentionPolicies: state.retentionPolicies.map(p =>
|
|
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
|
|
),
|
|
})
|
|
|
|
case 'ADD_PROCESSING_ACTIVITY':
|
|
return updateState({
|
|
vvt: [...state.vvt, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_PROCESSING_ACTIVITY':
|
|
return updateState({
|
|
vvt: state.vvt.map(p =>
|
|
p.id === action.payload.id ? { ...p, ...action.payload.data } : p
|
|
),
|
|
})
|
|
|
|
case 'ADD_DOCUMENT':
|
|
return updateState({
|
|
documents: [...state.documents, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_DOCUMENT':
|
|
return updateState({
|
|
documents: state.documents.map(d =>
|
|
d.id === action.payload.id ? { ...d, ...action.payload.data } : d
|
|
),
|
|
})
|
|
|
|
case 'SET_COOKIE_BANNER':
|
|
return updateState({ cookieBanner: action.payload })
|
|
|
|
case 'SET_DSR_CONFIG':
|
|
return updateState({ dsrConfig: action.payload })
|
|
|
|
case 'ADD_ESCALATION_WORKFLOW':
|
|
return updateState({
|
|
escalationWorkflows: [...state.escalationWorkflows, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_ESCALATION_WORKFLOW':
|
|
return updateState({
|
|
escalationWorkflows: state.escalationWorkflows.map(w =>
|
|
w.id === action.payload.id ? { ...w, ...action.payload.data } : w
|
|
),
|
|
})
|
|
|
|
case 'ADD_SECURITY_ISSUE':
|
|
return updateState({
|
|
securityIssues: [...state.securityIssues, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_SECURITY_ISSUE':
|
|
return updateState({
|
|
securityIssues: state.securityIssues.map(i =>
|
|
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
|
|
),
|
|
})
|
|
|
|
case 'ADD_BACKLOG_ITEM':
|
|
return updateState({
|
|
securityBacklog: [...state.securityBacklog, action.payload],
|
|
})
|
|
|
|
case 'UPDATE_BACKLOG_ITEM':
|
|
return updateState({
|
|
securityBacklog: state.securityBacklog.map(i =>
|
|
i.id === action.payload.id ? { ...i, ...action.payload.data } : i
|
|
),
|
|
})
|
|
|
|
case 'ADD_COMMAND_HISTORY':
|
|
return updateState({
|
|
commandBarHistory: [action.payload, ...state.commandBarHistory].slice(0, 50),
|
|
})
|
|
|
|
case 'SET_PREFERENCES':
|
|
return updateState({
|
|
preferences: { ...state.preferences, ...action.payload },
|
|
})
|
|
|
|
case 'RESET_STATE':
|
|
return { ...initialState, lastModified: new Date() }
|
|
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONTEXT TYPES
|
|
// =============================================================================
|
|
|
|
interface SDKContextValue {
|
|
state: SDKState
|
|
dispatch: React.Dispatch<ExtendedSDKAction>
|
|
|
|
// Navigation
|
|
currentStep: SDKStep | undefined
|
|
goToStep: (stepId: string) => void
|
|
goToNextStep: () => void
|
|
goToPreviousStep: () => void
|
|
canGoNext: boolean
|
|
canGoPrevious: boolean
|
|
|
|
// Progress
|
|
completionPercentage: number
|
|
phase1Completion: number
|
|
phase2Completion: number
|
|
packageCompletion: Record<SDKPackageId, number>
|
|
|
|
// Customer Type
|
|
setCustomerType: (type: CustomerType) => void
|
|
|
|
// Company Profile
|
|
setCompanyProfile: (profile: CompanyProfile) => void
|
|
updateCompanyProfile: (updates: Partial<CompanyProfile>) => void
|
|
|
|
// Compliance Scope
|
|
setComplianceScope: (scope: import('./compliance-scope-types').ComplianceScopeState) => void
|
|
updateComplianceScope: (updates: Partial<import('./compliance-scope-types').ComplianceScopeState>) => void
|
|
|
|
// Import (for existing customers)
|
|
addImportedDocument: (doc: ImportedDocument) => void
|
|
setGapAnalysis: (analysis: GapAnalysis) => void
|
|
|
|
// 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>
|
|
|
|
// Demo Data
|
|
loadDemoData: (demoState: Partial<SDKState>) => void
|
|
seedDemoData: () => Promise<{ success: boolean; message: string }>
|
|
clearDemoData: () => Promise<boolean>
|
|
isDemoDataLoaded: boolean
|
|
|
|
// Sync
|
|
syncState: SyncState
|
|
forceSyncToServer: () => Promise<void>
|
|
isOnline: boolean
|
|
|
|
// Export
|
|
exportState: (format: 'json' | 'pdf' | 'zip') => Promise<Blob>
|
|
|
|
// Command Bar
|
|
isCommandBarOpen: boolean
|
|
setCommandBarOpen: (open: boolean) => void
|
|
}
|
|
|
|
const SDKContext = createContext<SDKContextValue | null>(null)
|
|
|
|
// =============================================================================
|
|
// PROVIDER
|
|
// =============================================================================
|
|
|
|
const SDK_STORAGE_KEY = 'ai-compliance-sdk-state'
|
|
|
|
interface SDKProviderProps {
|
|
children: React.ReactNode
|
|
tenantId?: string
|
|
userId?: string
|
|
enableBackendSync?: boolean
|
|
}
|
|
|
|
export function SDKProvider({
|
|
children,
|
|
tenantId = 'default',
|
|
userId = 'default',
|
|
enableBackendSync = false,
|
|
}: SDKProviderProps) {
|
|
const router = useRouter()
|
|
const pathname = usePathname()
|
|
const [state, dispatch] = useReducer(sdkReducer, {
|
|
...initialState,
|
|
tenantId,
|
|
userId,
|
|
})
|
|
const [isCommandBarOpen, setCommandBarOpen] = React.useState(false)
|
|
const [isInitialized, setIsInitialized] = React.useState(false)
|
|
const [syncState, setSyncState] = React.useState<SyncState>({
|
|
status: 'idle',
|
|
lastSyncedAt: null,
|
|
localVersion: 0,
|
|
serverVersion: 0,
|
|
pendingChanges: 0,
|
|
error: null,
|
|
})
|
|
const [isOnline, setIsOnline] = React.useState(true)
|
|
|
|
// Refs for sync manager and API client
|
|
const apiClientRef = useRef<SDKApiClient | null>(null)
|
|
const syncManagerRef = useRef<StateSyncManager | null>(null)
|
|
|
|
// Initialize API client and sync manager
|
|
useEffect(() => {
|
|
if (enableBackendSync && typeof window !== 'undefined') {
|
|
apiClientRef.current = getSDKApiClient(tenantId)
|
|
|
|
syncManagerRef.current = createStateSyncManager(
|
|
apiClientRef.current,
|
|
tenantId,
|
|
{
|
|
debounceMs: 2000,
|
|
maxRetries: 3,
|
|
},
|
|
{
|
|
onSyncStart: () => {
|
|
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
|
},
|
|
onSyncComplete: (syncedState) => {
|
|
setSyncState(prev => ({
|
|
...prev,
|
|
status: 'idle',
|
|
lastSyncedAt: new Date(),
|
|
pendingChanges: 0,
|
|
}))
|
|
// Update state if it differs from current
|
|
if (syncedState.lastModified > state.lastModified) {
|
|
dispatch({ type: 'SET_STATE', payload: syncedState })
|
|
}
|
|
},
|
|
onSyncError: (error) => {
|
|
setSyncState(prev => ({
|
|
...prev,
|
|
status: 'error',
|
|
error: error.message,
|
|
}))
|
|
},
|
|
onConflict: () => {
|
|
setSyncState(prev => ({ ...prev, status: 'conflict' }))
|
|
},
|
|
onOffline: () => {
|
|
setIsOnline(false)
|
|
setSyncState(prev => ({ ...prev, status: 'offline' }))
|
|
},
|
|
onOnline: () => {
|
|
setIsOnline(true)
|
|
setSyncState(prev => ({ ...prev, status: 'idle' }))
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
return () => {
|
|
if (syncManagerRef.current) {
|
|
syncManagerRef.current.destroy()
|
|
syncManagerRef.current = null
|
|
}
|
|
if (enableBackendSync) {
|
|
resetSDKApiClient()
|
|
apiClientRef.current = null
|
|
}
|
|
}
|
|
}, [enableBackendSync, tenantId])
|
|
|
|
// Sync current step with URL
|
|
useEffect(() => {
|
|
if (pathname) {
|
|
const step = getStepByUrl(pathname)
|
|
if (step && step.id !== state.currentStep) {
|
|
dispatch({ type: 'SET_CURRENT_STEP', payload: step.id })
|
|
}
|
|
}
|
|
}, [pathname, state.currentStep])
|
|
|
|
// Load state on mount (localStorage first, then server)
|
|
useEffect(() => {
|
|
const loadInitialState = async () => {
|
|
try {
|
|
// First, try loading from localStorage
|
|
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, try loading from server if backend sync is enabled
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
const serverState = await syncManagerRef.current.loadFromServer()
|
|
if (serverState) {
|
|
// Server state is newer, use it
|
|
const localTime = stored ? new Date(JSON.parse(stored).lastModified).getTime() : 0
|
|
const serverTime = new Date(serverState.lastModified).getTime()
|
|
if (serverTime > localTime) {
|
|
dispatch({ type: 'SET_STATE', payload: serverState })
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load SDK state:', error)
|
|
}
|
|
setIsInitialized(true)
|
|
}
|
|
|
|
loadInitialState()
|
|
}, [tenantId, enableBackendSync])
|
|
|
|
// Auto-save to localStorage and sync to server
|
|
useEffect(() => {
|
|
if (!isInitialized || !state.preferences.autoSave) return
|
|
|
|
const saveTimeout = setTimeout(() => {
|
|
try {
|
|
// Save to localStorage
|
|
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
|
|
|
// Sync to server if backend sync is enabled
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
syncManagerRef.current.queueSync(state)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save SDK state:', error)
|
|
}
|
|
}, 1000)
|
|
|
|
return () => clearTimeout(saveTimeout)
|
|
}, [state, tenantId, isInitialized, enableBackendSync])
|
|
|
|
// Keyboard shortcut for Command Bar
|
|
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 })
|
|
router.push(step.url)
|
|
}
|
|
},
|
|
[router]
|
|
)
|
|
|
|
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(() => {
|
|
return getNextStep(state.currentStep) !== undefined
|
|
}, [state.currentStep])
|
|
|
|
const canGoPrevious = useMemo(() => {
|
|
return 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])
|
|
|
|
// Package Completion
|
|
const packageCompletion = useMemo(() => {
|
|
const completion: Record<SDKPackageId, number> = {
|
|
'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'),
|
|
'analyse': getPackageCompletionPercentage(state, 'analyse'),
|
|
'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'),
|
|
'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'),
|
|
'betrieb': getPackageCompletionPercentage(state, 'betrieb'),
|
|
}
|
|
return completion
|
|
}, [state])
|
|
|
|
// Customer Type
|
|
const setCustomerType = useCallback((type: CustomerType) => {
|
|
dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type })
|
|
}, [])
|
|
|
|
// Company Profile
|
|
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
|
|
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
|
|
}, [])
|
|
|
|
const updateCompanyProfile = useCallback((updates: Partial<CompanyProfile>) => {
|
|
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates })
|
|
}, [])
|
|
|
|
// Compliance Scope
|
|
const setComplianceScope = useCallback((scope: import('./compliance-scope-types').ComplianceScopeState) => {
|
|
dispatch({ type: 'SET_COMPLIANCE_SCOPE', payload: scope })
|
|
}, [])
|
|
|
|
const updateComplianceScope = useCallback((updates: Partial<import('./compliance-scope-types').ComplianceScopeState>) => {
|
|
dispatch({ type: 'UPDATE_COMPLIANCE_SCOPE', payload: updates })
|
|
}, [])
|
|
|
|
// Import Document
|
|
const addImportedDocument = useCallback((doc: ImportedDocument) => {
|
|
dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc })
|
|
}, [])
|
|
|
|
// Gap Analysis
|
|
const setGapAnalysis = useCallback((analysis: GapAnalysis) => {
|
|
dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis })
|
|
}, [])
|
|
|
|
// Checkpoints
|
|
const validateCheckpoint = useCallback(
|
|
async (checkpointId: string): Promise<CheckpointStatus> => {
|
|
// Try backend validation if available
|
|
if (enableBackendSync && apiClientRef.current) {
|
|
try {
|
|
const result = await apiClientRef.current.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 back to local validation
|
|
}
|
|
}
|
|
|
|
// Local validation
|
|
const status: CheckpointStatus = {
|
|
checkpointId,
|
|
passed: true,
|
|
validatedAt: new Date(),
|
|
validatedBy: 'SYSTEM',
|
|
errors: [],
|
|
warnings: [],
|
|
}
|
|
|
|
switch (checkpointId) {
|
|
case 'CP-PROF':
|
|
if (!state.companyProfile || !state.companyProfile.isComplete) {
|
|
status.passed = false
|
|
status.errors.push({
|
|
ruleId: 'prof-complete',
|
|
field: 'companyProfile',
|
|
message: 'Unternehmensprofil muss vollständig ausgefüllt werden',
|
|
severity: 'ERROR',
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'CP-UC':
|
|
if (state.useCases.length === 0) {
|
|
status.passed = false
|
|
status.errors.push({
|
|
ruleId: 'uc-min-count',
|
|
field: 'useCases',
|
|
message: 'Mindestens ein Anwendungsfall muss erstellt werden',
|
|
severity: 'ERROR',
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'CP-SCAN':
|
|
if (!state.screening || state.screening.status !== 'COMPLETED') {
|
|
status.passed = false
|
|
status.errors.push({
|
|
ruleId: 'scan-complete',
|
|
field: 'screening',
|
|
message: 'Security Scan muss abgeschlossen sein',
|
|
severity: 'ERROR',
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'CP-MOD':
|
|
if (state.modules.length === 0) {
|
|
status.passed = false
|
|
status.errors.push({
|
|
ruleId: 'mod-min-count',
|
|
field: 'modules',
|
|
message: 'Mindestens ein Modul muss zugewiesen werden',
|
|
severity: 'ERROR',
|
|
})
|
|
}
|
|
break
|
|
|
|
case 'CP-RISK':
|
|
const criticalRisks = state.risks.filter(
|
|
r => r.severity === 'CRITICAL' || r.severity === 'HIGH'
|
|
)
|
|
const unmitigatedRisks = criticalRisks.filter(
|
|
r => r.mitigation.length === 0
|
|
)
|
|
if (unmitigatedRisks.length > 0) {
|
|
status.passed = false
|
|
status.errors.push({
|
|
ruleId: 'critical-risks-mitigated',
|
|
field: 'risks',
|
|
message: `${unmitigatedRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
|
|
severity: 'ERROR',
|
|
})
|
|
}
|
|
break
|
|
}
|
|
|
|
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
|
return status
|
|
},
|
|
[state, enableBackendSync]
|
|
)
|
|
|
|
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 } })
|
|
},
|
|
[]
|
|
)
|
|
|
|
// Demo Data Loading
|
|
const loadDemoData = useCallback((demoState: Partial<SDKState>) => {
|
|
dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState })
|
|
}, [])
|
|
|
|
// Seed demo data via API (stores like real customer data)
|
|
const seedDemoData = useCallback(async (): Promise<{ success: boolean; message: string }> => {
|
|
try {
|
|
// Generate demo state
|
|
const demoState = generateDemoState(tenantId, userId) as SDKState
|
|
|
|
// Save via API (same path as real customer data)
|
|
if (enableBackendSync && apiClientRef.current) {
|
|
await apiClientRef.current.saveState(demoState)
|
|
}
|
|
|
|
// Also save to localStorage for immediate availability
|
|
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(demoState))
|
|
|
|
// Update local state
|
|
dispatch({ type: 'LOAD_DEMO_DATA', payload: demoState })
|
|
|
|
return { success: true, message: `Demo-Daten erfolgreich geladen für Tenant ${tenantId}` }
|
|
} catch (error) {
|
|
console.error('Failed to seed demo data:', error)
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : 'Unbekannter Fehler beim Laden der Demo-Daten',
|
|
}
|
|
}
|
|
}, [tenantId, userId, enableBackendSync])
|
|
|
|
// Clear demo data
|
|
const clearDemoData = useCallback(async (): Promise<boolean> => {
|
|
try {
|
|
// Delete from API
|
|
if (enableBackendSync && apiClientRef.current) {
|
|
await apiClientRef.current.deleteState()
|
|
}
|
|
|
|
// Clear localStorage
|
|
localStorage.removeItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
|
|
|
// Reset local state
|
|
dispatch({ type: 'RESET_STATE' })
|
|
|
|
return true
|
|
} catch (error) {
|
|
console.error('Failed to clear demo data:', error)
|
|
return false
|
|
}
|
|
}, [tenantId, enableBackendSync])
|
|
|
|
// Check if demo data is loaded (has use cases with demo- prefix)
|
|
const isDemoDataLoaded = useMemo(() => {
|
|
return state.useCases.length > 0 && state.useCases.some(uc => uc.id.startsWith('demo-'))
|
|
}, [state.useCases])
|
|
|
|
// Persistence
|
|
const saveState = useCallback(async (): Promise<void> => {
|
|
try {
|
|
localStorage.setItem(`${SDK_STORAGE_KEY}-${tenantId}`, JSON.stringify(state))
|
|
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
await syncManagerRef.current.forcSync(state)
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save SDK state:', error)
|
|
throw error
|
|
}
|
|
}, [state, tenantId, enableBackendSync])
|
|
|
|
const loadState = useCallback(async (): Promise<void> => {
|
|
try {
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
const serverState = await syncManagerRef.current.loadFromServer()
|
|
if (serverState) {
|
|
dispatch({ type: 'SET_STATE', payload: serverState })
|
|
return
|
|
}
|
|
}
|
|
|
|
// Fall back to localStorage
|
|
const stored = localStorage.getItem(`${SDK_STORAGE_KEY}-${tenantId}`)
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored)
|
|
dispatch({ type: 'SET_STATE', payload: parsed })
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load SDK state:', error)
|
|
throw error
|
|
}
|
|
}, [tenantId, enableBackendSync])
|
|
|
|
// Force sync to server
|
|
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
|
if (enableBackendSync && syncManagerRef.current) {
|
|
await syncManagerRef.current.forcSync(state)
|
|
}
|
|
}, [state, enableBackendSync])
|
|
|
|
// Export
|
|
const exportState = useCallback(
|
|
async (format: 'json' | 'pdf' | 'zip'): Promise<Blob> => {
|
|
switch (format) {
|
|
case 'json':
|
|
return new Blob([JSON.stringify(state, null, 2)], {
|
|
type: 'application/json',
|
|
})
|
|
|
|
case 'pdf':
|
|
return exportToPDF(state)
|
|
|
|
case 'zip':
|
|
return exportToZIP(state)
|
|
|
|
default:
|
|
throw new Error(`Unknown export format: ${format}`)
|
|
}
|
|
},
|
|
[state]
|
|
)
|
|
|
|
const value: SDKContextValue = {
|
|
state,
|
|
dispatch,
|
|
currentStep,
|
|
goToStep,
|
|
goToNextStep,
|
|
goToPreviousStep,
|
|
canGoNext,
|
|
canGoPrevious,
|
|
completionPercentage,
|
|
phase1Completion,
|
|
phase2Completion,
|
|
packageCompletion,
|
|
setCustomerType,
|
|
setCompanyProfile,
|
|
updateCompanyProfile,
|
|
setComplianceScope,
|
|
updateComplianceScope,
|
|
addImportedDocument,
|
|
setGapAnalysis,
|
|
validateCheckpoint,
|
|
overrideCheckpoint,
|
|
getCheckpointStatus,
|
|
updateUseCase,
|
|
addRisk,
|
|
updateControl,
|
|
saveState,
|
|
loadState,
|
|
loadDemoData,
|
|
seedDemoData,
|
|
clearDemoData,
|
|
isDemoDataLoaded,
|
|
syncState,
|
|
forceSyncToServer,
|
|
isOnline,
|
|
exportState,
|
|
isCommandBarOpen,
|
|
setCommandBarOpen,
|
|
}
|
|
|
|
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>
|
|
}
|
|
|
|
// =============================================================================
|
|
// HOOK
|
|
// =============================================================================
|
|
|
|
export function useSDK(): SDKContextValue {
|
|
const context = useContext(SDKContext)
|
|
if (!context) {
|
|
throw new Error('useSDK must be used within SDKProvider')
|
|
}
|
|
return context
|
|
}
|
|
|
|
// =============================================================================
|
|
// EXPORTS
|
|
// =============================================================================
|
|
|
|
export { SDKContext, initialState }
|