/** * AdminFeatureAccessPage * * Admin page for managing feature instances within mandates. * Allows creating, viewing, and managing feature instances. */ import React, { useState, useEffect, useMemo, useRef } from 'react'; import { useFeatureAccess, type FeatureInstance } from '../../hooks/useFeatureAccess'; import { useUserMandates, type Mandate } from '../../hooks/useUserMandates'; import { useFeatureStore } from '../../stores/featureStore'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import { useApiRequest } from '../../hooks/useApi'; import { fetchAttributes } from '../../api/attributesApi'; import { resolveColumnTypes } from '../../utils/columnTypeResolver'; import type { ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable'; import { ChatbotConfigSection } from './ChatbotConfigSection'; import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils'; export const AdminFeatureAccessPage: React.FC = () => { const { t } = useLanguage(); const { features, instances, instancesPagination, loading, error, fetchFeatures, fetchInstances, createInstance, updateInstance, deleteInstance, syncInstanceRoles, syncInstanceWorkflows, } = useFeatureAccess(); const { fetchMandates } = useUserMandates(); const { showSuccess, showError } = useToast(); const { loadFeatures } = useFeatureStore(); const { request } = useApiRequest(); // State const [mandates, setMandates] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(''); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [editingInstance, setEditingInstance] = useState(null); const [, setIsSubmitting] = useState(false); const [syncingInstance, setSyncingInstance] = useState(null); const [syncingWorkflowsInstance, setSyncingWorkflowsInstance] = useState(null); const [backendAttributes, setBackendAttributes] = useState([]); // Chatbot configuration state const [createFeatureCode, setCreateFeatureCode] = useState(''); const [createLabel, setCreateLabel] = useState(''); // Label field value const [chatbotConnectors, setChatbotConnectors] = useState(['preprocessor']); // Array for multiselect (database connectors only) const [chatbotSystemPrompt, setChatbotSystemPrompt] = useState(''); const [chatbotEnableWebResearch, setChatbotEnableWebResearch] = useState(true); // Enable Tavily web research const [chatbotAllowedProviders, setChatbotAllowedProviders] = useState([]); // Allowed LLM providers (empty = all) // Ref to track form data for featureCode detection const formDataRef = useRef>({}); // Load features, mandates, and attributes on mount useEffect(() => { fetchFeatures(); fetchMandates().then(data => { setMandates(data); if (data.length > 0 && !selectedMandateId) { setSelectedMandateId(data[0].id); } }); // Fetch FeatureInstance attributes from backend api.get('/api/attributes/FeatureInstance').then(response => { const attrs = response.data?.attributes || response.data || []; setBackendAttributes(Array.isArray(attrs) ? attrs : []); }).catch(() => setBackendAttributes([])); }, [fetchFeatures, fetchMandates]); // Load instances when mandate changes useEffect(() => { if (selectedMandateId) { fetchInstances(selectedMandateId); } }, [selectedMandateId, fetchInstances]); const _rawColumns: ColumnConfig[] = useMemo(() => [ { key: 'label', label: t('Name'), sortable: true, filterable: true, searchable: true, width: 200 }, { key: 'featureCode', label: t('Feature'), sortable: true, filterable: true, width: 150, formatter: (value: string) => { const feature = features.find(f => f.code === value); const label = feature ? (feature.label || value) : value; return label; }, }, { key: 'enabled', label: t('Aktiv'), sortable: true, filterable: true, width: 80 }, ], [features, t]); const columns = useMemo( () => resolveColumnTypes(_rawColumns, backendAttributes), [_rawColumns, backendAttributes], ); // Form attributes from backend - merge with dynamic feature options // Exclude featureCode, config, and label since we handle them separately const createFields: AttributeDefinition[] = useMemo(() => { const excludedFields = ['id', 'mandateId', 'featureCode', 'config', 'label']; // Exclude featureCode, config, and label - handled separately return backendAttributes .filter(attr => !excludedFields.includes(attr.name)) .map(attr => ({ ...attr, editable: attr.name === 'enabled' ? true : attr.editable, })) as AttributeDefinition[]; }, [backendAttributes]); // Handle create instance const handleCreateInstance = async (data: { featureCode: string; enabled?: boolean; copyTemplateRoles?: boolean }) => { if (!selectedMandateId) return; setIsSubmitting(true); try { // Validate label if (!createLabel || createLabel.trim() === '') { showError(t('Fehler'), t('Label ist erforderlich.')); setIsSubmitting(false); return; } // Build config for chatbot instances let config: Record | undefined = undefined; if (createFeatureCode === 'chatbot') { // Validate required fields if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.')); setIsSubmitting(false); return; } const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null; config = { connector: chatbotConnectors.length > 0 ? { types: chatbotConnectors, type: primaryConnector, customConnectorClass: null } : undefined, prompts: { useCustomPrompts: true, customAnalysisPrompt: chatbotSystemPrompt, customFinalAnswerPrompt: chatbotSystemPrompt }, behavior: { maxQueries: 5, enableWebResearch: chatbotEnableWebResearch, enableRetryOnEmpty: true, maxRetryAttempts: 2 }, allowedProviders: chatbotAllowedProviders }; } const result = await createInstance(selectedMandateId, { featureCode: createFeatureCode, label: createLabel, enabled: data.enabled !== false, copyTemplateRoles: data.copyTemplateRoles !== false, config: config }); if (result.success) { setShowCreateModal(false); setCreateFeatureCode(''); setCreateLabel(''); formDataRef.current = {}; setChatbotConnectors(['preprocessor']); setChatbotSystemPrompt(''); setChatbotEnableWebResearch(true); setChatbotAllowedProviders([]); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache showSuccess(t('Feature-Instanz erstellt'), t('Die Instanz "{name}" wurde erfolgreich erstellt.', { name: createLabel })); } else { showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Feature-Instanz')); } } finally { setIsSubmitting(false); } }; // Wrapper for form submission that includes featureCode from selector const handleFormSubmit = (data: Record) => { // Use label from state and featureCode from selector handleCreateInstance({ featureCode: createFeatureCode, ...(data as { enabled?: boolean; copyTemplateRoles?: boolean }) }); }; // Handle edit click const handleEditClick = (instance: FeatureInstance) => { setEditingInstance(instance); // Load chatbot config if it's a chatbot instance if (instance.featureCode === 'chatbot' && instance.config) { const config = instance.config as any; // Support both new array format and legacy single type format // Filter out 'websearch' if it exists (legacy) const connectorTypes = (config?.connector?.types || (config?.connector?.type ? [config.connector.type] : ['preprocessor'])) .filter((c: string) => c !== 'websearch'); // Remove websearch from connectors setChatbotConnectors(connectorTypes); setChatbotSystemPrompt(config?.prompts?.customAnalysisPrompt || config?.prompts?.customFinalAnswerPrompt || ''); setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false); setChatbotAllowedProviders(Array.isArray(config?.allowedProviders) ? config.allowedProviders : Array.isArray(config?.model?.allowedProviders) ? config.model.allowedProviders : []); } else { setChatbotConnectors([]); setChatbotSystemPrompt(''); setChatbotEnableWebResearch(true); setChatbotAllowedProviders([]); } setShowEditModal(true); }; // Handle update instance const handleUpdateInstance = async (data: { label: string; enabled?: boolean }) => { if (!selectedMandateId || !editingInstance) return; setIsSubmitting(true); try { // Build config for chatbot instances let config: Record | undefined = undefined; if (editingInstance.featureCode === 'chatbot') { // Validate required fields if (!chatbotSystemPrompt || chatbotSystemPrompt.trim() === '') { showError(t('Fehler'), t('System Prompt ist erforderlich für Chatbot-Instanzen.')); setIsSubmitting(false); return; } const existingConfig = editingInstance.config as any || {}; const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : null; config = { ...existingConfig, connector: chatbotConnectors.length > 0 ? { types: chatbotConnectors, type: primaryConnector, customConnectorClass: existingConfig.connector?.customConnectorClass || null } : undefined, prompts: { useCustomPrompts: true, // Always true since system prompt is required customAnalysisPrompt: chatbotSystemPrompt, customFinalAnswerPrompt: chatbotSystemPrompt }, behavior: { ...(existingConfig.behavior || {}), maxQueries: existingConfig.behavior?.maxQueries || 5, enableWebResearch: chatbotEnableWebResearch, enableRetryOnEmpty: existingConfig.behavior?.enableRetryOnEmpty !== false, maxRetryAttempts: existingConfig.behavior?.maxRetryAttempts || 2 }, allowedProviders: chatbotAllowedProviders }; } const result = await updateInstance(selectedMandateId, editingInstance.id, { label: data.label, enabled: data.enabled, config: config }); if (result.success) { setShowEditModal(false); setEditingInstance(null); setChatbotConnectors(['preprocessor']); setChatbotSystemPrompt(''); setChatbotAllowedProviders([]); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache showSuccess(t('Feature-Instanz aktualisiert'), t('Die Instanz "{name}" wurde erfolgreich aktualisiert.', { name: data.label })); } else { showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Feature-Instanz')); } } finally { setIsSubmitting(false); } }; // Handle delete instance - called by DeleteActionButton with instanceId const handleDeleteInstance = async (instanceId: string): Promise => { if (!selectedMandateId) return false; const result = await deleteInstance(selectedMandateId, instanceId); if (result.success) { loadFeatures(); // Refresh global navigation cache showSuccess(t('Instanz gelöscht'), t('Die Feature-Instanz wurde gelöscht.')); return true; } else { showError(t('Fehler'), result.error || t('Fehler beim Löschen der Feature-Instanz')); return false; } }; // Handle sync roles const handleSyncRoles = async (instance: FeatureInstance) => { if (!selectedMandateId) return; setSyncingInstance(instance.id); try { const result = await syncInstanceRoles(selectedMandateId, instance.id, true); if (result.success && result.data) { showSuccess( t('Rollen synchronisiert'), t('Hinzugefügt: {added}\nEntfernt: {removed}\nUnverändert: {unchanged}', { added: result.data.added, removed: result.data.removed, unchanged: result.data.unchanged, }) ); } else { showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Rollen')); } } finally { setSyncingInstance(null); } }; // Handle sync workflows const _handleSyncWorkflows = async (instance: FeatureInstance) => { if (!selectedMandateId) return; setSyncingWorkflowsInstance(instance.id); try { const result = await syncInstanceWorkflows(selectedMandateId, instance.id); if (result.success && result.data) { showSuccess( t('Workflows synchronisiert'), t('Hinzugefügt: {added}\nÜbersprungen: {skipped}\nTotal Templates: {total}', { added: result.data.added, skipped: result.data.skipped, total: result.data.total, }) ); } else { showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Workflows')); } } finally { setSyncingWorkflowsInstance(null); } }; // Get feature label const getFeatureLabel = (code: string) => { const feature = features.find(f => f.code === code); return feature ? (feature.label || code) : code; }; if (error && !selectedMandateId) { return (
⚠️

Fehler: {error}

); } return (

{t('Feature-Instanzen')}

{t('Verwalten Sie Feature-Instanzen für jeden')}

{/* Mandate Selector */}
{selectedMandateId && (
)}
{/* Available Features Info / Empty Features Warning */} {features.length > 0 ? (
{t('Verfügbare Features')} {features.map((f, i) => ( {i > 0 && ', '} {getFeatureLabel(f.code)} ))}
) : selectedMandateId && !loading ? (
{t('Keine Features geladen.')} {error ? ` Fehler: ${error}` : ` ${t('Die API hat keine Features zurückgegeben.')}`} {' '} {t('Öffnen Sie die Browser-Konsole (F12) und prüfen Sie den Netzwerk-Tab für /api/features/')}
) : null} {/* Content */} {!selectedMandateId ? (

{t('Kein Mandant ausgewählt')}

{t('Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.')}

) : (
, onClick: handleEditClick, title: t('Instanz bearbeiten'), }, { id: 'syncRoles', icon: , onClick: handleSyncRoles, title: t('Rollen synchronisieren'), loading: (row: FeatureInstance) => syncingInstance === row.id, disabled: (row: FeatureInstance) => !row.enabled, }, { id: 'syncWorkflows', icon: , onClick: _handleSyncWorkflows, title: t('Workflows synchronisieren'), loading: (row: FeatureInstance) => syncingWorkflowsInstance === row.id, disabled: (row: FeatureInstance) => !row.enabled, } ]} hookData={{ refetch: fetchInstances, pagination: instancesPagination, handleDelete: handleDeleteInstance, }} emptyMessage={t('Keine Feature-Instanzen gefunden')} />
)} {/* Create Instance Modal */} {showCreateModal && (

{t('Neue Feature-Instanz erstellen')}

{features.length === 0 ? (

{t('Keine Features verfügbar, bitte wenden')}

) : createFields.length === 0 ? (
{t('Lade Formular')}
) : (
{/* Feature Code Selector — buttons instead of dropdown */}
{features.map(f => ( ))}
{/* Chatbot Configuration Title - Show when chatbot is selected */} {createFeatureCode === 'chatbot' && (

{t('Chatbot-Konfiguration')}

)} {/* Label Field - Always shown after title */} {createFeatureCode && (
setCreateLabel(value)} placeholder={t('Instanzbezeichnung eingeben')} className={styles.configSelect} size="md" required={true} />
)} {/* Chatbot Configuration Section - Show when chatbot is selected */} {createFeatureCode === 'chatbot' && ( )} {/* Main Form - Only show if featureCode is selected */} {createFeatureCode && (
{ setShowCreateModal(false); setCreateFeatureCode(''); setCreateLabel(''); formDataRef.current = {}; setChatbotConnectors(['preprocessor']); setChatbotSystemPrompt(''); setChatbotEnableWebResearch(true); setChatbotAllowedProviders([]); }} submitButtonText={t('Erstellen')} cancelButtonText={t('Abbrechen')} />
)}
)}
)} {/* Edit Instance Modal */} {showEditModal && editingInstance && (

{t('Feature-Instanz bearbeiten')}

{ setShowEditModal(false); setEditingInstance(null); setChatbotConnectors(['preprocessor']); setChatbotSystemPrompt(''); setChatbotEnableWebResearch(true); setChatbotAllowedProviders([]); }} submitButtonText={t('Speichern')} cancelButtonText={t('Abbrechen')} /> {/* Chatbot Configuration Section */} {editingInstance?.featureCode === 'chatbot' && ( )}
)}
); }; export default AdminFeatureAccessPage;