Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
670 lines
19 KiB
TypeScript
670 lines
19 KiB
TypeScript
'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<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
|
|
}
|
|
|
|
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[]>> = {}
|
|
// 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<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(() => {
|
|
// Alle 7 Rechtsgrundlagen
|
|
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) {
|
|
// 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<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,
|
|
// 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 (
|
|
<EinwilligungenContext.Provider value={value}>{children}</EinwilligungenContext.Provider>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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 }
|