303 lines
9.5 KiB
TypeScript
303 lines
9.5 KiB
TypeScript
/**
|
||
* 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;
|