frontend_nyla/src/pages/admin/InstanceDetailModal.tsx

371 lines
12 KiB
TypeScript

/**
* InstanceDetailModal
*
* Modal for a feature instance: Benutzer (PermissionMatrix), Rollen (sync), Einstellungen.
*/
import React, { useState, useEffect, useMemo } from 'react';
import { useFeatureAccess, type FeatureInstance, type FeatureAccessUser } from '../../hooks/useFeatureAccess';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { Tabs } from '../../components/UiComponents/Tabs';
import { FaSync } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import { PermissionMatrix } from './PermissionMatrix';
import styles from './Admin.module.css';
import modalStyles from './InstanceDetailModal.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
export interface InstanceDetailModalProps {
instance: FeatureInstance;
mandateId: string;
mandateName: string;
featureLabel: string;
onClose: () => void;
onSaved: () => void;
}
export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instance,
mandateId,
mandateName,
featureLabel,
onClose,
onSaved,
}) => {
const {
fetchInstanceUsers,
fetchInstanceRoles,
addUserToInstance,
removeUserFromInstance,
updateInstanceUserRoles,
syncInstanceRoles,
updateInstance,
} = useFeatureAccess();
const { showSuccess, showError } = useToast();
const { t } = useLanguage();
const [users, setUsers] = useState<FeatureAccessUser[]>([]);
const [roles, setRoles] = useState<Array<{ id: string; roleLabel: string }>>([]);
const [allUsers, setAllUsers] = useState<Array<{ id: string; username: string; email?: string }>>([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [editingUser, setEditingUser] = useState<FeatureAccessUser | null>(null);
const [syncing, setSyncing] = useState(false);
const [roleOptions, setRoleOptions] = useState<AttributeDefinition['options']>([]);
const loadData = () => {
setLoading(true);
Promise.all([
fetchInstanceUsers(mandateId, instance.id),
fetchInstanceRoles(mandateId, instance.id),
])
.then(([userList, roleList]) => {
setUsers(Array.isArray(userList) ? userList : []);
setRoles(Array.isArray(roleList) ? roleList : []);
setRoleOptions(
(Array.isArray(roleList) ? roleList : []).map((r) => ({
value: r.id,
label: r.roleLabel,
}))
);
})
.catch(() => {
setUsers([]);
setRoles([]);
setRoleOptions([]);
})
.finally(() => setLoading(false));
};
useEffect(() => {
loadData();
}, [mandateId, instance.id]);
useEffect(() => {
api
.get(`/api/mandates/${mandateId}/users`)
.then((res) => {
const data = res.data?.items || res.data || [];
setAllUsers(
Array.isArray(data)
? data.map((u: { userId: string; username: string; email?: string }) => ({
id: u.userId,
username: u.username,
email: u.email,
}))
: []
);
})
.catch(() => setAllUsers([]));
}, [mandateId]);
const availableUsers = useMemo(() => {
const ids = new Set(users.map((u) => u.userId));
return allUsers.filter((u) => !ids.has(u.id));
}, [allUsers, users]);
const handleAddUser = async (data: { userId: string; roleIds: string[] }) => {
const result = await addUserToInstance(mandateId, instance.id, {
userId: data.userId,
roleIds: data.roleIds,
});
if (result.success) {
setShowAddModal(false);
loadData();
onSaved();
showSuccess(t('Benutzer hinzugefügt'), t('Der Benutzer wurde der Instanz hinzugefügt.'));
} else {
showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen'));
}
};
const handleRemoveUser = async (user: FeatureAccessUser) => {
const result = await removeUserFromInstance(mandateId, instance.id, user.userId);
if (result.success) {
loadData();
onSaved();
showSuccess(t('Benutzer entfernt'), t('"{name}" wurde entfernt.', { name: user.username }));
} else {
showError(t('Fehler'), result.error || t('Fehler beim Entfernen'));
}
};
const handleEditUser = (user: FeatureAccessUser) => {
setEditingUser(user);
};
const handleUpdateRoles = async (data: { roleIds: string[]; enabled?: boolean }) => {
if (!editingUser) return;
const result = await updateInstanceUserRoles(mandateId, instance.id, editingUser.userId, {
roleIds: data.roleIds,
enabled: data.enabled,
});
if (result.success) {
setEditingUser(null);
loadData();
onSaved();
showSuccess(t('Aktualisiert'), t('Rollen und Status wurden gespeichert.'));
} else {
showError(t('Fehler'), result.error || t('Fehler beim Speichern'));
}
};
const handleSyncRoles = async () => {
setSyncing(true);
try {
const result = await syncInstanceRoles(mandateId, instance.id, true);
if (result.success && result.data) {
loadData();
onSaved();
showSuccess(
t('Rollen synchronisiert'),
t('Hinzugefügt: {added}, Entfernt: {removed}', { added: result.data.added, removed: result.data.removed })
);
} else {
showError(t('Fehler'), result.error || t('Synchronisierung fehlgeschlagen'));
}
} finally {
setSyncing(false);
}
};
const handleUpdateInstance = async (data: { label?: string; enabled?: boolean }) => {
const result = await updateInstance(mandateId, instance.id, data);
if (result.success) {
onSaved();
showSuccess(t('Instanz aktualisiert'), t('Einstellungen wurden gespeichert.'));
} else {
showError(t('Fehler'), result.error || t('Fehler beim Speichern'));
}
};
const addUserFields: AttributeDefinition[] = useMemo(
() => [
{
name: 'userId',
label: t('Benutzer'),
type: 'enum' as const,
required: true,
options: availableUsers.map((u) => ({
value: u.id,
label: `${u.username}${u.email ? ` (${u.email})` : ''}`,
})),
},
{
name: 'roleIds',
label: t('Rollen'),
type: 'multiselect' as const,
required: true,
options: roleOptions as AttributeDefinition['options'],
},
],
[availableUsers, roleOptions, t]
);
const editRolesFields: AttributeDefinition[] = useMemo(
() => [
{
name: 'roleIds',
label: t('Rollen'),
type: 'multiselect' as const,
required: true,
options: roleOptions as AttributeDefinition['options'],
},
{
name: 'enabled',
label: t('Aktiv'),
type: 'checkbox' as const,
required: false,
},
],
[roleOptions, t]
);
const tabs = [
{
id: 'users',
label: t('Benutzer'),
content: (
<div className={modalStyles.tabContent}>
{loading ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>{t('Lade Benutzer')}</span>
</div>
) : (
<PermissionMatrix
users={users}
roles={roles}
onEditUser={handleEditUser}
onRemoveUser={handleRemoveUser}
onAddUser={() => setShowAddModal(true)}
/>
)}
</div>
),
},
{
id: 'roles',
label: t('Rollen'),
content: (
<div className={modalStyles.tabContent}>
<p className={modalStyles.rolesIntro}>
{t('Rollen werden von der Feature-Vorlage übernommen. Mit „Synchronisieren“ können Sie fehlende Rollen nachziehen.')}
</p>
<ul className={modalStyles.rolesList}>
{roles.map((r) => (
<li key={r.id}>{r.roleLabel}</li>
))}
</ul>
<button
type="button"
className={styles.secondaryButton}
onClick={handleSyncRoles}
disabled={syncing || roles.length === 0}
>
<FaSync className={syncing ? 'spinning' : ''} /> {t('Rollen synchronisieren')}
</button>
</div>
),
},
{
id: 'settings',
label: t('Einstellungen'),
content: (
<div className={modalStyles.tabContent}>
<FormGeneratorForm
attributes={[
{ name: 'label', type: 'string' as const, label: t('Bezeichnung'), required: true, editable: true },
{ name: 'enabled', type: 'boolean' as const, label: t('Aktiviert'), required: false, editable: true },
]}
data={instance}
mode="edit"
onSubmit={handleUpdateInstance}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
onCancel={() => {}}
/>
</div>
),
},
];
return (
<div className={styles.modalOverlay}>
<div className={`${styles.modal} ${modalStyles.modal}`}>
<div className={styles.modalHeader}>
<div>
<h2 className={styles.modalTitle}>{instance.label}</h2>
<p className={modalStyles.subtitle}>
{mandateName} · {featureLabel}
</p>
</div>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('Schließen')}>
</button>
</div>
<div className={styles.modalContent}>
<Tabs tabs={tabs} defaultTabId="users" />
</div>
</div>
{showAddModal && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Benutzer hinzufügen')}</h2>
<button type="button" className={styles.modalClose} onClick={() => setShowAddModal(false)}>
</button>
</div>
<div className={styles.modalContent}>
{availableUsers.length === 0 ? (
<p>{t('Alle Mandantenbenutzer haben bereits Zugriff')}</p>
) : addUserFields.length < 2 || !roleOptions?.length ? (
<p>{t('Laden')}</p>
) : (
<FormGeneratorForm
attributes={addUserFields}
mode="create"
onSubmit={handleAddUser}
onCancel={() => setShowAddModal(false)}
submitButtonText={t('Hinzufügen')}
cancelButtonText={t('Abbrechen')}
/>
)}
</div>
</div>
</div>
)}
{editingUser && (
<div className={styles.modalOverlay}>
<div className={styles.modal}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
{t('Rollen')}: {editingUser.username}
</h2>
<button type="button" className={styles.modalClose} onClick={() => setEditingUser(null)}>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
attributes={editRolesFields}
data={{ roleIds: editingUser.roleIds, enabled: editingUser.enabled }}
mode="edit"
onSubmit={handleUpdateRoles}
onCancel={() => setEditingUser(null)}
submitButtonText={t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>
</div>
)}
</div>
);
};
export default InstanceDetailModal;