Files split by agents before rate limit: - dsr/api.ts (669 → barrel + helpers) - einwilligungen/context.tsx (669 → barrel + hooks/reducer) - export.ts (753 → barrel + domain exporters) - incidents/api.ts (845 → barrel + api-helpers) - tom-generator/context.tsx (720 → barrel + hooks/reducer) - vendor-compliance/context.tsx (1010 → 234 provider + hooks/reducer) - api-docs/endpoints.ts — partially split (3 domain files created) - academy/api.ts — partially split (helpers extracted) - whistleblower/api.ts — partially split (helpers extracted) next build passes. api-client.ts (885) deferred to next session. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
449 lines
14 KiB
TypeScript
449 lines
14 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback } from 'react'
|
|
|
|
import {
|
|
VendorComplianceState,
|
|
VendorComplianceAction,
|
|
ProcessingActivity,
|
|
Vendor,
|
|
ContractDocument,
|
|
Finding,
|
|
ControlInstance,
|
|
ExportFormat,
|
|
} from './types'
|
|
|
|
const API_BASE = '/api/sdk/v1/vendor-compliance'
|
|
|
|
/**
|
|
* Encapsulates all vendor-compliance API action callbacks.
|
|
* Called from the provider so that dispatch/state stay internal.
|
|
*/
|
|
export function useVendorComplianceActions(
|
|
state: VendorComplianceState,
|
|
dispatch: React.Dispatch<VendorComplianceAction>
|
|
) {
|
|
// ==========================================
|
|
// DATA LOADING
|
|
// ==========================================
|
|
|
|
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(`${API_BASE}/processing-activities`),
|
|
fetch(`${API_BASE}/vendors`),
|
|
fetch(`${API_BASE}/contracts`),
|
|
fetch(`${API_BASE}/findings`),
|
|
fetch(`${API_BASE}/controls`),
|
|
fetch(`${API_BASE}/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 })
|
|
}
|
|
}, [dispatch])
|
|
|
|
const refresh = useCallback(async () => {
|
|
await loadData()
|
|
}, [loadData])
|
|
|
|
// ==========================================
|
|
// PROCESSING ACTIVITIES
|
|
// ==========================================
|
|
|
|
const createProcessingActivity = useCallback(
|
|
async (
|
|
data: Omit<ProcessingActivity, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
|
): Promise<ProcessingActivity> => {
|
|
const response = await fetch(`${API_BASE}/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()
|
|
dispatch({ type: 'ADD_PROCESSING_ACTIVITY', payload: result.data })
|
|
return result.data
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
const updateProcessingActivity = useCallback(
|
|
async (id: string, data: Partial<ProcessingActivity>): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 } })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
const deleteProcessingActivity = useCallback(
|
|
async (id: string): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
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
|
|
return createProcessingActivity({
|
|
...rest,
|
|
vvtId: '',
|
|
name: {
|
|
de: `${original.name.de} (Kopie)`,
|
|
en: `${original.name.en} (Copy)`,
|
|
},
|
|
status: 'DRAFT',
|
|
})
|
|
},
|
|
[state.processingActivities, createProcessingActivity]
|
|
)
|
|
|
|
// ==========================================
|
|
// VENDORS
|
|
// ==========================================
|
|
|
|
const createVendor = useCallback(
|
|
async (
|
|
data: Omit<Vendor, 'id' | 'tenantId' | 'createdAt' | 'updatedAt'>
|
|
): Promise<Vendor> => {
|
|
const response = await fetch(`${API_BASE}/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()
|
|
dispatch({ type: 'ADD_VENDOR', payload: result.data })
|
|
return result.data
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
const updateVendor = useCallback(
|
|
async (id: string, data: Partial<Vendor>): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 } })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
const deleteVendor = useCallback(
|
|
async (id: string): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
// ==========================================
|
|
// CONTRACTS
|
|
// ==========================================
|
|
|
|
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(`${API_BASE}/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 })
|
|
|
|
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
|
|
},
|
|
[dispatch, state.vendors]
|
|
)
|
|
|
|
const updateContract = useCallback(
|
|
async (id: string, data: Partial<ContractDocument>): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 } })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
const deleteContract = useCallback(
|
|
async (id: string): Promise<void> => {
|
|
const contract = state.contracts.find((c) => c.id === id)
|
|
const response = await fetch(`${API_BASE}/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 })
|
|
|
|
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) } },
|
|
})
|
|
}
|
|
}
|
|
},
|
|
[dispatch, 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(`${API_BASE}/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()
|
|
dispatch({
|
|
type: 'UPDATE_CONTRACT',
|
|
payload: {
|
|
id: contractId,
|
|
data: {
|
|
reviewStatus: 'COMPLETED',
|
|
reviewCompletedAt: new Date(),
|
|
complianceScore: result.data.complianceScore,
|
|
},
|
|
},
|
|
})
|
|
if (result.data.findings && result.data.findings.length > 0) {
|
|
dispatch({ type: 'ADD_FINDINGS', payload: result.data.findings })
|
|
}
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
// ==========================================
|
|
// FINDINGS
|
|
// ==========================================
|
|
|
|
const updateFinding = useCallback(
|
|
async (id: string, data: Partial<Finding>): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 } })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
const resolveFinding = useCallback(
|
|
async (id: string, resolution: string): Promise<void> => {
|
|
await updateFinding(id, {
|
|
status: 'RESOLVED',
|
|
resolution,
|
|
resolvedAt: new Date(),
|
|
})
|
|
},
|
|
[updateFinding]
|
|
)
|
|
|
|
// ==========================================
|
|
// CONTROLS
|
|
// ==========================================
|
|
|
|
const updateControlInstance = useCallback(
|
|
async (id: string, data: Partial<ControlInstance>): Promise<void> => {
|
|
const response = await fetch(`${API_BASE}/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 } })
|
|
},
|
|
[dispatch]
|
|
)
|
|
|
|
// ==========================================
|
|
// EXPORTS
|
|
// ==========================================
|
|
|
|
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(`${API_BASE}/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()
|
|
return URL.createObjectURL(blob)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const exportVendorAuditPack = useCallback(
|
|
async (vendorId: string, format: ExportFormat): Promise<string> => {
|
|
const params = new URLSearchParams({ format, vendorId })
|
|
const response = await fetch(`${API_BASE}/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()
|
|
return URL.createObjectURL(blob)
|
|
},
|
|
[]
|
|
)
|
|
|
|
const exportRoPA = useCallback(
|
|
async (format: ExportFormat): Promise<string> => {
|
|
const params = new URLSearchParams({ format })
|
|
const response = await fetch(`${API_BASE}/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()
|
|
return URL.createObjectURL(blob)
|
|
},
|
|
[]
|
|
)
|
|
|
|
return {
|
|
loadData,
|
|
refresh,
|
|
createProcessingActivity,
|
|
updateProcessingActivity,
|
|
deleteProcessingActivity,
|
|
duplicateProcessingActivity,
|
|
createVendor,
|
|
updateVendor,
|
|
deleteVendor,
|
|
uploadContract,
|
|
updateContract,
|
|
deleteContract,
|
|
startContractReview,
|
|
updateFinding,
|
|
resolveFinding,
|
|
updateControlInstance,
|
|
exportVVT,
|
|
exportVendorAuditPack,
|
|
exportRoPA,
|
|
}
|
|
}
|