ui-nyla/src/stores/featureStore.tsx
2026-04-11 19:44:52 +02:00

319 lines
9.6 KiB
TypeScript

/**
* 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<void>;
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<FeatureStore | undefined>(undefined);
// =============================================================================
// PROVIDER
// =============================================================================
interface FeatureProviderProps {
children: ReactNode;
}
export const FeatureProvider: React.FC<FeatureProviderProps> = ({ children }) => {
const { t } = useLanguage();
const [state, setState] = useState<FeatureState>(initialState);
// Cache für schnellen Zugriff auf Instanzen
const instanceCacheRef = useRef<Map<string, FeatureInstance>>(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<string, FeatureInstance>();
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<string, FeatureInstance>();
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 (
<FeatureContext.Provider value={store}>
{children}
</FeatureContext.Provider>
);
};
// =============================================================================
// 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;
}