'use client' import React, { createContext, useContext, useReducer, useCallback, useMemo, useEffect, useState, } from 'react' import { VendorComplianceState, VendorComplianceAction, VendorComplianceContextValue, ProcessingActivity, VendorStatistics, ComplianceStatistics, RiskOverview, 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 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 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 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] ) // ========================================== // INITIALIZATION // ========================================== useEffect(() => { if (!isInitialized) { loadData() setIsInitialized(true) } }, [isInitialized, loadData]) // ========================================== // CONTEXT VALUE // ========================================== const contextValue = useMemo( () => ({ ...state, dispatch, vendorStats, complianceStats, riskOverview, deleteProcessingActivity, duplicateProcessingActivity, deleteVendor, deleteContract, startContractReview, loadData, refresh, }), [ state, vendorStats, complianceStats, riskOverview, deleteProcessingActivity, duplicateProcessingActivity, deleteVendor, deleteContract, startContractReview, 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 }