Files
breakpilot-lehrer/admin-lehrer/lib/sdk/vendor-compliance/context.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

1011 lines
27 KiB
TypeScript

'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>): 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<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<VendorStatistics>(() => {
const vendors = state.vendors
const byStatus = vendors.reduce(
(acc, v) => {
acc[v.status] = (acc[v.status] || 0) + 1
return acc
},
{} as Record<VendorStatus, number>
)
const byRole = vendors.reduce(
(acc, v) => {
acc[v.role] = (acc[v.role] || 0) + 1
return acc
},
{} as Record<VendorRole, number>
)
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<RiskLevel, number>
)
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<ComplianceStatistics>(() => {
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<FindingType, number>
)
const findingsBySeverity = findings.reduce(
(acc, f) => {
acc[f.severity] = (acc[f.severity] || 0) + 1
return acc
},
{} as Record<FindingSeverity, number>
)
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<RiskOverview>(() => {
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<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<ProcessingActivity> => {
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<ProcessingActivity>): Promise<void> => {
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<void> => {
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<ProcessingActivity> => {
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<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
): Promise<Vendor> => {
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<Vendor>): Promise<void> => {
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<void> => {
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<ContractDocument>
): Promise<ContractDocument> => {
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<ContractDocument>): Promise<void> => {
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<void> => {
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<void> => {
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<Finding>): Promise<void> => {
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<void> => {
await updateFinding(id, {
status: 'RESOLVED',
resolution,
resolvedAt: new Date(),
})
},
[updateFinding]
)
// ==========================================
// CONTROL ACTIONS
// ==========================================
const updateControlInstance = useCallback(
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
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<string> => {
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<string> => {
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<string> => {
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<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])
}