/** * AdminFeatureInstanceUsersPage * * Admin page for managing user access to feature instances. * Allows adding, removing, and updating user roles within feature instances. */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { useFeatureAccess, type FeatureAccessUser, type FeatureInstanceRole, type PaginationParams, type PaginationMetadata } from '../../hooks/useFeatureAccess'; import { useUserMandates } from '../../hooks/useUserMandates'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; import { FaPlus, FaSync, FaBuilding, FaCube } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import { useFeatureStore } from '../../stores/featureStore'; import api from '../../api'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils'; export const AdminFeatureInstanceUsersPage: React.FC = () => { const { t } = useLanguage(); const { features, instances, loading, error, fetchFeatures, fetchInstanceUsers, addUserToInstance, removeUserFromInstance, updateInstanceUserRoles, fetchInstanceRoles, } = useFeatureAccess(); const { fetchMandates } = useUserMandates(); const { showSuccess, showError } = useToast(); const { loadFeatures } = useFeatureStore(); // Combined instance option type interface CombinedInstanceOption { mandateId: string; instanceId: string; mandateName: string; instanceLabel: string; featureCode: string; combinedKey: string; // mandateId:instanceId for unique identification } // State const [combinedOptions, setCombinedOptions] = useState([]); const [selectedCombinedKey, setSelectedCombinedKey] = useState(''); const [instanceUsers, setInstanceUsers] = useState([]); const [instanceRoles, setInstanceRoles] = useState([]); const [allUsers, setAllUsers] = useState>([]); const [showAddModal, setShowAddModal] = useState(false); const [editingUser, setEditingUser] = useState(null); const [, setIsSubmitting] = useState(false); const [usersLoading, setUsersLoading] = useState(false); const [usersPagination, setUsersPagination] = useState(null); // Extract mandateId and instanceId from combined key const selectedMandateId = useMemo(() => { if (!selectedCombinedKey) return ''; return selectedCombinedKey.split(':')[0] || ''; }, [selectedCombinedKey]); const selectedInstanceId = useMemo(() => { if (!selectedCombinedKey) return ''; return selectedCombinedKey.split(':')[1] || ''; }, [selectedCombinedKey]); // Load mandates and features on mount, then build combined options useEffect(() => { fetchFeatures(); const loadCombinedOptions = async () => { const loadedMandates = await fetchMandates(); // Load instances for all mandates in parallel const allOptions: CombinedInstanceOption[] = []; for (const mandate of loadedMandates) { try { const response = await api.get('/api/features/instances', { headers: { 'X-Mandate-Id': mandate.id } }); const instanceList = response.data?.items || response.data || []; if (Array.isArray(instanceList)) { for (const inst of instanceList) { allOptions.push({ mandateId: mandate.id, instanceId: inst.id, mandateName: mandateDisplayLabel(mandate), instanceLabel: inst.label || inst.id, featureCode: inst.featureCode, combinedKey: `${mandate.id}:${inst.id}`, }); } } } catch (err) { console.error(`Error loading instances for mandate ${mandate.id}:`, err); } } // Sort by mandate name, then by instance label allOptions.sort((a, b) => { const mandateCompare = a.mandateName.localeCompare(b.mandateName); if (mandateCompare !== 0) return mandateCompare; return a.instanceLabel.localeCompare(b.instanceLabel); }); setCombinedOptions(allOptions); if (allOptions.length > 0 && !selectedCombinedKey) { setSelectedCombinedKey(allOptions[0].combinedKey); } }; loadCombinedOptions(); }, [fetchFeatures, fetchMandates]); // Load users and roles when instance changes useEffect(() => { if (selectedMandateId && selectedInstanceId) { setUsersLoading(true); Promise.all([ fetchInstanceUsers(selectedMandateId, selectedInstanceId), fetchInstanceRoles(selectedMandateId, selectedInstanceId), ]).then(([users, roles]) => { setInstanceUsers(users); setInstanceRoles(roles); }).finally(() => { setUsersLoading(false); }); } }, [selectedMandateId, selectedInstanceId, fetchInstanceUsers, fetchInstanceRoles]); // Load mandate members for the add modal (only users who are members of the selected mandate) useEffect(() => { if (!selectedMandateId) { setAllUsers([]); return; } api.get(`/api/mandates/${selectedMandateId}/users`).then(response => { const data = response.data?.items || response.data || []; // Map MandateUserInfo to the expected format const mappedUsers = Array.isArray(data) ? data.map((u: any) => ({ id: u.userId, username: u.username, email: u.email, fullName: u.fullName })) : []; setAllUsers(mappedUsers); }).catch(() => setAllUsers([])); }, [selectedMandateId]); // Refresh instance users with optional pagination const refreshUsers = useCallback(async (paginationParams?: PaginationParams) => { if (selectedMandateId && selectedInstanceId) { setUsersLoading(true); try { // Build query params const params = new URLSearchParams(); if (paginationParams && Object.keys(paginationParams).length > 0) { params.append('pagination', JSON.stringify(paginationParams)); } const url = params.toString() ? `/api/features/instances/${selectedInstanceId}/users?${params.toString()}` : `/api/features/instances/${selectedInstanceId}/users`; const response = await api.get(url, { headers: { 'X-Mandate-Id': selectedMandateId } }); if (response.data?.items && Array.isArray(response.data.items)) { setInstanceUsers(response.data.items); if (response.data.pagination) { setUsersPagination(response.data.pagination); } } else { const users = Array.isArray(response.data) ? response.data : []; setInstanceUsers(users); } } catch (err) { console.error('Error refreshing users:', err); setInstanceUsers([]); } finally { setUsersLoading(false); } } }, [selectedMandateId, selectedInstanceId]); // Get users not yet in the instance const availableUsers = useMemo(() => { const existingUserIds = new Set(instanceUsers.map(u => u.userId)); return allUsers.filter(u => !existingUserIds.has(u.id)); }, [allUsers, instanceUsers]); // Table columns const columns = useMemo(() => [ { key: 'username', label: t('Benutzername'), type: 'text' as const, sortable: true, filterable: true, searchable: true, width: 150, }, { key: 'email', label: t('E-Mail'), type: 'text' as const, sortable: true, filterable: true, searchable: true, width: 200, }, { key: 'fullName', label: t('Vollständiger Name'), type: 'text' as const, sortable: true, filterable: true, searchable: true, width: 180, }, { key: 'roleLabels', label: t('Rollen'), type: 'text' as const, sortable: false, filterable: false, searchable: true, width: 200, render: (value: string[]) => { if (!value || value.length === 0) return '-'; return value.join(', '); }, }, { key: 'enabled', label: t('Aktiv'), type: 'boolean' as const, sortable: true, filterable: true, searchable: false, width: 80, }, ], [t]); // Dynamic options for forms (users and roles) const userOptions = useMemo(() => availableUsers.map(u => ({ value: u.id, label: `${u.username} ${u.email ? `(${u.email})` : ''}` })), [availableUsers]); const roleOptions = useMemo(() => instanceRoles.map(r => ({ value: r.id, label: r.roleLabel })), [instanceRoles]); // Form attributes for adding a user const addUserFields: AttributeDefinition[] = useMemo(() => { return [ { name: 'userId', label: t('Benutzer'), type: 'enum' as const, required: true, options: userOptions, }, { name: 'roleIds', label: t('Rollen'), type: 'multiselect' as const, required: true, options: roleOptions, } ]; }, [userOptions, roleOptions, t]); // Form attributes for editing user roles and active flag const editRolesFields: AttributeDefinition[] = useMemo(() => { return [ { name: 'roleIds', label: t('Rollen'), type: 'multiselect' as const, required: true, options: roleOptions, }, { name: 'enabled', label: t('Aktiv'), type: 'checkbox' as const, required: false, }, ]; }, [roleOptions, t]); // Handle add user submit const handleAddUser = async (data: { userId: string; roleIds: string[] }) => { if (!selectedMandateId || !selectedInstanceId) return; setIsSubmitting(true); try { const result = await addUserToInstance(selectedMandateId, selectedInstanceId, data); if (result.success) { setShowAddModal(false); refreshUsers(); loadFeatures(); // Refresh global navigation cache showSuccess(t('Benutzer hinzugefügt'), t('Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.')); } else { showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers')); } } finally { setIsSubmitting(false); } }; // Handle edit roles and active submit const handleEditRoles = async (data: { roleIds: string[]; enabled?: boolean }) => { if (!selectedMandateId || !selectedInstanceId || !editingUser) return; setIsSubmitting(true); try { const result = await updateInstanceUserRoles( selectedMandateId, selectedInstanceId, editingUser.userId, { roleIds: data.roleIds, enabled: data.enabled } ); if (result.success) { setEditingUser(null); refreshUsers(); loadFeatures(); // Refresh global navigation cache showSuccess(t('Eintrag aktualisiert'), t('Rollen und Aktiv-Status wurden erfolgreich aktualisiert.')); } else { showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren')); } } finally { setIsSubmitting(false); } }; // Handle remove user (confirmation handled by DeleteActionButton) const handleRemoveUser = async (user: FeatureAccessUser) => { if (!selectedMandateId || !selectedInstanceId) return; const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId); if (result.success) { refreshUsers(); loadFeatures(); // Refresh global navigation cache showSuccess(t('Benutzer entfernt'), t('"{name}" wurde aus der Feature-Instanz entfernt.', { name: user.username })); } else { showError(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers')); } }; // Handle edit click const handleEditClick = (user: FeatureAccessUser) => { setEditingUser(user); }; // Get feature label const getFeatureLabel = (code: string) => { const feature = features.find(f => f.code === code); return feature ? (feature.label || code) : code; }; // Get selected instance info from combined options const selectedInstance = useMemo(() => { const option = combinedOptions.find(o => o.combinedKey === selectedCombinedKey); if (!option) return null; return instances.find(i => i.id === option.instanceId) || { id: option.instanceId, label: option.instanceLabel, featureCode: option.featureCode, mandateId: option.mandateId, }; }, [combinedOptions, selectedCombinedKey, instances]); // Get selected combined option for display const selectedOption = useMemo(() => { return combinedOptions.find(o => o.combinedKey === selectedCombinedKey); }, [combinedOptions, selectedCombinedKey]); if (error && !selectedCombinedKey) { return (
⚠️

{t('Fehler')}: {error}

); } return (

{t('Feature-Instanz-Benutzer')}

{t('Verwalten Sie Benutzerzugriffe auf Feature-Instanzen')}

{/* Combined Selector: Mandate + Feature Instance */}
{selectedCombinedKey && (
)}
{/* Info box when instance is selected */} {selectedOption && (
{t('Mandant')}: {selectedOption.mandateName} | {t('Instanz')}: {selectedOption.instanceLabel} ({selectedOption.featureCode})
)} {/* Roles info box */} {selectedInstance && instanceRoles.length > 0 && (
{t('Verfügbare Rollen')} {instanceRoles.map((r, i) => ( {i > 0 && ', '} {r.roleLabel} ))}
)} {/* Warning if no roles available */} {selectedInstance && instanceRoles.length === 0 && !usersLoading && (
⚠️ {t('Diese Instanz hat noch keine')}
)} {/* Content */} {!selectedCombinedKey ? (

{t('Keine Feature-Instanz ausgewählt')}

{combinedOptions.length === 0 ? t('Es gibt noch keine Feature-Instanzen') : t('Wählen Sie eine Feature-Instanz aus')}

) : (
{ // Find user by FeatureAccess ID to get userId for API call const user = instanceUsers.find(u => u.id === featureAccessId); if (user) { const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId); return result.success; } return false; }, }} emptyMessage={t('Keine Benutzer gefunden')} />
)} {/* Add User Modal */} {showAddModal && (

{t('Benutzer zur Feature-Instanz hinzufügen')}

{availableUsers.length === 0 ? (

{t('Alle Benutzer haben bereits Zugriff')}

) : instanceRoles.length === 0 ? (

{t('Diese Feature-Instanz hat keine Rollen')}

) : ( setShowAddModal(false)} submitButtonText={t('Hinzufügen')} cancelButtonText={t('Abbrechen')} /> )}
)} {/* Edit Roles Modal */} {editingUser && (

{t('Rollen bearbeiten')}: {editingUser.username}

setEditingUser(null)} submitButtonText={t('Speichern')} cancelButtonText={t('Abbrechen')} />
)}
); }; export default AdminFeatureInstanceUsersPage;