fixed uid mapping to id
This commit is contained in:
parent
7f07a55c91
commit
537b624c59
24 changed files with 1385 additions and 142 deletions
14
src/App.tsx
14
src/App.tsx
|
|
@ -27,6 +27,7 @@ import { InvitePage } from './pages/InvitePage';
|
|||
import { AuthProvider } from './providers/auth/AuthProvider';
|
||||
import { ProtectedRoute } from './providers/auth/ProtectedRoute';
|
||||
import { LanguageProvider } from './providers/language/LanguageContext';
|
||||
import { ToastProvider } from './contexts/ToastContext';
|
||||
|
||||
// Layouts
|
||||
import { MainLayout } from './layouts/MainLayout';
|
||||
|
|
@ -36,7 +37,7 @@ import { FeatureLayout } from './layouts/FeatureLayout';
|
|||
import { DashboardPage } from './pages/Dashboard';
|
||||
import { SettingsPage } from './pages/Settings';
|
||||
import { FeatureViewPage } from './pages/FeatureView';
|
||||
import { AdminMandatesPage, AdminUsersPage, AdminRolesPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage } from './pages/admin';
|
||||
import { AdminMandatesPage, AdminUsersPage, AdminRolesPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureInstanceUsersPage } from './pages/admin';
|
||||
|
||||
function App() {
|
||||
// Load saved theme preference and set app name on app mount
|
||||
|
|
@ -64,8 +65,9 @@ function App() {
|
|||
return (
|
||||
<LanguageProvider>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<ToastProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
{/* ================================================== */}
|
||||
{/* PUBLIC AUTH ROUTES - NO AUTHENTICATION REQUIRED */}
|
||||
{/* ================================================== */}
|
||||
|
|
@ -123,6 +125,7 @@ function App() {
|
|||
<Route path="roles" element={<AdminRolesPage />} />
|
||||
<Route path="user-mandates" element={<AdminUserMandatesPage />} />
|
||||
<Route path="feature-instances" element={<AdminFeatureAccessPage />} />
|
||||
<Route path="feature-users" element={<AdminFeatureInstanceUsersPage />} />
|
||||
<Route path="invitations" element={<AdminInvitationsPage />} />
|
||||
<Route path="mandate-roles" element={<AdminMandateRolesPage />} />
|
||||
</Route>
|
||||
|
|
@ -136,8 +139,9 @@ function App() {
|
|||
<MainLayout />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Routes>
|
||||
</Router>
|
||||
</ToastProvider>
|
||||
</AuthProvider>
|
||||
</LanguageProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Table Container */
|
||||
/* Table Container - flipped to show horizontal scrollbar at top */
|
||||
.tableContainer {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
|
|
@ -30,6 +30,13 @@
|
|||
min-height: 0;
|
||||
/* Ensure scrolling within container */
|
||||
max-height: 100%;
|
||||
/* Flip container to move horizontal scrollbar to top */
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
/* Flip table content back to normal orientation */
|
||||
.tableContainer > * {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
/* Empty table styling - no extra space, just header */
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { useMandates, useFeatureStore } from '../../stores/featureStore';
|
|||
import { useCurrentUser } from '../../hooks/useUsers';
|
||||
import { FEATURE_REGISTRY, getLabel } from '../../types/mandate';
|
||||
import type { Mandate, MandateFeature, FeatureInstance } from '../../types/mandate';
|
||||
import { FaHome, FaCog, FaBriefcase, FaRobot, FaPlay, FaBuilding, FaUsers, FaUserShield, FaUserTag, FaCubes, FaEnvelopeOpenText, FaKey } from 'react-icons/fa';
|
||||
import { FaHome, FaCog, FaBriefcase, FaRobot, FaPlay, FaBuilding, FaUsers, FaUserShield, FaUserTag, FaCubes, FaEnvelopeOpenText, FaKey, FaUsersCog } from 'react-icons/fa';
|
||||
import { TreeNavigation, type TreeItem, type TreeNodeItem } from './TreeNavigation';
|
||||
import styles from './MandateNavigation.module.css';
|
||||
|
||||
|
|
@ -237,6 +237,12 @@ export const MandateNavigation: React.FC = () => {
|
|||
icon: <FaCubes />,
|
||||
path: '/admin/feature-instances',
|
||||
},
|
||||
{
|
||||
id: 'admin-feature-users',
|
||||
label: 'Feature-Benutzer',
|
||||
icon: <FaUsersCog />,
|
||||
path: '/admin/feature-users',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
|||
130
src/components/UiComponents/Toast/Toast.module.css
Normal file
130
src/components/UiComponents/Toast/Toast.module.css
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
.toastContainer {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-width: 400px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
background: var(--surface-color, #ffffff);
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
pointer-events: auto;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-left: 4px solid #22c55e;
|
||||
background: linear-gradient(90deg, rgba(34, 197, 94, 0.08) 0%, var(--surface-color, #ffffff) 100%);
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-left: 4px solid #ef4444;
|
||||
background: linear-gradient(90deg, rgba(239, 68, 68, 0.08) 0%, var(--surface-color, #ffffff) 100%);
|
||||
}
|
||||
|
||||
.toast.warning {
|
||||
border-left: 4px solid #f59e0b;
|
||||
background: linear-gradient(90deg, rgba(245, 158, 11, 0.08) 0%, var(--surface-color, #ffffff) 100%);
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
border-left: 4px solid #3b82f6;
|
||||
background: linear-gradient(90deg, rgba(59, 130, 246, 0.08) 0%, var(--surface-color, #ffffff) 100%);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.icon.success {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.icon.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.icon.warning {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.icon.info {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color, #1f2937);
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary, #6b7280);
|
||||
margin: 0;
|
||||
white-space: pre-line;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary, #9ca3af);
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
background: var(--hover-color, rgba(0, 0, 0, 0.05));
|
||||
color: var(--text-color, #374151);
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
:global(.dark) .toast {
|
||||
background: var(--surface-color, #1f2937);
|
||||
border-color: var(--border-color, #374151);
|
||||
}
|
||||
|
||||
:global(.dark) .toast.success {
|
||||
background: linear-gradient(90deg, rgba(34, 197, 94, 0.12) 0%, var(--surface-color, #1f2937) 100%);
|
||||
}
|
||||
|
||||
:global(.dark) .toast.error {
|
||||
background: linear-gradient(90deg, rgba(239, 68, 68, 0.12) 0%, var(--surface-color, #1f2937) 100%);
|
||||
}
|
||||
|
||||
:global(.dark) .toast.warning {
|
||||
background: linear-gradient(90deg, rgba(245, 158, 11, 0.12) 0%, var(--surface-color, #1f2937) 100%);
|
||||
}
|
||||
|
||||
:global(.dark) .toast.info {
|
||||
background: linear-gradient(90deg, rgba(59, 130, 246, 0.12) 0%, var(--surface-color, #1f2937) 100%);
|
||||
}
|
||||
76
src/components/UiComponents/Toast/Toast.tsx
Normal file
76
src/components/UiComponents/Toast/Toast.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { FaCheckCircle, FaExclamationCircle, FaExclamationTriangle, FaInfoCircle, FaTimes } from 'react-icons/fa';
|
||||
import styles from './Toast.module.css';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface ToastData {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
title: string;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ToastProps {
|
||||
toast: ToastData;
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
const _getIcon = (type: ToastType) => {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return <FaCheckCircle />;
|
||||
case 'error':
|
||||
return <FaExclamationCircle />;
|
||||
case 'warning':
|
||||
return <FaExclamationTriangle />;
|
||||
case 'info':
|
||||
return <FaInfoCircle />;
|
||||
}
|
||||
};
|
||||
|
||||
export const Toast: React.FC<ToastProps> = ({ toast, onClose }) => {
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.toast} ${styles[toast.type]}`}
|
||||
initial={{ opacity: 0, x: 100, scale: 0.9 }}
|
||||
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, x: 100, scale: 0.9 }}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
|
||||
layout
|
||||
>
|
||||
<div className={`${styles.icon} ${styles[toast.type]}`}>
|
||||
{_getIcon(toast.type)}
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<p className={styles.title}>{toast.title}</p>
|
||||
{toast.message && <p className={styles.message}>{toast.message}</p>}
|
||||
</div>
|
||||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={() => onClose(toast.id)}
|
||||
aria-label="Schließen"
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ToastContainerProps {
|
||||
toasts: ToastData[];
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ToastContainer: React.FC<ToastContainerProps> = ({ toasts, onClose }) => {
|
||||
return (
|
||||
<div className={styles.toastContainer}>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{toasts.map((toast) => (
|
||||
<Toast key={toast.id} toast={toast} onClose={onClose} />
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
2
src/components/UiComponents/Toast/index.ts
Normal file
2
src/components/UiComponents/Toast/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Toast, ToastContainer } from './Toast';
|
||||
export type { ToastType, ToastData } from './Toast';
|
||||
|
|
@ -19,3 +19,4 @@ export type { WorkflowStatusProps } from './WorkflowStatus/WorkflowStatusTypes';
|
|||
export * from './AutoScroll';
|
||||
export * from './Tabs';
|
||||
export type { TabsProps, Tab } from './Tabs';
|
||||
export * from './Toast';
|
||||
|
|
|
|||
97
src/contexts/ToastContext.tsx
Normal file
97
src/contexts/ToastContext.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { ToastContainer, ToastData, ToastType } from '../components/UiComponents/Toast';
|
||||
|
||||
interface ToastOptions {
|
||||
title: string;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ToastContextValue {
|
||||
showToast: (type: ToastType, options: ToastOptions) => void;
|
||||
showSuccess: (title: string, message?: string) => void;
|
||||
showError: (title: string, message?: string) => void;
|
||||
showWarning: (title: string, message?: string) => void;
|
||||
showInfo: (title: string, message?: string) => void;
|
||||
closeToast: (id: string) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | null>(null);
|
||||
|
||||
const DEFAULT_DURATION = 5000;
|
||||
|
||||
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [toasts, setToasts] = useState<ToastData[]>([]);
|
||||
const timeoutRefs = useRef<Map<string, NodeJS.Timeout>>(new Map());
|
||||
|
||||
const closeToast = useCallback((id: string) => {
|
||||
// Clear timeout if exists
|
||||
const timeout = timeoutRefs.current.get(id);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeoutRefs.current.delete(id);
|
||||
}
|
||||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback((type: ToastType, options: ToastOptions) => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const duration = options.duration ?? DEFAULT_DURATION;
|
||||
|
||||
const newToast: ToastData = {
|
||||
id,
|
||||
type,
|
||||
title: options.title,
|
||||
message: options.message,
|
||||
duration,
|
||||
};
|
||||
|
||||
setToasts((prev) => [...prev, newToast]);
|
||||
|
||||
// Auto-close after duration
|
||||
if (duration > 0) {
|
||||
const timeout = setTimeout(() => {
|
||||
closeToast(id);
|
||||
}, duration);
|
||||
timeoutRefs.current.set(id, timeout);
|
||||
}
|
||||
}, [closeToast]);
|
||||
|
||||
const showSuccess = useCallback((title: string, message?: string) => {
|
||||
showToast('success', { title, message });
|
||||
}, [showToast]);
|
||||
|
||||
const showError = useCallback((title: string, message?: string) => {
|
||||
showToast('error', { title, message, duration: 8000 }); // Errors stay longer
|
||||
}, [showToast]);
|
||||
|
||||
const showWarning = useCallback((title: string, message?: string) => {
|
||||
showToast('warning', { title, message, duration: 6000 });
|
||||
}, [showToast]);
|
||||
|
||||
const showInfo = useCallback((title: string, message?: string) => {
|
||||
showToast('info', { title, message });
|
||||
}, [showToast]);
|
||||
|
||||
// Cleanup timeouts on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
timeoutRefs.current.forEach((timeout) => clearTimeout(timeout));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast, showSuccess, showError, showWarning, showInfo, closeToast }}>
|
||||
{children}
|
||||
<ToastContainer toasts={toasts} onClose={closeToast} />
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useToast = (): ToastContextValue => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) {
|
||||
throw new Error('useToast must be used within a ToastProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -2357,8 +2357,6 @@ const PageRenderer: React.FC<PageRendererProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Overlay Component */}
|
||||
{hookData?.MessageOverlayComponent && <hookData.MessageOverlayComponent />}
|
||||
</div>
|
||||
</DragDropOverlay>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -82,8 +82,7 @@ const createFilesHook = () => {
|
|||
previewingFiles,
|
||||
editingFiles,
|
||||
downloadingFiles,
|
||||
uploadingFile,
|
||||
MessageOverlayComponent
|
||||
uploadingFile
|
||||
} = useFileOperations();
|
||||
|
||||
const generatedColumns = attributes && attributes.length > 0
|
||||
|
|
@ -148,8 +147,6 @@ const createFilesHook = () => {
|
|||
uploadingFile,
|
||||
// Upload functionality for button and drag-and-drop
|
||||
handleUpload,
|
||||
// Message overlay component
|
||||
MessageOverlayComponent,
|
||||
// Attributes and permissions for dynamic column/button generation
|
||||
attributes,
|
||||
permissions,
|
||||
|
|
|
|||
|
|
@ -251,8 +251,6 @@ export interface GenericDataHook {
|
|||
dashboardTree?: any; // Dashboard log tree structure
|
||||
onToggleOperationExpanded?: (operationId: string) => void;
|
||||
getChildOperations?: (parentId: string | null) => string[];
|
||||
// Message overlay component
|
||||
MessageOverlayComponent?: () => React.ReactElement;
|
||||
// Settings-specific properties
|
||||
settingsData?: any; // Unified data object for settings fields
|
||||
settingsFields?: Record<string, SettingsFieldConfig[]>; // Field definitions per sectionId
|
||||
|
|
|
|||
|
|
@ -5,10 +5,25 @@
|
|||
* Uses the /api/features endpoints.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
// Types
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
filters?: Record<string, any>;
|
||||
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||
}
|
||||
|
||||
export interface PaginationMetadata {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface Feature {
|
||||
code: string;
|
||||
label: string | { [key: string]: string };
|
||||
|
|
@ -33,15 +48,29 @@ export interface FeatureAccess {
|
|||
}
|
||||
|
||||
export interface FeatureAccessUser {
|
||||
id: string; // FeatureAccess ID as primary key
|
||||
userId: string;
|
||||
username: string;
|
||||
email?: string;
|
||||
fullName?: string;
|
||||
featureAccessId: string;
|
||||
roleIds: string[];
|
||||
roleLabels: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface FeatureInstanceRole {
|
||||
id: string;
|
||||
roleLabel: string;
|
||||
description?: { [key: string]: string };
|
||||
featureCode?: string;
|
||||
isSystemRole?: boolean;
|
||||
}
|
||||
|
||||
export interface AddUserToInstanceRequest {
|
||||
userId: string;
|
||||
roleIds: string[];
|
||||
}
|
||||
|
||||
export interface FeatureInstanceCreate {
|
||||
featureCode: string;
|
||||
label: string;
|
||||
|
|
@ -56,6 +85,11 @@ export function useFeatureAccess() {
|
|||
const [instances, setInstances] = useState<FeatureInstance[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [instancesPagination, setInstancesPagination] = useState<PaginationMetadata | null>(null);
|
||||
|
||||
// Store current context for refetch
|
||||
const currentMandateIdRef = useRef<string>('');
|
||||
const currentFeatureCodeRef = useRef<string | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Fetch all available features
|
||||
|
|
@ -79,29 +113,73 @@ export function useFeatureAccess() {
|
|||
}, []);
|
||||
|
||||
/**
|
||||
* Fetch feature instances for a mandate
|
||||
* Fetch feature instances for a mandate with optional pagination
|
||||
*/
|
||||
const fetchInstances = useCallback(async (mandateId: string, featureCode?: string): Promise<FeatureInstance[]> => {
|
||||
const fetchInstances = useCallback(async (
|
||||
mandateIdOrPagination?: string | PaginationParams,
|
||||
featureCode?: string
|
||||
): Promise<FeatureInstance[]> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
let mandateId: string;
|
||||
let paginationParams: PaginationParams = {};
|
||||
|
||||
// Handle backward compatibility
|
||||
if (typeof mandateIdOrPagination === 'string') {
|
||||
mandateId = mandateIdOrPagination;
|
||||
currentMandateIdRef.current = mandateId;
|
||||
currentFeatureCodeRef.current = featureCode;
|
||||
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
|
||||
paginationParams = mandateIdOrPagination;
|
||||
mandateId = currentMandateIdRef.current;
|
||||
featureCode = currentFeatureCodeRef.current;
|
||||
} else {
|
||||
mandateId = currentMandateIdRef.current;
|
||||
featureCode = currentFeatureCodeRef.current;
|
||||
}
|
||||
|
||||
if (!mandateId) {
|
||||
setLoading(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
let url = '/api/features/instances';
|
||||
const params = new URLSearchParams();
|
||||
if (featureCode) {
|
||||
url += `?featureCode=${encodeURIComponent(featureCode)}`;
|
||||
params.append('featureCode', featureCode);
|
||||
}
|
||||
if (Object.keys(paginationParams).length > 0) {
|
||||
params.append('pagination', JSON.stringify(paginationParams));
|
||||
}
|
||||
|
||||
const url = params.toString()
|
||||
? `/api/features/instances?${params.toString()}`
|
||||
: '/api/features/instances';
|
||||
|
||||
const response = await api.get(url, {
|
||||
headers: {
|
||||
'X-Mandate-Id': mandateId
|
||||
}
|
||||
});
|
||||
const data = Array.isArray(response.data) ? response.data : [];
|
||||
|
||||
let data: FeatureInstance[] = [];
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
data = response.data.items;
|
||||
if (response.data.pagination) {
|
||||
setInstancesPagination(response.data.pagination);
|
||||
}
|
||||
} else {
|
||||
data = Array.isArray(response.data) ? response.data : [];
|
||||
}
|
||||
|
||||
setInstances(data);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch feature instances';
|
||||
setError(errorMessage);
|
||||
setInstances([]);
|
||||
setInstancesPagination(null);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -231,9 +309,142 @@ export function useFeatureAccess() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// Feature Instance Users Management
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Fetch all users with access to a specific feature instance
|
||||
*/
|
||||
const fetchInstanceUsers = useCallback(async (
|
||||
mandateId: string,
|
||||
instanceId: string
|
||||
): Promise<FeatureAccessUser[]> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await api.get(`/api/features/instances/${instanceId}/users`, {
|
||||
headers: {
|
||||
'X-Mandate-Id': mandateId
|
||||
}
|
||||
});
|
||||
return Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch instance users';
|
||||
setError(errorMessage);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Add a user to a feature instance with specified roles
|
||||
*/
|
||||
const addUserToInstance = useCallback(async (
|
||||
mandateId: string,
|
||||
instanceId: string,
|
||||
data: AddUserToInstanceRequest
|
||||
): Promise<{ success: boolean; data?: any; error?: string }> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await api.post(`/api/features/instances/${instanceId}/users`, data, {
|
||||
headers: {
|
||||
'X-Mandate-Id': mandateId
|
||||
}
|
||||
});
|
||||
return { success: true, data: response.data };
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to add user to instance';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Remove a user's access from a feature instance
|
||||
*/
|
||||
const removeUserFromInstance = useCallback(async (
|
||||
mandateId: string,
|
||||
instanceId: string,
|
||||
userId: string
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.delete(`/api/features/instances/${instanceId}/users/${userId}`, {
|
||||
headers: {
|
||||
'X-Mandate-Id': mandateId
|
||||
}
|
||||
});
|
||||
return { success: true };
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to remove user from instance';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Update a user's roles in a feature instance
|
||||
*/
|
||||
const updateInstanceUserRoles = useCallback(async (
|
||||
mandateId: string,
|
||||
instanceId: string,
|
||||
userId: string,
|
||||
roleIds: string[]
|
||||
): Promise<{ success: boolean; data?: any; error?: string }> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await api.put(
|
||||
`/api/features/instances/${instanceId}/users/${userId}/roles`,
|
||||
roleIds,
|
||||
{
|
||||
headers: {
|
||||
'X-Mandate-Id': mandateId
|
||||
}
|
||||
}
|
||||
);
|
||||
return { success: true, data: response.data };
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to update user roles';
|
||||
setError(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get available roles for a feature instance
|
||||
*/
|
||||
const fetchInstanceRoles = useCallback(async (
|
||||
mandateId: string,
|
||||
instanceId: string
|
||||
): Promise<FeatureInstanceRole[]> => {
|
||||
try {
|
||||
const response = await api.get(`/api/features/instances/${instanceId}/available-roles`, {
|
||||
headers: {
|
||||
'X-Mandate-Id': mandateId
|
||||
}
|
||||
});
|
||||
return Array.isArray(response.data) ? response.data : [];
|
||||
} catch (err: any) {
|
||||
console.error('Error fetching instance roles:', err);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
features,
|
||||
instances,
|
||||
instancesPagination,
|
||||
loading,
|
||||
error,
|
||||
fetchFeatures,
|
||||
|
|
@ -243,6 +454,12 @@ export function useFeatureAccess() {
|
|||
syncInstanceRoles,
|
||||
fetchMyFeatureInstances,
|
||||
fetchTemplateRoles,
|
||||
// Instance users management
|
||||
fetchInstanceUsers,
|
||||
addUserToInstance,
|
||||
removeUserFromInstance,
|
||||
updateInstanceUserRoles,
|
||||
fetchInstanceRoles,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import api from '../api';
|
||||
import { MessageOverlay } from '../components/UiComponents';
|
||||
import type { MessageMode } from '../components/UiComponents';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
import { getUserDataCache } from '../utils/userCache';
|
||||
import { useApiRequest } from './useApi';
|
||||
|
|
@ -319,9 +318,8 @@ export function useFileOperations() {
|
|||
const [previewingFiles, setPreviewingFiles] = useState<Set<string>>(new Set());
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
|
||||
// Warning state
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const [warningData, setWarningData] = useState<{ header: string; message: string; mode: MessageMode } | null>(null);
|
||||
// Toast for notifications
|
||||
const { showWarning } = useToast();
|
||||
|
||||
// Language context
|
||||
const { t } = useLanguage();
|
||||
|
|
@ -502,27 +500,7 @@ export function useFileOperations() {
|
|||
const fileName = fileData.originalFileName || file.name;
|
||||
const messageTemplate = t('warning.duplicate_file.message');
|
||||
const message = messageTemplate.replace('{fileName}', fileName);
|
||||
|
||||
// Close any existing warning first
|
||||
if (showWarning) {
|
||||
setShowWarning(false);
|
||||
// Wait a moment before showing the new warning
|
||||
setTimeout(() => {
|
||||
setWarningData({
|
||||
header: t('warning.duplicate_file.title'),
|
||||
message: message,
|
||||
mode: 'warning'
|
||||
});
|
||||
setShowWarning(true);
|
||||
}, 600);
|
||||
} else {
|
||||
setWarningData({
|
||||
header: t('warning.duplicate_file.title'),
|
||||
message: message,
|
||||
mode: 'warning'
|
||||
});
|
||||
setShowWarning(true);
|
||||
}
|
||||
showWarning(t('warning.duplicate_file.title'), message);
|
||||
}
|
||||
|
||||
return { success: true, fileData };
|
||||
|
|
@ -936,15 +914,6 @@ export function useFileOperations() {
|
|||
}
|
||||
};
|
||||
|
||||
// Function to close warning
|
||||
const closeWarning = useCallback(() => {
|
||||
setShowWarning(false);
|
||||
// Delay clearing the data to allow exit animation to complete (matches CSS transition)
|
||||
setTimeout(() => {
|
||||
setWarningData(null);
|
||||
}, 700);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
downloadingFiles,
|
||||
deletingFiles,
|
||||
|
|
@ -961,16 +930,6 @@ export function useFileOperations() {
|
|||
handleFileUpload,
|
||||
handleFileUpdate,
|
||||
handleFilePreview,
|
||||
isLoading,
|
||||
// Message overlay component
|
||||
MessageOverlayComponent: () => React.createElement(MessageOverlay, {
|
||||
header: warningData?.header || '',
|
||||
message: warningData?.message || '',
|
||||
isVisible: showWarning,
|
||||
mode: warningData?.mode || 'info',
|
||||
onClose: closeWarning,
|
||||
autoClose: true,
|
||||
autoCloseDelay: 5000
|
||||
})
|
||||
isLoading
|
||||
};
|
||||
}
|
||||
|
|
@ -5,10 +5,25 @@
|
|||
* Uses the /api/invitations endpoints.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
// Types
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
filters?: Record<string, any>;
|
||||
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||
}
|
||||
|
||||
export interface PaginationMetadata {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface Invitation {
|
||||
id: string;
|
||||
token: string;
|
||||
|
|
@ -63,31 +78,76 @@ export function useInvitations() {
|
|||
const [invitations, setInvitations] = useState<Invitation[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
|
||||
|
||||
// Store current context for refetch
|
||||
const currentMandateIdRef = useRef<string>('');
|
||||
const currentOptionsRef = useRef<{ includeUsed?: boolean; includeExpired?: boolean }>({});
|
||||
|
||||
/**
|
||||
* Fetch all invitations for a mandate
|
||||
* Fetch all invitations for a mandate with optional pagination
|
||||
*/
|
||||
const fetchInvitations = useCallback(async (
|
||||
mandateId: string,
|
||||
mandateIdOrPagination?: string | PaginationParams,
|
||||
options?: { includeUsed?: boolean; includeExpired?: boolean }
|
||||
): Promise<Invitation[]> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
let mandateId: string;
|
||||
let paginationParams: PaginationParams = {};
|
||||
|
||||
// Handle backward compatibility
|
||||
if (typeof mandateIdOrPagination === 'string') {
|
||||
mandateId = mandateIdOrPagination;
|
||||
currentMandateIdRef.current = mandateId;
|
||||
if (options) {
|
||||
currentOptionsRef.current = options;
|
||||
}
|
||||
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
|
||||
// Called with pagination params only (refetch from FormGeneratorTable)
|
||||
paginationParams = mandateIdOrPagination;
|
||||
mandateId = currentMandateIdRef.current;
|
||||
} else {
|
||||
mandateId = currentMandateIdRef.current;
|
||||
}
|
||||
|
||||
if (!mandateId) {
|
||||
setLoading(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
const fetchOptions = options || currentOptionsRef.current;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.includeUsed) params.append('includeUsed', 'true');
|
||||
if (options?.includeExpired) params.append('includeExpired', 'true');
|
||||
if (fetchOptions?.includeUsed) params.append('includeUsed', 'true');
|
||||
if (fetchOptions?.includeExpired) params.append('includeExpired', 'true');
|
||||
if (Object.keys(paginationParams).length > 0) {
|
||||
params.append('pagination', JSON.stringify(paginationParams));
|
||||
}
|
||||
|
||||
const response = await api.get(`/api/invitations/?${params.toString()}`, {
|
||||
headers: { 'X-Mandate-Id': mandateId }
|
||||
});
|
||||
const data = Array.isArray(response.data) ? response.data : [];
|
||||
|
||||
let data: Invitation[] = [];
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
data = response.data.items;
|
||||
if (response.data.pagination) {
|
||||
setPagination(response.data.pagination);
|
||||
}
|
||||
} else {
|
||||
data = Array.isArray(response.data) ? response.data : [];
|
||||
}
|
||||
|
||||
setInvitations(data);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch invitations';
|
||||
setError(errorMessage);
|
||||
setInvitations([]);
|
||||
setPagination(null);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -210,6 +270,7 @@ export function useInvitations() {
|
|||
invitations,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchInvitations,
|
||||
createInvitation,
|
||||
revokeInvitation,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Uses the /api/rbac/roles endpoints.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
// Types
|
||||
|
|
@ -36,6 +36,21 @@ export interface RoleUpdate {
|
|||
mandateId?: string | null;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
filters?: Record<string, any>;
|
||||
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||
}
|
||||
|
||||
export interface PaginationMetadata {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing mandate roles
|
||||
*/
|
||||
|
|
@ -43,39 +58,78 @@ export function useMandateRoles() {
|
|||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
|
||||
|
||||
// Store current mandateId for refetch
|
||||
const currentMandateIdRef = useRef<string | undefined>();
|
||||
|
||||
/**
|
||||
* Fetch all roles (optionally filtered by mandate)
|
||||
* Fetch all roles with pagination support
|
||||
* @param mandateIdOrParams - Either a mandateId string (backward compatible) or pagination params
|
||||
*/
|
||||
const fetchRoles = useCallback(async (mandateId?: string): Promise<Role[]> => {
|
||||
const fetchRoles = useCallback(async (
|
||||
mandateIdOrParams?: string | PaginationParams
|
||||
): Promise<Role[]> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
let paginationParams: PaginationParams = {};
|
||||
let mandateId: string | undefined;
|
||||
|
||||
// Handle backward compatibility: first param can be mandateId string or pagination object
|
||||
if (typeof mandateIdOrParams === 'string') {
|
||||
mandateId = mandateIdOrParams;
|
||||
currentMandateIdRef.current = mandateId;
|
||||
} else if (mandateIdOrParams && typeof mandateIdOrParams === 'object') {
|
||||
paginationParams = mandateIdOrParams;
|
||||
mandateId = currentMandateIdRef.current;
|
||||
}
|
||||
|
||||
if (mandateId) {
|
||||
headers['X-Mandate-Id'] = mandateId;
|
||||
}
|
||||
|
||||
const response = await api.get('/api/rbac/roles', { headers });
|
||||
// Build query params for pagination
|
||||
const queryParams: Record<string, string> = {};
|
||||
if (Object.keys(paginationParams).length > 0) {
|
||||
queryParams.pagination = JSON.stringify(paginationParams);
|
||||
}
|
||||
// Include templates by default for mandate roles view
|
||||
queryParams.includeTemplates = 'true';
|
||||
|
||||
const response = await api.get('/api/rbac/roles', {
|
||||
headers,
|
||||
params: queryParams
|
||||
});
|
||||
|
||||
let data: Role[] = [];
|
||||
let paginationMeta: PaginationMetadata | null = null;
|
||||
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
data = response.data.items;
|
||||
if (response.data.pagination) {
|
||||
paginationMeta = response.data.pagination;
|
||||
}
|
||||
} else if (Array.isArray(response.data)) {
|
||||
data = response.data;
|
||||
}
|
||||
|
||||
// Filter to only show roles for this mandate (or global roles)
|
||||
if (mandateId) {
|
||||
// Only do client-side filtering if no pagination was requested
|
||||
if (mandateId && Object.keys(paginationParams).length === 0) {
|
||||
data = data.filter(r => !r.mandateId || r.mandateId === mandateId);
|
||||
}
|
||||
|
||||
setRoles(data);
|
||||
setPagination(paginationMeta);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch roles';
|
||||
setError(errorMessage);
|
||||
setRoles([]);
|
||||
setPagination(null);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -225,6 +279,7 @@ export function useMandateRoles() {
|
|||
roles,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchRoles,
|
||||
getRole,
|
||||
createRole,
|
||||
|
|
|
|||
|
|
@ -5,17 +5,32 @@
|
|||
* Uses the /api/mandates/{mandateId}/users endpoints.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
// Types
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
filters?: Record<string, any>;
|
||||
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
|
||||
}
|
||||
|
||||
export interface PaginationMetadata {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface MandateUser {
|
||||
id: string; // UserMandate ID as primary key
|
||||
userId: string;
|
||||
username: string;
|
||||
email: string | null;
|
||||
firstname: string | null;
|
||||
lastname: string | null;
|
||||
userMandateId: string;
|
||||
roleIds: string[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
|
@ -26,7 +41,7 @@ export interface UserMandateCreate {
|
|||
}
|
||||
|
||||
export interface UserMandateResponse {
|
||||
userMandateId: string;
|
||||
id: string; // UserMandate ID as primary key
|
||||
userId: string;
|
||||
mandateId: string;
|
||||
roleIds: string[];
|
||||
|
|
@ -56,22 +71,68 @@ export function useUserMandates() {
|
|||
const [users, setUsers] = useState<MandateUser[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState<PaginationMetadata | null>(null);
|
||||
|
||||
// Store current mandate for refetch
|
||||
const currentMandateIdRef = useRef<string>('');
|
||||
|
||||
/**
|
||||
* Fetch all users in a specific mandate
|
||||
* Fetch all users in a specific mandate with optional pagination
|
||||
*/
|
||||
const fetchMandateUsers = useCallback(async (mandateId: string): Promise<MandateUser[]> => {
|
||||
const fetchMandateUsers = useCallback(async (
|
||||
mandateIdOrPagination?: string | PaginationParams
|
||||
): Promise<MandateUser[]> => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
let mandateId: string;
|
||||
let paginationParams: PaginationParams = {};
|
||||
|
||||
// Handle backward compatibility
|
||||
if (typeof mandateIdOrPagination === 'string') {
|
||||
mandateId = mandateIdOrPagination;
|
||||
currentMandateIdRef.current = mandateId;
|
||||
} else if (mandateIdOrPagination && typeof mandateIdOrPagination === 'object') {
|
||||
paginationParams = mandateIdOrPagination;
|
||||
mandateId = currentMandateIdRef.current;
|
||||
} else {
|
||||
mandateId = currentMandateIdRef.current;
|
||||
}
|
||||
|
||||
if (!mandateId) {
|
||||
setLoading(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await api.get(`/api/mandates/${mandateId}/users`);
|
||||
const data = Array.isArray(response.data) ? response.data : [];
|
||||
const params = new URLSearchParams();
|
||||
if (Object.keys(paginationParams).length > 0) {
|
||||
params.append('pagination', JSON.stringify(paginationParams));
|
||||
}
|
||||
|
||||
const url = params.toString()
|
||||
? `/api/mandates/${mandateId}/users?${params.toString()}`
|
||||
: `/api/mandates/${mandateId}/users`;
|
||||
|
||||
const response = await api.get(url);
|
||||
|
||||
let data: MandateUser[] = [];
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
data = response.data.items;
|
||||
if (response.data.pagination) {
|
||||
setPagination(response.data.pagination);
|
||||
}
|
||||
} else {
|
||||
data = Array.isArray(response.data) ? response.data : [];
|
||||
}
|
||||
|
||||
setUsers(data);
|
||||
return data;
|
||||
} catch (err: any) {
|
||||
const errorMessage = err.response?.data?.detail || err.message || 'Failed to fetch mandate users';
|
||||
setError(errorMessage);
|
||||
setUsers([]);
|
||||
setPagination(null);
|
||||
return [];
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -210,6 +271,7 @@ export function useUserMandates() {
|
|||
users,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchMandateUsers,
|
||||
addUserToMandate,
|
||||
removeUserFromMandate,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
import {
|
||||
deleteWorkflowApi,
|
||||
|
|
@ -15,11 +15,7 @@ import {
|
|||
type AttributeDefinition,
|
||||
type StartWorkflowRequest
|
||||
} from '../api/workflowApi';
|
||||
import { MessageOverlay } from '../components/UiComponents';
|
||||
import type { MessageMode } from '../components/UiComponents';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
import { useWorkflowSelection } from '../contexts/WorkflowSelectionContext';
|
||||
// import { getUserDataCache } from '../utils/userCache'; // Unused import
|
||||
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||
|
||||
// Workflow interface matching backend
|
||||
|
|
@ -395,13 +391,6 @@ export function useWorkflowOperations() {
|
|||
const [deleteMessageError, setDeleteMessageError] = useState<string | null>(null);
|
||||
const [deleteFileError, setDeleteFileError] = useState<string | null>(null);
|
||||
|
||||
// Warning state
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const [warningData, setWarningData] = useState<{ header: string; message: string; mode: MessageMode } | null>(null);
|
||||
|
||||
// Language context
|
||||
const { t: _t } = useLanguage();
|
||||
|
||||
// Workflow selection context - to clear selection if deleted workflow is selected
|
||||
const { selectedWorkflowId, clearWorkflow } = useWorkflowSelection();
|
||||
|
||||
|
|
@ -618,15 +607,6 @@ export function useWorkflowOperations() {
|
|||
}
|
||||
};
|
||||
|
||||
// Function to close warning
|
||||
const closeWarning = useCallback(() => {
|
||||
setShowWarning(false);
|
||||
// Delay clearing the data to allow exit animation to complete (matches CSS transition)
|
||||
setTimeout(() => {
|
||||
setWarningData(null);
|
||||
}, 700);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// Loading states
|
||||
startingWorkflow,
|
||||
|
|
@ -649,16 +629,6 @@ export function useWorkflowOperations() {
|
|||
handleWorkflowDeleteMultiple,
|
||||
handleWorkflowUpdate,
|
||||
deleteMessage,
|
||||
deleteFileFromMessage,
|
||||
// Message overlay component
|
||||
MessageOverlayComponent: () => React.createElement(MessageOverlay, {
|
||||
header: warningData?.header || '',
|
||||
message: warningData?.message || '',
|
||||
isVisible: showWarning,
|
||||
mode: warningData?.mode || 'info',
|
||||
onClose: closeWarning,
|
||||
autoClose: true,
|
||||
autoCloseDelay: 5000
|
||||
})
|
||||
deleteFileFromMessage
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@
|
|||
.tableContainer {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
background: var(--surface-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@
|
|||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useFeatureAccess, type Feature, type FeatureInstance } from '../../hooks/useFeatureAccess';
|
||||
import { useFeatureAccess, type FeatureInstance } from '../../hooks/useFeatureAccess';
|
||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
const {
|
||||
features,
|
||||
instances,
|
||||
instancesPagination,
|
||||
loading,
|
||||
error,
|
||||
fetchFeatures,
|
||||
|
|
@ -28,12 +30,13 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
} = useFeatureAccess();
|
||||
|
||||
const { fetchMandates } = useUserMandates();
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
// State
|
||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [, setIsSubmitting] = useState(false);
|
||||
const [syncingInstance, setSyncingInstance] = useState<string | null>(null);
|
||||
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
|
||||
|
||||
|
|
@ -107,8 +110,9 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
if (result.success) {
|
||||
setShowCreateModal(false);
|
||||
fetchInstances(selectedMandateId);
|
||||
showSuccess('Feature-Instanz erstellt', `Die Instanz "${data.label}" wurde erfolgreich erstellt.`);
|
||||
} else {
|
||||
alert(result.error || 'Fehler beim Erstellen der Feature-Instanz');
|
||||
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz');
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -120,8 +124,10 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
if (!selectedMandateId) return;
|
||||
if (window.confirm(`Möchten Sie die Feature-Instanz "${instance.label}" wirklich löschen? Alle zugehörigen Daten werden gelöscht.`)) {
|
||||
const result = await deleteInstance(selectedMandateId, instance.id);
|
||||
if (!result.success) {
|
||||
alert(result.error || 'Fehler beim Löschen der Feature-Instanz');
|
||||
if (result.success) {
|
||||
showSuccess('Instanz gelöscht', `Die Feature-Instanz "${instance.label}" wurde gelöscht.`);
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Löschen der Feature-Instanz');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -133,9 +139,12 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
try {
|
||||
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
|
||||
if (result.success && result.data) {
|
||||
alert(`Rollen synchronisiert:\n- Hinzugefügt: ${result.data.added}\n- Entfernt: ${result.data.removed}\n- Unverändert: ${result.data.unchanged}`);
|
||||
showSuccess(
|
||||
'Rollen synchronisiert',
|
||||
`Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}`
|
||||
);
|
||||
} else {
|
||||
alert(result.error || 'Fehler beim Synchronisieren der Rollen');
|
||||
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen');
|
||||
}
|
||||
} finally {
|
||||
setSyncingInstance(null);
|
||||
|
|
@ -297,7 +306,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleDeleteInstance}
|
||||
hookData={{
|
||||
refetch: () => fetchInstances(selectedMandateId),
|
||||
refetch: fetchInstances,
|
||||
pagination: instancesPagination,
|
||||
handleDelete: handleDeleteInstance,
|
||||
}}
|
||||
emptyMessage="Keine Feature-Instanzen gefunden"
|
||||
|
|
|
|||
577
src/pages/admin/AdminFeatureInstanceUsersPage.tsx
Normal file
577
src/pages/admin/AdminFeatureInstanceUsersPage.tsx
Normal file
|
|
@ -0,0 +1,577 @@
|
|||
/**
|
||||
* AdminFeatureInstanceUsersPage
|
||||
*
|
||||
* Admin page for managing user access to feature instances.
|
||||
* Allows adding, removing, and updating user roles within feature instances.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useFeatureAccess, type FeatureAccessUser, type FeatureInstanceRole, type PaginationParams, type PaginationMetadata } from '../../hooks/useFeatureAccess';
|
||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUsers, FaBuilding, FaCube } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||
const {
|
||||
features,
|
||||
instances,
|
||||
loading,
|
||||
error,
|
||||
fetchFeatures,
|
||||
fetchInstances,
|
||||
fetchInstanceUsers,
|
||||
addUserToInstance,
|
||||
removeUserFromInstance,
|
||||
updateInstanceUserRoles,
|
||||
fetchInstanceRoles,
|
||||
} = useFeatureAccess();
|
||||
|
||||
const { fetchMandates } = useUserMandates();
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
// State
|
||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||||
const [selectedInstanceId, setSelectedInstanceId] = useState<string>('');
|
||||
const [instanceUsers, setInstanceUsers] = useState<FeatureAccessUser[]>([]);
|
||||
const [instanceRoles, setInstanceRoles] = useState<FeatureInstanceRole[]>([]);
|
||||
const [allUsers, setAllUsers] = useState<Array<{ id: string; username: string; email?: string; fullName?: string }>>([]);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<FeatureAccessUser | null>(null);
|
||||
const [, setIsSubmitting] = useState(false);
|
||||
const [usersLoading, setUsersLoading] = useState(false);
|
||||
const [usersPagination, setUsersPagination] = useState<PaginationMetadata | null>(null);
|
||||
|
||||
// Load mandates and features on mount
|
||||
useEffect(() => {
|
||||
fetchFeatures();
|
||||
fetchMandates().then(setMandates);
|
||||
}, [fetchFeatures, fetchMandates]);
|
||||
|
||||
// Load instances when mandate changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchInstances(selectedMandateId);
|
||||
setSelectedInstanceId('');
|
||||
setInstanceUsers([]);
|
||||
setInstanceRoles([]);
|
||||
}
|
||||
}, [selectedMandateId, fetchInstances]);
|
||||
|
||||
// Load users and roles when instance changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId && selectedInstanceId) {
|
||||
setUsersLoading(true);
|
||||
Promise.all([
|
||||
fetchInstanceUsers(selectedMandateId, selectedInstanceId),
|
||||
fetchInstanceRoles(selectedMandateId, selectedInstanceId),
|
||||
]).then(([users, roles]) => {
|
||||
setInstanceUsers(users);
|
||||
setInstanceRoles(roles);
|
||||
}).finally(() => {
|
||||
setUsersLoading(false);
|
||||
});
|
||||
}
|
||||
}, [selectedMandateId, selectedInstanceId, fetchInstanceUsers, fetchInstanceRoles]);
|
||||
|
||||
// Load mandate members for the add modal (only users who are members of the selected mandate)
|
||||
useEffect(() => {
|
||||
if (!selectedMandateId) {
|
||||
setAllUsers([]);
|
||||
return;
|
||||
}
|
||||
api.get(`/api/mandates/${selectedMandateId}/users`).then(response => {
|
||||
const data = response.data?.items || response.data || [];
|
||||
// Map MandateUserInfo to the expected format
|
||||
const mappedUsers = Array.isArray(data) ? data.map((u: any) => ({
|
||||
id: u.userId,
|
||||
username: u.username,
|
||||
email: u.email,
|
||||
fullName: u.fullName
|
||||
})) : [];
|
||||
setAllUsers(mappedUsers);
|
||||
}).catch(() => setAllUsers([]));
|
||||
}, [selectedMandateId]);
|
||||
|
||||
// Refresh instance users with optional pagination
|
||||
const refreshUsers = useCallback(async (paginationParams?: PaginationParams) => {
|
||||
if (selectedMandateId && selectedInstanceId) {
|
||||
setUsersLoading(true);
|
||||
try {
|
||||
// Build query params
|
||||
const params = new URLSearchParams();
|
||||
if (paginationParams && Object.keys(paginationParams).length > 0) {
|
||||
params.append('pagination', JSON.stringify(paginationParams));
|
||||
}
|
||||
|
||||
const url = params.toString()
|
||||
? `/api/features/instances/${selectedInstanceId}/users?${params.toString()}`
|
||||
: `/api/features/instances/${selectedInstanceId}/users`;
|
||||
|
||||
const response = await api.get(url, {
|
||||
headers: { 'X-Mandate-Id': selectedMandateId }
|
||||
});
|
||||
|
||||
if (response.data?.items && Array.isArray(response.data.items)) {
|
||||
setInstanceUsers(response.data.items);
|
||||
if (response.data.pagination) {
|
||||
setUsersPagination(response.data.pagination);
|
||||
}
|
||||
} else {
|
||||
const users = Array.isArray(response.data) ? response.data : [];
|
||||
setInstanceUsers(users);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refreshing users:', err);
|
||||
setInstanceUsers([]);
|
||||
} finally {
|
||||
setUsersLoading(false);
|
||||
}
|
||||
}
|
||||
}, [selectedMandateId, selectedInstanceId]);
|
||||
|
||||
// Get users not yet in the instance
|
||||
const availableUsers = useMemo(() => {
|
||||
const existingUserIds = new Set(instanceUsers.map(u => u.userId));
|
||||
return allUsers.filter(u => !existingUserIds.has(u.id));
|
||||
}, [allUsers, instanceUsers]);
|
||||
|
||||
// Table columns
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Benutzername',
|
||||
type: 'text' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'E-Mail',
|
||||
type: 'text' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
key: 'fullName',
|
||||
label: 'Vollständiger Name',
|
||||
type: 'text' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
key: 'roleLabels',
|
||||
label: 'Rollen',
|
||||
type: 'text' as const,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
searchable: true,
|
||||
width: 200,
|
||||
render: (value: string[]) => {
|
||||
if (!value || value.length === 0) return '-';
|
||||
return value.join(', ');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
label: 'Aktiv',
|
||||
type: 'boolean' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: false,
|
||||
width: 80,
|
||||
},
|
||||
], []);
|
||||
|
||||
// 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(() =>
|
||||
instanceRoles.map(r => ({
|
||||
value: r.id,
|
||||
label: r.roleLabel
|
||||
})), [instanceRoles]);
|
||||
|
||||
// Form attributes for adding a user
|
||||
const addUserFields: AttributeDefinition[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'userId',
|
||||
label: 'Benutzer',
|
||||
type: 'enum' as const,
|
||||
required: true,
|
||||
options: userOptions,
|
||||
},
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: 'Rollen',
|
||||
type: 'multiselect' as const,
|
||||
required: true,
|
||||
options: roleOptions,
|
||||
}
|
||||
];
|
||||
}, [userOptions, roleOptions]);
|
||||
|
||||
// Form attributes for editing user roles
|
||||
const editRolesFields: AttributeDefinition[] = useMemo(() => {
|
||||
return [{
|
||||
name: 'roleIds',
|
||||
label: 'Rollen',
|
||||
type: 'multiselect' as const,
|
||||
required: true,
|
||||
options: roleOptions,
|
||||
}];
|
||||
}, [roleOptions]);
|
||||
|
||||
// Handle add user submit
|
||||
const handleAddUser = async (data: { userId: string; roleIds: string[] }) => {
|
||||
if (!selectedMandateId || !selectedInstanceId) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await addUserToInstance(selectedMandateId, selectedInstanceId, data);
|
||||
if (result.success) {
|
||||
setShowAddModal(false);
|
||||
refreshUsers();
|
||||
showSuccess('Benutzer hinzugefügt', 'Der Benutzer wurde erfolgreich zur Feature-Instanz hinzugefügt.');
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Hinzufügen des Benutzers');
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit roles submit
|
||||
const handleEditRoles = async (data: { roleIds: string[] }) => {
|
||||
if (!selectedMandateId || !selectedInstanceId || !editingUser) return;
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const result = await updateInstanceUserRoles(
|
||||
selectedMandateId,
|
||||
selectedInstanceId,
|
||||
editingUser.userId,
|
||||
data.roleIds
|
||||
);
|
||||
if (result.success) {
|
||||
setEditingUser(null);
|
||||
refreshUsers();
|
||||
showSuccess('Rollen aktualisiert', 'Die Benutzerrollen wurden erfolgreich aktualisiert.');
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Rollen');
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle remove user
|
||||
const handleRemoveUser = async (user: FeatureAccessUser) => {
|
||||
if (!selectedMandateId || !selectedInstanceId) return;
|
||||
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich aus dieser Feature-Instanz entfernen?`)) {
|
||||
const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId);
|
||||
if (result.success) {
|
||||
refreshUsers();
|
||||
showSuccess('Benutzer entfernt', `"${user.username}" wurde aus der Feature-Instanz entfernt.`);
|
||||
} else {
|
||||
showError('Fehler', result.error || 'Fehler beim Entfernen des Benutzers');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit click
|
||||
const handleEditClick = (user: FeatureAccessUser) => {
|
||||
setEditingUser(user);
|
||||
};
|
||||
|
||||
// Get mandate name
|
||||
const getMandateName = (mandate: Mandate) => {
|
||||
if (typeof mandate.name === 'object') {
|
||||
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
|
||||
}
|
||||
return mandate.name || mandate.id;
|
||||
};
|
||||
|
||||
// Get feature label
|
||||
const getFeatureLabel = (code: string) => {
|
||||
const feature = features.find(f => f.code === code);
|
||||
if (feature) {
|
||||
return typeof feature.label === 'object'
|
||||
? (feature.label.de || feature.label.en || code)
|
||||
: (feature.label || code);
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
// Get selected instance info
|
||||
const selectedInstance = useMemo(() => {
|
||||
return instances.find(i => i.id === selectedInstanceId);
|
||||
}, [instances, selectedInstanceId]);
|
||||
|
||||
if (error && !selectedMandateId) {
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>Fehler: {error}</p>
|
||||
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
|
||||
<FaSync /> Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Feature-Benutzer</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Benutzerzugriffe auf Feature-Instanzen</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selectors */}
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
Mandant:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selectedMandateId && (
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
Feature-Instanz:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={selectedInstanceId}
|
||||
onChange={(e) => setSelectedInstanceId(e.target.value)}
|
||||
disabled={loading || instances.length === 0}
|
||||
>
|
||||
<option value="">-- Instanz wählen --</option>
|
||||
{instances.map(i => (
|
||||
<option key={i.id} value={i.id}>
|
||||
{i.label} ({getFeatureLabel(i.featureCode)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedInstanceId && (
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refreshUsers()}
|
||||
disabled={usersLoading}
|
||||
>
|
||||
<FaSync className={usersLoading ? 'spinning' : ''} /> Aktualisieren
|
||||
</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
disabled={availableUsers.length === 0 || instanceRoles.length === 0}
|
||||
>
|
||||
<FaPlus /> Benutzer hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info box when instance is selected */}
|
||||
{selectedInstance && instanceRoles.length > 0 && (
|
||||
<div className={styles.infoBox}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Verfügbare Rollen: </span>
|
||||
{instanceRoles.map((r, i) => (
|
||||
<span key={r.id}>
|
||||
{i > 0 && ', '}
|
||||
<strong>{r.roleLabel}</strong>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning if no roles available */}
|
||||
{selectedInstance && instanceRoles.length === 0 && !usersLoading && (
|
||||
<div className={styles.warningBox || styles.infoBox}>
|
||||
<span>⚠️ </span>
|
||||
<span>Diese Instanz hat noch keine Rollen. Bitte synchronisieren Sie die Rollen zuerst unter "Feature-Instanzen".</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
{!selectedMandateId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu sehen.
|
||||
</p>
|
||||
</div>
|
||||
) : !selectedInstanceId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Feature-Instanz ausgewählt</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
{instances.length === 0
|
||||
? 'Dieser Mandant hat noch keine Feature-Instanzen.'
|
||||
: 'Wählen Sie eine Feature-Instanz aus, um deren Benutzer zu verwalten.'}
|
||||
</p>
|
||||
</div>
|
||||
) : usersLoading && instanceUsers.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Benutzer...</span>
|
||||
</div>
|
||||
) : instanceUsers.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaUsers className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Benutzer</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Dieser Feature-Instanz sind noch keine Benutzer zugewiesen.
|
||||
</p>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
disabled={availableUsers.length === 0 || instanceRoles.length === 0}
|
||||
>
|
||||
<FaPlus /> Ersten Benutzer hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.tableContainer}>
|
||||
<FormGeneratorTable
|
||||
data={instanceUsers}
|
||||
columns={columns}
|
||||
loading={usersLoading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
searchable={true}
|
||||
filterable={true}
|
||||
sortable={true}
|
||||
selectable={false}
|
||||
actionButtons={[
|
||||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Rollen bearbeiten',
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Aus Instanz entfernen',
|
||||
}
|
||||
]}
|
||||
onDelete={handleRemoveUser}
|
||||
hookData={{
|
||||
refetch: refreshUsers,
|
||||
pagination: usersPagination,
|
||||
handleDelete: async (featureAccessId: string) => {
|
||||
// Find user by FeatureAccess ID to get userId for API call
|
||||
const user = instanceUsers.find(u => u.id === featureAccessId);
|
||||
if (user) {
|
||||
const result = await removeUserFromInstance(selectedMandateId, selectedInstanceId, user.userId);
|
||||
return result.success;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}}
|
||||
emptyMessage="Keine Benutzer gefunden"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add User Modal */}
|
||||
{showAddModal && (
|
||||
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Benutzer zur Feature-Instanz hinzufügen</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowAddModal(false)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{availableUsers.length === 0 ? (
|
||||
<p>Alle Benutzer haben bereits Zugriff auf diese Feature-Instanz.</p>
|
||||
) : instanceRoles.length === 0 ? (
|
||||
<p>Diese Feature-Instanz hat keine Rollen. Bitte synchronisieren Sie zuerst die Rollen.</p>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={addUserFields}
|
||||
mode="create"
|
||||
onSubmit={handleAddUser}
|
||||
onCancel={() => setShowAddModal(false)}
|
||||
submitButtonText="Hinzufügen"
|
||||
cancelButtonText="Abbrechen"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Roles Modal */}
|
||||
{editingUser && (
|
||||
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Rollen bearbeiten: {editingUser.username}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setEditingUser(null)}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
<FormGeneratorForm
|
||||
attributes={editRolesFields}
|
||||
data={{ roleIds: editingUser.roleIds }}
|
||||
mode="edit"
|
||||
onSubmit={handleEditRoles}
|
||||
onCancel={() => setEditingUser(null)}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminFeatureInstanceUsersPage;
|
||||
|
|
@ -5,8 +5,8 @@
|
|||
* Allows creating, viewing, and revoking invitations.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useInvitations, type Invitation, type InvitationCreate } from '../../hooks/useInvitations';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useInvitations, type Invitation, type InvitationCreate, type PaginationParams } from '../../hooks/useInvitations';
|
||||
import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
|
|
@ -19,6 +19,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
invitations,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchInvitations,
|
||||
createInvitation,
|
||||
revokeInvitation,
|
||||
|
|
@ -353,7 +354,8 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleRevokeInvitation}
|
||||
hookData={{
|
||||
refetch: () => fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
|
||||
refetch: fetchInvitations,
|
||||
pagination,
|
||||
}}
|
||||
emptyMessage="Keine Einladungen gefunden"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
* Allows creating, viewing, editing, and deleting mandate-level roles.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useMandateRoles, type Role, type RoleCreate, type RoleUpdate } from '../../hooks/useMandateRoles';
|
||||
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useMandateRoles, type Role, type RoleCreate, type RoleUpdate, type PaginationParams } from '../../hooks/useMandateRoles';
|
||||
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
|
|
@ -19,6 +19,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
roles,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchRoles,
|
||||
createRole,
|
||||
updateRole,
|
||||
|
|
@ -60,7 +61,16 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
}
|
||||
}, [selectedMandateId, fetchRoles]);
|
||||
|
||||
// Refetch wrapper that accepts pagination params from FormGeneratorTable
|
||||
const refetchWithPagination = useCallback(async (paginationParams?: PaginationParams) => {
|
||||
if (!selectedMandateId) return;
|
||||
// Pass pagination params to fetchRoles
|
||||
return fetchRoles(paginationParams || {});
|
||||
}, [selectedMandateId, fetchRoles]);
|
||||
|
||||
// Filter roles based on selection and add scopeType field
|
||||
// Note: This client-side filtering is still needed for the roleFilter dropdown
|
||||
// Backend pagination handles page/sort/search, but roleFilter is UI-specific
|
||||
const filteredRoles = useMemo(() => {
|
||||
if (!selectedMandateId) return [];
|
||||
|
||||
|
|
@ -433,7 +443,8 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleDeleteRole}
|
||||
hookData={{
|
||||
refetch: () => fetchRoles(selectedMandateId),
|
||||
refetch: refetchWithPagination,
|
||||
pagination: pagination,
|
||||
handleDelete: handleDeleteRole,
|
||||
}}
|
||||
emptyMessage="Keine Rollen gefunden"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
* Allows assigning users to mandates and managing their roles within mandates.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useUserMandates, type MandateUser, type Mandate, type Role } from '../../hooks/useUserMandates';
|
||||
import React, { useState, useEffect, useMemo, useCallback } 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, FaUsers, FaBuilding } from 'react-icons/fa';
|
||||
|
|
@ -18,6 +18,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
users,
|
||||
loading,
|
||||
error,
|
||||
pagination,
|
||||
fetchMandateUsers,
|
||||
addUserToMandate,
|
||||
removeUserFromMandate,
|
||||
|
|
@ -356,17 +357,18 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleRemoveUser}
|
||||
hookData={{
|
||||
refetch: () => fetchMandateUsers(selectedMandateId),
|
||||
handleDelete: async (userId: string) => {
|
||||
const user = users.find(u => u.userId === userId);
|
||||
refetch: fetchMandateUsers,
|
||||
pagination,
|
||||
handleDelete: async (userMandateId: string) => {
|
||||
// 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, userId);
|
||||
const result = await removeUserFromMandate(selectedMandateId, user.userId);
|
||||
return result.success;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}}
|
||||
idField="userId"
|
||||
emptyMessage="Keine Mitglieder gefunden"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ export { AdminRolesPage } from './AdminRolesPage';
|
|||
export { AdminUserMandatesPage } from './AdminUserMandatesPage';
|
||||
export { AdminFeatureAccessPage } from './AdminFeatureAccessPage';
|
||||
export { AdminInvitationsPage } from './AdminInvitationsPage';
|
||||
export { AdminMandateRolesPage } from './AdminMandateRolesPage';
|
||||
export { AdminMandateRolesPage } from './AdminMandateRolesPage';
|
||||
export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage';
|
||||
Loading…
Reference in a new issue