'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 // Computed Values allDataPoints: DataPoint[] selectedDataPointsData: DataPoint[] dataPointsByCategory: Record categoryStats: Record riskStats: Record legalBasisStats: Record // Actions initializeCatalog: (tenantId: string) => void loadCatalog: (tenantId: string) => Promise saveCatalog: () => Promise toggleDataPoint: (id: string) => void addCustomDataPoint: (dataPoint: DataPoint) => void updateDataPoint: (id: string, data: Partial) => void deleteCustomDataPoint: (id: string) => void setActiveTab: (tab: EinwilligungenTab) => void setPreviewLanguage: (language: SupportedLanguage) => void setPreviewFormat: (format: ExportFormat) => void setCompanyInfo: (info: CompanyInfo) => void generatePrivacyPolicy: () => Promise generateCookieBannerConfig: () => void } export const EinwilligungenContext = createContext(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> = {} 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 }, [selectedDataPointsData]) const categoryStats = useMemo(() => { const counts: Partial> = {} for (const dp of selectedDataPointsData) { counts[dp.category] = (counts[dp.category] || 0) + 1 } return counts as Record }, [selectedDataPointsData]) const riskStats = useMemo(() => { const counts: Record = { LOW: 0, MEDIUM: 0, HIGH: 0 } for (const dp of selectedDataPointsData) { counts[dp.riskLevel]++ } return counts }, [selectedDataPointsData]) const legalBasisStats = useMemo(() => { const counts: Record = { 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) => { 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 ( {children} ) }