refactor(admin): split lib/sdk/context.tsx (1280 LOC) into focused modules
Extract the monolithic SDK context provider into seven focused modules: - context-types.ts (203 LOC): SDKContextValue interface, initialState, ExtendedSDKAction - context-reducer.ts (353 LOC): sdkReducer with all action handlers - context-provider.tsx (495 LOC): SDKProvider component + SDKContext - context-hooks.ts (17 LOC): useSDK hook - context-validators.ts (94 LOC): local checkpoint validation logic - context-projects.ts (67 LOC): project management API helpers - context-sync-helpers.ts (145 LOC): sync infrastructure init/cleanup/callbacks - context.tsx (23 LOC): barrel re-export preserving existing import paths All files under the 500-line hard cap. Build verified with `npx next build`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17
admin-compliance/lib/sdk/context-hooks.ts
Normal file
17
admin-compliance/lib/sdk/context-hooks.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
'use client'
|
||||
|
||||
import { useContext } from 'react'
|
||||
import { SDKContextValue } from './context-types'
|
||||
import { SDKContext } from './context-provider'
|
||||
|
||||
// =============================================================================
|
||||
// HOOK
|
||||
// =============================================================================
|
||||
|
||||
export function useSDK(): SDKContextValue {
|
||||
const context = useContext(SDKContext)
|
||||
if (!context) {
|
||||
throw new Error('useSDK must be used within SDKProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
67
admin-compliance/lib/sdk/context-projects.ts
Normal file
67
admin-compliance/lib/sdk/context-projects.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import { SDKApiClient, getSDKApiClient } from './api-client'
|
||||
import { CustomerType, ProjectInfo } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// PROJECT MANAGEMENT HELPERS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Ensures an API client is available. If the ref is null and backend sync is
|
||||
* enabled, lazily initialises one. Returns the client or throws.
|
||||
*/
|
||||
export function ensureApiClient(
|
||||
apiClientRef: React.MutableRefObject<SDKApiClient | null>,
|
||||
enableBackendSync: boolean,
|
||||
tenantId: string,
|
||||
projectId?: string
|
||||
): SDKApiClient {
|
||||
if (!apiClientRef.current && enableBackendSync) {
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
}
|
||||
if (!apiClientRef.current) {
|
||||
throw new Error('Backend sync not enabled')
|
||||
}
|
||||
return apiClientRef.current
|
||||
}
|
||||
|
||||
export async function createProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
name: string,
|
||||
customerType: CustomerType,
|
||||
copyFromProjectId?: string
|
||||
): Promise<ProjectInfo> {
|
||||
return apiClient.createProject({
|
||||
name,
|
||||
customer_type: customerType,
|
||||
copy_from_project_id: copyFromProjectId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function listProjectsApi(
|
||||
apiClient: SDKApiClient
|
||||
): Promise<ProjectInfo[]> {
|
||||
const result = await apiClient.listProjects()
|
||||
return result.projects
|
||||
}
|
||||
|
||||
export async function archiveProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
archiveId: string
|
||||
): Promise<void> {
|
||||
await apiClient.archiveProject(archiveId)
|
||||
}
|
||||
|
||||
export async function restoreProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
restoreId: string
|
||||
): Promise<ProjectInfo> {
|
||||
return apiClient.restoreProject(restoreId)
|
||||
}
|
||||
|
||||
export async function permanentlyDeleteProjectApi(
|
||||
apiClient: SDKApiClient,
|
||||
deleteId: string
|
||||
): Promise<void> {
|
||||
await apiClient.permanentlyDeleteProject(deleteId)
|
||||
}
|
||||
495
admin-compliance/lib/sdk/context-provider.tsx
Normal file
495
admin-compliance/lib/sdk/context-provider.tsx
Normal file
@@ -0,0 +1,495 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useReducer, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import {
|
||||
SDKState,
|
||||
CheckpointStatus,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
CustomerType,
|
||||
CompanyProfile,
|
||||
ImportedDocument,
|
||||
GapAnalysis,
|
||||
SDKPackageId,
|
||||
ProjectInfo,
|
||||
getStepById,
|
||||
getStepByUrl,
|
||||
getNextStep,
|
||||
getPreviousStep,
|
||||
getCompletionPercentage,
|
||||
getPhaseCompletionPercentage,
|
||||
getPackageCompletionPercentage,
|
||||
} from './types'
|
||||
import { exportToPDF, exportToZIP } from './export'
|
||||
import { SDKApiClient, getSDKApiClient } from './api-client'
|
||||
import { StateSyncManager, SyncState } from './sync'
|
||||
import { generateDemoState } from './demo-data'
|
||||
import { SDKContextValue, initialState, SDK_STORAGE_KEY } from './context-types'
|
||||
import { sdkReducer } from './context-reducer'
|
||||
import { validateCheckpointLocally } from './context-validators'
|
||||
import {
|
||||
ensureApiClient,
|
||||
createProjectApi,
|
||||
listProjectsApi,
|
||||
archiveProjectApi,
|
||||
restoreProjectApi,
|
||||
permanentlyDeleteProjectApi,
|
||||
} from './context-projects'
|
||||
import {
|
||||
buildSyncCallbacks,
|
||||
loadInitialState,
|
||||
initSyncInfra,
|
||||
cleanupSyncInfra,
|
||||
} from './context-sync-helpers'
|
||||
|
||||
export const SDKContext = createContext<SDKContextValue | null>(null)
|
||||
|
||||
interface SDKProviderProps {
|
||||
children: React.ReactNode
|
||||
tenantId?: string
|
||||
userId?: string
|
||||
projectId?: string
|
||||
enableBackendSync?: boolean
|
||||
}
|
||||
|
||||
export function SDKProvider({
|
||||
children,
|
||||
tenantId = '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e',
|
||||
userId = 'default',
|
||||
projectId,
|
||||
enableBackendSync = false,
|
||||
}: SDKProviderProps) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const [state, dispatch] = useReducer(sdkReducer, {
|
||||
...initialState,
|
||||
tenantId,
|
||||
userId,
|
||||
projectId: projectId || '',
|
||||
})
|
||||
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)
|
||||
const stateRef = useRef(state)
|
||||
stateRef.current = state
|
||||
|
||||
// Initialize API client and sync manager
|
||||
useEffect(() => {
|
||||
const callbacks = buildSyncCallbacks(setSyncState, setIsOnline, dispatch, stateRef)
|
||||
initSyncInfra(enableBackendSync, tenantId, projectId, apiClientRef, syncManagerRef, callbacks)
|
||||
return () => cleanupSyncInfra(enableBackendSync, syncManagerRef, apiClientRef)
|
||||
}, [enableBackendSync, tenantId, projectId])
|
||||
|
||||
// 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])
|
||||
|
||||
// Storage key — per tenant+project
|
||||
const storageKey = projectId
|
||||
? `${SDK_STORAGE_KEY}-${tenantId}-${projectId}`
|
||||
: `${SDK_STORAGE_KEY}-${tenantId}`
|
||||
|
||||
// Load state on mount (localStorage first, then server)
|
||||
useEffect(() => {
|
||||
loadInitialState({
|
||||
storageKey,
|
||||
enableBackendSync,
|
||||
projectId,
|
||||
syncManager: syncManagerRef.current,
|
||||
apiClient: apiClientRef.current,
|
||||
dispatch,
|
||||
})
|
||||
.catch(error => console.error('Failed to load SDK state:', error))
|
||||
.finally(() => setIsInitialized(true))
|
||||
}, [tenantId, projectId, enableBackendSync, storageKey])
|
||||
|
||||
// Auto-save to localStorage and sync to server
|
||||
useEffect(() => {
|
||||
if (!isInitialized || !state.preferences.autoSave) return
|
||||
|
||||
const saveTimeout = setTimeout(() => {
|
||||
try {
|
||||
// Save to localStorage (per tenant+project)
|
||||
localStorage.setItem(storageKey, 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, projectId, isInitialized, enableBackendSync, storageKey])
|
||||
|
||||
// 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 })
|
||||
const url = projectId ? `${step.url}?project=${projectId}` : step.url
|
||||
router.push(url)
|
||||
}
|
||||
},
|
||||
[router, projectId]
|
||||
)
|
||||
|
||||
const goToNextStep = useCallback(() => {
|
||||
const nextStep = getNextStep(state.currentStep, state)
|
||||
if (nextStep) {
|
||||
goToStep(nextStep.id)
|
||||
}
|
||||
}, [state, goToStep])
|
||||
|
||||
const goToPreviousStep = useCallback(() => {
|
||||
const prevStep = getPreviousStep(state.currentStep, state)
|
||||
if (prevStep) {
|
||||
goToStep(prevStep.id)
|
||||
}
|
||||
}, [state, goToStep])
|
||||
|
||||
const canGoNext = useMemo(() => {
|
||||
return getNextStep(state.currentStep, state) !== undefined
|
||||
}, [state])
|
||||
|
||||
const canGoPrevious = useMemo(() => {
|
||||
return getPreviousStep(state.currentStep, state) !== undefined
|
||||
}, [state])
|
||||
|
||||
// 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])
|
||||
|
||||
// Simple dispatch callbacks
|
||||
const setCustomerType = useCallback((type: CustomerType) => dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type }), [])
|
||||
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 }), [])
|
||||
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 }), [])
|
||||
const addImportedDocument = useCallback((doc: ImportedDocument) => dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc }), [])
|
||||
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 = validateCheckpointLocally(checkpointId, state)
|
||||
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status } })
|
||||
return status
|
||||
},
|
||||
[state, enableBackendSync]
|
||||
)
|
||||
|
||||
const overrideCheckpoint = useCallback(async (checkpointId: string, reason: string): Promise<void> => {
|
||||
const existing = state.checkpoints[checkpointId]
|
||||
const overridden: CheckpointStatus = {
|
||||
...existing, checkpointId, passed: true, overrideReason: reason,
|
||||
overriddenBy: state.userId, overriddenAt: new Date(),
|
||||
errors: [], warnings: existing?.warnings || [],
|
||||
}
|
||||
dispatch({ type: 'SET_CHECKPOINT_STATUS', payload: { id: checkpointId, status: overridden } })
|
||||
}, [state.checkpoints, state.userId])
|
||||
|
||||
const getCheckpointStatus = useCallback(
|
||||
(checkpointId: string) => state.checkpoints[checkpointId],
|
||||
[state.checkpoints]
|
||||
)
|
||||
|
||||
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 } }), [])
|
||||
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(storageKey, 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, storageKey])
|
||||
|
||||
// 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(storageKey)
|
||||
|
||||
// Reset local state
|
||||
dispatch({ type: 'RESET_STATE' })
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to clear demo data:', error)
|
||||
return false
|
||||
}
|
||||
}, [storageKey, 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(storageKey, 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, storageKey, 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(storageKey)
|
||||
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
|
||||
}
|
||||
}, [storageKey, enableBackendSync])
|
||||
|
||||
// Force sync to server
|
||||
const forceSyncToServer = useCallback(async (): Promise<void> => {
|
||||
if (enableBackendSync && syncManagerRef.current) {
|
||||
await syncManagerRef.current.forcSync(state)
|
||||
}
|
||||
}, [state, enableBackendSync])
|
||||
|
||||
// Project Management
|
||||
const createProject = useCallback(
|
||||
async (name: string, customerType: CustomerType, copyFromProjectId?: string): Promise<ProjectInfo> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
return createProjectApi(client, name, customerType, copyFromProjectId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
const listProjectsFn = useCallback(async (): Promise<ProjectInfo[]> => {
|
||||
if (!apiClientRef.current && enableBackendSync) {
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
}
|
||||
if (!apiClientRef.current) {
|
||||
return []
|
||||
}
|
||||
return listProjectsApi(apiClientRef.current)
|
||||
}, [enableBackendSync, tenantId, projectId])
|
||||
|
||||
const switchProject = useCallback(
|
||||
(newProjectId: string) => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
params.set('project', newProjectId)
|
||||
router.push(`/sdk?${params.toString()}`)
|
||||
},
|
||||
[router]
|
||||
)
|
||||
|
||||
const archiveProjectFn = useCallback(
|
||||
async (archiveId: string): Promise<void> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
await archiveProjectApi(client, archiveId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
const restoreProjectFn = useCallback(
|
||||
async (restoreId: string): Promise<ProjectInfo> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
return restoreProjectApi(client, restoreId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
const permanentlyDeleteProjectFn = useCallback(
|
||||
async (deleteId: string): Promise<void> => {
|
||||
const client = ensureApiClient(apiClientRef, enableBackendSync, tenantId, projectId)
|
||||
await permanentlyDeleteProjectApi(client, deleteId)
|
||||
},
|
||||
[enableBackendSync, tenantId, projectId]
|
||||
)
|
||||
|
||||
// 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,
|
||||
projectId,
|
||||
createProject,
|
||||
listProjects: listProjectsFn,
|
||||
switchProject,
|
||||
archiveProject: archiveProjectFn,
|
||||
restoreProject: restoreProjectFn,
|
||||
permanentlyDeleteProject: permanentlyDeleteProjectFn,
|
||||
}
|
||||
|
||||
return <SDKContext.Provider value={value}>{children}</SDKContext.Provider>
|
||||
}
|
||||
353
admin-compliance/lib/sdk/context-reducer.ts
Normal file
353
admin-compliance/lib/sdk/context-reducer.ts
Normal file
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
SDKState,
|
||||
getStepById,
|
||||
} from './types'
|
||||
import { ExtendedSDKAction, initialState } from './context-types'
|
||||
|
||||
// =============================================================================
|
||||
// REDUCER
|
||||
// =============================================================================
|
||||
|
||||
export 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 'ADD_CUSTOM_CATALOG_ENTRY': {
|
||||
const entry = action.payload
|
||||
const existing = state.customCatalogs[entry.catalogId] || []
|
||||
return updateState({
|
||||
customCatalogs: {
|
||||
...state.customCatalogs,
|
||||
[entry.catalogId]: [...existing, entry],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'UPDATE_CUSTOM_CATALOG_ENTRY': {
|
||||
const { catalogId, entryId, data } = action.payload
|
||||
const entries = state.customCatalogs[catalogId] || []
|
||||
return updateState({
|
||||
customCatalogs: {
|
||||
...state.customCatalogs,
|
||||
[catalogId]: entries.map(e =>
|
||||
e.id === entryId ? { ...e, data: { ...e.data, ...data }, updatedAt: new Date().toISOString() } : e
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'DELETE_CUSTOM_CATALOG_ENTRY': {
|
||||
const { catalogId, entryId } = action.payload
|
||||
const items = state.customCatalogs[catalogId] || []
|
||||
return updateState({
|
||||
customCatalogs: {
|
||||
...state.customCatalogs,
|
||||
[catalogId]: items.filter(e => e.id !== entryId),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'RESET_STATE':
|
||||
return { ...initialState, lastModified: new Date() }
|
||||
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
145
admin-compliance/lib/sdk/context-sync-helpers.ts
Normal file
145
admin-compliance/lib/sdk/context-sync-helpers.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import React from 'react'
|
||||
import { SDKState } from './types'
|
||||
import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client'
|
||||
import { StateSyncManager, createStateSyncManager, SyncState, SyncCallbacks } from './sync'
|
||||
import { ExtendedSDKAction } from './context-types'
|
||||
|
||||
// =============================================================================
|
||||
// SYNC CALLBACK BUILDER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Builds the SyncCallbacks object used by the StateSyncManager.
|
||||
* Keeps the provider component cleaner by extracting this factory.
|
||||
*/
|
||||
export function buildSyncCallbacks(
|
||||
setSyncState: React.Dispatch<React.SetStateAction<SyncState>>,
|
||||
setIsOnline: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
dispatch: React.Dispatch<ExtendedSDKAction>,
|
||||
stateRef: React.MutableRefObject<SDKState>
|
||||
): SyncCallbacks {
|
||||
return {
|
||||
onSyncStart: () => {
|
||||
setSyncState(prev => ({ ...prev, status: 'syncing' }))
|
||||
},
|
||||
onSyncComplete: (syncedState) => {
|
||||
setSyncState(prev => ({
|
||||
...prev,
|
||||
status: 'idle',
|
||||
lastSyncedAt: new Date(),
|
||||
pendingChanges: 0,
|
||||
}))
|
||||
if (syncedState.lastModified > stateRef.current.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' }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE LOADER
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Loads SDK state from localStorage and optionally from the server,
|
||||
* dispatching SET_STATE as appropriate.
|
||||
*/
|
||||
export async function loadInitialState(params: {
|
||||
storageKey: string
|
||||
enableBackendSync: boolean
|
||||
projectId?: string
|
||||
syncManager: StateSyncManager | null
|
||||
apiClient: SDKApiClient | null
|
||||
dispatch: React.Dispatch<ExtendedSDKAction>
|
||||
}): Promise<void> {
|
||||
const { storageKey, enableBackendSync, projectId, syncManager, apiClient, dispatch } = params
|
||||
|
||||
// First, try loading from localStorage
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
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 && syncManager) {
|
||||
const serverState = await syncManager.loadFromServer()
|
||||
if (serverState) {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load project metadata (name, status, etc.) from backend
|
||||
if (enableBackendSync && projectId && apiClient) {
|
||||
try {
|
||||
const info = await apiClient.getProject(projectId)
|
||||
dispatch({ type: 'SET_STATE', payload: { projectInfo: info } })
|
||||
} catch (err) {
|
||||
console.warn('Failed to load project info:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// INIT / CLEANUP HELPERS
|
||||
// =============================================================================
|
||||
|
||||
export function initSyncInfra(
|
||||
enableBackendSync: boolean,
|
||||
tenantId: string,
|
||||
projectId: string | undefined,
|
||||
apiClientRef: React.MutableRefObject<SDKApiClient | null>,
|
||||
syncManagerRef: React.MutableRefObject<StateSyncManager | null>,
|
||||
callbacks: SyncCallbacks
|
||||
): void {
|
||||
if (!enableBackendSync || typeof window === 'undefined') return
|
||||
|
||||
apiClientRef.current = getSDKApiClient(tenantId, projectId)
|
||||
syncManagerRef.current = createStateSyncManager(
|
||||
apiClientRef.current,
|
||||
tenantId,
|
||||
{ debounceMs: 2000, maxRetries: 3 },
|
||||
callbacks,
|
||||
projectId
|
||||
)
|
||||
}
|
||||
|
||||
export function cleanupSyncInfra(
|
||||
enableBackendSync: boolean,
|
||||
syncManagerRef: React.MutableRefObject<StateSyncManager | null>,
|
||||
apiClientRef: React.MutableRefObject<SDKApiClient | null>
|
||||
): void {
|
||||
if (syncManagerRef.current) {
|
||||
syncManagerRef.current.destroy()
|
||||
syncManagerRef.current = null
|
||||
}
|
||||
if (enableBackendSync) {
|
||||
resetSDKApiClient()
|
||||
apiClientRef.current = null
|
||||
}
|
||||
}
|
||||
203
admin-compliance/lib/sdk/context-types.ts
Normal file
203
admin-compliance/lib/sdk/context-types.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
SDKState,
|
||||
SDKAction,
|
||||
SDKStep,
|
||||
CheckpointStatus,
|
||||
UseCaseAssessment,
|
||||
Risk,
|
||||
Control,
|
||||
UserPreferences,
|
||||
CustomerType,
|
||||
CompanyProfile,
|
||||
ImportedDocument,
|
||||
GapAnalysis,
|
||||
SDKPackageId,
|
||||
ProjectInfo,
|
||||
} from './types'
|
||||
import { SyncState } from './sync'
|
||||
|
||||
// =============================================================================
|
||||
// INITIAL STATE
|
||||
// =============================================================================
|
||||
|
||||
const initialPreferences: UserPreferences = {
|
||||
language: 'de',
|
||||
theme: 'light',
|
||||
compactMode: false,
|
||||
showHints: true,
|
||||
autoSave: true,
|
||||
autoValidate: true,
|
||||
allowParallelWork: true, // Standard: Paralleles Arbeiten erlaubt
|
||||
}
|
||||
|
||||
export const initialState: SDKState = {
|
||||
// Metadata
|
||||
version: '1.0.0',
|
||||
projectVersion: 1,
|
||||
lastModified: new Date(),
|
||||
|
||||
// Tenant & User
|
||||
tenantId: '',
|
||||
userId: '',
|
||||
subscription: 'PROFESSIONAL',
|
||||
|
||||
// Project Context
|
||||
projectId: '',
|
||||
projectInfo: null,
|
||||
|
||||
// Customer Type
|
||||
customerType: null,
|
||||
|
||||
// Company Profile
|
||||
companyProfile: null,
|
||||
|
||||
// Compliance Scope
|
||||
complianceScope: null,
|
||||
|
||||
// Source Policy
|
||||
sourcePolicy: 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: [],
|
||||
|
||||
// IACE (Industrial AI Compliance Engine)
|
||||
iaceProjects: [],
|
||||
|
||||
// RAG Corpus Versioning
|
||||
ragCorpusStatus: null,
|
||||
|
||||
// Security
|
||||
sbom: null,
|
||||
securityIssues: [],
|
||||
securityBacklog: [],
|
||||
|
||||
// Catalog Manager
|
||||
customCatalogs: {},
|
||||
|
||||
// UI State
|
||||
commandBarHistory: [],
|
||||
recentSearches: [],
|
||||
preferences: initialPreferences,
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXTENDED ACTION TYPES
|
||||
// =============================================================================
|
||||
|
||||
// Extended action type to include demo data loading
|
||||
export type ExtendedSDKAction =
|
||||
| SDKAction
|
||||
| { type: 'LOAD_DEMO_DATA'; payload: Partial<SDKState> }
|
||||
|
||||
// =============================================================================
|
||||
// CONTEXT TYPES
|
||||
// =============================================================================
|
||||
|
||||
export 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
|
||||
|
||||
// Project Management
|
||||
projectId: string | undefined
|
||||
createProject: (name: string, customerType: CustomerType, copyFromProjectId?: string) => Promise<ProjectInfo>
|
||||
listProjects: () => Promise<ProjectInfo[]>
|
||||
switchProject: (projectId: string) => void
|
||||
archiveProject: (projectId: string) => Promise<void>
|
||||
restoreProject: (projectId: string) => Promise<ProjectInfo>
|
||||
permanentlyDeleteProject: (projectId: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const SDK_STORAGE_KEY = 'ai-compliance-sdk-state'
|
||||
94
admin-compliance/lib/sdk/context-validators.ts
Normal file
94
admin-compliance/lib/sdk/context-validators.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { SDKState, CheckpointStatus } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// LOCAL CHECKPOINT VALIDATION
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Performs local (client-side) checkpoint validation against the current SDK state.
|
||||
* Returns a CheckpointStatus with errors/warnings populated.
|
||||
*/
|
||||
export function validateCheckpointLocally(
|
||||
checkpointId: string,
|
||||
state: SDKState
|
||||
): CheckpointStatus {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user