updated tenant role page to be user role templates page
This commit is contained in:
parent
dfa36c17ef
commit
849efa6ed7
8 changed files with 575 additions and 523 deletions
|
|
@ -39,7 +39,7 @@ import { GDPRPage } from './pages/GDPR';
|
|||
import StorePage from './pages/Store';
|
||||
import { IntegrationsOverviewPage } from './pages/IntegrationsOverviewPage';
|
||||
import { FeatureViewPage } from './pages/FeatureView';
|
||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage } from './pages/admin';
|
||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminUserRoleTemplatesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage } from './pages/admin';
|
||||
import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards';
|
||||
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
|
||||
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
|
||||
|
|
@ -212,7 +212,8 @@ function App() {
|
|||
<Route path="feature-roles" element={<AdminFeatureRolesPage />} />
|
||||
<Route path="feature-users" element={<AdminFeatureInstanceUsersPage />} />
|
||||
<Route path="invitations" element={<AdminInvitationsPage />} />
|
||||
<Route path="mandate-roles" element={<AdminMandateRolesPage />} />
|
||||
<Route path="user-role-templates" element={<AdminUserRoleTemplatesPage />} />
|
||||
<Route path="mandate-roles" element={<Navigate to="/admin/user-role-templates" replace />} />
|
||||
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
|
||||
<Route path="billing">
|
||||
<Route index element={<Navigate to="/billing/admin" replace />} />
|
||||
|
|
|
|||
|
|
@ -175,6 +175,35 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.createTemplateField {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.createTemplateLabel {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.createTemplateSelect {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg-secondary, #fff);
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.createTemplateHint {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary, #666);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.panelInfoBoxIcon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.15em;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|||
import {
|
||||
useMandateRoles,
|
||||
type Role,
|
||||
type RoleCreate,
|
||||
type RoleUpdate,
|
||||
} from '../../hooks/useMandateRoles';
|
||||
import { useApiRequest } from '../../hooks/useApi';
|
||||
|
|
@ -74,7 +73,8 @@ export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanel
|
|||
loading,
|
||||
error,
|
||||
fetchRoles,
|
||||
createRole,
|
||||
fetchTemplatesOnly,
|
||||
createRoleFromTemplate,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
} = useMandateRoles();
|
||||
|
|
@ -85,6 +85,10 @@ export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanel
|
|||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
const [deletingRoleId, setDeletingRoleId] = useState<string | null>(null);
|
||||
const [userRoleTemplates, setUserRoleTemplates] = useState<Role[]>([]);
|
||||
const [templatesLoading, setTemplatesLoading] = useState(false);
|
||||
const [selectedTemplateId, setSelectedTemplateId] = useState('');
|
||||
const [createFormData, setCreateFormData] = useState<Record<string, unknown>>({});
|
||||
|
||||
const shouldShowInfoBox = showInfoBox ?? showRoleActions;
|
||||
const shouldShowCreateRole = showCreateRole ?? showRoleActions;
|
||||
|
|
@ -107,6 +111,42 @@ export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanel
|
|||
}
|
||||
}, [request, needsRoleFormAttributes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showCreateModal || !shouldShowCreateRole) return;
|
||||
setTemplatesLoading(true);
|
||||
// fetchTemplatesOnly makes a direct API call without touching the shared
|
||||
// `roles` state, so the displayed mandate roles are never clobbered.
|
||||
fetchTemplatesOnly()
|
||||
.then(setUserRoleTemplates)
|
||||
.catch(() => setUserRoleTemplates([]))
|
||||
.finally(() => setTemplatesLoading(false));
|
||||
}, [showCreateModal, shouldShowCreateRole, fetchTemplatesOnly]);
|
||||
|
||||
const existingMandateRoleLabels = useMemo(
|
||||
() => new Set(roles.map(r => r.roleLabel)),
|
||||
[roles],
|
||||
);
|
||||
|
||||
const closeCreateModal = () => {
|
||||
if (isSubmitting) return;
|
||||
setShowCreateModal(false);
|
||||
setSelectedTemplateId('');
|
||||
setCreateFormData({});
|
||||
};
|
||||
|
||||
const handleTemplateSelect = (templateId: string) => {
|
||||
setSelectedTemplateId(templateId);
|
||||
const template = userRoleTemplates.find(t => t.id === templateId);
|
||||
if (template) {
|
||||
setCreateFormData({
|
||||
roleLabel: template.roleLabel,
|
||||
description: template.description ?? {},
|
||||
});
|
||||
} else {
|
||||
setCreateFormData({});
|
||||
}
|
||||
};
|
||||
|
||||
const scopeTypeLabel = useCallback(
|
||||
(scopeType?: Role['scopeType']) => {
|
||||
switch (scopeType) {
|
||||
|
|
@ -125,25 +165,10 @@ export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanel
|
|||
|
||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType'];
|
||||
const fields = backendAttributes
|
||||
return backendAttributes
|
||||
.filter(attr => !excludedFields.includes(attr.name))
|
||||
.map(attr => ({ ...attr })) as AttributeDefinition[];
|
||||
|
||||
if (fields.length > 0) {
|
||||
fields.push({
|
||||
name: 'scope',
|
||||
label: t('Geltungsbereich'),
|
||||
type: 'enum' as AttributeDefinition['type'],
|
||||
required: true,
|
||||
default: scopeFilter === 'global' ? 'global' : 'mandate',
|
||||
options: [
|
||||
{ value: 'mandate', label: t('Nur dieser Mandant') },
|
||||
{ value: 'global', label: t('Template bei neuen Mandanten') },
|
||||
],
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
}, [backendAttributes, scopeFilter, t]);
|
||||
}, [backendAttributes]);
|
||||
|
||||
const editFields: AttributeDefinition[] = useMemo(() => {
|
||||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType'];
|
||||
|
|
@ -155,28 +180,29 @@ export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanel
|
|||
})) as AttributeDefinition[];
|
||||
}, [backendAttributes]);
|
||||
|
||||
const handleCreateRole = async (data: {
|
||||
const handleCreateRoleFromTemplate = async (data: {
|
||||
roleLabel: string;
|
||||
description?: Record<string, string>;
|
||||
scope: 'mandate' | 'global';
|
||||
}) => {
|
||||
if (!mandateId) return;
|
||||
if (!selectedTemplateId) {
|
||||
showError(t('Fehler'), t('Bitte eine Rollen-Vorlage wählen'));
|
||||
return;
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const roleData: RoleCreate = {
|
||||
const result = await createRoleFromTemplate(selectedTemplateId, mandateId, {
|
||||
roleLabel: data.roleLabel.toLowerCase().replace(/\s+/g, '_'),
|
||||
description: data.description,
|
||||
mandateId: data.scope === 'mandate' ? mandateId : undefined,
|
||||
};
|
||||
const result = await createRole(roleData, mandateId);
|
||||
});
|
||||
if (result.success) {
|
||||
setShowCreateModal(false);
|
||||
closeCreateModal();
|
||||
await loadRoles();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle'));
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle aus Vorlage'));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('Fehler beim Erstellen der Rolle');
|
||||
const message = err instanceof Error ? err.message : t('Fehler beim Erstellen der Rolle aus Vorlage');
|
||||
showError(t('Fehler'), message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -439,25 +465,63 @@ export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanel
|
|||
{showCreateModal && (
|
||||
<Popup
|
||||
isOpen={true}
|
||||
title={t('Neue Rolle erstellen')}
|
||||
onClose={() => !isSubmitting && setShowCreateModal(false)}
|
||||
title={t('Rolle aus Vorlage erstellen')}
|
||||
onClose={closeCreateModal}
|
||||
size="medium"
|
||||
closable={!isSubmitting}
|
||||
>
|
||||
{createFields.length === 0 ? (
|
||||
{createFields.length === 0 || templatesLoading ? (
|
||||
<div className={adminStyles.loadingContainer}>
|
||||
<div className={adminStyles.spinner} />
|
||||
<span>{t('Lade Formular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={createFields}
|
||||
mode="create"
|
||||
onSubmit={handleCreateRole}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
<>
|
||||
<div className={panelStyles.createTemplateField}>
|
||||
<label className={panelStyles.createTemplateLabel} htmlFor="role-template-select">
|
||||
{t('Rollen-Vorlage')} *
|
||||
</label>
|
||||
<select
|
||||
id="role-template-select"
|
||||
className={panelStyles.createTemplateSelect}
|
||||
value={selectedTemplateId}
|
||||
onChange={e => handleTemplateSelect(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<option value="">{t('Vorlage wählen')}</option>
|
||||
{userRoleTemplates.map(template => {
|
||||
const alreadyInMandate = existingMandateRoleLabels.has(template.roleLabel);
|
||||
return (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.roleLabel}
|
||||
{template.isSystemRole ? ` (${t('System-Template')})` : ''}
|
||||
{template.scopeType === 'global' && !template.isSystemRole
|
||||
? ` (${t('Template')})`
|
||||
: ''}
|
||||
{alreadyInMandate ? ` — ${t('bereits vorhanden')}` : ''}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<p className={panelStyles.createTemplateHint}>
|
||||
{userRoleTemplates.length === 0
|
||||
? t('Keine Vorlagen verfügbar.')
|
||||
: t(
|
||||
'Berechtigungen (AccessRules) der Vorlage werden in die neue Mandanten-Rolle übernommen. Bezeichnung und Beschreibung können angepasst werden.',
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<FormGeneratorForm
|
||||
key={selectedTemplateId || 'no-template'}
|
||||
attributes={createFields}
|
||||
data={createFormData as { roleLabel: string; description?: Record<string, string> }}
|
||||
mode="create"
|
||||
onSubmit={handleCreateRoleFromTemplate}
|
||||
onCancel={closeCreateModal}
|
||||
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle aus Vorlage erstellen')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
FaHome, FaCog, FaBriefcase, FaPlay, FaBuilding, FaUsers, FaUserTag,
|
||||
FaCubes, FaEnvelopeOpenText, FaKey, FaUsersCog, FaCube, FaShieldAlt,
|
||||
FaCubes, FaEnvelopeOpenText, FaUsersCog, FaCube, FaShieldAlt,
|
||||
FaLightbulb, FaRegFileAlt, FaLink, FaComments,
|
||||
FaListAlt, FaChartLine, FaChartBar, FaFileAlt, FaUserShield, FaDatabase,
|
||||
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock,
|
||||
|
|
@ -64,7 +64,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'page.admin.users': <FaUsers />,
|
||||
'page.admin.invitations': <FaEnvelopeOpenText />,
|
||||
'page.admin.mandates': <FaBuilding />,
|
||||
'page.admin.roles': <FaKey />,
|
||||
'page.admin.userRoleTemplates': <FaUserTag />,
|
||||
'page.admin.role-permissions': <FaShieldAlt />,
|
||||
'page.admin.user-mandates': <FaUserTag />,
|
||||
'page.admin.userMandates': <FaUserTag />,
|
||||
|
|
|
|||
|
|
@ -278,12 +278,83 @@ export function useMandateRoles() {
|
|||
return roles.filter(r => r.isTemplate === true);
|
||||
}, [roles]);
|
||||
|
||||
/** Global + system user role templates (no mandateId).
|
||||
* Used by AdminUserRoleTemplatesPage — updates the shared `roles` state.
|
||||
* NOTE: passes `undefined` as first arg so fetchRoles skips both branches and
|
||||
* does NOT inherit currentMandateIdRef, ensuring no mandate header is sent. */
|
||||
const fetchUserRoleTemplates = useCallback(
|
||||
async (paginationParams?: PaginationParams): Promise<Role[]> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return fetchRoles(undefined as any, { ...paginationParams });
|
||||
},
|
||||
[fetchRoles],
|
||||
);
|
||||
|
||||
/** Fetch global/system templates without touching the shared `roles` state.
|
||||
* Safe to call from components that already manage their own role list
|
||||
* (e.g. MandateRolesPermissionsPanel) so the displayed roles aren't clobbered. */
|
||||
const fetchTemplatesOnly = useCallback(async (): Promise<Role[]> => {
|
||||
try {
|
||||
const response = await api.get('/api/rbac/roles', {
|
||||
params: { includeTemplates: 'false', scopeFilter: 'global' },
|
||||
});
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
return response.data.items as Role[];
|
||||
}
|
||||
if (Array.isArray(response.data)) {
|
||||
return response.data as Role[];
|
||||
}
|
||||
return [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Create a mandate role from a user role template (copies AccessRules).
|
||||
*/
|
||||
const createRoleFromTemplate = useCallback(
|
||||
async (
|
||||
templateRoleId: string,
|
||||
targetMandateId: string,
|
||||
data?: { roleLabel?: string; description?: Record<string, string> },
|
||||
): Promise<{ success: boolean; data?: Role; error?: string }> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const headers: Record<string, string> = { 'X-Mandate-Id': targetMandateId };
|
||||
const response = await api.post(
|
||||
'/api/rbac/roles/from-template',
|
||||
{
|
||||
templateRoleId,
|
||||
mandateId: targetMandateId,
|
||||
roleLabel: data?.roleLabel,
|
||||
description: data?.description,
|
||||
},
|
||||
{ headers },
|
||||
);
|
||||
return { success: true, data: response.data };
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.detail || err.message || 'Failed to create role from template';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
roles,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchRoles,
|
||||
fetchUserRoleTemplates,
|
||||
fetchTemplatesOnly,
|
||||
createRoleFromTemplate,
|
||||
getRole,
|
||||
createRole,
|
||||
updateRole,
|
||||
|
|
|
|||
|
|
@ -1,479 +0,0 @@
|
|||
/**
|
||||
* AdminMandateRolesPage
|
||||
*
|
||||
* Admin page for managing ALL ROLES (system + global + mandate-specific).
|
||||
* Consolidated view replacing separate System-Roles page.
|
||||
*
|
||||
* Shows:
|
||||
* - System roles (admin, user, viewer) - read-only, cannot be deleted
|
||||
* - Global roles (mandateId=null) - CRUD available
|
||||
* - Mandate-specific roles (mandateId=xyz) - CRUD available
|
||||
* - Feature-template roles are managed in AdminFeatureRolesPage
|
||||
*
|
||||
* ALL filtering, sorting, and pagination is handled by the backend.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useMandateRoles, type Role, type RoleCreate, type RoleUpdate, type PaginationParams } from '../../hooks/useMandateRoles';
|
||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUserShield, FaBuilding, FaCube } 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 AdminMandateRolesPage: React.FC = () => {
|
||||
const { t, currentLanguage } = useLanguage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { request } = useApiRequest();
|
||||
const { showError, showWarning } = useToast();
|
||||
const {
|
||||
roles,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchRoles,
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
} = useMandateRoles();
|
||||
|
||||
const { fetchMandates } = useUserMandates();
|
||||
|
||||
// State
|
||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('mandate');
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
|
||||
// Store current filter state for refetch
|
||||
const currentScopeFilterRef = useRef(scopeFilter);
|
||||
currentScopeFilterRef.current = scopeFilter;
|
||||
|
||||
// Load mandates and attributes on mount
|
||||
useEffect(() => {
|
||||
const loadMandates = async () => {
|
||||
const data = await fetchMandates();
|
||||
setMandates(data);
|
||||
if (data.length > 0 && !selectedMandateId) {
|
||||
setSelectedMandateId(data[0].id);
|
||||
}
|
||||
};
|
||||
loadMandates();
|
||||
fetchAttributes(request, 'RoleView')
|
||||
.then(setBackendAttributes)
|
||||
.catch(() => setBackendAttributes([]));
|
||||
}, [fetchMandates, request]);
|
||||
|
||||
// Load roles when mandate or scopeFilter changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
||||
|
||||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
||||
// and includes the current mandateId and scopeFilter
|
||||
const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => {
|
||||
if (!selectedMandateId) return;
|
||||
// Merge pagination params with current filter state
|
||||
return fetchRoles(selectedMandateId, {
|
||||
...paginationParams,
|
||||
scopeFilter: currentScopeFilterRef.current
|
||||
});
|
||||
}, [selectedMandateId, fetchRoles]);
|
||||
|
||||
const getDescriptionText = (desc: any) => {
|
||||
if (!desc) return '-';
|
||||
if (typeof desc === 'string') return desc;
|
||||
if (typeof desc === 'object') {
|
||||
return desc[currentLanguage] || desc['xx'] || Object.values(desc).find((v: any) => typeof v === 'string' && v.trim()) || '-';
|
||||
}
|
||||
return String(desc);
|
||||
};
|
||||
|
||||
const _rawColumns: ColumnConfig[] = useMemo(() => [
|
||||
{ key: 'roleLabel', sortable: true, filterable: true, searchable: true, width: 150 },
|
||||
{
|
||||
key: 'description',
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
width: 250,
|
||||
formatter: (value: string) => getDescriptionText(value),
|
||||
},
|
||||
{ key: 'scopeType', sortable: true, filterable: true, width: 160 },
|
||||
{ key: 'userCount', sortable: true, filterable: true, width: 100 },
|
||||
], []);
|
||||
|
||||
const columns = useMemo(
|
||||
() => resolveColumnTypes(_rawColumns, backendAttributes),
|
||||
[_rawColumns, backendAttributes],
|
||||
);
|
||||
|
||||
// Form attributes from backend - for create form
|
||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType'];
|
||||
|
||||
const fields = backendAttributes
|
||||
.filter(attr => !excludedFields.includes(attr.name))
|
||||
.map(attr => ({ ...attr })) as AttributeDefinition[];
|
||||
|
||||
// Add scope field for mandate/global selection (not a model attribute)
|
||||
if (fields.length > 0) {
|
||||
fields.push({
|
||||
name: 'scope',
|
||||
label: t('Geltungsbereich'),
|
||||
type: 'enum' as any,
|
||||
required: true,
|
||||
default: 'mandate',
|
||||
options: [
|
||||
{ value: 'mandate', label: t('Nur dieser Mandant') },
|
||||
{ value: 'global', label: t('Template bei neuen Mandanten') },
|
||||
]
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
}, [backendAttributes, t]);
|
||||
|
||||
// Form attributes from backend - for edit form
|
||||
// NOTE: mandateId/featureInstanceId/featureCode are IMMUTABLE - only description can be edited
|
||||
const editFields: AttributeDefinition[] = useMemo(() => {
|
||||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType'];
|
||||
|
||||
const fields = backendAttributes
|
||||
.filter(attr => !excludedFields.includes(attr.name))
|
||||
.map(attr => ({
|
||||
...attr,
|
||||
// Mark roleLabel as readonly (cannot change after creation)
|
||||
readonly: attr.name === 'roleLabel' ? true : attr.readonly,
|
||||
})) as AttributeDefinition[];
|
||||
|
||||
// No scope field for edit - context is immutable!
|
||||
return fields;
|
||||
}, [backendAttributes]);
|
||||
|
||||
// Handle create role
|
||||
const handleCreateRole = async (data: { roleLabel: string; description?: Record<string, string>; scope: 'mandate' | 'global' }) => {
|
||||
if (!selectedMandateId) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const roleData: RoleCreate = {
|
||||
roleLabel: data.roleLabel.toLowerCase().replace(/\s+/g, '_'),
|
||||
description: data.description,
|
||||
mandateId: data.scope === 'mandate' ? selectedMandateId : undefined
|
||||
};
|
||||
|
||||
const result = await createRole(roleData, selectedMandateId);
|
||||
|
||||
if (result.success) {
|
||||
setShowCreateModal(false);
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle'));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Create role error:', err);
|
||||
showError(t('Fehler'), err.message || t('Fehler beim Erstellen der Rolle'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit role
|
||||
const handleEditRole = async (data: RoleUpdate & { scope?: 'mandate' | 'global' }) => {
|
||||
if (!editingRole) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Convert scope to mandateId - NOTE: Context fields are IMMUTABLE per concept!
|
||||
// We should not be changing mandateId after creation
|
||||
const updateData: RoleUpdate = {
|
||||
roleLabel: data.roleLabel,
|
||||
description: data.description,
|
||||
// mandateId is immutable - don't include in update
|
||||
};
|
||||
|
||||
const result = await updateRole(editingRole.id, updateData);
|
||||
|
||||
if (result.success) {
|
||||
setEditingRole(null);
|
||||
if (selectedMandateId) {
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rolle'));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Update role error:', err);
|
||||
showError(t('Fehler'), err.message || t('Fehler beim Aktualisieren der Rolle'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete role (confirmation handled by DeleteActionButton)
|
||||
const handleDeleteRole = async (role: Role) => {
|
||||
if (role.isSystemRole) {
|
||||
showWarning(t('Nicht erlaubt'), t('System-Rollen können nicht gelöscht werden.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteRole(role.id);
|
||||
if (result.success) {
|
||||
// Refetch to update the list
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Rolle'));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit click
|
||||
const handleEditClick = (role: Role) => {
|
||||
setEditingRole(role);
|
||||
};
|
||||
|
||||
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('Rollen')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('Verwalten Sie systemweite und globale')}</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => navigate('/admin/feature-roles')}
|
||||
>
|
||||
<FaCube /> {t('Feature Rollen & Rechte')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mandate Selector and Filters */}
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
{t('Mandant')}:
|
||||
</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>
|
||||
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>{t('Filter')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={scopeFilter}
|
||||
onChange={(e) => setScopeFilter(e.target.value as 'all' | 'mandate' | 'global')}
|
||||
style={{ minWidth: 150 }}
|
||||
>
|
||||
<option value="mandate">{t('Mandanten-Rollen')}</option>
|
||||
<option value="all">{t('Alle inkl. Templates')}</option>
|
||||
<option value="global">{t('Nur Templates')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedMandateId && (
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchRoles(selectedMandateId, { scopeFilter })}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<FaPlus /> {t('Neue Rolle')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
{selectedMandateId && (
|
||||
<div className={styles.infoBox}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
<strong>{t('System-Templates')}</strong>{' '}
|
||||
{t('(admin, user, viewer) werden bei der Mandant-Erstellung automatisch als Mandanten-Instanz-Rollen kopiert. Templates selbst können nicht gelöscht werden.')}{' '}
|
||||
<strong>{t('Mandanten-Rollen')}</strong>{' '}
|
||||
{t('gelten nur für den ausgewählten Mandanten und sind den Benutzern zuweisbar.')}
|
||||
</span>
|
||||
</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 Rollen zu verwalten.')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.tableContainer}>
|
||||
<FormGeneratorTable
|
||||
data={roles}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/rbac/roles"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={true}
|
||||
actionButtons={[
|
||||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: t('Rolle bearbeiten'),
|
||||
disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: t('System-Rollen können nicht bearbeitet werden') } : false
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: t('Rolle löschen'),
|
||||
disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: t('System-Rollen können nicht gelöscht werden') } : false
|
||||
}
|
||||
]}
|
||||
onDelete={handleDeleteRole}
|
||||
hookData={{
|
||||
refetch: refetchWithParams,
|
||||
pagination: pagination,
|
||||
handleDelete: handleDeleteRole,
|
||||
}}
|
||||
emptyMessage={t('Keine Rollen gefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Role Modal */}
|
||||
{showCreateModal && (
|
||||
<div className={styles.modalOverlay}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>{t('Neue Rolle erstellen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{createFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Formular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={createFields}
|
||||
mode="create"
|
||||
onSubmit={handleCreateRole}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Role Modal */}
|
||||
{editingRole && (
|
||||
<div className={styles.modalOverlay}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>
|
||||
{t('Rolle bearbeiten')}: {editingRole.roleLabel}
|
||||
</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setEditingRole(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{editFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Formular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
{t('Geltungsbereich')}:{' '}
|
||||
<strong>
|
||||
{editingRole.mandateId ? t('Mandanten-Instanz') : t('Template (global)')}
|
||||
</strong>{' '}
|
||||
({t('kann nicht geändert werden')})
|
||||
</span>
|
||||
</div>
|
||||
<FormGeneratorForm
|
||||
attributes={editFields}
|
||||
data={editingRole}
|
||||
mode="edit"
|
||||
onSubmit={handleEditRole}
|
||||
onCancel={() => setEditingRole(null)}
|
||||
submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminMandateRolesPage;
|
||||
366
src/pages/admin/AdminUserRoleTemplatesPage.tsx
Normal file
366
src/pages/admin/AdminUserRoleTemplatesPage.tsx
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
/**
|
||||
* AdminUserRoleTemplatesPage
|
||||
*
|
||||
* Manage user role templates (system + global templates with mandateId=null).
|
||||
* Mandate-specific roles are managed per tenant on Admin Mandates (roles popup).
|
||||
* Feature role templates: AdminFeatureRolesPage.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
useMandateRoles,
|
||||
type Role,
|
||||
type RoleCreate,
|
||||
type RoleUpdate,
|
||||
type PaginationParams,
|
||||
} from '../../hooks/useMandateRoles';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUserShield } 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';
|
||||
|
||||
export const AdminUserRoleTemplatesPage: React.FC = () => {
|
||||
const { t, currentLanguage } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const { showError, showWarning } = useToast();
|
||||
const {
|
||||
roles,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchUserRoleTemplates,
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
} = useMandateRoles();
|
||||
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAttributes(request, 'RoleView')
|
||||
.then(setBackendAttributes)
|
||||
.catch(() => setBackendAttributes([]));
|
||||
}, [request]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUserRoleTemplates();
|
||||
}, [fetchUserRoleTemplates]);
|
||||
|
||||
const refetchWithParams = useCallback(
|
||||
async (paginationParams?: PaginationParams) => {
|
||||
return fetchUserRoleTemplates(paginationParams);
|
||||
},
|
||||
[fetchUserRoleTemplates],
|
||||
);
|
||||
|
||||
const getDescriptionText = (desc: unknown) => {
|
||||
if (!desc) return '-';
|
||||
if (typeof desc === 'string') return desc;
|
||||
if (typeof desc === 'object') {
|
||||
const record = desc as Record<string, string>;
|
||||
return (
|
||||
record[currentLanguage] ||
|
||||
record['xx'] ||
|
||||
Object.values(record).find(v => typeof v === 'string' && v.trim()) ||
|
||||
'-'
|
||||
);
|
||||
}
|
||||
return String(desc);
|
||||
};
|
||||
|
||||
const _rawColumns: ColumnConfig[] = useMemo(
|
||||
() => [
|
||||
{ key: 'roleLabel', sortable: true, filterable: true, searchable: true, width: 150 },
|
||||
{
|
||||
key: 'description',
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
width: 250,
|
||||
formatter: (value: string) => getDescriptionText(value),
|
||||
},
|
||||
{ key: 'scopeType', sortable: true, filterable: true, width: 160 },
|
||||
{ key: 'userCount', sortable: true, filterable: true, width: 100 },
|
||||
],
|
||||
[currentLanguage],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => resolveColumnTypes(_rawColumns, backendAttributes),
|
||||
[_rawColumns, backendAttributes],
|
||||
);
|
||||
|
||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType'];
|
||||
return backendAttributes
|
||||
.filter(attr => !excludedFields.includes(attr.name))
|
||||
.map(attr => ({ ...attr })) as AttributeDefinition[];
|
||||
}, [backendAttributes]);
|
||||
|
||||
const editFields: AttributeDefinition[] = useMemo(() => {
|
||||
const excludedFields = ['id', 'mandateId', 'featureInstanceId', 'featureCode', 'isSystemRole', 'scopeType'];
|
||||
return backendAttributes
|
||||
.filter(attr => !excludedFields.includes(attr.name))
|
||||
.map(attr => ({
|
||||
...attr,
|
||||
readonly: attr.name === 'roleLabel' ? true : attr.readonly,
|
||||
})) as AttributeDefinition[];
|
||||
}, [backendAttributes]);
|
||||
|
||||
const handleCreateTemplate = async (data: { roleLabel: string; description?: Record<string, string> }) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const roleData: RoleCreate = {
|
||||
roleLabel: data.roleLabel.toLowerCase().replace(/\s+/g, '_'),
|
||||
description: data.description,
|
||||
mandateId: undefined,
|
||||
};
|
||||
|
||||
const result = await createRole(roleData);
|
||||
if (result.success) {
|
||||
setShowCreateModal(false);
|
||||
await fetchUserRoleTemplates();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen des Rollen-Templates'));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('Fehler beim Erstellen des Rollen-Templates');
|
||||
showError(t('Fehler'), message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTemplate = async (data: RoleUpdate) => {
|
||||
if (!editingRole) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const updateData: RoleUpdate = {
|
||||
roleLabel: data.roleLabel,
|
||||
description: data.description,
|
||||
};
|
||||
|
||||
const result = await updateRole(editingRole.id, updateData);
|
||||
if (result.success) {
|
||||
setEditingRole(null);
|
||||
await fetchUserRoleTemplates();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren des Rollen-Templates'));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('Fehler beim Aktualisieren des Rollen-Templates');
|
||||
showError(t('Fehler'), message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteTemplate = async (roleId: string): Promise<boolean> => {
|
||||
const role = roles.find(r => r.id === roleId);
|
||||
if (role?.isSystemRole) {
|
||||
showWarning(t('Nicht erlaubt'), t('System-Rollen können nicht gelöscht werden.'));
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await deleteRole(roleId);
|
||||
if (result.success) {
|
||||
await fetchUserRoleTemplates();
|
||||
return true;
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Löschen des Rollen-Templates'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (role: Role) => {
|
||||
setEditingRole(role);
|
||||
};
|
||||
|
||||
if (error) {
|
||||
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 type="button" className={styles.secondaryButton} onClick={() => fetchUserRoleTemplates()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>{t('User Role Templates')}</h1>
|
||||
<p className={styles.pageSubtitle}>
|
||||
{t('Verwalten Sie System- und globale Rollen-Vorlagen. Diese werden bei neuen Mandanten kopiert.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => fetchUserRoleTemplates()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
>
|
||||
<FaPlus /> {t('Neues Rollen-Template')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.infoBox}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
<strong>{t('System-Templates')}</strong>{' '}
|
||||
{t(
|
||||
'(admin, user, viewer) werden bei der Mandant-Erstellung automatisch als Mandanten-Instanz-Rollen kopiert und können hier nicht gelöscht werden.',
|
||||
)}{' '}
|
||||
<strong>{t('Globale Templates')}</strong>{' '}
|
||||
{t('gelten für alle neuen Mandanten. Mandanten-spezifische Rollen verwalten Sie unter Mandanten.')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.tableContainer}>
|
||||
<FormGeneratorTable
|
||||
data={roles}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/rbac/roles"
|
||||
loading={loading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={true}
|
||||
actionButtons={[
|
||||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: t('Rollen-Template bearbeiten'),
|
||||
disabled: (row: Role) =>
|
||||
row.isSystemRole
|
||||
? { disabled: true, message: t('System-Rollen können nicht bearbeitet werden') }
|
||||
: false,
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: t('Rollen-Template löschen'),
|
||||
disabled: (row: Role) =>
|
||||
row.isSystemRole
|
||||
? { disabled: true, message: t('System-Rollen können nicht gelöscht werden') }
|
||||
: false,
|
||||
},
|
||||
]}
|
||||
onDelete={handleDeleteTemplate}
|
||||
hookData={{
|
||||
refetch: refetchWithParams,
|
||||
pagination: pagination,
|
||||
handleDelete: handleDeleteTemplate,
|
||||
}}
|
||||
emptyMessage={t('Keine Rollen-Templates gefunden')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showCreateModal && (
|
||||
<div className={styles.modalOverlay}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>{t('Neues Rollen-Template erstellen')}</h2>
|
||||
<button type="button" className={styles.modalClose} onClick={() => setShowCreateModal(false)}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{createFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Formular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={createFields}
|
||||
mode="create"
|
||||
onSubmit={handleCreateTemplate}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rollen-Template erstellen')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editingRole && (
|
||||
<div className={styles.modalOverlay}>
|
||||
<div className={styles.modal}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>
|
||||
{t('Rollen-Template bearbeiten')}: {editingRole.roleLabel}
|
||||
</h2>
|
||||
<button type="button" className={styles.modalClose} onClick={() => setEditingRole(null)}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{editFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Formular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||
<FaUserShield style={{ marginRight: 8 }} />
|
||||
<span>
|
||||
{t('Geltungsbereich')}:{' '}
|
||||
<strong>
|
||||
{editingRole.isSystemRole ? t('System-Template') : t('Template (global)')}
|
||||
</strong>{' '}
|
||||
({t('kann nicht geändert werden')})
|
||||
</span>
|
||||
</div>
|
||||
<FormGeneratorForm
|
||||
attributes={editFields}
|
||||
data={editingRole}
|
||||
mode="edit"
|
||||
onSubmit={handleEditTemplate}
|
||||
onCancel={() => setEditingRole(null)}
|
||||
submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')}
|
||||
cancelButtonText={t('Abbrechen')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminUserRoleTemplatesPage;
|
||||
|
|
@ -10,7 +10,7 @@ export { AdminUsersPage } from './AdminUsersPage';
|
|||
export { AdminUserMandatesPage } from './AdminUserMandatesPage';
|
||||
export { AdminFeatureAccessPage } from './AdminFeatureAccessPage';
|
||||
export { AdminInvitationsPage } from './AdminInvitationsPage';
|
||||
export { AdminMandateRolesPage } from './AdminMandateRolesPage';
|
||||
export { AdminUserRoleTemplatesPage } from './AdminUserRoleTemplatesPage';
|
||||
export { AdminFeatureRolesPage } from './AdminFeatureRolesPage';
|
||||
export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
|
||||
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
|
||||
|
|
|
|||
Loading…
Reference in a new issue