Files
breakpilot-compliance/admin-compliance/lib/sdk/context-provider.tsx
Sharang Parnerkar 786bb409e4 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>
2026-04-10 13:55:42 +02:00

496 lines
16 KiB
TypeScript

'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>
}