Files
breakpilot-compliance/admin-compliance/lib/sdk/einwilligungen/provider.tsx
Sharang Parnerkar 58e95d5e8e 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>
2026-04-10 19:12:09 +02:00

385 lines
12 KiB
TypeScript

'use client'
/**
* Einwilligungen Provider
*
* React Context Provider fuer das Einwilligungen-Modul.
* Stellt State, computed values und Actions bereit.
*/
import {
createContext,
useReducer,
useCallback,
useMemo,
ReactNode,
Dispatch,
} from 'react'
import {
EinwilligungenState,
EinwilligungenAction,
EinwilligungenTab,
DataPoint,
CookieBannerConfig,
CompanyInfo,
SupportedLanguage,
ExportFormat,
DataPointCategory,
LegalBasis,
RiskLevel,
} from './types'
import {
PREDEFINED_DATA_POINTS,
DEFAULT_COOKIE_CATEGORIES,
createDefaultCatalog,
} from './catalog/loader'
import { einwilligungenReducer, initialState } from './reducer'
// =============================================================================
// CONTEXT
// =============================================================================
export interface EinwilligungenContextValue {
state: EinwilligungenState
dispatch: Dispatch<EinwilligungenAction>
// Computed Values
allDataPoints: DataPoint[]
selectedDataPointsData: DataPoint[]
dataPointsByCategory: Record<DataPointCategory, DataPoint[]>
categoryStats: Record<DataPointCategory, number>
riskStats: Record<RiskLevel, number>
legalBasisStats: Record<LegalBasis, number>
// Actions
initializeCatalog: (tenantId: string) => void
loadCatalog: (tenantId: string) => Promise<void>
saveCatalog: () => Promise<void>
toggleDataPoint: (id: string) => void
addCustomDataPoint: (dataPoint: DataPoint) => void
updateDataPoint: (id: string, data: Partial<DataPoint>) => void
deleteCustomDataPoint: (id: string) => void
setActiveTab: (tab: EinwilligungenTab) => void
setPreviewLanguage: (language: SupportedLanguage) => void
setPreviewFormat: (format: ExportFormat) => void
setCompanyInfo: (info: CompanyInfo) => void
generatePrivacyPolicy: () => Promise<void>
generateCookieBannerConfig: () => void
}
export const EinwilligungenContext = createContext<EinwilligungenContextValue | null>(null)
// =============================================================================
// PROVIDER
// =============================================================================
interface EinwilligungenProviderProps {
children: ReactNode
tenantId?: string
}
export function EinwilligungenProvider({ children, tenantId }: EinwilligungenProviderProps) {
const [state, dispatch] = useReducer(einwilligungenReducer, initialState)
// ---------------------------------------------------------------------------
// COMPUTED VALUES
// ---------------------------------------------------------------------------
const allDataPoints = useMemo(() => {
if (!state.catalog) return PREDEFINED_DATA_POINTS
return [...state.catalog.dataPoints, ...state.catalog.customDataPoints]
}, [state.catalog])
const selectedDataPointsData = useMemo(() => {
return allDataPoints.filter((dp) => state.selectedDataPoints.includes(dp.id))
}, [allDataPoints, state.selectedDataPoints])
const dataPointsByCategory = useMemo(() => {
const result: Partial<Record<DataPointCategory, DataPoint[]>> = {}
const categories: DataPointCategory[] = [
'MASTER_DATA', 'CONTACT_DATA', 'AUTHENTICATION', 'CONSENT',
'COMMUNICATION', 'PAYMENT', 'USAGE_DATA', 'LOCATION',
'DEVICE_DATA', 'MARKETING', 'ANALYTICS', 'SOCIAL_MEDIA',
'HEALTH_DATA', 'EMPLOYEE_DATA', 'CONTRACT_DATA', 'LOG_DATA',
'AI_DATA', 'SECURITY',
]
for (const cat of categories) {
result[cat] = selectedDataPointsData.filter((dp) => dp.category === cat)
}
return result as Record<DataPointCategory, DataPoint[]>
}, [selectedDataPointsData])
const categoryStats = useMemo(() => {
const counts: Partial<Record<DataPointCategory, number>> = {}
for (const dp of selectedDataPointsData) {
counts[dp.category] = (counts[dp.category] || 0) + 1
}
return counts as Record<DataPointCategory, number>
}, [selectedDataPointsData])
const riskStats = useMemo(() => {
const counts: Record<RiskLevel, number> = { LOW: 0, MEDIUM: 0, HIGH: 0 }
for (const dp of selectedDataPointsData) {
counts[dp.riskLevel]++
}
return counts
}, [selectedDataPointsData])
const legalBasisStats = useMemo(() => {
const counts: Record<LegalBasis, number> = {
CONTRACT: 0, CONSENT: 0, EXPLICIT_CONSENT: 0,
LEGITIMATE_INTEREST: 0, LEGAL_OBLIGATION: 0,
VITAL_INTERESTS: 0, PUBLIC_INTEREST: 0,
}
for (const dp of selectedDataPointsData) {
counts[dp.legalBasis]++
}
return counts
}, [selectedDataPointsData])
// ---------------------------------------------------------------------------
// ACTIONS
// ---------------------------------------------------------------------------
const initializeCatalog = useCallback(
(tid: string) => {
const catalog = createDefaultCatalog(tid)
dispatch({ type: 'SET_CATALOG', payload: catalog })
},
[dispatch]
)
const loadCatalog = useCallback(
async (tid: string) => {
dispatch({ type: 'SET_LOADING', payload: true })
dispatch({ type: 'SET_ERROR', payload: null })
try {
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
headers: { 'X-Tenant-ID': tid },
})
if (response.ok) {
const data = await response.json()
dispatch({ type: 'SET_CATALOG', payload: data.catalog })
if (data.companyInfo) {
dispatch({ type: 'SET_COMPANY_INFO', payload: data.companyInfo })
}
if (data.cookieBannerConfig) {
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: data.cookieBannerConfig })
}
} else if (response.status === 404) {
initializeCatalog(tid)
} else {
throw new Error('Failed to load catalog')
}
} catch (error) {
console.error('Error loading catalog:', error)
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Laden des Katalogs' })
initializeCatalog(tid)
} finally {
dispatch({ type: 'SET_LOADING', payload: false })
}
},
[dispatch, initializeCatalog]
)
const saveCatalog = useCallback(async () => {
if (!state.catalog) return
dispatch({ type: 'SET_SAVING', payload: true })
dispatch({ type: 'SET_ERROR', payload: null })
try {
const response = await fetch(`/api/sdk/v1/einwilligungen/catalog`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': state.catalog.tenantId,
},
body: JSON.stringify({
catalog: state.catalog,
companyInfo: state.companyInfo,
cookieBannerConfig: state.cookieBannerConfig,
}),
})
if (!response.ok) {
throw new Error('Failed to save catalog')
}
} catch (error) {
console.error('Error saving catalog:', error)
dispatch({ type: 'SET_ERROR', payload: 'Fehler beim Speichern des Katalogs' })
} finally {
dispatch({ type: 'SET_SAVING', payload: false })
}
}, [state.catalog, state.companyInfo, state.cookieBannerConfig, dispatch])
const toggleDataPoint = useCallback(
(id: string) => {
dispatch({ type: 'TOGGLE_DATA_POINT', payload: id })
},
[dispatch]
)
const addCustomDataPoint = useCallback(
(dataPoint: DataPoint) => {
dispatch({ type: 'ADD_CUSTOM_DATA_POINT', payload: { ...dataPoint, isCustom: true } })
},
[dispatch]
)
const updateDataPoint = useCallback(
(id: string, data: Partial<DataPoint>) => {
dispatch({ type: 'UPDATE_DATA_POINT', payload: { id, data } })
},
[dispatch]
)
const deleteCustomDataPoint = useCallback(
(id: string) => {
dispatch({ type: 'DELETE_CUSTOM_DATA_POINT', payload: id })
},
[dispatch]
)
const setActiveTab = useCallback(
(tab: EinwilligungenTab) => {
dispatch({ type: 'SET_ACTIVE_TAB', payload: tab })
},
[dispatch]
)
const setPreviewLanguage = useCallback(
(language: SupportedLanguage) => {
dispatch({ type: 'SET_PREVIEW_LANGUAGE', payload: language })
},
[dispatch]
)
const setPreviewFormat = useCallback(
(format: ExportFormat) => {
dispatch({ type: 'SET_PREVIEW_FORMAT', payload: format })
},
[dispatch]
)
const setCompanyInfo = useCallback(
(info: CompanyInfo) => {
dispatch({ type: 'SET_COMPANY_INFO', payload: info })
},
[dispatch]
)
const generatePrivacyPolicy = useCallback(async () => {
if (!state.catalog || !state.companyInfo) {
dispatch({ type: 'SET_ERROR', payload: 'Bitte zuerst Firmendaten eingeben' })
return
}
dispatch({ type: 'SET_LOADING', payload: true })
dispatch({ type: 'SET_ERROR', payload: null })
try {
const response = await fetch(`/api/sdk/v1/einwilligungen/privacy-policy/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': state.catalog.tenantId,
},
body: JSON.stringify({
dataPointIds: state.selectedDataPoints,
companyInfo: state.companyInfo,
language: state.previewLanguage,
format: state.previewFormat,
}),
})
if (response.ok) {
const policy = await response.json()
dispatch({ type: 'SET_PRIVACY_POLICY', payload: policy })
} else {
throw new Error('Failed to generate privacy policy')
}
} catch (error) {
console.error('Error generating privacy policy:', error)
dispatch({ type: 'SET_ERROR', payload: 'Fehler bei der Generierung der Datenschutzerklaerung' })
} finally {
dispatch({ type: 'SET_LOADING', payload: false })
}
}, [
state.catalog,
state.companyInfo,
state.selectedDataPoints,
state.previewLanguage,
state.previewFormat,
dispatch,
])
const generateCookieBannerConfig = useCallback(() => {
if (!state.catalog) return
const config: CookieBannerConfig = {
id: `cookie-banner-${state.catalog.tenantId}`,
tenantId: state.catalog.tenantId,
categories: DEFAULT_COOKIE_CATEGORIES.map((cat) => ({
...cat,
dataPointIds: cat.dataPointIds.filter((id) => state.selectedDataPoints.includes(id)),
})),
styling: {
position: 'BOTTOM',
theme: 'LIGHT',
primaryColor: '#6366f1',
borderRadius: 12,
},
texts: {
title: { de: 'Cookie-Einstellungen', en: 'Cookie Settings' },
description: {
de: 'Wir verwenden Cookies, um Ihnen die bestmoegliche Nutzung unserer Website zu ermoeglichen.',
en: 'We use cookies to provide you with the best possible experience on our website.',
},
acceptAll: { de: 'Alle akzeptieren', en: 'Accept All' },
rejectAll: { de: 'Alle ablehnen', en: 'Reject All' },
customize: { de: 'Anpassen', en: 'Customize' },
save: { de: 'Auswahl speichern', en: 'Save Selection' },
privacyPolicyLink: { de: 'Datenschutzerklaerung', en: 'Privacy Policy' },
},
updatedAt: new Date(),
}
dispatch({ type: 'SET_COOKIE_BANNER_CONFIG', payload: config })
}, [state.catalog, state.selectedDataPoints, dispatch])
// ---------------------------------------------------------------------------
// CONTEXT VALUE
// ---------------------------------------------------------------------------
const value: EinwilligungenContextValue = {
state,
dispatch,
allDataPoints,
selectedDataPointsData,
dataPointsByCategory,
categoryStats,
riskStats,
legalBasisStats,
initializeCatalog,
loadCatalog,
saveCatalog,
toggleDataPoint,
addCustomDataPoint,
updateDataPoint,
deleteCustomDataPoint,
setActiveTab,
setPreviewLanguage,
setPreviewFormat,
setCompanyInfo,
generatePrivacyPolicy,
generateCookieBannerConfig,
}
return (
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
)
}