319 lines
9.6 KiB
TypeScript
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;
|
|
}
|