/** * Feature Store * * Verwaltet alle Mandate → Features → Instanzen → Permissions * Ein User gehört keinem Mandanten direkt an, sondern hat Zugriff auf Feature-Instanzen. */ import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'; import { useLanguage } from '../providers/language/LanguageContext'; import type { Mandate, MandateFeature, FeatureInstance, FeaturesMyResponse, } from '../types/mandate'; // ============================================================================= // STORE STATE // ============================================================================= interface FeatureState { mandates: Mandate[]; loading: boolean; error: string | null; initialized: boolean; } interface FeatureActions { // Laden loadFeatures: () => Promise; setFeatures: (response: FeaturesMyResponse) => void; // Getters getMandateById: (mandateId: string) => Mandate | undefined; getFeatureByCode: (mandateId: string, featureCode: string) => MandateFeature | undefined; getInstanceById: (instanceId: string) => FeatureInstance | undefined; getInstancesByFeature: (mandateId: string, featureCode: string) => FeatureInstance[]; // Alle Instanzen flach getAllInstances: () => FeatureInstance[]; // Prüfungen hasAnyInstance: () => boolean; // Reset reset: () => void; } type FeatureStore = FeatureState & FeatureActions; // ============================================================================= // INITIAL STATE // ============================================================================= const initialState: FeatureState = { mandates: [], loading: false, error: null, initialized: false, }; // ============================================================================= // CONTEXT // ============================================================================= const FeatureContext = createContext(undefined); // ============================================================================= // PROVIDER // ============================================================================= interface FeatureProviderProps { children: ReactNode; } export const FeatureProvider: React.FC = ({ children }) => { const { t } = useLanguage(); const [state, setState] = useState(initialState); // Cache für schnellen Zugriff auf Instanzen const instanceCacheRef = useRef>(new Map()); /** * Lädt alle Features vom Backend */ const loadFeatures = useCallback(async () => { setState(prev => ({ ...prev, loading: true, error: null })); try { // Import dynamisch um zirkuläre Abhängigkeiten zu vermeiden const { fetchMyFeatures } = await import('../api/featuresApi'); const response = await fetchMyFeatures(); // Cache aufbauen const cache = new Map(); response.mandates.forEach(mandate => { mandate.features.forEach(feature => { feature.instances.forEach(instance => { cache.set(instance.id, instance); // DEBUG: Log permissions for chatbot instances if (instance.featureCode === 'chatbot') { console.log('🔍 [DEBUG] Chatbot Instance Permissions (loadFeatures):', { instanceId: instance.id, instanceLabel: instance.instanceLabel, featureCode: instance.featureCode, userRoles: instance.userRoles, permissions: instance.permissions, views: instance.permissions?.views, viewKeys: instance.permissions?.views ? Object.keys(instance.permissions.views) : [], hasConversationsView: instance.permissions?.views?.['chatbot-conversations'] || instance.permissions?.views?.['ui.feature.chatbot.conversations'] || instance.permissions?.views?.['_all'], }); } }); }); }); instanceCacheRef.current = cache; setState({ mandates: response.mandates, loading: false, error: null, initialized: true, }); } catch (err) { const errorMessage = err instanceof Error ? err.message : t('Features konnten nicht geladen werden.'); console.error('FeatureStore: Error loading features:', err); setState(prev => ({ ...prev, loading: false, error: errorMessage, initialized: true, })); } }, [t]); /** * Setzt Features direkt (z.B. nach Login) */ const setFeatures = useCallback((response: FeaturesMyResponse) => { // Cache aufbauen const cache = new Map(); response.mandates.forEach(mandate => { mandate.features.forEach(feature => { feature.instances.forEach(instance => { cache.set(instance.id, instance); // DEBUG: Log permissions for chatbot instances if (instance.featureCode === 'chatbot') { console.log('🔍 [DEBUG] Chatbot Instance Permissions:', { instanceId: instance.id, instanceLabel: instance.instanceLabel, featureCode: instance.featureCode, userRoles: instance.userRoles, permissions: instance.permissions, views: instance.permissions?.views, viewKeys: instance.permissions?.views ? Object.keys(instance.permissions.views) : [], hasConversationsView: instance.permissions?.views?.['chatbot-conversations'] || instance.permissions?.views?.['ui.feature.chatbot.conversations'] || instance.permissions?.views?.['_all'], }); } }); }); }); instanceCacheRef.current = cache; setState({ mandates: response.mandates, loading: false, error: null, initialized: true, }); }, []); // Reload features when access changes (e.g. after accepting an invitation) useEffect(() => { const onFeaturesChanged = () => { loadFeatures(); }; window.addEventListener('features-changed', onFeaturesChanged); return () => window.removeEventListener('features-changed', onFeaturesChanged); }, [loadFeatures]); /** * Holt einen Mandanten per ID */ const getMandateById = useCallback((mandateId: string): Mandate | undefined => { return state.mandates.find(m => m.id === mandateId); }, [state.mandates]); /** * Holt ein Feature per Mandate-ID und Feature-Code */ const getFeatureByCode = useCallback((mandateId: string, featureCode: string): MandateFeature | undefined => { const mandate = state.mandates.find(m => m.id === mandateId); return mandate?.features.find(f => f.code === featureCode); }, [state.mandates]); /** * Holt eine Instanz per ID (schneller Cache-Zugriff) */ const getInstanceById = useCallback((instanceId: string): FeatureInstance | undefined => { return instanceCacheRef.current.get(instanceId); }, []); /** * Holt alle Instanzen für ein Feature in einem Mandanten */ const getInstancesByFeature = useCallback((mandateId: string, featureCode: string): FeatureInstance[] => { const feature = getFeatureByCode(mandateId, featureCode); return feature?.instances || []; }, [getFeatureByCode]); /** * Holt alle Instanzen flach */ const getAllInstances = useCallback((): FeatureInstance[] => { return Array.from(instanceCacheRef.current.values()); }, []); /** * Prüft ob der User mindestens eine Instanz hat */ const hasAnyInstance = useCallback((): boolean => { return instanceCacheRef.current.size > 0; }, []); /** * Reset (z.B. bei Logout) */ const reset = useCallback(() => { instanceCacheRef.current.clear(); setState(initialState); }, []); // Store zusammenbauen const store: FeatureStore = { ...state, loadFeatures, setFeatures, getMandateById, getFeatureByCode, getInstanceById, getInstancesByFeature, getAllInstances, hasAnyInstance, reset, }; return ( {children} ); }; // ============================================================================= // HOOKS // ============================================================================= /** * Hook für Zugriff auf den Feature Store */ export function useFeatureStore(): FeatureStore { const context = useContext(FeatureContext); if (!context) { throw new Error('useFeatureStore must be used within a FeatureProvider'); } return context; } /** * Hook für alle Mandate */ export function useMandates(): Mandate[] { const store = useFeatureStore(); return store.mandates; } /** * Hook für einen spezifischen Mandanten */ export function useMandateById(mandateId: string | undefined): Mandate | undefined { const store = useFeatureStore(); if (!mandateId) return undefined; return store.getMandateById(mandateId); } /** * Hook für eine spezifische Instanz */ export function useInstance(instanceId: string | undefined): FeatureInstance | undefined { const store = useFeatureStore(); if (!instanceId) return undefined; return store.getInstanceById(instanceId); } /** * Hook für Loading-State */ export function useFeaturesLoading(): boolean { const store = useFeatureStore(); return store.loading; } /** * Hook für Error-State */ export function useFeaturesError(): string | null { const store = useFeatureStore(); return store.error; } /** * Hook für Initialized-State */ export function useFeaturesInitialized(): boolean { const store = useFeatureStore(); return store.initialized; }