'use client' /** * Einwilligungen Context & Reducer * * Zentrale State-Verwaltung fuer das Datenpunktkatalog & DSI-Generator Modul. * Verwendet React Context + useReducer fuer vorhersehbare State-Updates. */ import { createContext, useContext, useReducer, useCallback, useMemo, ReactNode, Dispatch, } from 'react' import { EinwilligungenState, EinwilligungenAction, EinwilligungenTab, DataPoint, DataPointCatalog, GeneratedPrivacyPolicy, CookieBannerConfig, CompanyInfo, ConsentStatistics, PrivacyPolicySection, SupportedLanguage, ExportFormat, DataPointCategory, LegalBasis, RiskLevel, } from './types' import { PREDEFINED_DATA_POINTS, RETENTION_MATRIX, DEFAULT_COOKIE_CATEGORIES, createDefaultCatalog, getDataPointById, getDataPointsByCategory, countDataPointsByCategory, countDataPointsByRiskLevel, } from './catalog/loader' // ============================================================================= // INITIAL STATE // ============================================================================= const initialState: EinwilligungenState = { // Data catalog: null, selectedDataPoints: [], privacyPolicy: null, cookieBannerConfig: null, companyInfo: null, consentStatistics: null, // UI State activeTab: 'catalog', isLoading: false, isSaving: false, error: null, // Editor State editingDataPoint: null, editingSection: null, // Preview previewLanguage: 'de', previewFormat: 'HTML', } // ============================================================================= // REDUCER // ============================================================================= function einwilligungenReducer( state: EinwilligungenState, action: EinwilligungenAction ): EinwilligungenState { switch (action.type) { case 'SET_CATALOG': return { ...state, catalog: action.payload, // Automatisch alle aktiven Datenpunkte auswaehlen selectedDataPoints: [ ...action.payload.dataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id), ...action.payload.customDataPoints.filter((dp) => dp.isActive !== false).map((dp) => dp.id), ], } case 'SET_SELECTED_DATA_POINTS': return { ...state, selectedDataPoints: action.payload, } case 'TOGGLE_DATA_POINT': { const id = action.payload const isSelected = state.selectedDataPoints.includes(id) return { ...state, selectedDataPoints: isSelected ? state.selectedDataPoints.filter((dpId) => dpId !== id) : [...state.selectedDataPoints, id], } } case 'ADD_CUSTOM_DATA_POINT': if (!state.catalog) return state return { ...state, catalog: { ...state.catalog, customDataPoints: [...state.catalog.customDataPoints, action.payload], updatedAt: new Date(), }, selectedDataPoints: [...state.selectedDataPoints, action.payload.id], } case 'UPDATE_DATA_POINT': { if (!state.catalog) return state const { id, data } = action.payload // Pruefe ob es ein vordefinierter oder kundenspezifischer Datenpunkt ist const isCustom = state.catalog.customDataPoints.some((dp) => dp.id === id) if (isCustom) { return { ...state, catalog: { ...state.catalog, customDataPoints: state.catalog.customDataPoints.map((dp) => dp.id === id ? { ...dp, ...data } : dp ), updatedAt: new Date(), }, } } else { // Vordefinierte Datenpunkte: nur isActive aendern return { ...state, catalog: { ...state.catalog, dataPoints: state.catalog.dataPoints.map((dp) => dp.id === id ? { ...dp, ...data } : dp ), updatedAt: new Date(), }, } } } case 'DELETE_CUSTOM_DATA_POINT': if (!state.catalog) return state return { ...state, catalog: { ...state.catalog, customDataPoints: state.catalog.customDataPoints.filter((dp) => dp.id !== action.payload), updatedAt: new Date(), }, selectedDataPoints: state.selectedDataPoints.filter((id) => id !== action.payload), } case 'SET_PRIVACY_POLICY': return { ...state, privacyPolicy: action.payload, } case 'SET_COOKIE_BANNER_CONFIG': return { ...state, cookieBannerConfig: action.payload, } case 'UPDATE_COOKIE_BANNER_STYLING': if (!state.cookieBannerConfig) return state return { ...state, cookieBannerConfig: { ...state.cookieBannerConfig, styling: { ...state.cookieBannerConfig.styling, ...action.payload, }, updatedAt: new Date(), }, } case 'UPDATE_COOKIE_BANNER_TEXTS': if (!state.cookieBannerConfig) return state return { ...state, cookieBannerConfig: { ...state.cookieBannerConfig, texts: { ...state.cookieBannerConfig.texts, ...action.payload, }, updatedAt: new Date(), }, } case 'SET_COMPANY_INFO': return { ...state, companyInfo: action.payload, } case 'SET_CONSENT_STATISTICS': return { ...state, consentStatistics: action.payload, } case 'SET_ACTIVE_TAB': return { ...state, activeTab: action.payload, } case 'SET_LOADING': return { ...state, isLoading: action.payload, } case 'SET_SAVING': return { ...state, isSaving: action.payload, } case 'SET_ERROR': return { ...state, error: action.payload, } case 'SET_EDITING_DATA_POINT': return { ...state, editingDataPoint: action.payload, } case 'SET_EDITING_SECTION': return { ...state, editingSection: action.payload, } case 'SET_PREVIEW_LANGUAGE': return { ...state, previewLanguage: action.payload, } case 'SET_PREVIEW_FORMAT': return { ...state, previewFormat: action.payload, } case 'RESET_STATE': return initialState default: return state } } // ============================================================================= // CONTEXT // ============================================================================= 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 } 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> = {} // 18 Kategorien (A-R) const categories: DataPointCategory[] = [ 'MASTER_DATA', // A 'CONTACT_DATA', // B 'AUTHENTICATION', // C 'CONSENT', // D 'COMMUNICATION', // E 'PAYMENT', // F 'USAGE_DATA', // G 'LOCATION', // H 'DEVICE_DATA', // I 'MARKETING', // J 'ANALYTICS', // K 'SOCIAL_MEDIA', // L 'HEALTH_DATA', // M - Art. 9 DSGVO 'EMPLOYEE_DATA', // N - BDSG ยง 26 'CONTRACT_DATA', // O 'LOG_DATA', // P 'AI_DATA', // Q - AI Act 'SECURITY', // R ] 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(() => { // Alle 7 Rechtsgrundlagen 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) { // Katalog existiert noch nicht - erstelle Default 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' }) // Fallback zu Default 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, // Filtere nur die ausgewaehlten Datenpunkte 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, // Computed Values allDataPoints, selectedDataPointsData, dataPointsByCategory, categoryStats, riskStats, legalBasisStats, // Actions initializeCatalog, loadCatalog, saveCatalog, toggleDataPoint, addCustomDataPoint, updateDataPoint, deleteCustomDataPoint, setActiveTab, setPreviewLanguage, setPreviewFormat, setCompanyInfo, generatePrivacyPolicy, generateCookieBannerConfig, } return ( {children} ) } // ============================================================================= // HOOK // ============================================================================= export function useEinwilligungen(): EinwilligungenContextValue { const context = useContext(EinwilligungenContext) if (!context) { throw new Error('useEinwilligungen must be used within EinwilligungenProvider') } return context } // ============================================================================= // EXPORTS // ============================================================================= export { initialState, einwilligungenReducer }