From eb0a58aaa764e86ab9124366316839518f8dbef6 Mon Sep 17 00:00:00 2001 From: Ida Date: Thu, 4 Jun 2026 14:14:20 +0200 Subject: [PATCH] moved tenant member view and functionality to tenant page --- .../ExpandActionButton.module.css | 39 ++ .../ExpandActionButton/ExpandActionButton.tsx | 68 ++++ .../ActionButtons/ExpandActionButton/index.ts | 2 + .../FormGenerator/ActionButtons/index.ts | 2 + .../FormGeneratorTable.module.css | 28 ++ .../FormGeneratorTable/FormGeneratorTable.tsx | 194 +++++---- .../admin/InlineRoleMultiselect.module.css | 102 +++++ .../admin/InlineRoleMultiselect.tsx | 191 +++++++++ .../admin/MandateUsersPanel.module.css | 148 +++++++ src/components/admin/MandateUsersPanel.tsx | 385 ++++++++++++++++++ src/hooks/useMandates.ts | 37 +- src/hooks/useUserMandates.ts | 13 +- src/pages/admin/AdminMandatesPage.tsx | 31 +- src/pages/admin/AdminUserMandatesPage.tsx | 367 +---------------- 14 files changed, 1166 insertions(+), 441 deletions(-) create mode 100644 src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css create mode 100644 src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.tsx create mode 100644 src/components/FormGenerator/ActionButtons/ExpandActionButton/index.ts create mode 100644 src/components/admin/InlineRoleMultiselect.module.css create mode 100644 src/components/admin/InlineRoleMultiselect.tsx create mode 100644 src/components/admin/MandateUsersPanel.module.css create mode 100644 src/components/admin/MandateUsersPanel.tsx diff --git a/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css b/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css new file mode 100644 index 0000000..9437cb0 --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.module.css @@ -0,0 +1,39 @@ +.chevronBtn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.12s; + flex-shrink: 0; + border-radius: 3px; + width: 24px; + height: 24px; + color: var(--color-text-secondary, #64748b); +} + +.chevronBtn:hover:not(:disabled) { + background: var(--color-primary-light, rgba(74, 111, 165, 0.12)); + color: var(--color-primary, #4a6fa5); +} + +.chevronBtn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.chevronBtn:focus-visible { + outline: none; + box-shadow: 0 0 0 2px var(--color-bg, #fff), 0 0 0 4px rgba(74, 111, 165, 0.45); +} + +.chevronIcon { + font-size: 11px; + transition: transform 0.15s ease; +} + +.chevronIconExpanded { + transform: rotate(90deg); +} diff --git a/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.tsx b/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.tsx new file mode 100644 index 0000000..a08a6be --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/ExpandActionButton/ExpandActionButton.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { FaChevronRight } from 'react-icons/fa'; +import { useLanguage } from '../../../../providers/language/LanguageContext'; +import styles from './ExpandActionButton.module.css'; + +export interface ExpandActionButtonProps> { + row: T; + disabled?: boolean | { disabled: boolean; message?: string }; + loading?: boolean; + className?: string; + title?: string; + hookData: { + isRowExpanded?: (row: T) => boolean; + toggleExpandedRow?: (row: T) => void; + }; + idField?: string; +} + +export function ExpandActionButton>({ + row, + disabled = false, + loading = false, + className = '', + title, + hookData, + idField = 'id', +}: ExpandActionButtonProps) { + const { t } = useLanguage(); + + const isDisabled = typeof disabled === 'boolean' ? disabled : disabled?.disabled || false; + const disabledMessage = typeof disabled === 'object' ? disabled?.message : undefined; + + if (!hookData?.toggleExpandedRow) { + throw new Error('ExpandActionButton requires hookData.toggleExpandedRow'); + } + + const isExpanded = hookData.isRowExpanded?.(row) ?? false; + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + if (!isDisabled && !loading) { + hookData.toggleExpandedRow!(row); + } + }; + + const defaultTitle = isExpanded ? t('Zuklappen') : t('Aufklappen'); + const finalTitle = isDisabled && disabledMessage ? disabledMessage : (title || defaultTitle); + + return ( + + ); +} + +export default ExpandActionButton; diff --git a/src/components/FormGenerator/ActionButtons/ExpandActionButton/index.ts b/src/components/FormGenerator/ActionButtons/ExpandActionButton/index.ts new file mode 100644 index 0000000..69677f6 --- /dev/null +++ b/src/components/FormGenerator/ActionButtons/ExpandActionButton/index.ts @@ -0,0 +1,2 @@ +export { ExpandActionButton } from './ExpandActionButton'; +export type { ExpandActionButtonProps } from './ExpandActionButton'; diff --git a/src/components/FormGenerator/ActionButtons/index.ts b/src/components/FormGenerator/ActionButtons/index.ts index b62b6d7..42b37e8 100644 --- a/src/components/FormGenerator/ActionButtons/index.ts +++ b/src/components/FormGenerator/ActionButtons/index.ts @@ -6,6 +6,7 @@ export { CopyActionButton } from './CopyActionButton'; export { RemoveActionButton } from './RemoveActionButton'; export { DownloadActionButton } from './DownloadActionButton'; export { RolesActionButton } from './RolesActionButton'; +export { ExpandActionButton } from './ExpandActionButton'; // Generic Custom Action Button (for entity-specific actions) export { CustomActionButton } from './CustomActionButton'; @@ -18,4 +19,5 @@ export type { CopyActionButtonProps } from './CopyActionButton'; export type { RemoveActionButtonProps } from './RemoveActionButton'; export type { DownloadActionButtonProps } from './DownloadActionButton'; export type { RolesActionButtonProps } from './RolesActionButton'; +export type { ExpandActionButtonProps } from './ExpandActionButton'; export type { CustomActionButtonProps } from './CustomActionButton'; diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css index a2344d8..2cc58a6 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css @@ -578,6 +578,34 @@ cursor: pointer; } +.tr.expandedParentRow { + background: rgba(var(--color-secondary-rgb, 74, 111, 165), 0.06); +} + +.expandedDetailRow { + background: var(--bg-secondary, #f8fafc); +} + +.expandedDetailRow:hover { + background: var(--bg-secondary, #f8fafc); + box-shadow: none; +} + +.expandedDetailCell { + padding: 12px 16px 16px; + vertical-align: top; + border-bottom: 2px solid var(--color-border, #e2e8f0); + box-sizing: border-box; +} + +.expandedDetailInner { + width: 100%; + max-width: 100%; + min-width: 0; + overflow-x: auto; + overflow-y: visible; +} + /* Items that live inside a group — subtle tint + left connector */ .tr.groupedItem { border-left: 3px solid color-mix(in srgb, var(--color-primary, #4a6fa5) 35%, transparent); diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx index 00ded3d..2815c83 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx @@ -65,6 +65,7 @@ import { ViewActionButton, CopyActionButton, RolesActionButton, + ExpandActionButton, CustomActionButton } from '../ActionButtons'; import { formatUnixTimestamp } from '../../../utils/time'; @@ -265,7 +266,7 @@ export interface FormGeneratorTableProps { 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' | 'roles' | 'connect' | 'play'; + type: 'expand' | '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 }; @@ -356,6 +357,8 @@ export interface FormGeneratorTableProps { csvExportContextFilters?: Record; /** Optional download filename token (sanitized), e.g. group key for nested exports. */ csvExportFilenameSuffix?: string; + /** Renders content in a full-width row below the data row when expanded (requires hookData.isRowExpanded / toggleExpandedRow). */ + renderExpandedRow?: (row: T) => React.ReactNode; } const _FILTER_PAGE_SIZE = 100; @@ -713,6 +716,7 @@ export function FormGeneratorTable>({ csvExportQueryParams, csvExportContextFilters, csvExportFilenameSuffix, + renderExpandedRow, }: FormGeneratorTableProps) { const { t, currentLanguage: contextLanguage } = useLanguage(); // When only onDelete is provided, use it for multi-delete too so Delete stays visible with 2+ selected @@ -2645,79 +2649,131 @@ export function FormGeneratorTable>({ batchActions.length > 0 || (selectable && selectedIds.size > 0); + const expandActionConfig = useMemo( + () => actionButtons.find((ab) => ab.type === 'expand'), + [actionButtons], + ); + const _renderDataRow = (row: T, index: number) => { const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, index) : {}; const rowId = _getRowId(row); + const isExpanded = + !!renderExpandedRow && + !!hookData?.isRowExpanded && + hookData.isRowExpanded(row); + const detailColSpan = + (selectable ? 1 : 0) + (hasActionColumn ? 1 : 0) + detectedColumns.length; + + const _renderExpandButton = () => { + if (!expandActionConfig) return null; + const ab = expandActionConfig; + if (ab.visible && !ab.visible(row, hookData)) return null; + const abTitle = typeof ab.title === 'function' ? ab.title(row) : ab.title; + let dis: boolean | { disabled: boolean; message?: string } = false; + if (ab.disabled) { + dis = ab.disabled(row, hookData); + } + const isLd = ab.loading ? ab.loading(row, hookData) : false; + return ( + + ); + }; + return ( - onRowClick?.(row, index)} - draggable={rowDraggable} - onDragStart={(e) => { - if (rowDraggable && onRowDragStart) onRowDragStart(e, row); - }} - onDragEnd={() => {}} - {...Object.fromEntries(Object.entries(dataAttributes).map(([k, v]) => [`data-${k}`, v]))} - > - {selectable && ( - - handleRowSelect(row)} - onClick={(e) => e.stopPropagation()} - disabled={isRowSelectable && !isRowSelectable(row)} - title={isRowSelectable && !isRowSelectable(row) ? t('Dieses Element kann nicht ausgewählt werden') : t('Dieses Element auswählen')} - style={{ opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1, cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer' }} - /> - - )} - {hasActionColumn && ( - -
{ if (el) actionButtonsRefs.current.set(index, el); else actionButtonsRefs.current.delete(index); }} - className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}> - {actionButtons.map((ab, ai) => { - if (ab.visible && !ab.visible(row, hookData)) return null; - const abTitle = typeof ab.title === 'function' ? ab.title(row) : ab.title; - let dis: boolean | { disabled: boolean; message?: string } = false; - if (ab.disabled) { dis = ab.disabled(row, hookData); } - else if (row._permissions) { - if (ab.type === 'edit' && row._permissions.canUpdate === false) dis = true; - else if (ab.type === 'delete' && row._permissions.canDelete === false) dis = true; - } - const isLd = ab.loading ? ab.loading(row) : false; - const isProc = ab.isProcessing ? ab.isProcessing(row) : false; - const bp = { row, disabled: dis, loading: isLd, className: [compact ? actionBtnStyles.compact : '', ab.className ?? ''].filter(Boolean).join(' '), title: abTitle, idField: ab.idField ?? 'id', nameField: ab.nameField ?? 'name', typeField: ab.typeField ?? 'type', contentField: ab.contentField ?? 'content', operationName: ab.operationName, loadingStateName: ab.loadingStateName }; - switch (ab.type) { - case 'edit': return ; - case 'delete': return ; - case 'view': return {})} isViewing={isProc} hookData={hookData} />; - case 'copy': return ; - case 'roles': return ; - default: return null; - } - })} - {customActions.map((ca) => ( - - ))} -
- - )} - {detectedColumns.map((col) => { - const cv = row[col.key]; - const cCls = col.cellClassName ? col.cellClassName(cv, row) : ''; - const aStyle = _columnAlignStyle(col); - return ( - - {formatCellValue(cv, col, row)} + + onRowClick?.(row, index)} + draggable={rowDraggable} + onDragStart={(e) => { + if (rowDraggable && onRowDragStart) onRowDragStart(e, row); + }} + onDragEnd={() => {}} + {...Object.fromEntries(Object.entries(dataAttributes).map(([k, v]) => [`data-${k}`, v]))} + > + {selectable && ( + + handleRowSelect(row)} + onClick={(e) => e.stopPropagation()} + disabled={isRowSelectable && !isRowSelectable(row)} + title={isRowSelectable && !isRowSelectable(row) ? t('Dieses Element kann nicht ausgewählt werden') : t('Dieses Element auswählen')} + style={{ opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1, cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer' }} + /> - ); - })} - + )} + {hasActionColumn && ( + +
{ if (el) actionButtonsRefs.current.set(index, el); else actionButtonsRefs.current.delete(index); }} + className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}> + {_renderExpandButton()} + {actionButtons.map((ab, ai) => { + if (ab.type === 'expand') return null; + if (ab.visible && !ab.visible(row, hookData)) return null; + const abTitle = typeof ab.title === 'function' ? ab.title(row) : ab.title; + let dis: boolean | { disabled: boolean; message?: string } = false; + if (ab.disabled) { dis = ab.disabled(row, hookData); } + else if (row._permissions) { + if (ab.type === 'edit' && row._permissions.canUpdate === false) dis = true; + else if (ab.type === 'delete' && row._permissions.canDelete === false) dis = true; + } + const isLd = ab.loading ? ab.loading(row, hookData) : false; + const isProc = ab.isProcessing ? ab.isProcessing(row) : false; + const bp = { row, disabled: dis, loading: isLd, className: [compact ? actionBtnStyles.compact : '', ab.className ?? ''].filter(Boolean).join(' '), title: abTitle, idField: ab.idField ?? 'id', nameField: ab.nameField ?? 'name', typeField: ab.typeField ?? 'type', contentField: ab.contentField ?? 'content', operationName: ab.operationName, loadingStateName: ab.loadingStateName }; + switch (ab.type) { + case 'edit': return ; + case 'delete': return ; + case 'view': return {})} isViewing={isProc} hookData={hookData} />; + case 'copy': return ; + case 'roles': return ; + default: return null; + } + })} + {customActions.map((ca) => ( + + ))} +
+ + )} + {detectedColumns.map((col) => { + const cv = row[col.key]; + const cCls = col.cellClassName ? col.cellClassName(cv, row) : ''; + const aStyle = _columnAlignStyle(col); + return ( + + {formatCellValue(cv, col, row)} + + ); + })} + + {isExpanded && renderExpandedRow && ( + + e.stopPropagation()} + > +
+ {renderExpandedRow(row)} +
+ + + )} +
); }; diff --git a/src/components/admin/InlineRoleMultiselect.module.css b/src/components/admin/InlineRoleMultiselect.module.css new file mode 100644 index 0000000..1eab04d --- /dev/null +++ b/src/components/admin/InlineRoleMultiselect.module.css @@ -0,0 +1,102 @@ +.root { + position: relative; + width: 100%; + min-width: 0; + max-width: 100%; +} + +.trigger { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + width: 100%; + padding: 4px 8px; + font-size: 12px; + font-family: var(--font-family); + color: var(--color-text); + background: var(--color-bg, #fff); + border: 1px solid var(--color-border, #e2e8f0); + border-radius: 6px; + cursor: pointer; + text-align: left; + transition: border-color 0.12s, box-shadow 0.12s; +} + +.trigger:hover:not(:disabled) { + border-color: var(--color-primary, #4a6fa5); +} + +.triggerOpen { + border-color: var(--color-primary, #4a6fa5); + box-shadow: 0 0 0 2px rgba(74, 111, 165, 0.15); +} + +.triggerDisabled { + opacity: 0.65; + cursor: not-allowed; +} + +.triggerLabel { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.triggerChevron { + flex-shrink: 0; + font-size: 10px; + color: var(--color-text-secondary, #64748b); + transition: transform 0.15s ease; +} + +.triggerChevronOpen { + transform: rotate(180deg); +} + +.dropdownPortal { + max-height: 220px; + overflow-y: auto; + background: var(--color-bg, #fff); + border: 1px solid var(--color-border, #e2e8f0); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} + +.empty { + padding: 10px 12px; + font-size: 12px; + color: var(--color-text-secondary, #64748b); +} + +.optionList { + list-style: none; + margin: 0; + padding: 4px 0; +} + +.option { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + font-size: 12px; + cursor: pointer; + user-select: none; +} + +.option:hover { + background: var(--color-primary-light, rgba(74, 111, 165, 0.08)); +} + +.optionLocked { + opacity: 0.55; + cursor: not-allowed; +} + +.option input { + accent-color: var(--color-primary, #4a6fa5); + margin: 0; + flex-shrink: 0; +} diff --git a/src/components/admin/InlineRoleMultiselect.tsx b/src/components/admin/InlineRoleMultiselect.tsx new file mode 100644 index 0000000..f414481 --- /dev/null +++ b/src/components/admin/InlineRoleMultiselect.tsx @@ -0,0 +1,191 @@ +/** + * InlineRoleMultiselect — compact dropdown multiselect for mandate role assignment in tables. + * Dropdown renders in a portal so it is not clipped by table overflow containers. + */ + +import React, { useState, useRef, useEffect, useLayoutEffect, useCallback, useMemo } from 'react'; +import { createPortal } from 'react-dom'; +import { FaChevronDown } from 'react-icons/fa'; +import { useLanguage } from '../../providers/language/LanguageContext'; +import styles from './InlineRoleMultiselect.module.css'; + +export interface InlineRoleMultiselectOption { + value: string; + label: string; +} + +export interface InlineRoleMultiselectProps { + value: string[]; + options: InlineRoleMultiselectOption[]; + onChange: (roleIds: string[]) => void | Promise; + disabled?: boolean; + loading?: boolean; +} + +export const InlineRoleMultiselect: React.FC = ({ + value, + options, + onChange, + disabled = false, + loading = false, +}) => { + const { t } = useLanguage(); + const [open, setOpen] = useState(false); + const [pending, setPending] = useState(false); + const [dropdownRect, setDropdownRect] = useState<{ + top: number; + left: number; + width: number; + } | null>(null); + const containerRef = useRef(null); + + const selectedSet = useMemo(() => new Set(value), [value]); + + const triggerLabel = useMemo(() => { + if (!value.length) return t('Rollen wählen'); + const labels = value + .map((id) => options.find((o) => o.value === id)?.label) + .filter(Boolean) as string[]; + if (labels.length <= 2) return labels.join(', '); + return t('{count} Rollen', { count: String(value.length) }); + }, [value, options, t]); + + const updateDropdownPosition = useCallback(() => { + const el = containerRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + setDropdownRect({ + top: rect.bottom + 4, + left: rect.left, + width: Math.max(rect.width, 200), + }); + }, []); + + useLayoutEffect(() => { + if (!open) { + setDropdownRect(null); + return; + } + updateDropdownPosition(); + window.addEventListener('scroll', updateDropdownPosition, true); + window.addEventListener('resize', updateDropdownPosition); + return () => { + window.removeEventListener('scroll', updateDropdownPosition, true); + window.removeEventListener('resize', updateDropdownPosition); + }; + }, [open, updateDropdownPosition]); + + const handleClickOutside = useCallback((event: MouseEvent) => { + const target = event.target as Node; + if (containerRef.current?.contains(target)) return; + const portal = document.getElementById('inline-role-multiselect-portal'); + if (portal?.contains(target)) return; + setOpen(false); + }, []); + + useEffect(() => { + if (open) { + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + } + }, [open, handleClickOutside]); + + const handleToggleOption = async (optionValue: string) => { + if (disabled || loading || pending) return; + + const next = selectedSet.has(optionValue) + ? value.filter((id) => id !== optionValue) + : [...value, optionValue]; + + if (next.length === 0) return; + + setPending(true); + try { + await onChange(next); + } finally { + setPending(false); + } + }; + + const isBusy = disabled || loading || pending; + const noOptions = options.length === 0; + + const dropdownContent = + open && !isBusy && dropdownRect ? ( +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + {noOptions ? ( +
{t('Keine Rollen verfügbar')}
+ ) : ( +
    + {options.map((opt) => { + const checked = selectedSet.has(opt.value); + const isLastSelected = checked && value.length === 1; + return ( +
  • + +
  • + ); + })} +
+ )} +
+ ) : null; + + return ( +
+ + {typeof document !== 'undefined' && dropdownContent + ? createPortal(dropdownContent, document.body) + : null} +
+ ); +}; + +export default InlineRoleMultiselect; diff --git a/src/components/admin/MandateUsersPanel.module.css b/src/components/admin/MandateUsersPanel.module.css new file mode 100644 index 0000000..8ef4194 --- /dev/null +++ b/src/components/admin/MandateUsersPanel.module.css @@ -0,0 +1,148 @@ +.panel { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} + +.panelEmbedded { + width: 100%; + max-width: 100%; + min-width: 0; + padding: 0; + box-sizing: border-box; +} + +.card { + display: flex; + flex-direction: column; + min-width: 0; + width: 100%; +} + +.cardEmbedded { + width: 100%; + max-width: 100%; + border: 1px solid var(--color-border, #e2e8f0); + border-radius: 8px; + background: var(--color-bg, #fff); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + overflow: hidden; + box-sizing: border-box; +} + +.headerBar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-shrink: 0; + padding: 8px 12px; + background: var(--bg-secondary, #f8fafc); + border-bottom: 1px solid var(--color-border, #e2e8f0); +} + +.headerBarEmbedded { + padding: 6px 10px; +} + +.headerTitle { + margin: 0; + font-size: 0.8125rem; + font-weight: 600; + color: var(--text-primary, #0f172a); + letter-spacing: 0.01em; + white-space: nowrap; +} + +.headerActions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 6px; + flex-wrap: wrap; +} + +.btnCompactPrimary, +.btnCompactSecondary { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + font-size: 0.75rem; + font-weight: 500; + border-radius: 5px; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; +} + +.btnCompactPrimary { + background: var(--primary-color, #f25843); + color: #fff; + border: none; +} + +.btnCompactPrimary:hover:not(:disabled) { + background: var(--primary-dark, #d94d3a); +} + +.btnCompactPrimary:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.btnCompactSecondary { + background: var(--color-bg, #fff); + color: var(--text-primary, #0f172a); + border: 1px solid var(--color-border, #e2e8f0); +} + +.btnCompactSecondary:hover:not(:disabled) { + background: var(--bg-secondary, #f8fafc); +} + +.btnCompactSecondary:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.tableWrap { + min-height: 200px; + max-height: 420px; + display: flex; + flex-direction: column; +} + +.tableWrapEmbedded { + min-height: 200px; + height: 240px; + max-height: 280px; + width: 100%; + min-width: 0; + overflow: hidden; +} + +.tableWrap :global(.formGeneratorTable) { + height: 100%; + max-height: 420px; + min-width: 0; +} + +.tableWrapEmbedded :global(.formGeneratorTable) { + height: 100%; + min-height: 180px; + max-height: 100%; + font-size: 12px; + min-width: 0; +} + +.tableWrapEmbedded :global(.formGeneratorTable .tableWrapper) { + border: none; + border-radius: 0; + box-shadow: none; + min-width: 0; +} + +.tableWrapEmbedded :global(.formGeneratorTable .tableContainer) { + min-height: 120px; +} diff --git a/src/components/admin/MandateUsersPanel.tsx b/src/components/admin/MandateUsersPanel.tsx new file mode 100644 index 0000000..b6fac8c --- /dev/null +++ b/src/components/admin/MandateUsersPanel.tsx @@ -0,0 +1,385 @@ +/** + * MandateUsersPanel — manage users within a single mandate (members, roles, add/remove). + * Shared by AdminMandatesPage (expanded row) and AdminUserMandatesPage. + */ + +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import { + useUserMandates, + type MandateUser, + type Role, + type PaginationParams, +} from '../../hooks/useUserMandates'; +import { FormGeneratorTable } from '../FormGenerator/FormGeneratorTable'; +import { FormGeneratorForm, type AttributeDefinition } from '../FormGenerator/FormGeneratorForm'; +import type { ColumnConfig } from '../FormGenerator/FormGeneratorTable'; +import { FaPlus, FaSync } from 'react-icons/fa'; +import { useToast } from '../../contexts/ToastContext'; +import { useApiRequest } from '../../hooks/useApi'; +import { fetchAttributes } from '../../api/attributesApi'; +import { resolveColumnTypes } from '../../utils/columnTypeResolver'; +import { InlineRoleMultiselect } from './InlineRoleMultiselect'; +import adminStyles from '../../pages/admin/Admin.module.css'; +import panelStyles from './MandateUsersPanel.module.css'; + +import { useLanguage } from '../../providers/language/LanguageContext'; + +export interface MandateUsersPanelProps { + mandateId: string; + embedded?: boolean; +} + +export const MandateUsersPanel: React.FC = ({ + mandateId, + embedded = false, +}) => { + const { t } = useLanguage(); + const { showError, showSuccess } = useToast(); + const { request } = useApiRequest(); + const { + users, + loading, + pagination, + fetchMandateUsers, + addUserToMandate, + removeUserFromMandate, + updateUserRoles, + fetchRoles, + fetchAllUsers, + } = useUserMandates(); + + const currentMandateIdRef = useRef(mandateId); + currentMandateIdRef.current = mandateId; + + const [roles, setRoles] = useState([]); + const [allUsers, setAllUsers] = useState< + Array<{ id: string; username: string; email?: string; fullName?: string }> + >([]); + const [showAddModal, setShowAddModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [backendAttributes, setBackendAttributes] = useState([]); + const [savingUserId, setSavingUserId] = useState(null); + + useEffect(() => { + fetchAttributes(request, 'UserMandateView') + .then(setBackendAttributes) + .catch(() => setBackendAttributes([])); + }, [request]); + + useEffect(() => { + fetchAllUsers().then(setAllUsers); + }, [fetchAllUsers]); + + useEffect(() => { + if (!mandateId) return; + currentMandateIdRef.current = mandateId; + fetchMandateUsers(mandateId); + fetchRoles(mandateId).then(setRoles); + }, [mandateId, fetchMandateUsers, fetchRoles]); + + const refetchWithParams = useCallback( + async (paginationParams?: PaginationParams) => { + const id = currentMandateIdRef.current; + if (!id) return; + if (paginationParams && Object.keys(paginationParams).length > 0) { + return fetchMandateUsers(paginationParams); + } + return fetchMandateUsers(id); + }, + [fetchMandateUsers], + ); + + const availableUsers = useMemo(() => { + const existingUserIds = new Set(users.map((u) => u.userId)); + return allUsers.filter((u) => !existingUserIds.has(u.id)); + }, [allUsers, users]); + + const roleOptions = useMemo( + () => + roles + .filter((r) => !r.featureInstanceId && !r.featureCode) + .map((r) => ({ + value: r.id, + label: r.roleLabel || r.id, + })), + [roles], + ); + + const handleRoleChange = useCallback( + async (userId: string, roleIds: string[]) => { + if (!mandateId) return; + setSavingUserId(userId); + try { + const result = await updateUserRoles(mandateId, userId, roleIds); + if (!result.success) { + showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rollen')); + await fetchMandateUsers(mandateId); + } + } finally { + setSavingUserId(null); + } + }, + [mandateId, updateUserRoles, showError, t, fetchMandateUsers], + ); + + const _rawColumns: ColumnConfig[] = useMemo(() => { + const cols: ColumnConfig[] = [ + { + key: 'username', + label: t('Benutzername'), + sortable: true, + filterable: true, + searchable: true, + width: embedded ? 100 : 140, + }, + { + key: 'email', + label: t('E-Mail'), + sortable: true, + filterable: true, + searchable: true, + width: embedded ? 130 : 180, + }, + ]; + if (!embedded) { + cols.push({ + key: 'fullName', + label: t('Vollständiger Name'), + sortable: true, + filterable: true, + searchable: true, + width: 160, + }); + } + cols.push( + { + key: 'roleIds', + label: t('Rollen'), + sortable: false, + filterable: false, + searchable: false, + width: embedded ? 150 : 220, + formatter: (_value: unknown, row: MandateUser) => ( + handleRoleChange(row.userId, ids)} + loading={savingUserId === row.userId} + /> + ), + }, + { + key: 'enabled', + label: t('Aktiv'), + sortable: true, + filterable: true, + searchable: false, + width: embedded ? 52 : 72, + }, + ); + return cols; + }, [t, roleOptions, handleRoleChange, savingUserId, embedded]); + + const columns = useMemo( + () => resolveColumnTypes(_rawColumns, backendAttributes), + [_rawColumns, backendAttributes], + ); + + const userOptions = useMemo( + () => + availableUsers.map((u) => ({ + value: u.id, + label: `${u.username} ${u.email ? `(${u.email})` : ''}`, + })), + [availableUsers], + ); + + const addUserFields: AttributeDefinition[] = useMemo(() => { + const userIdAttr = backendAttributes.find((a) => a.name === 'userId'); + const roleIdsAttr = backendAttributes.find((a) => a.name === 'roleIds'); + + return [ + { + name: 'targetUserId', + label: userIdAttr?.label || t('Benutzer'), + type: 'enum' as AttributeDefinition['type'], + required: true, + options: userOptions, + }, + { + name: 'roleIds', + label: roleIdsAttr?.label || t('Rollen'), + type: 'multiselect' as AttributeDefinition['type'], + required: true, + options: roleOptions, + }, + ]; + }, [userOptions, roleOptions, backendAttributes, t]); + + const handleAddUser = async (data: { targetUserId: string; roleIds: string[] }) => { + if (!mandateId) return; + setIsSubmitting(true); + try { + const result = await addUserToMandate(mandateId, data); + if (result.success) { + setShowAddModal(false); + showSuccess(t('Hinzugefügt'), t('Benutzer wurde dem Mandanten zugewiesen.')); + fetchMandateUsers(mandateId); + } else { + showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers')); + } + } finally { + setIsSubmitting(false); + } + }; + + const handleRemoveUser = async (user: MandateUser) => { + if (!mandateId) return; + const result = await removeUserFromMandate(mandateId, user.userId); + if (!result.success) { + showError(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers')); + } + }; + + const refreshBtn = embedded ? ( + + ) : ( + + ); + + const addBtn = embedded ? ( + + ) : ( + + ); + + return ( +
+
+
+

{t('Mandanten-Benutzer')}

+
+ {refreshBtn} + {addBtn} +
+
+ +
+ { + const user = users.find((u) => u.id === userMandateId); + if (user) { + const result = await removeUserFromMandate(mandateId, user.userId); + return result.success; + } + return false; + }, + }} + emptyMessage={t('Keine Mitglieder gefunden')} + /> +
+
+ + {showAddModal && ( +
+
+
+

{t('Benutzer zum Mandanten hinzufügen')}

+ +
+
+ {availableUsers.length === 0 ? ( +

{t('Alle Benutzer sind bereits diesem Mandanten zugewiesen.')}

+ ) : roleOptions.length === 0 ? ( +
+
+ {t('Lade Rollen')} +
+ ) : ( + setShowAddModal(false)} + submitButtonText={isSubmitting ? t('Hinzufügen…') : t('Hinzufügen')} + cancelButtonText={t('Abbrechen')} + /> + )} +
+
+
+ )} +
+ ); +}; + +export default MandateUsersPanel; diff --git a/src/hooks/useMandates.ts b/src/hooks/useMandates.ts index 9444acd..955522d 100644 --- a/src/hooks/useMandates.ts +++ b/src/hooks/useMandates.ts @@ -30,6 +30,17 @@ import type { ColumnConfig } from '../components/FormGenerator/FormGeneratorTabl // Re-export types export type { Mandate, MandateCreateData, MandateUpdateData, PaginationParams }; +/** List-table columns only; invoice/address fields stay in the edit form. */ +const MANDATE_LIST_COLUMN_KEYS = ['name', 'label', 'enabled', 'mfaRequired', 'isSystem'] as const; + +const MANDATE_LIST_COLUMN_WIDTHS: Record = { + name: 120, + label: 200, + enabled: 88, + mfaRequired: 100, + isSystem: 110, +}; + export interface AttributeDefinition { name: string; type: string; @@ -157,17 +168,21 @@ export function useAdminMandates() { // Generate columns from attributes (types merged via resolveColumnTypes) const columns: ColumnConfig[] = useMemo(() => { - const raw = attributes.map(attr => ({ - key: attr.name, - label: attr.label || attr.name, - sortable: attr.sortable !== false, - filterable: attr.filterable !== false, - searchable: attr.searchable !== false, - width: attr.width || 150, - minWidth: attr.minWidth || 100, - maxWidth: attr.maxWidth || 400, - displayField: (attr as any).displayField, - })); + const raw = MANDATE_LIST_COLUMN_KEYS.map((key) => { + const attr = attributes.find((a) => a.name === key); + if (!attr) return null; + return { + key: attr.name, + label: attr.label || attr.name, + sortable: attr.sortable !== false, + filterable: attr.filterable !== false, + searchable: attr.searchable !== false, + width: MANDATE_LIST_COLUMN_WIDTHS[attr.name] ?? attr.width ?? 120, + minWidth: attr.minWidth ?? 72, + maxWidth: attr.maxWidth ?? 240, + displayField: (attr as { displayField?: string }).displayField, + }; + }).filter((c): c is NonNullable => c != null); return resolveColumnTypes(raw, attributes); }, [attributes]); diff --git a/src/hooks/useUserMandates.ts b/src/hooks/useUserMandates.ts index bc1dc43..f288898 100644 --- a/src/hooks/useUserMandates.ts +++ b/src/hooks/useUserMandates.ts @@ -235,13 +235,14 @@ export function useUserMandates() { * roles should be offered for user assignment - NOT global templates. */ const fetchRoles = useCallback(async (mandateId?: string): Promise => { + if (!mandateId) return []; try { - const params: Record = {}; - if (mandateId) { - params.mandateId = mandateId; - params.scopeFilter = 'mandate'; - } - const response = await api.get('/api/rbac/roles', { params }); + const headers: Record = { 'X-Mandate-Id': mandateId }; + const params: Record = { + mandateId, + includeTemplates: 'false', + }; + const response = await api.get('/api/rbac/roles', { headers, params }); let roles: Role[] = []; if (response.data?.items && Array.isArray(response.data.items)) { roles = response.data.items; diff --git a/src/pages/admin/AdminMandatesPage.tsx b/src/pages/admin/AdminMandatesPage.tsx index 138c7b1..b615f6a 100644 --- a/src/pages/admin/AdminMandatesPage.tsx +++ b/src/pages/admin/AdminMandatesPage.tsx @@ -4,7 +4,7 @@ * Admin page for managing Mandates (tenants) using FormGeneratorTable. */ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAdminMandates, useMandateFormAttributes, type Mandate } from '../../hooks/useMandates'; import { useApiRequest } from '../../hooks/useApi'; @@ -17,6 +17,7 @@ import { useToast } from '../../contexts/ToastContext'; import { usePrompt } from '../../hooks/usePrompt'; import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; +import { MandateUsersPanel } from '../../components/admin/MandateUsersPanel'; import { FaPlus, FaSync, FaUsers, FaLock, FaSkullCrossbones } from 'react-icons/fa'; import { getUserDataCache } from '../../utils/userCache'; import styles from './Admin.module.css'; @@ -59,6 +60,24 @@ export const AdminMandatesPage: React.FC = () => { /** Mandate row merged with billing fields for FormGenerator */ const [editingFormData, setEditingFormData] = useState | null>(null); const [editingBillingWarning, setEditingBillingWarning] = useState(null); + const [expandedMandateIds, setExpandedMandateIds] = useState>(new Set()); + + const isMandateExpanded = useCallback( + (mandate: Mandate) => expandedMandateIds.has(mandate.id), + [expandedMandateIds], + ); + + const toggleMandateExpand = useCallback((mandate: Mandate) => { + setExpandedMandateIds((prev) => { + const next = new Set(prev); + if (next.has(mandate.id)) { + next.delete(mandate.id); + } else { + next.add(mandate.id); + } + return next; + }); + }, []); const isPlatformAdmin = getUserDataCache()?.isPlatformAdmin === true; @@ -246,6 +265,10 @@ export const AdminMandatesPage: React.FC = () => { sortable={true} selectable={true} actionButtons={[ + { + type: 'expand' as const, + title: t('Benutzer anzeigen'), + }, ...(canUpdate ? [{ type: 'edit' as const, onAction: handleEditClick, @@ -273,6 +296,9 @@ export const AdminMandatesPage: React.FC = () => { : false, }] : []} onDelete={handleDeleteMandate} + renderExpandedRow={(mandate) => ( + + )} hookData={{ refetch, permissions, @@ -280,6 +306,9 @@ export const AdminMandatesPage: React.FC = () => { handleDelete, handleInlineUpdate, updateOptimistically, + expandedRowIds: expandedMandateIds, + isRowExpanded: isMandateExpanded, + toggleExpandedRow: toggleMandateExpand, }} emptyMessage={t('Keine Mandanten gefunden')} /> diff --git a/src/pages/admin/AdminUserMandatesPage.tsx b/src/pages/admin/AdminUserMandatesPage.tsx index 8d9f5fa..e7d0662 100644 --- a/src/pages/admin/AdminUserMandatesPage.tsx +++ b/src/pages/admin/AdminUserMandatesPage.tsx @@ -1,254 +1,37 @@ /** * AdminUserMandatesPage - * + * * Admin page for managing user-mandate memberships. * Allows assigning users to mandates and managing their roles within mandates. */ -import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; -import { useUserMandates, type MandateUser, type Mandate, type Role, type PaginationParams } from '../../hooks/useUserMandates'; -import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable'; -import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm'; -import { FaPlus, FaSync, FaBuilding } from 'react-icons/fa'; -import { useToast } from '../../contexts/ToastContext'; -import { useApiRequest } from '../../hooks/useApi'; -import { fetchAttributes } from '../../api/attributesApi'; -import { resolveColumnTypes } from '../../utils/columnTypeResolver'; -import type { ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable'; +import React, { useState, useEffect } from 'react'; +import { useUserMandates, type Mandate } from '../../hooks/useUserMandates'; +import { FaSync, FaBuilding } from 'react-icons/fa'; +import { MandateUsersPanel } from '../../components/admin/MandateUsersPanel'; import styles from './Admin.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; import { mandateDisplayLabel } from '../../utils/mandateDisplayUtils'; export const AdminUserMandatesPage: React.FC = () => { - const { t } = useLanguage(); + const { t } = useLanguage(); - const { showError } = useToast(); - const { request } = useApiRequest(); - const { - users, - loading, - error, - pagination, - fetchMandateUsers, - addUserToMandate, - removeUserFromMandate, - updateUserRoles, - fetchMandates, - fetchRoles, - fetchAllUsers, - } = useUserMandates(); - - // Store current mandateId for refetch - const currentMandateIdRef = useRef(''); + const { error, fetchMandates } = useUserMandates(); - // State const [mandates, setMandates] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(''); - const [roles, setRoles] = useState([]); - const [allUsers, setAllUsers] = useState>([]); - const [showAddModal, setShowAddModal] = useState(false); - const [editingUser, setEditingUser] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); - const [backendAttributes, setBackendAttributes] = useState([]); - // Load mandates and attributes on mount useEffect(() => { const loadMandates = async () => { const data = await fetchMandates(); setMandates(data); - // Auto-select first mandate if available - if (data.length > 0 && !selectedMandateId) { - setSelectedMandateId(data[0].id); + if (data.length > 0) { + setSelectedMandateId((prev) => prev || data[0].id); } }; loadMandates(); - fetchAttributes(request, 'UserMandateView') - .then(setBackendAttributes) - .catch(() => setBackendAttributes([])); - }, [fetchMandates, request]); - - // Load users when mandate changes - useEffect(() => { - if (selectedMandateId) { - currentMandateIdRef.current = selectedMandateId; - fetchMandateUsers(selectedMandateId); - fetchRoles(selectedMandateId).then(setRoles); - } - }, [selectedMandateId, fetchMandateUsers, fetchRoles]); - - // Refetch wrapper that accepts pagination params from FormGeneratorTable - const refetchWithParams = useCallback(async (paginationParams?: PaginationParams) => { - const mandateId = currentMandateIdRef.current; - if (!mandateId) return; - // If pagination params provided, pass them; otherwise just use mandateId - if (paginationParams && Object.keys(paginationParams).length > 0) { - return fetchMandateUsers(paginationParams); - } - return fetchMandateUsers(mandateId); - }, [fetchMandateUsers]); - - // Load all users for the add modal - useEffect(() => { - fetchAllUsers().then(setAllUsers); - }, [fetchAllUsers]); - - // Get users not yet in the mandate - const availableUsers = useMemo(() => { - const existingUserIds = new Set(users.map(u => u.userId)); - return allUsers.filter(u => !existingUserIds.has(u.id)); - }, [allUsers, users]); - - const _rawColumns: ColumnConfig[] = useMemo(() => [ - { - key: 'username', - label: t('Benutzername'), - sortable: true, - filterable: true, - searchable: true, - width: 150, - }, - { - key: 'email', - label: t('E-Mail'), - sortable: true, - filterable: true, - searchable: true, - width: 200, - }, - { - key: 'fullName', - label: t('Vollständiger Name'), - sortable: true, - filterable: true, - searchable: true, - width: 180, - }, - { - key: 'roleLabels', - label: t('Rollen'), - sortable: false, - filterable: false, - searchable: true, - width: 200, - formatter: (value: string[]) => { - if (!value || value.length === 0) return '-'; - return value.join(', '); - }, - }, - { - key: 'enabled', - label: t('Aktiv'), - sortable: true, - filterable: true, - searchable: false, - width: 80, - }, - ], [t]); - - const columns = useMemo( - () => resolveColumnTypes(_rawColumns, backendAttributes), - [_rawColumns, backendAttributes], - ); - - // Dynamic options for forms (users and roles) - const userOptions = useMemo(() => - availableUsers.map(u => ({ - value: u.id, - label: `${u.username} ${u.email ? `(${u.email})` : ''}` - })), [availableUsers]); - - const roleOptions = useMemo(() => - roles.filter(r => !r.featureInstanceId).map(r => ({ - value: r.id, - label: r.roleLabel - })), [roles]); - - // Form attributes for adding a user - uses dynamic options - // Note: This is an operational form for junction table, not direct model editing - const addUserFields: AttributeDefinition[] = useMemo(() => { - // Check if backend has userId attribute to get label/description - const userIdAttr = backendAttributes.find(a => a.name === 'userId'); - const roleIdsAttr = backendAttributes.find(a => a.name === 'roleIds'); - - return [ - { - name: 'targetUserId', - label: userIdAttr?.label || t('Benutzer'), - type: 'enum' as any, - required: true, - options: userOptions, - }, - { - name: 'roleIds', - label: roleIdsAttr?.label || t('Rollen'), - type: 'multiselect' as any, - required: true, - options: roleOptions, - } - ]; - }, [userOptions, roleOptions, backendAttributes, t]); - - // Form attributes for editing user roles - const editRolesFields: AttributeDefinition[] = useMemo(() => { - const roleIdsAttr = backendAttributes.find(a => a.name === 'roleIds'); - - return [{ - name: 'roleIds', - label: roleIdsAttr?.label || t('Rollen'), - type: 'multiselect' as any, - required: true, - options: roleOptions, - }]; - }, [roleOptions, backendAttributes, t]); - - // Handle add user submit - const handleAddUser = async (data: { targetUserId: string; roleIds: string[] }) => { - if (!selectedMandateId) return; - setIsSubmitting(true); - try { - const result = await addUserToMandate(selectedMandateId, data); - if (result.success) { - setShowAddModal(false); - fetchMandateUsers(selectedMandateId); - } else { - showError(t('Fehler'), result.error || t('Fehler beim Hinzufügen des Benutzers')); - } - } finally { - setIsSubmitting(false); - } - }; - - // Handle edit roles submit - const handleEditRoles = async (data: { roleIds: string[] }) => { - if (!selectedMandateId || !editingUser) return; - setIsSubmitting(true); - try { - const result = await updateUserRoles(selectedMandateId, editingUser.userId, data.roleIds); - if (result.success) { - setEditingUser(null); - fetchMandateUsers(selectedMandateId); - } else { - showError(t('Fehler'), result.error || t('Fehler beim Aktualisieren der Rollen')); - } - } finally { - setIsSubmitting(false); - } - }; - - // Handle remove user (confirmation handled by DeleteActionButton) - const handleRemoveUser = async (user: MandateUser) => { - if (!selectedMandateId) return; - const result = await removeUserFromMandate(selectedMandateId, user.userId); - if (!result.success) { - showError(t('Fehler'), result.error || t('Fehler beim Entfernen des Benutzers')); - } - }; - - // Handle edit click - const handleEditClick = (user: MandateUser) => { - setEditingUser(user); - }; + }, [fetchMandates]); if (error && !selectedMandateId) { return ( @@ -258,7 +41,7 @@ export const AdminUserMandatesPage: React.FC = () => {

{t('Fehler')}: {error}

-
@@ -275,7 +58,6 @@ export const AdminUserMandatesPage: React.FC = () => { - {/* Mandate Selector */}
- - {selectedMandateId && ( -
- - -
- )}
- {/* Content */} {!selectedMandateId ? (
@@ -327,110 +89,7 @@ export const AdminUserMandatesPage: React.FC = () => {
) : (
- { - // Find user by UserMandate ID to get userId for API call - const user = users.find(u => u.id === userMandateId); - if (user) { - const result = await removeUserFromMandate(selectedMandateId, user.userId); - return result.success; - } - return false; - }, - }} - emptyMessage={t('Keine Mitglieder gefunden')} - /> -
- )} - - {/* Add User Modal */} - {showAddModal && ( -
-
-
-

{t('Benutzer zum Mandanten hinzufügen')}

- -
-
- {availableUsers.length === 0 ? ( -

{t('Alle Benutzer sind bereits diesem')}

- ) : roleOptions.length === 0 ? ( -
-
- {t('Lade Rollen')} -
- ) : ( - setShowAddModal(false)} - submitButtonText={isSubmitting ? t('Hinzufügen') : t('Hinzufügen')} - cancelButtonText={t('Abbrechen')} - /> - )} -
-
-
- )} - - {/* Edit Roles Modal */} - {editingUser && ( -
-
-
-

- {t('Rollen bearbeiten')}: {editingUser.username} -

- -
-
- setEditingUser(null)} - submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')} - cancelButtonText={t('Abbrechen')} - /> -
-
+
)}