frontend_nyla/src/pages/admin/AdminUsersPage.tsx
2026-04-11 19:44:52 +02:00

303 lines
9.5 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.

/**
* AdminUsersPage
*
* Admin page for managing Users using FormGeneratorTable.
*/
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaKey, FaEnvelopeOpenText, FaUserShield } from 'react-icons/fa';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
interface User {
id: string;
username: string;
email: string;
fullName: string;
enabled: boolean;
isSysAdmin?: boolean;
[key: string]: any;
}
export const AdminUsersPage: React.FC = () => {
const { t } = useLanguage();
const navigate = useNavigate();
// Use two hooks: one for data, one for operations
const {
data: users,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchUserById,
updateOptimistically,
} = useOrgUsers();
const {
handleUserCreate: createUser,
handleUserUpdate: updateUser,
handleUserDelete: deleteUser,
handleSendPasswordLink,
handleInlineUpdate,
sendingPasswordLink: sendingPasswordLinkState,
} = useUserOperations();
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
// Generate columns from attributes
const columns = useMemo(() => {
return (attributes || []).map(attr => ({
key: attr.name,
label: attr.label || attr.name,
type: attr.type as any,
sortable: attr.sortable !== false,
filterable: attr.filterable !== false,
searchable: attr.searchable !== false,
width: attr.width || 150,
minWidth: attr.minWidth || 100,
maxWidth: attr.maxWidth || 400,
fkSource: (attr as any).fkSource,
fkDisplayField: (attr as any).fkDisplayField,
}));
}, [attributes]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (user: User) => {
const fullUser = await fetchUserById(user.id);
if (fullUser) {
setEditingUser(fullUser as User);
}
};
// Handle create submit
const handleCreateSubmit = async (data: Partial<User>) => {
const result = await createUser(data as Omit<User, 'id'>);
if (result.success) {
setShowCreateModal(false);
refetch(); // Refresh the list
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<User>) => {
if (!editingUser) return;
const result = await updateUser(editingUser.id, data);
if (result.success) {
setEditingUser(null);
refetch(); // Refresh the list
}
};
// Handle delete (confirmation handled by DeleteActionButton)
const handleDeleteUser = async (user: User) => {
const success = await deleteUser(user.id);
if (success) {
refetch(); // Refresh the list
}
};
// Handle send password link
const handleSendPassword = async (user: User) => {
await handleSendPasswordLink(user.id);
};
// Form attributes from backend - filter for create/edit forms
const formAttributes = useMemo(() => {
const excludedFields = ['id', 'hashedPassword', 'authenticationAuthority'];
return (attributes || [])
.filter(attr => !excludedFields.includes(attr.name))
.map(attr => ({
...attr,
// Mark username as readonly for edit mode (will be handled by FormGeneratorForm)
editable: attr.name === 'username' ? false : attr.editable,
}));
}, [attributes]);
if (error) {
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>
{t('Fehler beim Laden der Benutzer')}: {error}
</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('Benutzer')}</h1>
<p className={styles.pageSubtitle}>{t('Verwalten Sie alle Benutzer im')}</p>
</div>
<div className={styles.headerActions}>
<button
type="button"
className={styles.secondaryButton}
onClick={() => navigate('/admin/user-access-overview')}
>
<FaUserShield /> {t('Zugriffsübersicht')}
</button>
<button
type="button"
className={styles.secondaryButton}
onClick={() => navigate('/admin/invitations')}
>
<FaEnvelopeOpenText /> {t('Einladungen')}
</button>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> {t('Neuer Benutzer')}
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
<FormGeneratorTable
data={users}
columns={columns}
apiEndpoint="/api/users/"
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: t('Bearbeiten'),
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: t('Löschen'),
}] : []),
]}
customActions={canUpdate ? [
{
id: 'sendPasswordLink',
icon: <FaKey />,
onClick: handleSendPassword,
title: t('Passwort-Link senden'),
loading: (row: User) => sendingPasswordLinkState.has(row.id),
}
] : []}
onDelete={handleDeleteUser}
hookData={{
refetch,
permissions,
pagination,
handleDelete: deleteUser,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage={t('Keine Benutzer gefunden')}
/>
</div>
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neuer Benutzer')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText={t('Erstellen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
</div>
</div>
)}
{/* Edit 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}>{t('Benutzer bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingUser(null)}
>
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Lade Formular')}</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
data={editingUser}
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingUser(null)}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default AdminUsersPage;