'use client' import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect, useState, } from 'react' import { VendorComplianceState, VendorComplianceAction, VendorComplianceContextValue, ProcessingActivity, Vendor, ContractDocument, Finding, Control, ControlInstance, RiskAssessment, VendorStatistics, ComplianceStatistics, RiskOverview, ExportFormat, VendorStatus, VendorRole, RiskLevel, FindingType, FindingSeverity, getRiskLevelFromScore, } from './types' // ========================================== // INITIAL STATE // ========================================== const initialState: VendorComplianceState = { processingActivities: [], vendors: [], contracts: [], findings: [], controls: [], controlInstances: [], riskAssessments: [], isLoading: false, error: null, selectedVendorId: null, selectedActivityId: null, activeTab: 'overview', lastModified: null, } // ========================================== // REDUCER // ========================================== function vendorComplianceReducer( state: VendorComplianceState, action: VendorComplianceAction ): VendorComplianceState { const updateState = (updates: Partial): VendorComplianceState => ({ ...state, ...updates, lastModified: new Date(), }) switch (action.type) { // Processing Activities case 'SET_PROCESSING_ACTIVITIES': return updateState({ processingActivities: action.payload }) case 'ADD_PROCESSING_ACTIVITY': return updateState({ processingActivities: [...state.processingActivities, action.payload], }) case 'UPDATE_PROCESSING_ACTIVITY': return updateState({ processingActivities: state.processingActivities.map((activity) => activity.id === action.payload.id ? { ...activity, ...action.payload.data, updatedAt: new Date() } : activity ), }) case 'DELETE_PROCESSING_ACTIVITY': return updateState({ processingActivities: state.processingActivities.filter( (activity) => activity.id !== action.payload ), }) // Vendors case 'SET_VENDORS': return updateState({ vendors: action.payload }) case 'ADD_VENDOR': return updateState({ vendors: [...state.vendors, action.payload], }) case 'UPDATE_VENDOR': return updateState({ vendors: state.vendors.map((vendor) => vendor.id === action.payload.id ? { ...vendor, ...action.payload.data, updatedAt: new Date() } : vendor ), }) case 'DELETE_VENDOR': return updateState({ vendors: state.vendors.filter((vendor) => vendor.id !== action.payload), }) // Contracts case 'SET_CONTRACTS': return updateState({ contracts: action.payload }) case 'ADD_CONTRACT': return updateState({ contracts: [...state.contracts, action.payload], }) case 'UPDATE_CONTRACT': return updateState({ contracts: state.contracts.map((contract) => contract.id === action.payload.id ? { ...contract, ...action.payload.data, updatedAt: new Date() } : contract ), }) case 'DELETE_CONTRACT': return updateState({ contracts: state.contracts.filter((contract) => contract.id !== action.payload), }) // Findings case 'SET_FINDINGS': return updateState({ findings: action.payload }) case 'ADD_FINDINGS': return updateState({ findings: [...state.findings, ...action.payload], }) case 'UPDATE_FINDING': return updateState({ findings: state.findings.map((finding) => finding.id === action.payload.id ? { ...finding, ...action.payload.data, updatedAt: new Date() } : finding ), }) // Controls case 'SET_CONTROLS': return updateState({ controls: action.payload }) case 'SET_CONTROL_INSTANCES': return updateState({ controlInstances: action.payload }) case 'UPDATE_CONTROL_INSTANCE': return updateState({ controlInstances: state.controlInstances.map((instance) => instance.id === action.payload.id ? { ...instance, ...action.payload.data } : instance ), }) // Risk Assessments case 'SET_RISK_ASSESSMENTS': return updateState({ riskAssessments: action.payload }) case 'UPDATE_RISK_ASSESSMENT': return updateState({ riskAssessments: state.riskAssessments.map((assessment) => assessment.id === action.payload.id ? { ...assessment, ...action.payload.data } : assessment ), }) // UI State case 'SET_LOADING': return { ...state, isLoading: action.payload } case 'SET_ERROR': return { ...state, error: action.payload } case 'SET_SELECTED_VENDOR': return { ...state, selectedVendorId: action.payload } case 'SET_SELECTED_ACTIVITY': return { ...state, selectedActivityId: action.payload } case 'SET_ACTIVE_TAB': return { ...state, activeTab: action.payload } default: return state } } // ========================================== // CONTEXT // ========================================== const VendorComplianceContext = createContext(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(() => { const vendors = state.vendors const byStatus = vendors.reduce( (acc, v) => { acc[v.status] = (acc[v.status] || 0) + 1 return acc }, {} as Record ) const byRole = vendors.reduce( (acc, v) => { acc[v.role] = (acc[v.role] || 0) + 1 return acc }, {} as Record ) const byRiskLevel = vendors.reduce( (acc, v) => { const level = getRiskLevelFromScore(v.residualRiskScore / 4) // Normalize to 1-25 acc[level] = (acc[level] || 0) + 1 return acc }, {} as Record ) const now = new Date() const pendingReviews = vendors.filter( (v) => v.nextReviewDate && new Date(v.nextReviewDate) <= now ).length const withExpiredContracts = vendors.filter((v) => state.contracts.some( (c) => c.vendorId === v.id && c.expirationDate && new Date(c.expirationDate) <= now && c.status === 'ACTIVE' ) ).length return { total: vendors.length, byStatus, byRole, byRiskLevel, pendingReviews, withExpiredContracts, } }, [state.vendors, state.contracts]) const complianceStats = useMemo(() => { const findings = state.findings const contracts = state.contracts const controlInstances = state.controlInstances const averageComplianceScore = contracts.length > 0 ? contracts.reduce((sum, c) => sum + (c.complianceScore || 0), 0) / contracts.filter((c) => c.complianceScore !== undefined).length || 0 : 0 const findingsByType = findings.reduce( (acc, f) => { acc[f.type] = (acc[f.type] || 0) + 1 return acc }, {} as Record ) const findingsBySeverity = findings.reduce( (acc, f) => { acc[f.severity] = (acc[f.severity] || 0) + 1 return acc }, {} as Record ) const openFindings = findings.filter( (f) => f.status === 'OPEN' || f.status === 'IN_PROGRESS' ).length const resolvedFindings = findings.filter( (f) => f.status === 'RESOLVED' || f.status === 'FALSE_POSITIVE' ).length const passedControls = controlInstances.filter( (ci) => ci.status === 'PASS' ).length const applicableControls = controlInstances.filter( (ci) => ci.status !== 'NOT_APPLICABLE' ).length const controlPassRate = applicableControls > 0 ? (passedControls / applicableControls) * 100 : 0 return { averageComplianceScore, findingsByType, findingsBySeverity, openFindings, resolvedFindings, controlPassRate, } }, [state.findings, state.contracts, state.controlInstances]) const riskOverview = useMemo(() => { const vendors = state.vendors const findings = state.findings const averageInherentRisk = vendors.length > 0 ? vendors.reduce((sum, v) => sum + v.inherentRiskScore, 0) / vendors.length : 0 const averageResidualRisk = vendors.length > 0 ? vendors.reduce((sum, v) => sum + v.residualRiskScore, 0) / vendors.length : 0 const highRiskVendors = vendors.filter( (v) => v.residualRiskScore >= 60 ).length const criticalFindings = findings.filter( (f) => f.severity === 'CRITICAL' && f.status === 'OPEN' ).length const transfersToThirdCountries = vendors.filter((v) => v.processingLocations.some((pl) => !pl.isEU && !pl.isAdequate) ).length return { averageInherentRisk, averageResidualRisk, highRiskVendors, criticalFindings, transfersToThirdCountries, } }, [state.vendors, state.findings]) // ========================================== // API CALLS // ========================================== const apiBase = '/api/sdk/v1/vendor-compliance' const loadData = useCallback(async () => { dispatch({ type: 'SET_LOADING', payload: true }) dispatch({ type: 'SET_ERROR', payload: null }) try { const [ activitiesRes, vendorsRes, contractsRes, findingsRes, controlsRes, controlInstancesRes, ] = await Promise.all([ fetch(`${apiBase}/processing-activities`), fetch(`${apiBase}/vendors`), fetch(`${apiBase}/contracts`), fetch(`${apiBase}/findings`), fetch(`${apiBase}/controls`), fetch(`${apiBase}/control-instances`), ]) if (activitiesRes.ok) { const data = await activitiesRes.json() dispatch({ type: 'SET_PROCESSING_ACTIVITIES', payload: data.data || [] }) } if (vendorsRes.ok) { const data = await vendorsRes.json() dispatch({ type: 'SET_VENDORS', payload: data.data || [] }) } if (contractsRes.ok) { const data = await contractsRes.json() dispatch({ type: 'SET_CONTRACTS', payload: data.data || [] }) } if (findingsRes.ok) { const data = await findingsRes.json() dispatch({ type: 'SET_FINDINGS', payload: data.data || [] }) } if (controlsRes.ok) { const data = await controlsRes.json() dispatch({ type: 'SET_CONTROLS', payload: data.data || [] }) } if (controlInstancesRes.ok) { const data = await controlInstancesRes.json() dispatch({ type: 'SET_CONTROL_INSTANCES', payload: data.data || [] }) } } catch (error) { console.error('Failed to load vendor compliance data:', error) dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden der Daten', }) } finally { dispatch({ type: 'SET_LOADING', payload: false }) } }, [apiBase]) const refresh = useCallback(async () => { await loadData() }, [loadData]) // ========================================== // PROCESSING ACTIVITIES ACTIONS // ========================================== const createProcessingActivity = useCallback( async ( data: Omit ): Promise => { const response = await fetch(`${apiBase}/processing-activities`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Erstellen der Verarbeitungstätigkeit') } const result = await response.json() const activity = result.data dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: activity }) return activity }, [apiBase] ) const updateProcessingActivity = useCallback( async (id: string, data: Partial): Promise => { const response = await fetch(`${apiBase}/processing-activities/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Aktualisieren der Verarbeitungstätigkeit') } dispatch({ type: 'UPDATE_PROCESSING_ACTIVITY', payload: { id, data } }) }, [apiBase] ) const deleteProcessingActivity = useCallback( async (id: string): Promise => { const response = await fetch(`${apiBase}/processing-activities/${id}`, { method: 'DELETE', }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Löschen der Verarbeitungstätigkeit') } dispatch({ type: 'DELETE_PROCESSING_ACTIVITY', payload: id }) }, [apiBase] ) const duplicateProcessingActivity = useCallback( async (id: string): Promise => { const original = state.processingActivities.find((a) => a.id === id) if (!original) { throw new Error('Verarbeitungstätigkeit nicht gefunden') } const { id: _id, vvtId: _vvtId, createdAt: _createdAt, updatedAt: _updatedAt, tenantId: _tenantId, ...rest } = original const newActivity = await createProcessingActivity({ ...rest, vvtId: '', // Will be generated by backend name: { de: `${original.name.de} (Kopie)`, en: `${original.name.en} (Copy)`, }, status: 'DRAFT', }) return newActivity }, [state.processingActivities, createProcessingActivity] ) // ========================================== // VENDOR ACTIONS // ========================================== const createVendor = useCallback( async ( data: Omit ): Promise => { const response = await fetch(`${apiBase}/vendors`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Erstellen des Vendors') } const result = await response.json() const vendor = result.data dispatch({ type: 'ADD_VENDOR', payload: vendor }) return vendor }, [apiBase] ) const updateVendor = useCallback( async (id: string, data: Partial): Promise => { const response = await fetch(`${apiBase}/vendors/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Aktualisieren des Vendors') } dispatch({ type: 'UPDATE_VENDOR', payload: { id, data } }) }, [apiBase] ) const deleteVendor = useCallback( async (id: string): Promise => { const response = await fetch(`${apiBase}/vendors/${id}`, { method: 'DELETE', }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Löschen des Vendors') } dispatch({ type: 'DELETE_VENDOR', payload: id }) }, [apiBase] ) // ========================================== // CONTRACT ACTIONS // ========================================== const uploadContract = useCallback( async ( vendorId: string, file: File, metadata: Partial ): Promise => { const formData = new FormData() formData.append('file', file) formData.append('vendorId', vendorId) formData.append('metadata', JSON.stringify(metadata)) const response = await fetch(`${apiBase}/contracts`, { method: 'POST', body: formData, }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Hochladen des Vertrags') } const result = await response.json() const contract = result.data dispatch({ type: 'ADD_CONTRACT', payload: contract }) // Update vendor's contracts list const vendor = state.vendors.find((v) => v.id === vendorId) if (vendor) { dispatch({ type: 'UPDATE_VENDOR', payload: { id: vendorId, data: { contracts: [...vendor.contracts, contract.id] }, }, }) } return contract }, [apiBase, state.vendors] ) const updateContract = useCallback( async (id: string, data: Partial): Promise => { const response = await fetch(`${apiBase}/contracts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Aktualisieren des Vertrags') } dispatch({ type: 'UPDATE_CONTRACT', payload: { id, data } }) }, [apiBase] ) const deleteContract = useCallback( async (id: string): Promise => { const contract = state.contracts.find((c) => c.id === id) const response = await fetch(`${apiBase}/contracts/${id}`, { method: 'DELETE', }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Löschen des Vertrags') } dispatch({ type: 'DELETE_CONTRACT', payload: id }) // Update vendor's contracts list if (contract) { const vendor = state.vendors.find((v) => v.id === contract.vendorId) if (vendor) { dispatch({ type: 'UPDATE_VENDOR', payload: { id: vendor.id, data: { contracts: vendor.contracts.filter((cId) => cId !== id) }, }, }) } } }, [apiBase, state.contracts, state.vendors] ) const startContractReview = useCallback( async (contractId: string): Promise => { dispatch({ type: 'UPDATE_CONTRACT', payload: { id: contractId, data: { reviewStatus: 'IN_PROGRESS' } }, }) const response = await fetch(`${apiBase}/contracts/${contractId}/review`, { method: 'POST', }) if (!response.ok) { dispatch({ type: 'UPDATE_CONTRACT', payload: { id: contractId, data: { reviewStatus: 'FAILED' } }, }) const error = await response.json() throw new Error(error.error || 'Fehler beim Starten der Vertragsprüfung') } const result = await response.json() // Update contract with review results dispatch({ type: 'UPDATE_CONTRACT', payload: { id: contractId, data: { reviewStatus: 'COMPLETED', reviewCompletedAt: new Date(), complianceScore: result.data.complianceScore, }, }, }) // Add findings if (result.data.findings && result.data.findings.length > 0) { dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings }) } }, [apiBase] ) // ========================================== // FINDINGS ACTIONS // ========================================== const updateFinding = useCallback( async (id: string, data: Partial): Promise => { const response = await fetch(`${apiBase}/findings/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Aktualisieren des Findings') } dispatch({ type: 'UPDATE_FINDING', payload: { id, data } }) }, [apiBase] ) const resolveFinding = useCallback( async (id: string, resolution: string): Promise => { await updateFinding(id, { status: 'RESOLVED', resolution, resolvedAt: new Date(), }) }, [updateFinding] ) // ========================================== // CONTROL ACTIONS // ========================================== const updateControlInstance = useCallback( async (id: string, data: Partial): Promise => { const response = await fetch(`${apiBase}/control-instances/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Aktualisieren des Control-Status') } dispatch({ type: 'UPDATE_CONTROL_INSTANCE', payload: { id, data } }) }, [apiBase] ) // ========================================== // EXPORT ACTIONS // ========================================== const exportVVT = useCallback( async (format: ExportFormat, activityIds?: string[]): Promise => { const params = new URLSearchParams({ format }) if (activityIds && activityIds.length > 0) { params.append('activityIds', activityIds.join(',')) } const response = await fetch(`${apiBase}/export/vvt?${params}`) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Exportieren des VVT') } const blob = await response.blob() const url = URL.createObjectURL(blob) return url }, [apiBase] ) const exportVendorAuditPack = useCallback( async (vendorId: string, format: ExportFormat): Promise => { const params = new URLSearchParams({ format, vendorId }) const response = await fetch(`${apiBase}/export/vendor-audit?${params}`) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Exportieren des Vendor Audit Packs') } const blob = await response.blob() const url = URL.createObjectURL(blob) return url }, [apiBase] ) const exportRoPA = useCallback( async (format: ExportFormat): Promise => { const params = new URLSearchParams({ format }) const response = await fetch(`${apiBase}/export/ropa?${params}`) if (!response.ok) { const error = await response.json() throw new Error(error.error || 'Fehler beim Exportieren des RoPA') } const blob = await response.blob() const url = URL.createObjectURL(blob) return url }, [apiBase] ) // ========================================== // INITIALIZATION // ========================================== useEffect(() => { if (!isInitialized) { loadData() setIsInitialized(true) } }, [isInitialized, loadData]) // ========================================== // CONTEXT VALUE // ========================================== const contextValue = useMemo( () => ({ ...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 ( {children} ) } // ========================================== // 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]) }