Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
711 lines
21 KiB
TypeScript
711 lines
21 KiB
TypeScript
'use client'
|
|
|
|
// =============================================================================
|
|
// TOM Generator Context
|
|
// State management for the TOM Generator Wizard
|
|
// =============================================================================
|
|
|
|
import React, {
|
|
createContext,
|
|
useContext,
|
|
useReducer,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
ReactNode,
|
|
} from 'react'
|
|
import {
|
|
TOMGeneratorState,
|
|
TOMGeneratorStepId,
|
|
CompanyProfile,
|
|
DataProfile,
|
|
ArchitectureProfile,
|
|
SecurityProfile,
|
|
RiskProfile,
|
|
EvidenceDocument,
|
|
DerivedTOM,
|
|
GapAnalysisResult,
|
|
ExportRecord,
|
|
WizardStep,
|
|
createInitialTOMGeneratorState,
|
|
TOM_GENERATOR_STEPS,
|
|
getStepIndex,
|
|
calculateProtectionLevel,
|
|
isDSFARequired,
|
|
hasSpecialCategories,
|
|
} from './types'
|
|
import { TOMRulesEngine } from './rules-engine'
|
|
|
|
// =============================================================================
|
|
// ACTION TYPES
|
|
// =============================================================================
|
|
|
|
type TOMGeneratorAction =
|
|
| { type: 'INITIALIZE'; payload: { tenantId: string; state?: TOMGeneratorState } }
|
|
| { type: 'RESET'; payload: { tenantId: string } }
|
|
| { type: 'SET_CURRENT_STEP'; payload: TOMGeneratorStepId }
|
|
| { type: 'SET_COMPANY_PROFILE'; payload: CompanyProfile }
|
|
| { type: 'UPDATE_COMPANY_PROFILE'; payload: Partial<CompanyProfile> }
|
|
| { type: 'SET_DATA_PROFILE'; payload: DataProfile }
|
|
| { type: 'UPDATE_DATA_PROFILE'; payload: Partial<DataProfile> }
|
|
| { type: 'SET_ARCHITECTURE_PROFILE'; payload: ArchitectureProfile }
|
|
| { type: 'UPDATE_ARCHITECTURE_PROFILE'; payload: Partial<ArchitectureProfile> }
|
|
| { type: 'SET_SECURITY_PROFILE'; payload: SecurityProfile }
|
|
| { type: 'UPDATE_SECURITY_PROFILE'; payload: Partial<SecurityProfile> }
|
|
| { type: 'SET_RISK_PROFILE'; payload: RiskProfile }
|
|
| { type: 'UPDATE_RISK_PROFILE'; payload: Partial<RiskProfile> }
|
|
| { type: 'COMPLETE_STEP'; payload: { stepId: TOMGeneratorStepId; data: unknown } }
|
|
| { type: 'UNCOMPLETE_STEP'; payload: TOMGeneratorStepId }
|
|
| { type: 'ADD_EVIDENCE'; payload: EvidenceDocument }
|
|
| { type: 'UPDATE_EVIDENCE'; payload: { id: string; data: Partial<EvidenceDocument> } }
|
|
| { type: 'DELETE_EVIDENCE'; payload: string }
|
|
| { type: 'SET_DERIVED_TOMS'; payload: DerivedTOM[] }
|
|
| { type: 'UPDATE_DERIVED_TOM'; payload: { id: string; data: Partial<DerivedTOM> } }
|
|
| { type: 'SET_GAP_ANALYSIS'; payload: GapAnalysisResult }
|
|
| { type: 'ADD_EXPORT'; payload: ExportRecord }
|
|
| { type: 'BULK_UPDATE_TOMS'; payload: { updates: Array<{ id: string; data: Partial<DerivedTOM> }> } }
|
|
| { type: 'LOAD_STATE'; payload: TOMGeneratorState }
|
|
|
|
// =============================================================================
|
|
// REDUCER
|
|
// =============================================================================
|
|
|
|
function tomGeneratorReducer(
|
|
state: TOMGeneratorState,
|
|
action: TOMGeneratorAction
|
|
): TOMGeneratorState {
|
|
const updateState = (updates: Partial<TOMGeneratorState>): TOMGeneratorState => ({
|
|
...state,
|
|
...updates,
|
|
updatedAt: new Date(),
|
|
})
|
|
|
|
switch (action.type) {
|
|
case 'INITIALIZE': {
|
|
if (action.payload.state) {
|
|
return action.payload.state
|
|
}
|
|
return createInitialTOMGeneratorState(action.payload.tenantId)
|
|
}
|
|
|
|
case 'RESET': {
|
|
return createInitialTOMGeneratorState(action.payload.tenantId)
|
|
}
|
|
|
|
case 'SET_CURRENT_STEP': {
|
|
return updateState({ currentStep: action.payload })
|
|
}
|
|
|
|
case 'SET_COMPANY_PROFILE': {
|
|
return updateState({ companyProfile: action.payload })
|
|
}
|
|
|
|
case 'UPDATE_COMPANY_PROFILE': {
|
|
if (!state.companyProfile) return state
|
|
return updateState({
|
|
companyProfile: { ...state.companyProfile, ...action.payload },
|
|
})
|
|
}
|
|
|
|
case 'SET_DATA_PROFILE': {
|
|
// Automatically set hasSpecialCategories based on categories
|
|
const profile: DataProfile = {
|
|
...action.payload,
|
|
hasSpecialCategories: hasSpecialCategories(action.payload.categories),
|
|
}
|
|
return updateState({ dataProfile: profile })
|
|
}
|
|
|
|
case 'UPDATE_DATA_PROFILE': {
|
|
if (!state.dataProfile) return state
|
|
const updatedProfile = { ...state.dataProfile, ...action.payload }
|
|
// Recalculate hasSpecialCategories if categories changed
|
|
if (action.payload.categories) {
|
|
updatedProfile.hasSpecialCategories = hasSpecialCategories(
|
|
action.payload.categories
|
|
)
|
|
}
|
|
return updateState({ dataProfile: updatedProfile })
|
|
}
|
|
|
|
case 'SET_ARCHITECTURE_PROFILE': {
|
|
return updateState({ architectureProfile: action.payload })
|
|
}
|
|
|
|
case 'UPDATE_ARCHITECTURE_PROFILE': {
|
|
if (!state.architectureProfile) return state
|
|
return updateState({
|
|
architectureProfile: { ...state.architectureProfile, ...action.payload },
|
|
})
|
|
}
|
|
|
|
case 'SET_SECURITY_PROFILE': {
|
|
return updateState({ securityProfile: action.payload })
|
|
}
|
|
|
|
case 'UPDATE_SECURITY_PROFILE': {
|
|
if (!state.securityProfile) return state
|
|
return updateState({
|
|
securityProfile: { ...state.securityProfile, ...action.payload },
|
|
})
|
|
}
|
|
|
|
case 'SET_RISK_PROFILE': {
|
|
// Automatically calculate protection level and DSFA requirement
|
|
const profile: RiskProfile = {
|
|
...action.payload,
|
|
protectionLevel: calculateProtectionLevel(action.payload.ciaAssessment),
|
|
dsfaRequired: isDSFARequired(state.dataProfile, action.payload),
|
|
}
|
|
return updateState({ riskProfile: profile })
|
|
}
|
|
|
|
case 'UPDATE_RISK_PROFILE': {
|
|
if (!state.riskProfile) return state
|
|
const updatedProfile = { ...state.riskProfile, ...action.payload }
|
|
// Recalculate protection level if CIA assessment changed
|
|
if (action.payload.ciaAssessment) {
|
|
updatedProfile.protectionLevel = calculateProtectionLevel(
|
|
action.payload.ciaAssessment
|
|
)
|
|
}
|
|
// Recalculate DSFA requirement
|
|
updatedProfile.dsfaRequired = isDSFARequired(state.dataProfile, updatedProfile)
|
|
return updateState({ riskProfile: updatedProfile })
|
|
}
|
|
|
|
case 'COMPLETE_STEP': {
|
|
const updatedSteps = state.steps.map((step) =>
|
|
step.id === action.payload.stepId
|
|
? {
|
|
...step,
|
|
completed: true,
|
|
data: action.payload.data,
|
|
validatedAt: new Date(),
|
|
}
|
|
: step
|
|
)
|
|
return updateState({ steps: updatedSteps })
|
|
}
|
|
|
|
case 'UNCOMPLETE_STEP': {
|
|
const updatedSteps = state.steps.map((step) =>
|
|
step.id === action.payload
|
|
? { ...step, completed: false, validatedAt: null }
|
|
: step
|
|
)
|
|
return updateState({ steps: updatedSteps })
|
|
}
|
|
|
|
case 'ADD_EVIDENCE': {
|
|
return updateState({
|
|
documents: [...state.documents, action.payload],
|
|
})
|
|
}
|
|
|
|
case 'UPDATE_EVIDENCE': {
|
|
const updatedDocuments = state.documents.map((doc) =>
|
|
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
|
|
)
|
|
return updateState({ documents: updatedDocuments })
|
|
}
|
|
|
|
case 'DELETE_EVIDENCE': {
|
|
return updateState({
|
|
documents: state.documents.filter((doc) => doc.id !== action.payload),
|
|
})
|
|
}
|
|
|
|
case 'SET_DERIVED_TOMS': {
|
|
return updateState({ derivedTOMs: action.payload })
|
|
}
|
|
|
|
case 'UPDATE_DERIVED_TOM': {
|
|
const updatedTOMs = state.derivedTOMs.map((tom) =>
|
|
tom.id === action.payload.id ? { ...tom, ...action.payload.data } : tom
|
|
)
|
|
return updateState({ derivedTOMs: updatedTOMs })
|
|
}
|
|
|
|
case 'SET_GAP_ANALYSIS': {
|
|
return updateState({ gapAnalysis: action.payload })
|
|
}
|
|
|
|
case 'ADD_EXPORT': {
|
|
return updateState({
|
|
exports: [...state.exports, action.payload],
|
|
})
|
|
}
|
|
|
|
case 'BULK_UPDATE_TOMS': {
|
|
let updatedTOMs = [...state.derivedTOMs]
|
|
for (const update of action.payload.updates) {
|
|
updatedTOMs = updatedTOMs.map((tom) =>
|
|
tom.id === update.id ? { ...tom, ...update.data } : tom
|
|
)
|
|
}
|
|
return updateState({ derivedTOMs: updatedTOMs })
|
|
}
|
|
|
|
case 'LOAD_STATE': {
|
|
return action.payload
|
|
}
|
|
|
|
default:
|
|
return state
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONTEXT VALUE INTERFACE
|
|
// =============================================================================
|
|
|
|
interface TOMGeneratorContextValue {
|
|
state: TOMGeneratorState
|
|
dispatch: React.Dispatch<TOMGeneratorAction>
|
|
|
|
// Navigation
|
|
currentStepIndex: number
|
|
totalSteps: number
|
|
canGoNext: boolean
|
|
canGoPrevious: boolean
|
|
goToStep: (stepId: TOMGeneratorStepId) => void
|
|
goToNextStep: () => void
|
|
goToPreviousStep: () => void
|
|
completeCurrentStep: (data: unknown) => void
|
|
|
|
// Profile setters
|
|
setCompanyProfile: (profile: CompanyProfile) => void
|
|
updateCompanyProfile: (data: Partial<CompanyProfile>) => void
|
|
setDataProfile: (profile: DataProfile) => void
|
|
updateDataProfile: (data: Partial<DataProfile>) => void
|
|
setArchitectureProfile: (profile: ArchitectureProfile) => void
|
|
updateArchitectureProfile: (data: Partial<ArchitectureProfile>) => void
|
|
setSecurityProfile: (profile: SecurityProfile) => void
|
|
updateSecurityProfile: (data: Partial<SecurityProfile>) => void
|
|
setRiskProfile: (profile: RiskProfile) => void
|
|
updateRiskProfile: (data: Partial<RiskProfile>) => void
|
|
|
|
// Evidence management
|
|
addEvidence: (document: EvidenceDocument) => void
|
|
updateEvidence: (id: string, data: Partial<EvidenceDocument>) => void
|
|
deleteEvidence: (id: string) => void
|
|
|
|
// TOM derivation
|
|
deriveTOMs: () => void
|
|
updateDerivedTOM: (id: string, data: Partial<DerivedTOM>) => void
|
|
bulkUpdateTOMs: (updates: Array<{ id: string; data: Partial<DerivedTOM> }>) => void
|
|
|
|
// Gap analysis
|
|
runGapAnalysis: () => void
|
|
|
|
// Export
|
|
addExport: (record: ExportRecord) => void
|
|
|
|
// Persistence
|
|
saveState: () => Promise<void>
|
|
loadState: () => Promise<void>
|
|
resetState: () => void
|
|
|
|
// Status
|
|
isStepCompleted: (stepId: TOMGeneratorStepId) => boolean
|
|
getCompletionPercentage: () => number
|
|
isLoading: boolean
|
|
error: string | null
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONTEXT
|
|
// =============================================================================
|
|
|
|
const TOMGeneratorContext = createContext<TOMGeneratorContextValue | null>(null)
|
|
|
|
// =============================================================================
|
|
// STORAGE KEYS
|
|
// =============================================================================
|
|
|
|
const STORAGE_KEY_PREFIX = 'tom-generator-state-'
|
|
|
|
function getStorageKey(tenantId: string): string {
|
|
return `${STORAGE_KEY_PREFIX}${tenantId}`
|
|
}
|
|
|
|
// =============================================================================
|
|
// PROVIDER COMPONENT
|
|
// =============================================================================
|
|
|
|
interface TOMGeneratorProviderProps {
|
|
children: ReactNode
|
|
tenantId: string
|
|
initialState?: TOMGeneratorState
|
|
enablePersistence?: boolean
|
|
}
|
|
|
|
export function TOMGeneratorProvider({
|
|
children,
|
|
tenantId,
|
|
initialState,
|
|
enablePersistence = true,
|
|
}: TOMGeneratorProviderProps) {
|
|
const [state, dispatch] = useReducer(
|
|
tomGeneratorReducer,
|
|
initialState ?? createInitialTOMGeneratorState(tenantId)
|
|
)
|
|
|
|
const [isLoading, setIsLoading] = React.useState(false)
|
|
const [error, setError] = React.useState<string | null>(null)
|
|
|
|
const rulesEngineRef = useRef<TOMRulesEngine | null>(null)
|
|
|
|
// Initialize rules engine
|
|
useEffect(() => {
|
|
if (!rulesEngineRef.current) {
|
|
rulesEngineRef.current = new TOMRulesEngine()
|
|
}
|
|
}, [])
|
|
|
|
// Load state from localStorage on mount
|
|
useEffect(() => {
|
|
if (enablePersistence && typeof window !== 'undefined') {
|
|
try {
|
|
const stored = localStorage.getItem(getStorageKey(tenantId))
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored)
|
|
// Convert date strings back to Date objects
|
|
if (parsed.createdAt) parsed.createdAt = new Date(parsed.createdAt)
|
|
if (parsed.updatedAt) parsed.updatedAt = new Date(parsed.updatedAt)
|
|
if (parsed.steps) {
|
|
parsed.steps = parsed.steps.map((step: WizardStep) => ({
|
|
...step,
|
|
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
|
}))
|
|
}
|
|
if (parsed.documents) {
|
|
parsed.documents = parsed.documents.map((doc: EvidenceDocument) => ({
|
|
...doc,
|
|
uploadedAt: new Date(doc.uploadedAt),
|
|
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
|
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
|
aiAnalysis: doc.aiAnalysis
|
|
? {
|
|
...doc.aiAnalysis,
|
|
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
|
}
|
|
: null,
|
|
}))
|
|
}
|
|
if (parsed.derivedTOMs) {
|
|
parsed.derivedTOMs = parsed.derivedTOMs.map((tom: DerivedTOM) => ({
|
|
...tom,
|
|
implementationDate: tom.implementationDate
|
|
? new Date(tom.implementationDate)
|
|
: null,
|
|
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
|
}))
|
|
}
|
|
if (parsed.gapAnalysis?.generatedAt) {
|
|
parsed.gapAnalysis.generatedAt = new Date(parsed.gapAnalysis.generatedAt)
|
|
}
|
|
if (parsed.exports) {
|
|
parsed.exports = parsed.exports.map((exp: ExportRecord) => ({
|
|
...exp,
|
|
generatedAt: new Date(exp.generatedAt),
|
|
}))
|
|
}
|
|
dispatch({ type: 'LOAD_STATE', payload: parsed })
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load TOM Generator state from localStorage:', e)
|
|
}
|
|
}
|
|
}, [tenantId, enablePersistence])
|
|
|
|
// Save state to localStorage on changes
|
|
useEffect(() => {
|
|
if (enablePersistence && typeof window !== 'undefined') {
|
|
try {
|
|
localStorage.setItem(getStorageKey(tenantId), JSON.stringify(state))
|
|
} catch (e) {
|
|
console.error('Failed to save TOM Generator state to localStorage:', e)
|
|
}
|
|
}
|
|
}, [state, tenantId, enablePersistence])
|
|
|
|
// Navigation helpers
|
|
const currentStepIndex = getStepIndex(state.currentStep)
|
|
const totalSteps = TOM_GENERATOR_STEPS.length
|
|
|
|
const canGoNext = currentStepIndex < totalSteps - 1
|
|
const canGoPrevious = currentStepIndex > 0
|
|
|
|
const goToStep = useCallback((stepId: TOMGeneratorStepId) => {
|
|
dispatch({ type: 'SET_CURRENT_STEP', payload: stepId })
|
|
}, [])
|
|
|
|
const goToNextStep = useCallback(() => {
|
|
if (canGoNext) {
|
|
const nextStep = TOM_GENERATOR_STEPS[currentStepIndex + 1]
|
|
dispatch({ type: 'SET_CURRENT_STEP', payload: nextStep.id })
|
|
}
|
|
}, [canGoNext, currentStepIndex])
|
|
|
|
const goToPreviousStep = useCallback(() => {
|
|
if (canGoPrevious) {
|
|
const prevStep = TOM_GENERATOR_STEPS[currentStepIndex - 1]
|
|
dispatch({ type: 'SET_CURRENT_STEP', payload: prevStep.id })
|
|
}
|
|
}, [canGoPrevious, currentStepIndex])
|
|
|
|
const completeCurrentStep = useCallback(
|
|
(data: unknown) => {
|
|
dispatch({
|
|
type: 'COMPLETE_STEP',
|
|
payload: { stepId: state.currentStep, data },
|
|
})
|
|
},
|
|
[state.currentStep]
|
|
)
|
|
|
|
// Profile setters
|
|
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
|
|
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
|
|
}, [])
|
|
|
|
const updateCompanyProfile = useCallback((data: Partial<CompanyProfile>) => {
|
|
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: data })
|
|
}, [])
|
|
|
|
const setDataProfile = useCallback((profile: DataProfile) => {
|
|
dispatch({ type: 'SET_DATA_PROFILE', payload: profile })
|
|
}, [])
|
|
|
|
const updateDataProfile = useCallback((data: Partial<DataProfile>) => {
|
|
dispatch({ type: 'UPDATE_DATA_PROFILE', payload: data })
|
|
}, [])
|
|
|
|
const setArchitectureProfile = useCallback((profile: ArchitectureProfile) => {
|
|
dispatch({ type: 'SET_ARCHITECTURE_PROFILE', payload: profile })
|
|
}, [])
|
|
|
|
const updateArchitectureProfile = useCallback(
|
|
(data: Partial<ArchitectureProfile>) => {
|
|
dispatch({ type: 'UPDATE_ARCHITECTURE_PROFILE', payload: data })
|
|
},
|
|
[]
|
|
)
|
|
|
|
const setSecurityProfile = useCallback((profile: SecurityProfile) => {
|
|
dispatch({ type: 'SET_SECURITY_PROFILE', payload: profile })
|
|
}, [])
|
|
|
|
const updateSecurityProfile = useCallback((data: Partial<SecurityProfile>) => {
|
|
dispatch({ type: 'UPDATE_SECURITY_PROFILE', payload: data })
|
|
}, [])
|
|
|
|
const setRiskProfile = useCallback((profile: RiskProfile) => {
|
|
dispatch({ type: 'SET_RISK_PROFILE', payload: profile })
|
|
}, [])
|
|
|
|
const updateRiskProfile = useCallback((data: Partial<RiskProfile>) => {
|
|
dispatch({ type: 'UPDATE_RISK_PROFILE', payload: data })
|
|
}, [])
|
|
|
|
// Evidence management
|
|
const addEvidence = useCallback((document: EvidenceDocument) => {
|
|
dispatch({ type: 'ADD_EVIDENCE', payload: document })
|
|
}, [])
|
|
|
|
const updateEvidence = useCallback(
|
|
(id: string, data: Partial<EvidenceDocument>) => {
|
|
dispatch({ type: 'UPDATE_EVIDENCE', payload: { id, data } })
|
|
},
|
|
[]
|
|
)
|
|
|
|
const deleteEvidence = useCallback((id: string) => {
|
|
dispatch({ type: 'DELETE_EVIDENCE', payload: id })
|
|
}, [])
|
|
|
|
// TOM derivation
|
|
const deriveTOMs = useCallback(() => {
|
|
if (!rulesEngineRef.current) return
|
|
|
|
const derivedTOMs = rulesEngineRef.current.deriveAllTOMs({
|
|
companyProfile: state.companyProfile,
|
|
dataProfile: state.dataProfile,
|
|
architectureProfile: state.architectureProfile,
|
|
securityProfile: state.securityProfile,
|
|
riskProfile: state.riskProfile,
|
|
})
|
|
|
|
dispatch({ type: 'SET_DERIVED_TOMS', payload: derivedTOMs })
|
|
}, [
|
|
state.companyProfile,
|
|
state.dataProfile,
|
|
state.architectureProfile,
|
|
state.securityProfile,
|
|
state.riskProfile,
|
|
])
|
|
|
|
const updateDerivedTOM = useCallback(
|
|
(id: string, data: Partial<DerivedTOM>) => {
|
|
dispatch({ type: 'UPDATE_DERIVED_TOM', payload: { id, data } })
|
|
},
|
|
[]
|
|
)
|
|
|
|
// Gap analysis
|
|
const runGapAnalysis = useCallback(() => {
|
|
if (!rulesEngineRef.current) return
|
|
|
|
const result = rulesEngineRef.current.performGapAnalysis(
|
|
state.derivedTOMs,
|
|
state.documents
|
|
)
|
|
|
|
dispatch({ type: 'SET_GAP_ANALYSIS', payload: result })
|
|
}, [state.derivedTOMs, state.documents])
|
|
|
|
// Export
|
|
const addExport = useCallback((record: ExportRecord) => {
|
|
dispatch({ type: 'ADD_EXPORT', payload: record })
|
|
}, [])
|
|
|
|
// Persistence
|
|
const saveState = useCallback(async () => {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
try {
|
|
// API call to save state
|
|
const response = await fetch('/api/sdk/v1/tom-generator/state', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ tenantId, state }),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to save state')
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
throw e
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [tenantId, state])
|
|
|
|
const loadState = useCallback(async () => {
|
|
setIsLoading(true)
|
|
setError(null)
|
|
try {
|
|
const response = await fetch(
|
|
`/api/sdk/v1/tom-generator/state?tenantId=${tenantId}`
|
|
)
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load state')
|
|
}
|
|
|
|
const data = await response.json()
|
|
if (data.state) {
|
|
dispatch({ type: 'LOAD_STATE', payload: data.state })
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unknown error')
|
|
throw e
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}, [tenantId])
|
|
|
|
const resetState = useCallback(() => {
|
|
dispatch({ type: 'RESET', payload: { tenantId } })
|
|
}, [tenantId])
|
|
|
|
// Status helpers
|
|
const isStepCompleted = useCallback(
|
|
(stepId: TOMGeneratorStepId) => {
|
|
const step = state.steps.find((s) => s.id === stepId)
|
|
return step?.completed ?? false
|
|
},
|
|
[state.steps]
|
|
)
|
|
|
|
const getCompletionPercentage = useCallback(() => {
|
|
const completedSteps = state.steps.filter((s) => s.completed).length
|
|
return Math.round((completedSteps / totalSteps) * 100)
|
|
}, [state.steps, totalSteps])
|
|
|
|
const contextValue: TOMGeneratorContextValue = {
|
|
state,
|
|
dispatch,
|
|
|
|
currentStepIndex,
|
|
totalSteps,
|
|
canGoNext,
|
|
canGoPrevious,
|
|
goToStep,
|
|
goToNextStep,
|
|
goToPreviousStep,
|
|
completeCurrentStep,
|
|
|
|
setCompanyProfile,
|
|
updateCompanyProfile,
|
|
setDataProfile,
|
|
updateDataProfile,
|
|
setArchitectureProfile,
|
|
updateArchitectureProfile,
|
|
setSecurityProfile,
|
|
updateSecurityProfile,
|
|
setRiskProfile,
|
|
updateRiskProfile,
|
|
|
|
addEvidence,
|
|
updateEvidence,
|
|
deleteEvidence,
|
|
|
|
deriveTOMs,
|
|
updateDerivedTOM,
|
|
|
|
runGapAnalysis,
|
|
|
|
addExport,
|
|
|
|
saveState,
|
|
loadState,
|
|
resetState,
|
|
|
|
isStepCompleted,
|
|
getCompletionPercentage,
|
|
isLoading,
|
|
error,
|
|
}
|
|
|
|
return (
|
|
<TOMGeneratorContext.Provider value={contextValue}>
|
|
{children}
|
|
</TOMGeneratorContext.Provider>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// HOOK
|
|
// =============================================================================
|
|
|
|
export function useTOMGenerator(): TOMGeneratorContextValue {
|
|
const context = useContext(TOMGeneratorContext)
|
|
if (!context) {
|
|
throw new Error(
|
|
'useTOMGenerator must be used within a TOMGeneratorProvider'
|
|
)
|
|
}
|
|
return context
|
|
}
|
|
|
|
// =============================================================================
|
|
// EXPORTS
|
|
// =============================================================================
|
|
|
|
export { TOMGeneratorContext }
|
|
export type { TOMGeneratorAction, TOMGeneratorContextValue }
|