frontend_nyla/src/pages/admin/AdminUserMandatesPage.tsx
2026-02-10 00:10:10 +01:00

461 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 (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;
};
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}
apiEndpoint={selectedMandateId ? `/api/mandates/${selectedMandateId}/users` : undefined}
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;