refactor(admin): split 9 more oversized lib/ files into focused modules
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>
This commit is contained in:
@@ -1,30 +1,17 @@
|
||||
'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,
|
||||
@@ -33,185 +20,20 @@ import {
|
||||
getRiskLevelFromScore,
|
||||
} from './types'
|
||||
|
||||
// ==========================================
|
||||
// INITIAL STATE
|
||||
// ==========================================
|
||||
import { initialState, vendorComplianceReducer } from './reducer'
|
||||
import { VendorComplianceContext } from './hooks'
|
||||
import { useVendorComplianceActions } from './use-actions'
|
||||
|
||||
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)
|
||||
// Re-export hooks and selectors for barrel
|
||||
export {
|
||||
useVendorCompliance,
|
||||
useVendor,
|
||||
useProcessingActivity,
|
||||
useVendorContracts,
|
||||
useVendorFindings,
|
||||
useContractFindings,
|
||||
useControlInstancesForEntity,
|
||||
} from './hooks'
|
||||
|
||||
// ==========================================
|
||||
// PROVIDER
|
||||
@@ -229,6 +51,8 @@ export function VendorComplianceProvider({
|
||||
const [state, dispatch] = useReducer(vendorComplianceReducer, initialState)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
const actions = useVendorComplianceActions(state, dispatch)
|
||||
|
||||
// ==========================================
|
||||
// COMPUTED VALUES
|
||||
// ==========================================
|
||||
@@ -254,7 +78,7 @@ export function VendorComplianceProvider({
|
||||
|
||||
const byRiskLevel = vendors.reduce(
|
||||
(acc, v) => {
|
||||
const level = getRiskLevelFromScore(v.residualRiskScore / 4) // Normalize to 1-25
|
||||
const level = getRiskLevelFromScore(v.residualRiskScore / 4)
|
||||
acc[level] = (acc[level] || 0) + 1
|
||||
return acc
|
||||
},
|
||||
@@ -375,496 +199,16 @@ export function VendorComplianceProvider({
|
||||
}
|
||||
}, [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()
|
||||
actions.loadData()
|
||||
setIsInitialized(true)
|
||||
}
|
||||
}, [isInitialized, loadData])
|
||||
}, [isInitialized, actions])
|
||||
|
||||
// ==========================================
|
||||
// CONTEXT VALUE
|
||||
@@ -877,51 +221,9 @@ export function VendorComplianceProvider({
|
||||
vendorStats,
|
||||
complianceStats,
|
||||
riskOverview,
|
||||
createProcessingActivity,
|
||||
updateProcessingActivity,
|
||||
deleteProcessingActivity,
|
||||
duplicateProcessingActivity,
|
||||
createVendor,
|
||||
updateVendor,
|
||||
deleteVendor,
|
||||
uploadContract,
|
||||
updateContract,
|
||||
deleteContract,
|
||||
startContractReview,
|
||||
updateFinding,
|
||||
resolveFinding,
|
||||
updateControlInstance,
|
||||
exportVVT,
|
||||
exportVendorAuditPack,
|
||||
exportRoPA,
|
||||
loadData,
|
||||
refresh,
|
||||
...actions,
|
||||
}),
|
||||
[
|
||||
state,
|
||||
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, actions]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -930,81 +232,3 @@ export function VendorComplianceProvider({
|
||||
</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])
|
||||
}
|
||||
|
||||
88
admin-compliance/lib/sdk/vendor-compliance/hooks.ts
Normal file
88
admin-compliance/lib/sdk/vendor-compliance/hooks.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { useContext, useMemo, createContext } from 'react'
|
||||
import { VendorComplianceContextValue } from './types'
|
||||
|
||||
// ==========================================
|
||||
// CONTEXT
|
||||
// ==========================================
|
||||
|
||||
export const VendorComplianceContext = createContext<VendorComplianceContextValue | null>(null)
|
||||
|
||||
// ==========================================
|
||||
// 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])
|
||||
}
|
||||
178
admin-compliance/lib/sdk/vendor-compliance/reducer.ts
Normal file
178
admin-compliance/lib/sdk/vendor-compliance/reducer.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
VendorComplianceState,
|
||||
VendorComplianceAction,
|
||||
} from './types'
|
||||
|
||||
// ==========================================
|
||||
// INITIAL STATE
|
||||
// ==========================================
|
||||
|
||||
export const initialState: VendorComplianceState = {
|
||||
processingActivities: [],
|
||||
vendors: [],
|
||||
contracts: [],
|
||||
findings: [],
|
||||
controls: [],
|
||||
controlInstances: [],
|
||||
riskAssessments: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
selectedVendorId: null,
|
||||
selectedActivityId: null,
|
||||
activeTab: 'overview',
|
||||
lastModified: null,
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// REDUCER
|
||||
// ==========================================
|
||||
|
||||
export 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
|
||||
}
|
||||
}
|
||||
448
admin-compliance/lib/sdk/vendor-compliance/use-actions.ts
Normal file
448
admin-compliance/lib/sdk/vendor-compliance/use-actions.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
'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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user