371 lines
12 KiB
TypeScript
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;
|