/** * 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, FaUsers, FaBuilding, FaCube } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import api from '../../api'; import styles from './Admin.module.css'; export const AdminFeatureInstanceUsersPage: React.FC = () => { const { features, instances, loading, error, fetchFeatures, fetchInstanceUsers, addUserToInstance, removeUserFromInstance, updateInstanceUserRoles, fetchInstanceRoles, } = useFeatureAccess(); const { fetchMandates } = useUserMandates(); const { showSuccess, showError } = useToast(); // 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: typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id), 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); }; 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: 'Benutzername', type: 'text' as const, sortable: true, filterable: true, searchable: true, width: 150, }, { key: 'email', label: 'E-Mail', type: 'text' as const, sortable: true, filterable: true, searchable: true, width: 200, }, { key: 'fullName', label: 'Vollständiger Name', type: 'text' as const, sortable: true, filterable: true, searchable: true, width: 180, }, { key: 'roleLabels', label: '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: 'Aktiv', type: 'boolean' as const, sortable: true, filterable: true, searchable: false, width: 80, }, ], []); // 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: 'Benutzer', type: 'enum' as const, required: true, options: userOptions, }, { name: 'roleIds', label: 'Rollen', type: 'multiselect' as const, required: true, options: roleOptions, } ]; }, [userOptions, roleOptions]); // Form attributes for editing user roles and active flag const editRolesFields: AttributeDefinition[] = useMemo(() => { return [ { name: 'roleIds', label: 'Rollen', type: 'multiselect' as const, required: true, options: roleOptions, }, { name: 'enabled', label: 'Aktiv', type: 'checkbox' as const, required: false, }, ]; }, [roleOptions]); // 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(); showSuccess('Benutzer hinzugefügt', 'Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.'); } else { showError('Fehler', result.error || '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(); showSuccess('Eintrag aktualisiert', 'Rollen und Aktiv-Status wurden erfolgreich aktualisiert.'); } else { showError('Fehler', result.error || '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(); showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`); } else { showError('Fehler', result.error || '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); if (feature) { return typeof feature.label === 'object' ? (feature.label.de || feature.label.en || code) : (feature.label || code); } return 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 (
⚠️

Fehler: {error}

); } return (

Feature Instanz Benutzer

Verwalten Sie Benutzerzugriffe auf Feature-Instanzen

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

Keine Feature-Instanz ausgewählt

{combinedOptions.length === 0 ? 'Es gibt noch keine Feature-Instanzen. Erstellen Sie zuerst Feature-Instanzen unter "Feature-Instanzen".' : 'Wählen Sie eine Feature-Instanz aus, um deren Benutzer zu verwalten.'}

) : usersLoading && instanceUsers.length === 0 ? (
Lade Benutzer...
) : instanceUsers.length === 0 ? (

Keine Benutzer

Dieser Feature-Instanz sind noch keine Benutzer zugewiesen.

) : (
{ // 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="Keine Benutzer gefunden" />
)} {/* Add User Modal */} {showAddModal && (
setShowAddModal(false)}>
e.stopPropagation()}>

Benutzer zur Feature-Instanz hinzufügen

{availableUsers.length === 0 ? (

Alle Benutzer haben bereits Zugriff auf diese Feature-Instanz.

) : instanceRoles.length === 0 ? (

Diese Feature-Instanz hat keine Rollen. Bitte synchronisieren Sie zuerst die Rollen.

) : ( setShowAddModal(false)} submitButtonText="Hinzufügen" cancelButtonText="Abbrechen" /> )}
)} {/* Edit Roles Modal */} {editingUser && (
setEditingUser(null)}>
e.stopPropagation()}>

Rollen bearbeiten: {editingUser.username}

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