ui-nyla/src/pages/admin/AdminUserMandatesPage.tsx
2026-04-26 18:11:52 +02:00

440 lines
14 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, FaBuilding } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import { useApiRequest } from '../../hooks/useApi';
import { fetchAttributes } from '../../api/attributesApi';
import { resolveColumnTypes } from '../../utils/columnTypeResolver';
import type { ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils';
export const AdminUserMandatesPage: React.FC = () => {
const { t } = useLanguage();
const { showError } = useToast();
const { request } = useApiRequest();
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();
fetchAttributes(request, 'UserMandateView')
.then(setBackendAttributes)
.catch(() => setBackendAttributes([]));
}, [fetchMandates, request]);
// 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]);
const _rawColumns: ColumnConfig[] = useMemo(() => [
{
key: 'username',
label: t('Benutzername'),
sortable: true,
filterable: true,
searchable: true,
width: 150,
},
{
key: 'email',
label: t('E-Mail'),
sortable: true,
filterable: true,
searchable: true,
width: 200,
},
{
key: 'fullName',
label: t('Vollständiger Name'),
sortable: true,
filterable: true,
searchable: true,
width: 180,
},
{
key: 'roleLabels',
label: t('Rollen'),
sortable: false,
filterable: false,
searchable: true,
width: 200,
formatter: (value: string[]) => {
if (!value || value.length === 0) return '-';
return value.join(', ');
},
},
{
key: 'enabled',
label: t('Aktiv'),
sortable: true,
filterable: true,
searchable: false,
width: 80,
},
], [t]);
const columns = useMemo(
() => resolveColumnTypes(_rawColumns, backendAttributes),
[_rawColumns, backendAttributes],
);
// 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 || t('Benutzer'),
type: 'enum' as any,
required: true,
options: userOptions,
},
{
name: 'roleIds',
label: roleIdsAttr?.label || t('Rollen'),
type: 'multiselect' as any,
required: true,
options: roleOptions,
}
];
}, [userOptions, roleOptions, backendAttributes, t]);
// Form attributes for editing user roles
const editRolesFields: AttributeDefinition[] = useMemo(() => {
const roleIdsAttr = backendAttributes.find(a => a.name === 'roleIds');
return [{
name: 'roleIds',
label: roleIdsAttr?.label || t('Rollen'),
type: 'multiselect' as any,
required: true,
options: roleOptions,
}];
}, [roleOptions, backendAttributes, t]);
// 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(t('Fehler'), result.error || t('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(t('Fehler'), result.error || t('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(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers'));
}
};
// Handle edit click
const handleEditClick = (user: MandateUser) => {
setEditingUser(user);
};
if (error && !selectedMandateId) {
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>
{t('Fehler')}: {error}
</p>
<button className={styles.secondaryButton} onClick={() => fetchMandates()}>
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('Mandanten-Mitglieder')}</h1>
<p className={styles.pageSubtitle}>{t('Verwalten Sie, welche Benutzer Zugriff')}</p>
</div>
</div>
{/* Mandate Selector */}
<div className={styles.filterSection}>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>
<FaBuilding style={{ marginRight: 8 }} />
{t('Mandant auswählen')}:
</label>
<select
className={styles.filterSelect}
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">{t('Mandant wählen')}</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>
{mandateDisplayLabel(m)}
</option>
))}
</select>
</div>
{selectedMandateId && (
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => fetchMandateUsers(selectedMandateId)}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
<button
className={styles.primaryButton}
onClick={() => setShowAddModal(true)}
disabled={availableUsers.length === 0}
>
<FaPlus /> {t('Benutzer hinzufügen')}
</button>
</div>
)}
</div>
{/* Content */}
{!selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Mandant ausgewählt')}</h3>
<p className={styles.emptyDescription}>
{t('Wählen Sie einen Mandanten aus, um dessen Mitglieder zu verwalten.')}
</p>
</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={true}
actionButtons={[
{
type: 'edit' as const,
onAction: handleEditClick,
title: t('Rollen bearbeiten'),
},
{
type: 'delete' as const,
title: t('Vom Mandat 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={t('Keine Mitglieder gefunden')}
/>
</div>
)}
{/* Add User Modal */}
{showAddModal && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Benutzer zum Mandanten hinzufügen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowAddModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
{availableUsers.length === 0 ? (
<p>{t('Alle Benutzer sind bereits diesem')}</p>
) : roleOptions.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Lade Rollen')}</span>
</div>
) : (
<FormGeneratorForm
attributes={addUserFields}
mode="create"
onSubmit={handleAddUser}
onCancel={() => setShowAddModal(false)}
submitButtonText={isSubmitting ? t('Hinzufügen') : t('Hinzufügen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
</div>
</div>
)}
{/* Edit Roles Modal */}
{editingUser && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{t('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 ? t('Speichern') : t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminUserMandatesPage;