moved tenant role permission functionality to tenant page, removed tenant role permission page
This commit is contained in:
parent
059bbe956a
commit
dfa36c17ef
17 changed files with 863 additions and 618 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, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminLogsPage, AdminDemoConfigPage } from './pages/admin';
|
||||
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, 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';
|
||||
|
|
@ -213,7 +213,6 @@ function App() {
|
|||
<Route path="feature-users" element={<AdminFeatureInstanceUsersPage />} />
|
||||
<Route path="invitations" element={<AdminInvitationsPage />} />
|
||||
<Route path="mandate-roles" element={<AdminMandateRolesPage />} />
|
||||
<Route path="mandate-role-permissions" element={<AdminMandateRolePermissionsPage />} />
|
||||
<Route path="user-access-overview" element={<AdminUserAccessOverviewPage />} />
|
||||
<Route path="billing">
|
||||
<Route index element={<Navigate to="/billing/admin" replace />} />
|
||||
|
|
|
|||
|
|
@ -175,6 +175,16 @@
|
|||
box-shadow: 0 3px 8px rgba(74, 111, 165, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.actionButton.roles {
|
||||
background: linear-gradient(180deg, #6b9a7a 0%, #38a169 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.actionButton.roles:hover {
|
||||
background: linear-gradient(180deg, #38a169 0%, #2f855a 100%);
|
||||
box-shadow: 0 3px 8px rgba(56, 161, 105, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.actionButton.copy {
|
||||
background: linear-gradient(180deg, #8494a7 0%, #6b7b8d 100%);
|
||||
color: white;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
import React, { useState } from 'react';
|
||||
import { FaShieldAlt } from 'react-icons/fa';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
import { Popup } from '../../../UiComponents/Popup';
|
||||
import { MandateRolesPermissionsPanel } from '../../../admin/MandateRolesPermissionsPanel';
|
||||
import { mandateDisplayLabel } from '../../../../utils/mandateDisplayUtils';
|
||||
import styles from '../ActionButton.module.css';
|
||||
|
||||
export interface RolesActionButtonProps<T = Record<string, unknown>> {
|
||||
row: T;
|
||||
disabled?: boolean | { disabled: boolean; message?: string };
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
title?: string;
|
||||
idField?: string;
|
||||
labelField?: string;
|
||||
nameField?: string;
|
||||
}
|
||||
|
||||
export function RolesActionButton<T = Record<string, unknown>>({
|
||||
row,
|
||||
disabled = false,
|
||||
loading = false,
|
||||
className = '',
|
||||
title,
|
||||
idField = 'id',
|
||||
labelField = 'label',
|
||||
nameField = 'name',
|
||||
}: RolesActionButtonProps<T>) {
|
||||
const { t } = useLanguage();
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
|
||||
const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false;
|
||||
const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined;
|
||||
|
||||
const rowRecord = row as Record<string, unknown>;
|
||||
const mandateId = String(rowRecord[idField] ?? '');
|
||||
const mandateLabel = mandateDisplayLabel({
|
||||
label: rowRecord[labelField] as string | undefined,
|
||||
name: rowRecord[nameField] as string | undefined,
|
||||
id: mandateId,
|
||||
});
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!isDisabled && !loading && mandateId) {
|
||||
setIsPopupOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonTitle = title || t('Rollen & Berechtigungen');
|
||||
const finalTitle = isDisabled && disabledMessage ? disabledMessage : buttonTitle;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
className={`${styles.actionButton} ${styles.roles} ${loading ? styles.loading : ''} ${isDisabled ? styles.disabled : ''} ${className}`}
|
||||
title={finalTitle}
|
||||
disabled={isDisabled || loading || !mandateId}
|
||||
>
|
||||
<span className={styles.actionIcon}>
|
||||
{loading ? '⏳' : <FaShieldAlt />}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Popup
|
||||
isOpen={isPopupOpen}
|
||||
title={`${t('Rollen-Berechtigungen')}: ${mandateLabel}`}
|
||||
onClose={() => setIsPopupOpen(false)}
|
||||
size="xlarge"
|
||||
closable={true}
|
||||
closeOnBackdropClick={false}
|
||||
>
|
||||
{isPopupOpen && mandateId && (
|
||||
<MandateRolesPermissionsPanel
|
||||
mandateId={mandateId}
|
||||
mandateLabel={mandateLabel}
|
||||
showRoleActions={true}
|
||||
showCreateRole={true}
|
||||
scopeFilter="mandate"
|
||||
showInfoBox={true}
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RolesActionButton;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { RolesActionButton } from './RolesActionButton';
|
||||
export type { RolesActionButtonProps } from './RolesActionButton';
|
||||
|
|
@ -5,6 +5,7 @@ export { ViewActionButton } from './ViewActionButton';
|
|||
export { CopyActionButton } from './CopyActionButton';
|
||||
export { RemoveActionButton } from './RemoveActionButton';
|
||||
export { DownloadActionButton } from './DownloadActionButton';
|
||||
export { RolesActionButton } from './RolesActionButton';
|
||||
|
||||
// Generic Custom Action Button (for entity-specific actions)
|
||||
export { CustomActionButton } from './CustomActionButton';
|
||||
|
|
@ -16,4 +17,5 @@ export type { ViewActionButtonProps } from './ViewActionButton';
|
|||
export type { CopyActionButtonProps } from './CopyActionButton';
|
||||
export type { RemoveActionButtonProps } from './RemoveActionButton';
|
||||
export type { DownloadActionButtonProps } from './DownloadActionButton';
|
||||
export type { RolesActionButtonProps } from './RolesActionButton';
|
||||
export type { CustomActionButtonProps } from './CustomActionButton';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
DeleteActionButton,
|
||||
ViewActionButton,
|
||||
CopyActionButton,
|
||||
RolesActionButton,
|
||||
CustomActionButton
|
||||
} from '../ActionButtons';
|
||||
import { FaDownload, FaLink, FaPlay } from 'react-icons/fa';
|
||||
|
|
@ -52,7 +53,7 @@ export interface FormGeneratorListProps<T = any> {
|
|||
loading?: boolean;
|
||||
emptyMessage?: string;
|
||||
actionButtons?: {
|
||||
type: 'edit' | 'delete' | 'download' | 'view' | 'copy' | 'connect' | 'play';
|
||||
type: 'edit' | 'delete' | 'download' | 'view' | 'copy' | 'roles' | 'connect' | 'play';
|
||||
onAction?: (row: T) => Promise<void> | void;
|
||||
disabled?: (row: T, hookData?: any) => boolean | { disabled: boolean; message?: string };
|
||||
loading?: (row: T, hookData?: any) => boolean;
|
||||
|
|
@ -858,6 +859,18 @@ export function FormGeneratorList<T extends Record<string, any>>({
|
|||
return <ViewActionButton key={actionIndex} {...baseProps} onView={actionButton.onAction || (() => {})} isViewing={isProcessing} hookData={hookData} />;
|
||||
case 'copy':
|
||||
return <CopyActionButton key={actionIndex} {...baseProps} onCopy={actionButton.onAction} isCopying={isProcessing} contentField={actionButton.contentField} />;
|
||||
case 'roles':
|
||||
return (
|
||||
<RolesActionButton
|
||||
key={actionIndex}
|
||||
row={row}
|
||||
disabled={disabledResult}
|
||||
loading={isLoading}
|
||||
className={actionButton.className}
|
||||
title={actionTitle}
|
||||
idField={actionButton.idField ?? 'id'}
|
||||
/>
|
||||
);
|
||||
case 'connect':
|
||||
return <CustomActionButton
|
||||
key={actionIndex}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import {
|
|||
DeleteActionButton,
|
||||
ViewActionButton,
|
||||
CopyActionButton,
|
||||
RolesActionButton,
|
||||
CustomActionButton
|
||||
} from '../ActionButtons';
|
||||
import { formatUnixTimestamp } from '../../../utils/time';
|
||||
|
|
@ -264,7 +265,7 @@ export interface FormGeneratorTableProps<T = any> {
|
|||
idField?: string; // Field name for unique row identifier (default: 'id')
|
||||
// Standard action buttons (edit, delete, view, copy, connect, play)
|
||||
actionButtons?: {
|
||||
type: 'edit' | 'delete' | 'view' | 'copy' | 'connect' | 'play';
|
||||
type: 'edit' | 'delete' | 'view' | 'copy' | 'roles' | 'connect' | 'play';
|
||||
onAction?: (row: T) => Promise<void> | void;
|
||||
visible?: (row: T, hookData?: any) => boolean;
|
||||
disabled?: (row: T, hookData?: any) => boolean | { disabled: boolean; message?: string };
|
||||
|
|
@ -2692,6 +2693,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
case 'delete': return <DeleteActionButton key={`a-${ai}`} {...bp} containerRef={{ current: actionButtonsRefs.current.get(index) || null }} hookData={hookData} />;
|
||||
case 'view': return <ViewActionButton key={`a-${ai}`} {...bp} onView={ab.onAction || (() => {})} isViewing={isProc} hookData={hookData} />;
|
||||
case 'copy': return <CopyActionButton key={`a-${ai}`} {...bp} onCopy={ab.onAction} isCopying={isProc} contentField={ab.contentField} />;
|
||||
case 'roles': return <RolesActionButton key={`a-${ai}`} row={row} disabled={dis} loading={isLd} className={bp.className} title={abTitle} idField={ab.idField ?? 'id'} />;
|
||||
default: return null;
|
||||
}
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@
|
|||
min-width: 600px;
|
||||
}
|
||||
|
||||
.xlarge {
|
||||
width: min(1400px, 96vw);
|
||||
max-width: 96vw;
|
||||
min-width: min(1100px, 96vw);
|
||||
}
|
||||
|
||||
.fullscreen {
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
|
|
@ -186,7 +192,9 @@
|
|||
.content {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Footer section */
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export interface PopupProps {
|
|||
children: React.ReactNode;
|
||||
footerContent?: React.ReactNode;
|
||||
className?: string;
|
||||
size?: 'small' | 'medium' | 'large' | 'fullscreen';
|
||||
size?: 'small' | 'medium' | 'large' | 'xlarge' | 'fullscreen';
|
||||
closable?: boolean;
|
||||
closeOnBackdropClick?: boolean;
|
||||
closeOnEscape?: boolean;
|
||||
|
|
|
|||
214
src/components/admin/MandateRolesPermissionsPanel.module.css
Normal file
214
src/components/admin/MandateRolesPermissionsPanel.module.css
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
/* Table-style role list: one row per role card header */
|
||||
.rolesTable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rolesTableHead,
|
||||
.roleHeaderTable {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rolesTableHead {
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-secondary, #666);
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
background: var(--bg-tertiary, #f8f9fa);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.roleHeaderTable {
|
||||
grid-template-columns:
|
||||
24px
|
||||
minmax(100px, 0.9fr)
|
||||
minmax(140px, 2fr)
|
||||
minmax(120px, 0.75fr)
|
||||
minmax(64px, 0.35fr);
|
||||
}
|
||||
|
||||
.roleHeaderTableWithActions {
|
||||
grid-template-columns:
|
||||
24px
|
||||
minmax(100px, 0.9fr)
|
||||
minmax(140px, 2fr)
|
||||
minmax(120px, 0.75fr)
|
||||
minmax(64px, 0.35fr)
|
||||
72px;
|
||||
}
|
||||
|
||||
.rolesTableHeadWithActions {
|
||||
grid-template-columns:
|
||||
24px
|
||||
minmax(100px, 0.9fr)
|
||||
minmax(140px, 2fr)
|
||||
minmax(120px, 0.75fr)
|
||||
minmax(64px, 0.35fr)
|
||||
72px;
|
||||
}
|
||||
|
||||
.roleHeaderTableRow {
|
||||
padding: 0.75rem 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.roleHeaderTableRow:hover {
|
||||
background: var(--bg-tertiary, #f8f9fa);
|
||||
}
|
||||
|
||||
.cellExpand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cellLabel {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cellDescription {
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.875rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cellScope,
|
||||
.cellUsers {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary, #666);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cellScope strong,
|
||||
.cellUsers strong {
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.roleHeaderActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.roleActionBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 5px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.roleActionBtn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.roleActionBtnEdit {
|
||||
background: linear-gradient(180deg, #5a82b5 0%, var(--color-secondary, #4A6FA5) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roleActionBtnDelete {
|
||||
background: linear-gradient(180deg, #8494a7 0%, #6b7b8d 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.roleActionBtnDelete:hover:not(:disabled) {
|
||||
background: linear-gradient(180deg, #d44040 0%, var(--color-red, #C53030) 100%);
|
||||
}
|
||||
|
||||
.panelRoot {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.panelHeaderRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panelInfoBox {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 0;
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.panelCreateButton {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.panelInfoBoxIcon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.15em;
|
||||
}
|
||||
|
||||
.panelInfoBoxText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.roleContentInfoBox {
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.roleContentInfoBoxText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.roleCardTable {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.roleCardTable .roleHeaderTableRow {
|
||||
border: none;
|
||||
}
|
||||
510
src/components/admin/MandateRolesPermissionsPanel.tsx
Normal file
510
src/components/admin/MandateRolesPermissionsPanel.tsx
Normal file
|
|
@ -0,0 +1,510 @@
|
|||
/**
|
||||
* MandateRolesPermissionsPanel
|
||||
*
|
||||
* Expandable role list with AccessRulesEditor per role.
|
||||
* Used in RolesActionButton popup on Admin Mandates.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import {
|
||||
useMandateRoles,
|
||||
type Role,
|
||||
type RoleCreate,
|
||||
type RoleUpdate,
|
||||
} from '../../hooks/useMandateRoles';
|
||||
import { useApiRequest } from '../../hooks/useApi';
|
||||
import { fetchAttributes } from '../../api/attributesApi';
|
||||
import { AccessRulesEditor } from '../AccessRules';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../FormGenerator/FormGeneratorForm';
|
||||
import { Popup } from '../UiComponents/Popup';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import {
|
||||
FaUserShield,
|
||||
FaSync,
|
||||
FaChevronDown,
|
||||
FaChevronRight,
|
||||
FaPlus,
|
||||
} from 'react-icons/fa';
|
||||
import { MdModeEdit } from 'react-icons/md';
|
||||
import { IoIosTrash } from 'react-icons/io';
|
||||
import adminStyles from '../../pages/admin/Admin.module.css';
|
||||
import panelStyles from './MandateRolesPermissionsPanel.module.css';
|
||||
|
||||
export interface MandateRolesPermissionsPanelProps {
|
||||
mandateId: string;
|
||||
mandateLabel?: string;
|
||||
/** When false, hide per-role edit/delete (e.g. standalone permissions page). Default true. */
|
||||
showRoleActions?: boolean;
|
||||
/** Backend scope filter. Default mandate-only for mandates popup. */
|
||||
scopeFilter?: 'all' | 'mandate' | 'global';
|
||||
/** Show info box above role list. Default true when showRoleActions, else from parent. */
|
||||
showInfoBox?: boolean;
|
||||
/** Increment to trigger refetch (e.g. parent refresh button). */
|
||||
refreshToken?: number;
|
||||
/** Show «Neue Rolle» button and create flow. Default: same as showRoleActions. */
|
||||
showCreateRole?: boolean;
|
||||
}
|
||||
|
||||
const SCOPE_FILTER_DEFAULT: MandateRolesPermissionsPanelProps['scopeFilter'] = 'mandate';
|
||||
|
||||
function getTextValue(value: string | Record<string, string> | undefined): string {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return Object.values(value).find(v => !!v) || '';
|
||||
}
|
||||
|
||||
function isTemplateRole(role: Role): boolean {
|
||||
return !!role.isSystemRole || !role.mandateId;
|
||||
}
|
||||
|
||||
export const MandateRolesPermissionsPanel: React.FC<MandateRolesPermissionsPanelProps> = ({
|
||||
mandateId,
|
||||
showRoleActions = true,
|
||||
scopeFilter = SCOPE_FILTER_DEFAULT,
|
||||
showInfoBox,
|
||||
refreshToken = 0,
|
||||
showCreateRole,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const { showError, showWarning } = useToast();
|
||||
const {
|
||||
roles,
|
||||
loading,
|
||||
error,
|
||||
fetchRoles,
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
} = useMandateRoles();
|
||||
|
||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||
const [editingRole, setEditingRole] = useState<Role | null>(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
const [deletingRoleId, setDeletingRoleId] = useState<string | null>(null);
|
||||
|
||||
const shouldShowInfoBox = showInfoBox ?? showRoleActions;
|
||||
const shouldShowCreateRole = showCreateRole ?? showRoleActions;
|
||||
const needsRoleFormAttributes = showRoleActions || shouldShowCreateRole;
|
||||
|
||||
const loadRoles = useCallback(async () => {
|
||||
if (!mandateId) return;
|
||||
await fetchRoles(mandateId, { scopeFilter });
|
||||
}, [mandateId, scopeFilter, fetchRoles]);
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles();
|
||||
}, [loadRoles, refreshToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (needsRoleFormAttributes) {
|
||||
fetchAttributes(request, 'RoleView')
|
||||
.then(setBackendAttributes)
|
||||
.catch(() => setBackendAttributes([]));
|
||||
}
|
||||
}, [request, needsRoleFormAttributes]);
|
||||
|
||||
const scopeTypeLabel = useCallback(
|
||||
(scopeType?: Role['scopeType']) => {
|
||||
switch (scopeType) {
|
||||
case 'system':
|
||||
return t('System-Template');
|
||||
case 'global':
|
||||
return t('Template');
|
||||
case 'mandate':
|
||||
return t('Mandant');
|
||||
default:
|
||||
return '—';
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
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[];
|
||||
|
||||
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]);
|
||||
|
||||
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 handleCreateRole = async (data: {
|
||||
roleLabel: string;
|
||||
description?: Record<string, string>;
|
||||
scope: 'mandate' | 'global';
|
||||
}) => {
|
||||
if (!mandateId) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const roleData: RoleCreate = {
|
||||
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);
|
||||
await loadRoles();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Erstellen der Rolle'));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('Fehler beim Erstellen der Rolle');
|
||||
showError(t('Fehler'), message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleRole = (roleId: string) => {
|
||||
setExpandedRoleId(prev => (prev === roleId ? null : roleId));
|
||||
};
|
||||
|
||||
const handleEditRole = 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 loadRoles();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rolle'));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('Fehler beim Aktualisieren der Rolle');
|
||||
showError(t('Fehler'), message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteRole = async (role: Role) => {
|
||||
if (role.isSystemRole) {
|
||||
showWarning(t('Nicht erlaubt'), t('System-Rollen können nicht gelöscht werden.'));
|
||||
return;
|
||||
}
|
||||
if (!window.confirm(t('Rolle «{label}» wirklich löschen?', { label: role.roleLabel }))) {
|
||||
return;
|
||||
}
|
||||
setDeletingRoleId(role.id);
|
||||
try {
|
||||
const result = await deleteRole(role.id);
|
||||
if (result.success) {
|
||||
if (expandedRoleId === role.id) {
|
||||
setExpandedRoleId(null);
|
||||
}
|
||||
await loadRoles();
|
||||
} else {
|
||||
showError(t('Fehler'), result.error || t('Fehler beim Löschen der Rolle'));
|
||||
}
|
||||
} finally {
|
||||
setDeletingRoleId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={adminStyles.errorContainer}>
|
||||
<span className={adminStyles.errorIcon}>⚠️</span>
|
||||
<p className={adminStyles.errorMessage}>
|
||||
{t('Fehler beim Laden')}: {error}
|
||||
</p>
|
||||
<button type="button" className={adminStyles.secondaryButton} onClick={() => loadRoles()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={panelStyles.panelRoot}>
|
||||
{(shouldShowInfoBox || shouldShowCreateRole) && (
|
||||
<div className={panelStyles.panelHeaderRow}>
|
||||
{shouldShowInfoBox && (
|
||||
<div className={`${adminStyles.infoBox} ${panelStyles.panelInfoBox}`}>
|
||||
<FaUserShield
|
||||
className={panelStyles.panelInfoBoxIcon}
|
||||
style={{ marginRight: '0.5rem', color: 'var(--text-secondary, #666)' }}
|
||||
/>
|
||||
<span className={panelStyles.panelInfoBoxText}>
|
||||
{t('Klicken Sie auf eine Rolle, um deren Berechtigungen (AccessRules) zu bearbeiten.')}{' '}
|
||||
<strong>{t('Template-Rollen')}</strong>{' '}
|
||||
{t('sind schreibgeschützt – Änderungen an Templates wirken sich nur auf neu erstellte Mandanten aus.')}{' '}
|
||||
<strong>{t('Mandanten-Rollen')}</strong> {t('sind direkt bearbeitbar.')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{shouldShowCreateRole && (
|
||||
<button
|
||||
type="button"
|
||||
className={`${adminStyles.primaryButton} ${panelStyles.panelCreateButton}`}
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaPlus /> {t('Neue Rolle')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className={adminStyles.loadingContainer}>
|
||||
<div className={adminStyles.spinner} />
|
||||
<span>{t('Lade Rollen')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && roles.length === 0 && (
|
||||
<div className={adminStyles.emptyState}>
|
||||
<FaUserShield className={adminStyles.emptyIcon} />
|
||||
<p>{t('Keine Rollen gefunden')}</p>
|
||||
<p className={adminStyles.emptyHint}>
|
||||
{scopeFilter === 'mandate'
|
||||
? t('Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.')
|
||||
: scopeFilter === 'global'
|
||||
? t('Es gibt noch keine Rollentemplates')
|
||||
: t('Es gibt noch keine Rollen')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && roles.length > 0 && (
|
||||
<div className={panelStyles.rolesTable}>
|
||||
<div
|
||||
className={`${panelStyles.rolesTableHead} ${
|
||||
showRoleActions ? panelStyles.rolesTableHeadWithActions : ''
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span />
|
||||
<span>{t('Bezeichnung')}</span>
|
||||
<span>{t('Beschreibung')}</span>
|
||||
<span>{t('Geltungsbereich')}</span>
|
||||
<span>{t('Benutzer')}</span>
|
||||
{showRoleActions && <span>{t('Aktionen')}</span>}
|
||||
</div>
|
||||
|
||||
{roles.map(role => {
|
||||
const systemLocked = !!role.isSystemRole;
|
||||
const isExpanded = expandedRoleId === role.id;
|
||||
const headerGridClass = showRoleActions
|
||||
? panelStyles.roleHeaderTableWithActions
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={role.id}
|
||||
className={`${adminStyles.roleCard} ${panelStyles.roleCardTable}`}
|
||||
>
|
||||
<div
|
||||
className={`${panelStyles.roleHeaderTable} ${headerGridClass} ${panelStyles.roleHeaderTableRow}`}
|
||||
onClick={() => toggleRole(role.id)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleRole(role.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className={`${adminStyles.expandIcon} ${panelStyles.cellExpand}`}>
|
||||
{isExpanded ? <FaChevronDown /> : <FaChevronRight />}
|
||||
</span>
|
||||
<span className={panelStyles.cellLabel} title={role.roleLabel}>
|
||||
{role.roleLabel}
|
||||
</span>
|
||||
<span
|
||||
className={panelStyles.cellDescription}
|
||||
title={getTextValue(role.description)}
|
||||
>
|
||||
{getTextValue(role.description) || '—'}
|
||||
</span>
|
||||
<span className={panelStyles.cellScope}>
|
||||
<strong>{scopeTypeLabel(role.scopeType)}</strong>
|
||||
</span>
|
||||
<span className={panelStyles.cellUsers}>
|
||||
<strong>{role.userCount ?? 0}</strong>
|
||||
</span>
|
||||
|
||||
{showRoleActions && (
|
||||
<div className={panelStyles.roleHeaderActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${panelStyles.roleActionBtn} ${panelStyles.roleActionBtnEdit}`}
|
||||
title={
|
||||
systemLocked
|
||||
? t('System-Rollen können nicht bearbeitet werden')
|
||||
: t('Rolle bearbeiten')
|
||||
}
|
||||
disabled={systemLocked}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
if (!systemLocked) setEditingRole(role);
|
||||
}}
|
||||
>
|
||||
<MdModeEdit />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${panelStyles.roleActionBtn} ${panelStyles.roleActionBtnDelete}`}
|
||||
title={
|
||||
systemLocked
|
||||
? t('System-Rollen können nicht gelöscht werden')
|
||||
: t('Rolle löschen')
|
||||
}
|
||||
disabled={systemLocked || deletingRoleId === role.id}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
void handleDeleteRole(role);
|
||||
}}
|
||||
>
|
||||
{deletingRoleId === role.id ? '⏳' : <IoIosTrash />}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className={adminStyles.roleContent}>
|
||||
{isTemplateRole(role) && (
|
||||
<div
|
||||
className={`${adminStyles.infoBox} ${panelStyles.roleContentInfoBox}`}
|
||||
style={{
|
||||
marginBottom: '0.75rem',
|
||||
background: 'var(--warning-bg, #fffbeb)',
|
||||
borderColor: 'var(--warning-color, #d69e2e)',
|
||||
}}
|
||||
>
|
||||
<FaUserShield
|
||||
className={panelStyles.panelInfoBoxIcon}
|
||||
style={{ marginRight: '0.5rem', color: 'var(--warning-color, #d69e2e)' }}
|
||||
/>
|
||||
<span className={panelStyles.roleContentInfoBoxText}>
|
||||
{t('Dies ist eine')} <strong>{t('Template-Rolle')}</strong>.{' '}
|
||||
{t(
|
||||
'Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.',
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<AccessRulesEditor
|
||||
roleId={role.id}
|
||||
roleName={role.roleLabel}
|
||||
isTemplate={isTemplateRole(role)}
|
||||
readOnly={false}
|
||||
apiBasePath="/api/rbac"
|
||||
mandateId={mandateId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCreateModal && (
|
||||
<Popup
|
||||
isOpen={true}
|
||||
title={t('Neue Rolle erstellen')}
|
||||
onClose={() => !isSubmitting && setShowCreateModal(false)}
|
||||
size="medium"
|
||||
closable={!isSubmitting}
|
||||
>
|
||||
{createFields.length === 0 ? (
|
||||
<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')}
|
||||
/>
|
||||
)}
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
{editingRole && (
|
||||
<Popup
|
||||
isOpen={true}
|
||||
title={`${t('Rolle bearbeiten')}: ${editingRole.roleLabel}`}
|
||||
onClose={() => !isSubmitting && setEditingRole(null)}
|
||||
size="medium"
|
||||
closable={!isSubmitting}
|
||||
>
|
||||
{editFields.length === 0 ? (
|
||||
<div className={adminStyles.loadingContainer}>
|
||||
<div className={adminStyles.spinner} />
|
||||
<span>{t('Lade Formular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={`${adminStyles.infoBox} ${panelStyles.panelInfoBox}`}
|
||||
style={{ marginBottom: '1rem' }}
|
||||
>
|
||||
<FaUserShield className={panelStyles.panelInfoBoxIcon} style={{ marginRight: 8 }} />
|
||||
<span className={panelStyles.panelInfoBoxText}>
|
||||
{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')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MandateRolesPermissionsPanel;
|
||||
|
|
@ -66,7 +66,6 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'page.admin.mandates': <FaBuilding />,
|
||||
'page.admin.roles': <FaKey />,
|
||||
'page.admin.role-permissions': <FaShieldAlt />,
|
||||
'page.admin.mandateRolePermissions': <FaShieldAlt />,
|
||||
'page.admin.user-mandates': <FaUserTag />,
|
||||
'page.admin.userMandates': <FaUserTag />,
|
||||
'page.admin.feature-roles': <FaCube />,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export interface Role {
|
|||
featureCode?: string;
|
||||
isSystemRole?: boolean;
|
||||
isTemplate?: boolean;
|
||||
scopeType?: 'system' | 'global' | 'mandate';
|
||||
userCount?: number;
|
||||
createdAt?: number;
|
||||
updatedAt?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,603 +0,0 @@
|
|||
/**
|
||||
* AdminMandateRolePermissionsPage
|
||||
*
|
||||
* Admin page for managing access rules (permissions) for mandate-level roles.
|
||||
* Similar to TrusteeInstanceRolesView but for mandate/global roles.
|
||||
*
|
||||
* Shows:
|
||||
* - System roles (admin, user, viewer) - read-only permissions
|
||||
* - Global roles (mandateId=null) - editable permissions
|
||||
* - Mandate-specific roles (mandateId=xyz) - editable permissions
|
||||
*
|
||||
* Each role can be expanded to show/edit its AccessRules via AccessRulesEditor.
|
||||
*
|
||||
* Includes a "Cleanup Duplicates" tool to find and remove duplicate AccessRules.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useMandateRoles, type Role } from '../../hooks/useMandateRoles';
|
||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||
import { AccessRulesEditor } from '../../components/AccessRules';
|
||||
import api from '../../api';
|
||||
import {
|
||||
FaUserShield,
|
||||
FaShieldAlt,
|
||||
FaSync,
|
||||
FaChevronDown,
|
||||
FaChevronRight,
|
||||
FaGlobe,
|
||||
FaBuilding,
|
||||
FaFilter,
|
||||
FaBroom,
|
||||
FaTimes,
|
||||
FaExclamationTriangle,
|
||||
FaCheckCircle
|
||||
} from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils';
|
||||
|
||||
// Types for cleanup result
|
||||
interface DuplicateGroup {
|
||||
roleId: string;
|
||||
context: string;
|
||||
item: string;
|
||||
totalCount: number;
|
||||
keepId: string;
|
||||
deleteCount: number;
|
||||
deleteIds: string[];
|
||||
}
|
||||
|
||||
interface CleanupResult {
|
||||
totalRules: number;
|
||||
uniqueSignatures: number;
|
||||
duplicateGroups: number;
|
||||
duplicateRulesToDelete: number;
|
||||
deletedCount: number;
|
||||
details: DuplicateGroup[];
|
||||
}
|
||||
|
||||
interface TemplateFixDetail {
|
||||
userMandateRoleId: string;
|
||||
userMandateId: string;
|
||||
mandateId: string;
|
||||
templateRoleId: string;
|
||||
templateRoleLabel: string;
|
||||
instanceRoleId?: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface TemplateFixResult {
|
||||
totalUserMandateRoles: number;
|
||||
invalidAssignments: number;
|
||||
fixedCount: number;
|
||||
details: TemplateFixDetail[];
|
||||
}
|
||||
|
||||
export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const {
|
||||
roles,
|
||||
loading,
|
||||
error,
|
||||
fetchRoles,
|
||||
} = useMandateRoles();
|
||||
|
||||
const { fetchMandates } = useUserMandates();
|
||||
|
||||
// State
|
||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||||
const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('mandate');
|
||||
const [expandedRoleId, setExpandedRoleId] = useState<string | null>(null);
|
||||
|
||||
// Cleanup state
|
||||
const [showCleanupModal, setShowCleanupModal] = useState(false);
|
||||
const [cleanupLoading, setCleanupLoading] = useState(false);
|
||||
const [cleanupResult, setCleanupResult] = useState<CleanupResult | null>(null);
|
||||
const [templateFixResult, setTemplateFixResult] = useState<TemplateFixResult | null>(null);
|
||||
const [cleanupError, setCleanupError] = useState<string | null>(null);
|
||||
const [cleanupPhase, setCleanupPhase] = useState<'idle' | 'preview' | 'done'>('idle');
|
||||
|
||||
// Load mandates on mount
|
||||
useEffect(() => {
|
||||
const loadMandates = async () => {
|
||||
const data = await fetchMandates();
|
||||
setMandates(data);
|
||||
if (data.length > 0 && !selectedMandateId) {
|
||||
setSelectedMandateId(data[0].id);
|
||||
}
|
||||
};
|
||||
loadMandates();
|
||||
}, [fetchMandates]);
|
||||
|
||||
// Load roles when mandate or scopeFilter changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
||||
|
||||
// Refetch roles
|
||||
const handleRefresh = useCallback(async () => {
|
||||
if (selectedMandateId) {
|
||||
await fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, fetchRoles]);
|
||||
|
||||
const getTextValue = (value: string | Record<string, string> | undefined): string => {
|
||||
if (!value) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
return Object.values(value).find(v => !!v) || '';
|
||||
};
|
||||
|
||||
// Toggle role expansion
|
||||
const toggleRole = (roleId: string) => {
|
||||
setExpandedRoleId(prev => prev === roleId ? null : roleId);
|
||||
};
|
||||
|
||||
// Check if a role is a template (not bound to a specific mandate)
|
||||
const _isTemplateRole = (role: Role): boolean => {
|
||||
return !!role.isSystemRole || !role.mandateId;
|
||||
};
|
||||
|
||||
// Get scope badge
|
||||
const getScopeBadge = (role: Role) => {
|
||||
if (role.isSystemRole) {
|
||||
return (
|
||||
<span className={styles.badge} style={{ background: 'var(--warning-color, #d69e2e)', color: 'white' }}>
|
||||
<FaUserShield style={{ marginRight: 4 }} /> {t('System-Template')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (!role.mandateId) {
|
||||
return (
|
||||
<span className={styles.badge} style={{ background: 'var(--info-color, #3182ce)', color: 'white' }}>
|
||||
<FaGlobe style={{ marginRight: 4 }} /> {t('Template')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className={styles.badge} style={{ background: 'var(--success-color, #38a169)', color: 'white' }}>
|
||||
<FaBuilding style={{ marginRight: 4 }} /> {t('Mandant')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Cleanup functions ---
|
||||
const _openCleanupModal = useCallback(async () => {
|
||||
setShowCleanupModal(true);
|
||||
setCleanupError(null);
|
||||
setCleanupResult(null);
|
||||
setTemplateFixResult(null);
|
||||
setCleanupPhase('idle');
|
||||
setCleanupLoading(true);
|
||||
try {
|
||||
const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=true');
|
||||
const data = response.data;
|
||||
setCleanupResult(data.duplicateRules || data);
|
||||
setTemplateFixResult(data.templateRoleAssignments || null);
|
||||
setCleanupPhase('preview');
|
||||
} catch (err: any) {
|
||||
setCleanupError(err?.response?.data?.detail || err?.message || t('Fehler beim Laden der Duplikate'));
|
||||
} finally {
|
||||
setCleanupLoading(false);
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const _executeCleanup = useCallback(async () => {
|
||||
setCleanupLoading(true);
|
||||
setCleanupError(null);
|
||||
try {
|
||||
const response = await api.post('/api/rbac/cleanup/duplicate-rules?dryRun=false');
|
||||
const data = response.data;
|
||||
setCleanupResult(data.duplicateRules || data);
|
||||
setTemplateFixResult(data.templateRoleAssignments || null);
|
||||
setCleanupPhase('done');
|
||||
// Refresh roles after cleanup
|
||||
if (selectedMandateId) {
|
||||
fetchRoles(selectedMandateId, { scopeFilter });
|
||||
}
|
||||
} catch (err: any) {
|
||||
setCleanupError(err?.response?.data?.detail || err?.message || t('Fehler beim Bereinigen'));
|
||||
} finally {
|
||||
setCleanupLoading(false);
|
||||
}
|
||||
}, [selectedMandateId, scopeFilter, fetchRoles, t]);
|
||||
|
||||
const _closeCleanupModal = useCallback(() => {
|
||||
setShowCleanupModal(false);
|
||||
setCleanupResult(null);
|
||||
setCleanupError(null);
|
||||
setCleanupPhase('idle');
|
||||
}, []);
|
||||
|
||||
// Filter options for scope
|
||||
const scopeOptions = useMemo(() => [
|
||||
{ value: 'mandate', label: t('Mandanten-Rollen') },
|
||||
{ value: 'all', label: t('Alle inkl. Templates') },
|
||||
{ value: 'global', label: t('Nur Templates') },
|
||||
], [t]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>
|
||||
{t('Fehler beim Laden')}: {error}
|
||||
</p>
|
||||
<button className={styles.secondaryButton} onClick={handleRefresh}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
{/* Header */}
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>
|
||||
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
|
||||
{t('Rollen-Berechtigungen')}
|
||||
</h1>
|
||||
<p className={styles.pageSubtitle}>
|
||||
{t('Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen')}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={_openCleanupModal}
|
||||
disabled={loading}
|
||||
title={t('Doppelte Regeln finden und bereinigen')}
|
||||
>
|
||||
<FaBroom /> {t('Duplikate bereinigen')}
|
||||
</button>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className={styles.filterBar}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>{t('Mandant')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
{mandates.map(mandate => (
|
||||
<option key={mandate.id} value={mandate.id}>
|
||||
{mandateDisplayLabel(mandate)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaFilter style={{ marginRight: 4 }} /> {t('Bereich')}:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={scopeFilter}
|
||||
onChange={(e) => setScopeFilter(e.target.value as 'all' | 'mandate' | 'global')}
|
||||
>
|
||||
{scopeOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className={styles.infoBox}>
|
||||
<FaShieldAlt style={{ marginRight: '0.5rem' }} />
|
||||
<span>
|
||||
{t('Klicken Sie auf eine Rolle, um deren Berechtigungen (AccessRules) zu bearbeiten.')}{' '}
|
||||
<strong>{t('Template-Rollen')}</strong>{' '}
|
||||
{t('sind schreibgeschützt – Änderungen an Templates wirken sich nur auf neu erstellte Mandanten aus.')}{' '}
|
||||
<strong>{t('Mandanten-Rollen')}</strong> {t('sind direkt bearbeitbar.')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{t('Lade Rollen')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!loading && roles.length === 0 && (
|
||||
<div className={styles.emptyState}>
|
||||
<FaUserShield className={styles.emptyIcon} />
|
||||
<p>{t('Keine Rollen gefunden')}</p>
|
||||
<p className={styles.emptyHint}>
|
||||
{scopeFilter === 'mandate'
|
||||
? t('Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.')
|
||||
: scopeFilter === 'global'
|
||||
? t('Es gibt noch keine Rollentemplates')
|
||||
: t('Es gibt noch keine Rollen')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Roles List */}
|
||||
{!loading && roles.length > 0 && (
|
||||
<div className={styles.rolesList}>
|
||||
{roles.map(role => (
|
||||
<div key={role.id} className={styles.roleCard}>
|
||||
{/* Role Header - Clickable to expand */}
|
||||
<div
|
||||
className={styles.roleHeader}
|
||||
onClick={() => toggleRole(role.id)}
|
||||
>
|
||||
<div className={styles.roleInfo}>
|
||||
<span className={styles.expandIcon}>
|
||||
{expandedRoleId === role.id ? <FaChevronDown /> : <FaChevronRight />}
|
||||
</span>
|
||||
<span className={styles.roleLabel}>{role.roleLabel}</span>
|
||||
<span className={styles.roleDescription}>
|
||||
{getTextValue(role.description)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.roleBadges}>
|
||||
{getScopeBadge(role)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content - AccessRulesEditor */}
|
||||
{expandedRoleId === role.id && (
|
||||
<div className={styles.roleContent}>
|
||||
{_isTemplateRole(role) && (
|
||||
<div className={styles.infoBox} style={{ marginBottom: '0.75rem', background: 'var(--warning-bg, #fffbeb)', borderColor: 'var(--warning-color, #d69e2e)' }}>
|
||||
<FaUserShield style={{ marginRight: '0.5rem', color: 'var(--warning-color, #d69e2e)' }} />
|
||||
<span>
|
||||
{t('Dies ist eine')} <strong>{t('Template-Rolle')}</strong>.{' '}
|
||||
{t('Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<AccessRulesEditor
|
||||
roleId={role.id}
|
||||
roleName={role.roleLabel}
|
||||
isTemplate={_isTemplateRole(role)}
|
||||
readOnly={false}
|
||||
apiBasePath="/api/rbac"
|
||||
mandateId={selectedMandateId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cleanup Duplicates Modal */}
|
||||
{showCleanupModal && (
|
||||
<div className={styles.modalOverlay}>
|
||||
<div className={styles.modal} style={{ maxWidth: '750px' }}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h3 className={styles.modalTitle}>
|
||||
<FaBroom style={{ marginRight: '0.5rem' }} />
|
||||
{t('Doppelte Regeln bereinigen')}
|
||||
</h3>
|
||||
<button className={styles.modalClose} onClick={_closeCleanupModal}>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.modalContent}>
|
||||
{/* Loading */}
|
||||
{cleanupLoading && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{cleanupPhase === 'idle' ? t('Analysiere Duplikate') : t('Bereinige Duplikate')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{cleanupError && (
|
||||
<div style={{ padding: '1rem', background: '#fed7d7', borderRadius: '6px', color: '#c53030', marginBottom: '1rem' }}>
|
||||
<FaExclamationTriangle style={{ marginRight: '0.5rem' }} />
|
||||
{cleanupError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{cleanupResult && !cleanupLoading && (
|
||||
<>
|
||||
{/* Summary Cards */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1.25rem' }}>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.totalRules}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Regeln total')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.uniqueSignatures}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Eindeutige Regeln')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateGroups > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateGroups > 0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Duplikat-Gruppen')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateRulesToDelete > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateRulesToDelete > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateRulesToDelete > 0 ? '#c53030' : '#2f855a' }}>
|
||||
{cleanupPhase === 'done' ? cleanupResult.deletedCount : cleanupResult.duplicateRulesToDelete}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
||||
{cleanupPhase === 'done' ? t('Gelöscht') : t('Zu löschen')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
{cleanupPhase === 'done' && (
|
||||
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span><strong>{cleanupResult.deletedCount}</strong> {t('Doppelte Regeln wurden erfolgreich entfernt')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
|
||||
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span>{t('Keine Duplikate gefunden, alles sauber')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Details Table */}
|
||||
{cleanupResult.details && cleanupResult.details.length > 0 && (
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<h4 style={{ fontSize: '0.875rem', fontWeight: 600, marginBottom: '0.5rem', color: 'var(--text-secondary)' }}>
|
||||
{t('Duplikat-Details')}{' '}
|
||||
{cleanupResult.details.length < cleanupResult.duplicateGroups &&
|
||||
`(${cleanupResult.details.length} ${t('von')} ${cleanupResult.duplicateGroups})`}
|
||||
</h4>
|
||||
<div style={{ maxHeight: '300px', overflowY: 'auto', border: '1px solid var(--border-color)', borderRadius: '6px' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Kontext')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Item')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Total')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)', color: '#c53030' }}>{t('Duplikate')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cleanupResult.details.map((group, idx) => (
|
||||
<tr key={idx} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
||||
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||
{group.context}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||
{group.item}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center' }}>{group.totalCount}</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center', color: '#c53030', fontWeight: 600 }}>{group.deleteCount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template Role Assignments Section */}
|
||||
{templateFixResult && templateFixResult.invalidAssignments > 0 && (
|
||||
<div style={{ marginTop: '1.25rem', borderTop: '1px solid var(--border-color)', paddingTop: '1rem' }}>
|
||||
<h4 style={{ fontSize: '0.9375rem', fontWeight: 600, marginBottom: '0.75rem', color: 'var(--text-primary)' }}>
|
||||
{t('Template-Rollen-Zuweisungen')}
|
||||
</h4>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1rem' }}>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{templateFixResult.totalUserMandateRoles}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Rollenzuweisungen total')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: templateFixResult.invalidAssignments > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${templateFixResult.invalidAssignments > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.invalidAssignments > 0 ? '#c53030' : '#2f855a' }}>{templateFixResult.invalidAssignments}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('Template statt Instanz')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.fixedCount > 0 ? '#2f855a' : 'var(--text-primary)' }}>
|
||||
{cleanupPhase === 'done' ? templateFixResult.fixedCount : templateFixResult.invalidAssignments}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>
|
||||
{cleanupPhase === 'done' ? t('Repariert') : t('Zu reparieren')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{templateFixResult.details && templateFixResult.details.length > 0 && (
|
||||
<div style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid var(--border-color)', borderRadius: '6px' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Rolle')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Mandant')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('Aktion')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{templateFixResult.details.map((detail, idx) => (
|
||||
<tr key={idx} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
||||
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||
{detail.templateRoleLabel}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem' }}>
|
||||
<span style={{ fontFamily: 'monospace', fontSize: '0.75rem', background: 'var(--bg-tertiary)', padding: '0.125rem 0.375rem', borderRadius: '3px' }}>
|
||||
{detail.mandateId.substring(0, 8)}...
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '0.375rem 0.75rem', textAlign: 'center' }}>
|
||||
<span style={{
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.125rem 0.5rem',
|
||||
borderRadius: '10px',
|
||||
background: detail.action.includes('replace') ? '#ebf8ff' : detail.action.includes('delete') ? '#fff5f5' : '#f7fafc',
|
||||
color: detail.action.includes('replace') ? '#2b6cb0' : detail.action.includes('delete') ? '#c53030' : '#718096',
|
||||
}}>
|
||||
{detail.action}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{templateFixResult && templateFixResult.invalidAssignments === 0 && (
|
||||
<div style={{ marginTop: '1rem', padding: '0.5rem 0.75rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', fontSize: '0.875rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span>{t('Keine fehlerhaften Templaterollenzuweisungen')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.modalFooter}>
|
||||
<button className={styles.secondaryButton} onClick={_closeCleanupModal}>
|
||||
{cleanupPhase === 'done' ? t('Schließen') : t('Abbrechen')}
|
||||
</button>
|
||||
{cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && (
|
||||
<button
|
||||
className={styles.dangerButton}
|
||||
onClick={_executeCleanup}
|
||||
disabled={cleanupLoading}
|
||||
>
|
||||
<FaBroom /> {t('Bereinigung ausführen')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminMandateRolePermissionsPage;
|
||||
|
|
@ -19,7 +19,7 @@ import { useMandateRoles, type Role, type RoleCreate, type RoleUpdate, type Pagi
|
|||
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, FaShieldAlt, FaCube } from 'react-icons/fa';
|
||||
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';
|
||||
|
|
@ -267,13 +267,6 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
<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/mandate-role-permissions')}
|
||||
>
|
||||
<FaShieldAlt /> {t('Rollen-Berechtigungen')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.secondaryButton}
|
||||
|
|
|
|||
|
|
@ -251,6 +251,10 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
onAction: handleEditClick,
|
||||
title: t('Bearbeiten'),
|
||||
}] : []),
|
||||
...(canUpdate ? [{
|
||||
type: 'roles' as const,
|
||||
title: t('Rollen & Berechtigungen'),
|
||||
}] : []),
|
||||
...(canDelete ? [{
|
||||
type: 'delete' as const,
|
||||
title: t('Soft-Löschen deaktivieren'),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export { AdminInvitationsPage } from './AdminInvitationsPage';
|
|||
export { AdminMandateRolesPage } from './AdminMandateRolesPage';
|
||||
export { AdminFeatureRolesPage } from './AdminFeatureRolesPage';
|
||||
export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
|
||||
export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage';
|
||||
export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage';
|
||||
export { AdminLogsPage } from './AdminLogsPage';
|
||||
export { AdminLanguagesPage } from './AdminLanguagesPage';
|
||||
|
|
|
|||
Loading…
Reference in a new issue