/** * 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 { ChatbotConfigSection } from './ChatbotConfigSection'; import { DropdownSelect } from '../../components/UiComponents/DropdownSelect'; import { TextField } from '../../components/UiComponents/TextField'; import styles from './Admin.module.css'; export const AdminFeatureAccessPage: React.FC = () => { const { features, instances, instancesPagination, loading, error, fetchFeatures, fetchInstances, createInstance, updateInstance, deleteInstance, syncInstanceRoles, } = useFeatureAccess(); const { fetchMandates } = useUserMandates(); const { showSuccess, showError } = useToast(); const { loadFeatures } = useFeatureStore(); // 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 [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 // Ref to track form data for featureCode detection const formDataRef = useRef>({}); // Load features, mandates, and attributes on mount useEffect(() => { fetchFeatures(); fetchMandates().then(setMandates); // 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]); // Table columns const columns = useMemo(() => [ { key: 'label', label: 'Name', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 }, { key: 'featureCode', label: 'Feature', type: 'string' as const, sortable: true, filterable: true, width: 150, render: (value: string) => { const feature = features.find(f => f.code === value); if (feature) { const label = typeof feature.label === 'object' ? (feature.label.de || feature.label.en || value) : feature.label; return label; } return value; } }, { key: 'enabled', label: 'Aktiv', type: 'boolean' as const, sortable: true, filterable: true, width: 80 }, ], [features]); // 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('Fehler', '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('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.'); setIsSubmitting(false); return; } if (chatbotConnectors.length === 0) { showError('Fehler', 'Mindestens ein Connector muss ausgewählt werden.'); setIsSubmitting(false); return; } // Use first connector as primary type (for backward compatibility) // Store all connectors in types array const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : 'preprocessor'; config = { connector: { types: chatbotConnectors.length > 0 ? chatbotConnectors : ['preprocessor'], // Array of selected connectors type: primaryConnector, // Primary connector (for backward compatibility) customConnectorClass: null }, prompts: { useCustomPrompts: true, // Always true since system prompt is required customAnalysisPrompt: chatbotSystemPrompt, customFinalAnswerPrompt: chatbotSystemPrompt }, behavior: { maxQueries: 5, enableWebResearch: chatbotEnableWebResearch, enableRetryOnEmpty: true, maxRetryAttempts: 2 } }; } 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); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache showSuccess('Feature-Instanz erstellt', `Die Instanz "${createLabel}" wurde erfolgreich erstellt.`); } else { showError('Fehler', result.error || '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.length > 0 ? connectorTypes : ['preprocessor']); setChatbotSystemPrompt(config?.prompts?.customAnalysisPrompt || config?.prompts?.customFinalAnswerPrompt || ''); setChatbotEnableWebResearch(config?.behavior?.enableWebResearch !== false); // Default to true if not set } else { setChatbotConnectors(['preprocessor']); setChatbotSystemPrompt(''); setChatbotEnableWebResearch(true); } 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('Fehler', 'System Prompt ist erforderlich für Chatbot-Instanzen.'); setIsSubmitting(false); return; } if (chatbotConnectors.length === 0) { showError('Fehler', 'Mindestens ein Connector muss ausgewählt werden.'); setIsSubmitting(false); return; } // Merge with existing config if it exists const existingConfig = editingInstance.config as any || {}; // Use first connector as primary type (for backward compatibility) const primaryConnector = chatbotConnectors.length > 0 ? chatbotConnectors[0] : 'preprocessor'; config = { ...existingConfig, connector: { types: chatbotConnectors.length > 0 ? chatbotConnectors : ['preprocessor'], // Array of selected connectors type: primaryConnector, // Primary connector (for backward compatibility) customConnectorClass: existingConfig.connector?.customConnectorClass || null }, 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 } }; } 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(''); fetchInstances(selectedMandateId); loadFeatures(); // Refresh global navigation cache showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`); } else { showError('Fehler', result.error || '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('Instanz gelöscht', 'Die Feature-Instanz wurde gelöscht.'); return true; } else { showError('Fehler', result.error || '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( 'Rollen synchronisiert', `Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}` ); } else { showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen'); } } finally { setSyncingInstance(null); } }; // Get mandate name const getMandateName = (mandate: Mandate) => { if (mandate.label) return mandate.label; if (typeof mandate.name === 'object') { return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id; } return mandate.name || mandate.id; }; // Get feature label const getFeatureLabel = (code: string) => { const feature = features.find(f => f.code === code); if (feature) { return typeof feature.label === 'object' ? (feature.label.de || feature.label.en || code) : (feature.label || code); } return code; }; if (error && !selectedMandateId) { return (
⚠️

Fehler: {error}

); } return (

Feature-Instanzen

Verwalten Sie Feature-Instanzen für jeden Mandanten

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

Kein Mandant ausgewählt

Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.

) : loading && instances.length === 0 ? (
Lade Feature-Instanzen...
) : instances.length === 0 ? (

Keine Feature-Instanzen

Für diesen Mandanten wurden noch keine Feature-Instanzen erstellt.

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

Neue Feature-Instanz erstellen

{features.length === 0 ? (

Keine Features verfügbar. Bitte wenden Sie sich an den System-Administrator.

) : createFields.length === 0 ? (
Lade Formular...
) : (
{/* Feature Code Selector - Required for chatbot config */}
({ id: f.code, label: typeof f.label === 'object' ? (f.label.de || f.label.en || f.code) : (f.label || f.code), value: f.code }))} selectedItemId={createFeatureCode} onSelect={(item) => { const selectedCode = item?.value || ''; setCreateFeatureCode(selectedCode); // Reset chatbot config when switching setChatbotConnectors(['preprocessor']); setChatbotSystemPrompt(''); setChatbotEnableWebResearch(true); }} placeholder="Feature auswählen (erforderlich)" className={styles.configSelect} /> {!createFeatureCode && (

Bitte wählen Sie ein Feature aus, um fortzufahren.

)}
{/* Chatbot Configuration Title - Show when chatbot is selected */} {createFeatureCode === 'chatbot' && (

Chatbot-Konfiguration

)} {/* Label Field - Always shown after title */} {createFeatureCode && (
setCreateLabel(value)} placeholder="Instanz-Bezeichnung 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); }} submitButtonText="Erstellen" cancelButtonText="Abbrechen" />
)}
)}
)} {/* Edit Instance Modal */} {showEditModal && editingInstance && (
{ setShowEditModal(false); setEditingInstance(null); }}>
e.stopPropagation()}>

Feature-Instanz bearbeiten

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