'use client' import React, { useReducer, useMemo, useEffect, useState, } from 'react' import { VendorComplianceContextValue, ProcessingActivity, VendorStatistics, ComplianceStatistics, RiskOverview, VendorStatus, VendorRole, RiskLevel, FindingType, FindingSeverity, getRiskLevelFromScore, } from './types' import { initialState, vendorComplianceReducer } from './reducer' import { VendorComplianceContext } from './hooks' import { useVendorComplianceActions } from './use-actions' // Re-export hooks and selectors for barrel export { useVendorCompliance, useVendor, useProcessingActivity, useVendorContracts, useVendorFindings, useContractFindings, useControlInstancesForEntity, } from './hooks' // ========================================== // 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) const actions = useVendorComplianceActions(state, dispatch) // ========================================== // 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) 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) { actions.loadData() setIsInitialized(true) } }, [isInitialized, actions]) // ========================================== // 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 }