459 lines
15 KiB
TypeScript
459 lines
15 KiB
TypeScript
/**
|
||
* AdminUserMandatesPage
|
||
*
|
||
* Admin page for managing user-mandate memberships.
|
||
* Allows assigning users to mandates and managing their roles within mandates.
|
||
*/
|
||
|
||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||
import { useUserMandates, type MandateUser, type Mandate, type Role, type PaginationParams } from '../../hooks/useUserMandates';
|
||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||
import { FaPlus, FaSync, FaUsers, FaBuilding } from 'react-icons/fa';
|
||
import { useToast } from '../../contexts/ToastContext';
|
||
import api from '../../api';
|
||
import styles from './Admin.module.css';
|
||
|
||
export const AdminUserMandatesPage: React.FC = () => {
|
||
const { showError } = useToast();
|
||
const {
|
||
users,
|
||
loading,
|
||
error,
|
||
pagination,
|
||
fetchMandateUsers,
|
||
addUserToMandate,
|
||
removeUserFromMandate,
|
||
updateUserRoles,
|
||
fetchMandates,
|
||
fetchRoles,
|
||
fetchAllUsers,
|
||
} = useUserMandates();
|
||
|
||
// Store current mandateId for refetch
|
||
const currentMandateIdRef = useRef<string>('');
|
||
|
||
// State
|
||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||
const [roles, setRoles] = useState<Role[]>([]);
|
||
const [allUsers, setAllUsers] = useState<Array<{ id: string; username: string; email?: string; fullName?: string }>>([]);
|
||
const [showAddModal, setShowAddModal] = useState(false);
|
||
const [editingUser, setEditingUser] = useState<MandateUser | null>(null);
|
||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||
|
||
// Load mandates and attributes on mount
|
||
useEffect(() => {
|
||
const loadMandates = async () => {
|
||
const data = await fetchMandates();
|
||
setMandates(data);
|
||
// Auto-select first mandate if available
|
||
if (data.length > 0 && !selectedMandateId) {
|
||
setSelectedMandateId(data[0].id);
|
||
}
|
||
};
|
||
loadMandates();
|
||
// Fetch UserMandate attributes from backend (for table columns)
|
||
api.get('/api/attributes/UserMandate').then(response => {
|
||
const attrs = response.data?.attributes || response.data || [];
|
||
setBackendAttributes(Array.isArray(attrs) ? attrs : []);
|
||
}).catch(() => setBackendAttributes([]));
|
||
}, [fetchMandates]);
|
||
|
||
// Load users when mandate changes
|
||
useEffect(() => {
|
||
if (selectedMandateId) {
|
||
currentMandateIdRef.current = selectedMandateId;
|
||
fetchMandateUsers(selectedMandateId);
|
||
fetchRoles(selectedMandateId).then(setRoles);
|
||
}
|
||
}, [selectedMandateId, fetchMandateUsers, fetchRoles]);
|
||
|
||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
||
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
||
const mandateId = currentMandateIdRef.current;
|
||
if (!mandateId) return;
|
||
// If pagination params provided, pass them; otherwise just use mandateId
|
||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||
return fetchMandateUsers(paginationParams);
|
||
}
|
||
return fetchMandateUsers(mandateId);
|
||
}, [fetchMandateUsers]);
|
||
|
||
// Load all users for the add modal
|
||
useEffect(() => {
|
||
fetchAllUsers().then(setAllUsers);
|
||
}, [fetchAllUsers]);
|
||
|
||
// Get users not yet in the mandate
|
||
const availableUsers = useMemo(() => {
|
||
const existingUserIds = new Set(users.map(u => u.userId));
|
||
return allUsers.filter(u => !existingUserIds.has(u.id));
|
||
}, [allUsers, users]);
|
||
|
||
// Table columns - based on MandateUserInfo response structure
|
||
const columns = useMemo(() => {
|
||
return [
|
||
{
|
||
key: 'username',
|
||
label: 'Benutzername',
|
||
type: 'text' as any,
|
||
sortable: true,
|
||
filterable: true,
|
||
searchable: true,
|
||
width: 150,
|
||
},
|
||
{
|
||
key: 'email',
|
||
label: 'E-Mail',
|
||
type: 'text' as any,
|
||
sortable: true,
|
||
filterable: true,
|
||
searchable: true,
|
||
width: 200,
|
||
},
|
||
{
|
||
key: 'fullName',
|
||
label: 'Vollständiger Name',
|
||
type: 'text' as any,
|
||
sortable: true,
|
||
filterable: true,
|
||
searchable: true,
|
||
width: 180,
|
||
},
|
||
{
|
||
key: 'roleLabels',
|
||
label: 'Rollen',
|
||
type: 'text' as any,
|
||
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 any,
|
||
sortable: true,
|
||
filterable: true,
|
||
searchable: false,
|
||
width: 80,
|
||
},
|
||
];
|
||
}, []); // No dependencies - columns are static, roleLabels come from backend
|
||
|
||
// 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(() =>
|
||
roles.filter(r => !r.featureInstanceId).map(r => ({
|
||
value: r.id,
|
||
label: r.roleLabel
|
||
})), [roles]);
|
||
|
||
// Form attributes for adding a user - uses dynamic options
|
||
// Note: This is an operational form for junction table, not direct model editing
|
||
const addUserFields: AttributeDefinition[] = useMemo(() => {
|
||
// Check if backend has userId attribute to get label/description
|
||
const userIdAttr = backendAttributes.find(a => a.name === 'userId');
|
||
const roleIdsAttr = backendAttributes.find(a => a.name === 'roleIds');
|
||
|
||
return [
|
||
{
|
||
name: 'targetUserId',
|
||
label: userIdAttr?.label || 'Benutzer',
|
||
type: 'enum' as any,
|
||
required: true,
|
||
options: userOptions,
|
||
},
|
||
{
|
||
name: 'roleIds',
|
||
label: roleIdsAttr?.label || 'Rollen',
|
||
type: 'multiselect' as any,
|
||
required: true,
|
||
options: roleOptions,
|
||
}
|
||
];
|
||
}, [userOptions, roleOptions, backendAttributes]);
|
||
|
||
// Form attributes for editing user roles
|
||
const editRolesFields: AttributeDefinition[] = useMemo(() => {
|
||
const roleIdsAttr = backendAttributes.find(a => a.name === 'roleIds');
|
||
|
||
return [{
|
||
name: 'roleIds',
|
||
label: roleIdsAttr?.label || 'Rollen',
|
||
type: 'multiselect' as any,
|
||
required: true,
|
||
options: roleOptions,
|
||
}];
|
||
}, [roleOptions, backendAttributes]);
|
||
|
||
// Handle add user submit
|
||
const handleAddUser = async (data: { targetUserId: string; roleIds: string[] }) => {
|
||
if (!selectedMandateId) return;
|
||
setIsSubmitting(true);
|
||
try {
|
||
const result = await addUserToMandate(selectedMandateId, data);
|
||
if (result.success) {
|
||
setShowAddModal(false);
|
||
fetchMandateUsers(selectedMandateId);
|
||
} else {
|
||
showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers');
|
||
}
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Handle edit roles submit
|
||
const handleEditRoles = async (data: { roleIds: string[] }) => {
|
||
if (!selectedMandateId || !editingUser) return;
|
||
setIsSubmitting(true);
|
||
try {
|
||
const result = await updateUserRoles(selectedMandateId, editingUser.userId, data.roleIds);
|
||
if (result.success) {
|
||
setEditingUser(null);
|
||
fetchMandateUsers(selectedMandateId);
|
||
} else {
|
||
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rollen');
|
||
}
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
// Handle remove user (confirmation handled by DeleteActionButton)
|
||
const handleRemoveUser = async (user: MandateUser) => {
|
||
if (!selectedMandateId) return;
|
||
const result = await removeUserFromMandate(selectedMandateId, user.userId);
|
||
if (!result.success) {
|
||
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
|
||
}
|
||
};
|
||
|
||
// Handle edit click
|
||
const handleEditClick = (user: MandateUser) => {
|
||
setEditingUser(user);
|
||
};
|
||
|
||
// Get mandate name
|
||
const getMandateName = (mandate: Mandate) => {
|
||
if (typeof mandate.name === 'object') {
|
||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||
}
|
||
return mandate.name || mandate.id;
|
||
};
|
||
|
||
if (error && !selectedMandateId) {
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||
<button className={styles.secondaryButton} onClick={() => fetchMandates()}>
|
||
<FaSync /> Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>Mandanten-Mitglieder</h1>
|
||
<p className={styles.pageSubtitle}>Verwalten Sie, welche Benutzer Zugriff auf welche Mandanten haben</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mandate Selector */}
|
||
<div className={styles.filterSection}>
|
||
<div className={styles.filterGroup}>
|
||
<label className={styles.filterLabel}>
|
||
<FaBuilding style={{ marginRight: 8 }} />
|
||
Mandant auswählen:
|
||
</label>
|
||
<select
|
||
className={styles.filterSelect}
|
||
value={selectedMandateId}
|
||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||
>
|
||
<option value="">-- Mandant wählen --</option>
|
||
{mandates.map(m => (
|
||
<option key={m.id} value={m.id}>
|
||
{getMandateName(m)}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{selectedMandateId && (
|
||
<div className={styles.headerActions}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => fetchMandateUsers(selectedMandateId)}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||
</button>
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={() => setShowAddModal(true)}
|
||
disabled={availableUsers.length === 0}
|
||
>
|
||
<FaPlus /> Benutzer hinzufügen
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Content */}
|
||
{!selectedMandateId ? (
|
||
<div className={styles.emptyState}>
|
||
<FaBuilding className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Wählen Sie einen Mandanten aus, um dessen Mitglieder zu verwalten.
|
||
</p>
|
||
</div>
|
||
) : loading && users.length === 0 ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Mandanten-Mitglieder...</span>
|
||
</div>
|
||
) : users.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaUsers className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Keine Mitglieder</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Diesem Mandanten sind noch keine Benutzer zugewiesen.
|
||
</p>
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={() => setShowAddModal(true)}
|
||
disabled={availableUsers.length === 0}
|
||
>
|
||
<FaPlus /> Ersten Benutzer hinzufügen
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className={styles.tableContainer}>
|
||
<FormGeneratorTable
|
||
data={users}
|
||
columns={columns}
|
||
loading={loading}
|
||
pagination={true}
|
||
pageSize={25}
|
||
searchable={true}
|
||
filterable={true}
|
||
sortable={true}
|
||
selectable={false}
|
||
actionButtons={[
|
||
{
|
||
type: 'edit' as const,
|
||
onAction: handleEditClick,
|
||
title: 'Rollen bearbeiten',
|
||
},
|
||
{
|
||
type: 'delete' as const,
|
||
title: 'Aus Mandant entfernen',
|
||
}
|
||
]}
|
||
onDelete={handleRemoveUser}
|
||
hookData={{
|
||
refetch: refetchWithParams,
|
||
pagination: pagination,
|
||
handleDelete: async (userMandateId: string) => {
|
||
// Find user by UserMandate ID to get userId for API call
|
||
const user = users.find(u => u.id === userMandateId);
|
||
if (user) {
|
||
const result = await removeUserFromMandate(selectedMandateId, user.userId);
|
||
return result.success;
|
||
}
|
||
return false;
|
||
},
|
||
}}
|
||
emptyMessage="Keine Mitglieder gefunden"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Add User Modal */}
|
||
{showAddModal && (
|
||
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>Benutzer zum Mandanten hinzufügen</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={() => setShowAddModal(false)}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
{availableUsers.length === 0 ? (
|
||
<p>Alle Benutzer sind bereits diesem Mandanten zugewiesen.</p>
|
||
) : roleOptions.length === 0 ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Rollen...</span>
|
||
</div>
|
||
) : (
|
||
<FormGeneratorForm
|
||
attributes={addUserFields}
|
||
mode="create"
|
||
onSubmit={handleAddUser}
|
||
onCancel={() => setShowAddModal(false)}
|
||
submitButtonText={isSubmitting ? 'Hinzufügen...' : 'Hinzufügen'}
|
||
cancelButtonText="Abbrechen"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Edit Roles Modal */}
|
||
{editingUser && (
|
||
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
|
||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||
<div className={styles.modalHeader}>
|
||
<h2 className={styles.modalTitle}>Rollen bearbeiten: {editingUser.username}</h2>
|
||
<button
|
||
className={styles.modalClose}
|
||
onClick={() => setEditingUser(null)}
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div className={styles.modalContent}>
|
||
<FormGeneratorForm
|
||
attributes={editRolesFields}
|
||
data={{ roleIds: editingUser.roleIds }}
|
||
mode="edit"
|
||
onSubmit={handleEditRoles}
|
||
onCancel={() => setEditingUser(null)}
|
||
submitButtonText={isSubmitting ? 'Speichern...' : 'Speichern'}
|
||
cancelButtonText="Abbrechen"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AdminUserMandatesPage;
|