Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
377 lines
8.7 KiB
TypeScript
377 lines
8.7 KiB
TypeScript
'use client'
|
|
|
|
import React, {
|
|
createContext,
|
|
useContext,
|
|
useReducer,
|
|
useCallback,
|
|
useMemo,
|
|
useEffect,
|
|
useState,
|
|
} from 'react'
|
|
|
|
import type {
|
|
VendorComplianceContextValue,
|
|
ProcessingActivity,
|
|
Vendor,
|
|
ContractDocument,
|
|
Finding,
|
|
ControlInstance,
|
|
ExportFormat,
|
|
} from './types'
|
|
|
|
import {
|
|
vendorComplianceReducer,
|
|
initialState,
|
|
computeVendorStats,
|
|
computeComplianceStats,
|
|
computeRiskOverview,
|
|
} from './context-reducer'
|
|
|
|
import {
|
|
loadAllData,
|
|
apiCreateProcessingActivity,
|
|
apiUpdateProcessingActivity,
|
|
apiDeleteProcessingActivity,
|
|
apiDuplicateProcessingActivity,
|
|
apiCreateVendor,
|
|
apiUpdateVendor,
|
|
apiDeleteVendor,
|
|
apiUploadContract,
|
|
apiUpdateContract,
|
|
apiDeleteContract,
|
|
apiStartContractReview,
|
|
apiUpdateFinding,
|
|
apiResolveFinding,
|
|
apiUpdateControlInstance,
|
|
apiExportVVT,
|
|
apiExportVendorAuditPack,
|
|
apiExportRoPA,
|
|
} from './context-actions'
|
|
|
|
// ==========================================
|
|
// CONTEXT
|
|
// ==========================================
|
|
|
|
const VendorComplianceContext = createContext<VendorComplianceContextValue | null>(null)
|
|
|
|
// ==========================================
|
|
// PROVIDER
|
|
// ==========================================
|
|
|
|
interface VendorComplianceProviderProps {
|
|
children: React.ReactNode
|
|
tenantId?: string
|
|
}
|
|
|
|
export function VendorComplianceProvider({
|
|
children,
|
|
tenantId,
|
|
}: VendorComplianceProviderProps) {
|
|
const [state, dispatch] = useReducer(vendorComplianceReducer, initialState)
|
|
const [isInitialized, setIsInitialized] = useState(false)
|
|
|
|
// ==========================================
|
|
// COMPUTED VALUES
|
|
// ==========================================
|
|
|
|
const vendorStats = useMemo(
|
|
() => computeVendorStats(state),
|
|
[state.vendors, state.contracts]
|
|
)
|
|
|
|
const complianceStats = useMemo(
|
|
() => computeComplianceStats(state),
|
|
[state.findings, state.contracts, state.controlInstances]
|
|
)
|
|
|
|
const riskOverview = useMemo(
|
|
() => computeRiskOverview(state),
|
|
[state.vendors, state.findings]
|
|
)
|
|
|
|
// ==========================================
|
|
// ACTION WRAPPERS
|
|
// ==========================================
|
|
|
|
const loadData = useCallback(async () => {
|
|
await loadAllData(dispatch)
|
|
}, [])
|
|
|
|
const refresh = useCallback(async () => {
|
|
await loadData()
|
|
}, [loadData])
|
|
|
|
const createProcessingActivity = useCallback(
|
|
async (data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>) => {
|
|
return apiCreateProcessingActivity(dispatch, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const updateProcessingActivity = useCallback(
|
|
async (id: string, data: Partial<ProcessingActivity>) => {
|
|
await apiUpdateProcessingActivity(dispatch, id, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const deleteProcessingActivity = useCallback(
|
|
async (id: string) => {
|
|
await apiDeleteProcessingActivity(dispatch, id)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const duplicateProcessingActivity = useCallback(
|
|
async (id: string) => {
|
|
return apiDuplicateProcessingActivity(dispatch, state, id)
|
|
},
|
|
[state]
|
|
)
|
|
|
|
const createVendor = useCallback(
|
|
async (data: Omit<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>) => {
|
|
return apiCreateVendor(dispatch, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const updateVendor = useCallback(
|
|
async (id: string, data: Partial<Vendor>) => {
|
|
await apiUpdateVendor(dispatch, id, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const deleteVendor = useCallback(
|
|
async (id: string) => {
|
|
await apiDeleteVendor(dispatch, id)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const uploadContract = useCallback(
|
|
async (vendorId: string, file: File, metadata: Partial<ContractDocument>) => {
|
|
return apiUploadContract(dispatch, state, vendorId, file, metadata)
|
|
},
|
|
[state]
|
|
)
|
|
|
|
const updateContract = useCallback(
|
|
async (id: string, data: Partial<ContractDocument>) => {
|
|
await apiUpdateContract(dispatch, id, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const deleteContract = useCallback(
|
|
async (id: string) => {
|
|
await apiDeleteContract(dispatch, state, id)
|
|
},
|
|
[state]
|
|
)
|
|
|
|
const startContractReview = useCallback(
|
|
async (contractId: string) => {
|
|
await apiStartContractReview(dispatch, contractId)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const updateFinding = useCallback(
|
|
async (id: string, data: Partial<Finding>) => {
|
|
await apiUpdateFinding(dispatch, id, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const resolveFinding = useCallback(
|
|
async (id: string, resolution: string) => {
|
|
await apiResolveFinding(dispatch, id, resolution)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const updateControlInstance = useCallback(
|
|
async (id: string, data: Partial<ControlInstance>) => {
|
|
await apiUpdateControlInstance(dispatch, id, data)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const exportVVT = useCallback(
|
|
async (format: ExportFormat, activityIds?: string[]) => {
|
|
return apiExportVVT(format, activityIds)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const exportVendorAuditPack = useCallback(
|
|
async (vendorId: string, format: ExportFormat) => {
|
|
return apiExportVendorAuditPack(vendorId, format)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const exportRoPA = useCallback(
|
|
async (format: ExportFormat) => {
|
|
return apiExportRoPA(format)
|
|
},
|
|
[]
|
|
)
|
|
|
|
// ==========================================
|
|
// INITIALIZATION
|
|
// ==========================================
|
|
|
|
useEffect(() => {
|
|
if (!isInitialized) {
|
|
loadData()
|
|
setIsInitialized(true)
|
|
}
|
|
}, [isInitialized, loadData])
|
|
|
|
// ==========================================
|
|
// CONTEXT VALUE
|
|
// ==========================================
|
|
|
|
const contextValue = useMemo<VendorComplianceContextValue>(
|
|
() => ({
|
|
...state,
|
|
dispatch,
|
|
vendorStats,
|
|
complianceStats,
|
|
riskOverview,
|
|
createProcessingActivity,
|
|
updateProcessingActivity,
|
|
deleteProcessingActivity,
|
|
duplicateProcessingActivity,
|
|
createVendor,
|
|
updateVendor,
|
|
deleteVendor,
|
|
uploadContract,
|
|
updateContract,
|
|
deleteContract,
|
|
startContractReview,
|
|
updateFinding,
|
|
resolveFinding,
|
|
updateControlInstance,
|
|
exportVVT,
|
|
exportVendorAuditPack,
|
|
exportRoPA,
|
|
loadData,
|
|
refresh,
|
|
}),
|
|
[
|
|
state,
|
|
vendorStats,
|
|
complianceStats,
|
|
riskOverview,
|
|
createProcessingActivity,
|
|
updateProcessingActivity,
|
|
deleteProcessingActivity,
|
|
duplicateProcessingActivity,
|
|
createVendor,
|
|
updateVendor,
|
|
deleteVendor,
|
|
uploadContract,
|
|
updateContract,
|
|
deleteContract,
|
|
startContractReview,
|
|
updateFinding,
|
|
resolveFinding,
|
|
updateControlInstance,
|
|
exportVVT,
|
|
exportVendorAuditPack,
|
|
exportRoPA,
|
|
loadData,
|
|
refresh,
|
|
]
|
|
)
|
|
|
|
return (
|
|
<VendorComplianceContext.Provider value={contextValue}>
|
|
{children}
|
|
</VendorComplianceContext.Provider>
|
|
)
|
|
}
|
|
|
|
// ==========================================
|
|
// HOOK
|
|
// ==========================================
|
|
|
|
export function useVendorCompliance(): VendorComplianceContextValue {
|
|
const context = useContext(VendorComplianceContext)
|
|
|
|
if (!context) {
|
|
throw new Error(
|
|
'useVendorCompliance must be used within a VendorComplianceProvider'
|
|
)
|
|
}
|
|
|
|
return context
|
|
}
|
|
|
|
// ==========================================
|
|
// SELECTORS
|
|
// ==========================================
|
|
|
|
export function useVendor(vendorId: string | null) {
|
|
const { vendors } = useVendorCompliance()
|
|
return useMemo(
|
|
() => vendors.find((v) => v.id === vendorId) ?? null,
|
|
[vendors, vendorId]
|
|
)
|
|
}
|
|
|
|
export function useProcessingActivity(activityId: string | null) {
|
|
const { processingActivities } = useVendorCompliance()
|
|
return useMemo(
|
|
() => processingActivities.find((a) => a.id === activityId) ?? null,
|
|
[processingActivities, activityId]
|
|
)
|
|
}
|
|
|
|
export function useVendorContracts(vendorId: string | null) {
|
|
const { contracts } = useVendorCompliance()
|
|
return useMemo(
|
|
() => contracts.filter((c) => c.vendorId === vendorId),
|
|
[contracts, vendorId]
|
|
)
|
|
}
|
|
|
|
export function useVendorFindings(vendorId: string | null) {
|
|
const { findings } = useVendorCompliance()
|
|
return useMemo(
|
|
() => findings.filter((f) => f.vendorId === vendorId),
|
|
[findings, vendorId]
|
|
)
|
|
}
|
|
|
|
export function useContractFindings(contractId: string | null) {
|
|
const { findings } = useVendorCompliance()
|
|
return useMemo(
|
|
() => findings.filter((f) => f.contractId === contractId),
|
|
[findings, contractId]
|
|
)
|
|
}
|
|
|
|
export function useControlInstancesForEntity(
|
|
entityType: 'VENDOR' | 'PROCESSING_ACTIVITY',
|
|
entityId: string | null
|
|
) {
|
|
const { controlInstances, controls } = useVendorCompliance()
|
|
|
|
return useMemo(() => {
|
|
if (!entityId) return []
|
|
|
|
return controlInstances
|
|
.filter((ci) => ci.entityType === entityType && ci.entityId === entityId)
|
|
.map((ci) => ({
|
|
...ci,
|
|
control: controls.find((c) => c.id === ci.controlId),
|
|
}))
|
|
}, [controlInstances, controls, entityType, entityId])
|
|
}
|