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