From dfa36c17ef4794834713fe6760d5a72d41d77f62 Mon Sep 17 00:00:00 2001 From: Ida Date: Thu, 4 Jun 2026 13:08:52 +0200 Subject: [PATCH] moved tenant role permission functionality to tenant page, removed tenant role permission page --- src/App.tsx | 3 +- .../ActionButtons/ActionButton.module.css | 10 + .../RolesActionButton/RolesActionButton.tsx | 91 +++ .../ActionButtons/RolesActionButton/index.ts | 2 + .../FormGenerator/ActionButtons/index.ts | 2 + .../FormGeneratorList/FormGeneratorList.tsx | 15 +- .../FormGeneratorTable/FormGeneratorTable.tsx | 4 +- .../UiComponents/Popup/Popup.module.css | 8 + src/components/UiComponents/Popup/Popup.tsx | 2 +- .../MandateRolesPermissionsPanel.module.css | 214 +++++++ .../admin/MandateRolesPermissionsPanel.tsx | 510 +++++++++++++++ src/config/pageRegistry.tsx | 1 - src/hooks/useMandateRoles.ts | 2 + .../admin/AdminMandateRolePermissionsPage.tsx | 603 ------------------ src/pages/admin/AdminMandateRolesPage.tsx | 9 +- src/pages/admin/AdminMandatesPage.tsx | 4 + src/pages/admin/index.ts | 1 - 17 files changed, 863 insertions(+), 618 deletions(-) create mode 100644 src/components/FormGenerator/ActionButtons/RolesActionButton/RolesActionButton.tsx create mode 100644 src/components/FormGenerator/ActionButtons/RolesActionButton/index.ts create mode 100644 src/components/admin/MandateRolesPermissionsPanel.module.css create mode 100644 src/components/admin/MandateRolesPermissionsPanel.tsx delete mode 100644 src/pages/admin/AdminMandateRolePermissionsPage.tsx diff --git a/src/App.tsx b/src/App.tsx index eb120e0..7a8639b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> } /> } /> - } /> } /> } /> diff --git a/src/components/FormGenerator/ActionButtons/ActionButton.module.css b/src/components/FormGenerator/ActionButtons/ActionButton.module.css index f388910..b038efc 100644 --- a/src/components/FormGenerator/ActionButtons/ActionButton.module.css +++ b/src/components/FormGenerator/ActionButtons/ActionButton.module.css @@ -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; diff --git a/src/components/FormGenerator/ActionButtons/RolesActionButton/RolesActionButton.tsx b/src/components/FormGenerator/ActionButtons/RolesActionButton/RolesActionButton.tsx new file mode 100644 index 0000000..d2fa20e --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/RolesActionButton/RolesActionButton.tsx @@ -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> { + row: T; + disabled?: boolean | { disabled: boolean; message?: string }; + loading?: boolean; + className?: string; + title?: string; + idField?: string; + labelField?: string; + nameField?: string; +} + +export function RolesActionButton>({ + row, + disabled = false, + loading = false, + className = '', + title, + idField = 'id', + labelField = 'label', + nameField = 'name', +}: RolesActionButtonProps) { + 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; + 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 ( + <> + + + setIsPopupOpen(false)} + size="xlarge" + closable={true} + closeOnBackdropClick={false} + > + {isPopupOpen && mandateId && ( + + )} + + + ); +} + +export default RolesActionButton; diff --git a/src/components/FormGenerator/ActionButtons/RolesActionButton/index.ts b/src/components/FormGenerator/ActionButtons/RolesActionButton/index.ts new file mode 100644 index 0000000..7582fea --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/RolesActionButton/index.ts @@ -0,0 +1,2 @@ +export { RolesActionButton } from './RolesActionButton'; +export type { RolesActionButtonProps } from './RolesActionButton'; diff --git a/src/components/FormGenerator/ActionButtons/index.ts b/src/components/FormGenerator/ActionButtons/index.ts index 464bce2..b62b6d7 100644 --- a/src/components/FormGenerator/ActionButtons/index.ts +++ b/src/components/FormGenerator/ActionButtons/index.ts @@ -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'; diff --git a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx index 80a58f6..9fe2c9b 100644 --- a/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx +++ b/src/components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx @@ -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 { 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; disabled?: (row: T, hookData?: any) => boolean | { disabled: boolean; message?: string }; loading?: (row: T, hookData?: any) => boolean; @@ -858,6 +859,18 @@ export function FormGeneratorList>({ return {})} isViewing={isProcessing} hookData={hookData} />; case 'copy': return ; + case 'roles': + return ( + + ); case 'connect': return { 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; visible?: (row: T, hookData?: any) => boolean; disabled?: (row: T, hookData?: any) => boolean | { disabled: boolean; message?: string }; @@ -2692,6 +2693,7 @@ export function FormGeneratorTable>({ case 'delete': return ; case 'view': return {})} isViewing={isProc} hookData={hookData} />; case 'copy': return ; + case 'roles': return ; default: return null; } })} diff --git a/src/components/UiComponents/Popup/Popup.module.css b/src/components/UiComponents/Popup/Popup.module.css index 23b9985..b82a7f8 100644 --- a/src/components/UiComponents/Popup/Popup.module.css +++ b/src/components/UiComponents/Popup/Popup.module.css @@ -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 */ diff --git a/src/components/UiComponents/Popup/Popup.tsx b/src/components/UiComponents/Popup/Popup.tsx index c19894b..847c890 100644 --- a/src/components/UiComponents/Popup/Popup.tsx +++ b/src/components/UiComponents/Popup/Popup.tsx @@ -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; diff --git a/src/components/admin/MandateRolesPermissionsPanel.module.css b/src/components/admin/MandateRolesPermissionsPanel.module.css new file mode 100644 index 0000000..53fe9e6 --- /dev/null +++ b/src/components/admin/MandateRolesPermissionsPanel.module.css @@ -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; +} diff --git a/src/components/admin/MandateRolesPermissionsPanel.tsx b/src/components/admin/MandateRolesPermissionsPanel.tsx new file mode 100644 index 0000000..21dd3ab --- /dev/null +++ b/src/components/admin/MandateRolesPermissionsPanel.tsx @@ -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 | 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 = ({ + 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(null); + const [editingRole, setEditingRole] = useState(null); + const [showCreateModal, setShowCreateModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [backendAttributes, setBackendAttributes] = useState([]); + const [deletingRoleId, setDeletingRoleId] = useState(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; + 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 ( +
+ ⚠️ +

+ {t('Fehler beim Laden')}: {error} +

+ +
+ ); + } + + return ( +
+ {(shouldShowInfoBox || shouldShowCreateRole) && ( +
+ {shouldShowInfoBox && ( +
+ + + {t('Klicken Sie auf eine Rolle, um deren Berechtigungen (AccessRules) zu bearbeiten.')}{' '} + {t('Template-Rollen')}{' '} + {t('sind schreibgeschützt – Änderungen an Templates wirken sich nur auf neu erstellte Mandanten aus.')}{' '} + {t('Mandanten-Rollen')} {t('sind direkt bearbeitbar.')} + +
+ )} + {shouldShowCreateRole && ( + + )} +
+ )} + + {loading && ( +
+
+ {t('Lade Rollen')} +
+ )} + + {!loading && roles.length === 0 && ( +
+ +

{t('Keine Rollen gefunden')}

+

+ {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')} +

+
+ )} + + {!loading && roles.length > 0 && ( +
+ + + {roles.map(role => { + const systemLocked = !!role.isSystemRole; + const isExpanded = expandedRoleId === role.id; + const headerGridClass = showRoleActions + ? panelStyles.roleHeaderTableWithActions + : ''; + + return ( +
+
toggleRole(role.id)} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleRole(role.id); + } + }} + > + + {isExpanded ? : } + + + {role.roleLabel} + + + {getTextValue(role.description) || '—'} + + + {scopeTypeLabel(role.scopeType)} + + + {role.userCount ?? 0} + + + {showRoleActions && ( +
+ + +
+ )} +
+ + {isExpanded && ( +
+ {isTemplateRole(role) && ( +
+ + + {t('Dies ist eine')} {t('Template-Rolle')}.{' '} + {t( + 'Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.', + )} + +
+ )} + +
+ )} +
+ ); + })} +
+ )} + + {showCreateModal && ( + !isSubmitting && setShowCreateModal(false)} + size="medium" + closable={!isSubmitting} + > + {createFields.length === 0 ? ( +
+
+ {t('Lade Formular')} +
+ ) : ( + setShowCreateModal(false)} + submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')} + cancelButtonText={t('Abbrechen')} + /> + )} + + )} + + {editingRole && ( + !isSubmitting && setEditingRole(null)} + size="medium" + closable={!isSubmitting} + > + {editFields.length === 0 ? ( +
+
+ {t('Lade Formular')} +
+ ) : ( + <> +
+ + + {t('Geltungsbereich')}:{' '} + + {editingRole.mandateId ? t('Mandanten-Instanz') : t('Template (global)')} + {' '} + ({t('kann nicht geändert werden')}) + +
+ setEditingRole(null)} + submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')} + cancelButtonText={t('Abbrechen')} + /> + + )} + + )} +
+ ); +}; + +export default MandateRolesPermissionsPanel; diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index d2292cc..a53ba09 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -66,7 +66,6 @@ export const PAGE_ICONS: Record = { 'page.admin.mandates': , 'page.admin.roles': , 'page.admin.role-permissions': , - 'page.admin.mandateRolePermissions': , 'page.admin.user-mandates': , 'page.admin.userMandates': , 'page.admin.feature-roles': , diff --git a/src/hooks/useMandateRoles.ts b/src/hooks/useMandateRoles.ts index f530b9c..d1294b7 100644 --- a/src/hooks/useMandateRoles.ts +++ b/src/hooks/useMandateRoles.ts @@ -18,6 +18,8 @@ export interface Role { featureCode?: string; isSystemRole?: boolean; isTemplate?: boolean; + scopeType?: 'system' | 'global' | 'mandate'; + userCount?: number; createdAt?: number; updatedAt?: number; } diff --git a/src/pages/admin/AdminMandateRolePermissionsPage.tsx b/src/pages/admin/AdminMandateRolePermissionsPage.tsx deleted file mode 100644 index 0b6eac8..0000000 --- a/src/pages/admin/AdminMandateRolePermissionsPage.tsx +++ /dev/null @@ -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([]); - const [selectedMandateId, setSelectedMandateId] = useState(''); - const [scopeFilter, setScopeFilter] = useState<'all' | 'mandate' | 'global'>('mandate'); - const [expandedRoleId, setExpandedRoleId] = useState(null); - - // Cleanup state - const [showCleanupModal, setShowCleanupModal] = useState(false); - const [cleanupLoading, setCleanupLoading] = useState(false); - const [cleanupResult, setCleanupResult] = useState(null); - const [templateFixResult, setTemplateFixResult] = useState(null); - const [cleanupError, setCleanupError] = useState(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 | 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 ( - - {t('System-Template')} - - ); - } - if (!role.mandateId) { - return ( - - {t('Template')} - - ); - } - return ( - - {t('Mandant')} - - ); - }; - - // --- 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 ( -
-
- ⚠️ -

- {t('Fehler beim Laden')}: {error} -

- -
-
- ); - } - - return ( -
- {/* Header */} -
-
-

- - {t('Rollen-Berechtigungen')} -

-

- {t('Verwalten Sie die Zugriffsrechte für Mandanten- und globale Rollen')} -

-
-
- - -
-
- - {/* Filters */} -
-
- - -
- -
- - -
-
- - {/* Info Box */} -
- - - {t('Klicken Sie auf eine Rolle, um deren Berechtigungen (AccessRules) zu bearbeiten.')}{' '} - {t('Template-Rollen')}{' '} - {t('sind schreibgeschützt – Änderungen an Templates wirken sich nur auf neu erstellte Mandanten aus.')}{' '} - {t('Mandanten-Rollen')} {t('sind direkt bearbeitbar.')} - -
- - {/* Loading State */} - {loading && ( -
-
- {t('Lade Rollen')} -
- )} - - {/* Empty State */} - {!loading && roles.length === 0 && ( -
- -

{t('Keine Rollen gefunden')}

-

- {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')} -

-
- )} - - {/* Roles List */} - {!loading && roles.length > 0 && ( -
- {roles.map(role => ( -
- {/* Role Header - Clickable to expand */} -
toggleRole(role.id)} - > -
- - {expandedRoleId === role.id ? : } - - {role.roleLabel} - - {getTextValue(role.description)} - -
-
- {getScopeBadge(role)} -
-
- - {/* Expanded Content - AccessRulesEditor */} - {expandedRoleId === role.id && ( -
- {_isTemplateRole(role) && ( -
- - - {t('Dies ist eine')} {t('Template-Rolle')}.{' '} - {t('Änderungen an den Berechtigungen wirken sich nur auf neu erstellte Mandanten aus. Bestehende Mandanten-Instanzen werden nicht aktualisiert.')} - -
- )} - -
- )} -
- ))} -
- )} - - {/* Cleanup Duplicates Modal */} - {showCleanupModal && ( -
-
-
-

- - {t('Doppelte Regeln bereinigen')} -

- -
- -
- {/* Loading */} - {cleanupLoading && ( -
-
- {cleanupPhase === 'idle' ? t('Analysiere Duplikate') : t('Bereinige Duplikate')} -
- )} - - {/* Error */} - {cleanupError && ( -
- - {cleanupError} -
- )} - - {/* Results */} - {cleanupResult && !cleanupLoading && ( - <> - {/* Summary Cards */} -
-
-
{cleanupResult.totalRules}
-
{t('Regeln total')}
-
-
-
{cleanupResult.uniqueSignatures}
-
{t('Eindeutige Regeln')}
-
-
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}> -
0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}
-
{t('Duplikat-Gruppen')}
-
-
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateRulesToDelete > 0 ? '#fc8181' : '#9ae6b4'}` }}> -
0 ? '#c53030' : '#2f855a' }}> - {cleanupPhase === 'done' ? cleanupResult.deletedCount : cleanupResult.duplicateRulesToDelete} -
-
- {cleanupPhase === 'done' ? t('Gelöscht') : t('Zu löschen')} -
-
-
- - {/* Status Message */} - {cleanupPhase === 'done' && ( -
- - {cleanupResult.deletedCount} {t('Doppelte Regeln wurden erfolgreich entfernt')} -
- )} - - {cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && ( -
- - {t('Keine Duplikate gefunden, alles sauber')} -
- )} - - {/* Details Table */} - {cleanupResult.details && cleanupResult.details.length > 0 && ( -
-

- {t('Duplikat-Details')}{' '} - {cleanupResult.details.length < cleanupResult.duplicateGroups && - `(${cleanupResult.details.length} ${t('von')} ${cleanupResult.duplicateGroups})`} -

-
- - - - - - - - - - - {cleanupResult.details.map((group, idx) => ( - - - - - - - ))} - -
{t('Kontext')}{t('Item')}{t('Total')}{t('Duplikate')}
- - {group.context} - - - - {group.item} - - {group.totalCount}{group.deleteCount}
-
-
- )} - - {/* Template Role Assignments Section */} - {templateFixResult && templateFixResult.invalidAssignments > 0 && ( -
-

- {t('Template-Rollen-Zuweisungen')} -

-
-
-
{templateFixResult.totalUserMandateRoles}
-
{t('Rollenzuweisungen total')}
-
-
0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${templateFixResult.invalidAssignments > 0 ? '#fc8181' : '#9ae6b4'}` }}> -
0 ? '#c53030' : '#2f855a' }}>{templateFixResult.invalidAssignments}
-
{t('Template statt Instanz')}
-
-
-
0 ? '#2f855a' : 'var(--text-primary)' }}> - {cleanupPhase === 'done' ? templateFixResult.fixedCount : templateFixResult.invalidAssignments} -
-
- {cleanupPhase === 'done' ? t('Repariert') : t('Zu reparieren')} -
-
-
- - {templateFixResult.details && templateFixResult.details.length > 0 && ( -
- - - - - - - - - - {templateFixResult.details.map((detail, idx) => ( - - - - - - ))} - -
{t('Rolle')}{t('Mandant')}{t('Aktion')}
- - {detail.templateRoleLabel} - - - - {detail.mandateId.substring(0, 8)}... - - - - {detail.action} - -
-
- )} -
- )} - - {templateFixResult && templateFixResult.invalidAssignments === 0 && ( -
- - {t('Keine fehlerhaften Templaterollenzuweisungen')} -
- )} - - )} -
- -
- - {cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && ( - - )} -
-
-
- )} -
- ); -}; - -export default AdminMandateRolePermissionsPage; diff --git a/src/pages/admin/AdminMandateRolesPage.tsx b/src/pages/admin/AdminMandateRolesPage.tsx index 007ad89..b079165 100644 --- a/src/pages/admin/AdminMandateRolesPage.tsx +++ b/src/pages/admin/AdminMandateRolesPage.tsx @@ -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 = () => {

{t('Verwalten Sie systemweite und globale')}

-