saas multi mandate tested
This commit is contained in:
parent
34d4646667
commit
f99b6b7604
3 changed files with 146 additions and 90 deletions
|
|
@ -107,8 +107,9 @@ export function useMandateRoles() {
|
|||
if (Object.keys(paginationWithoutScope).length > 0) {
|
||||
queryParams.pagination = JSON.stringify(paginationWithoutScope);
|
||||
}
|
||||
// Include templates by default for mandate roles view
|
||||
queryParams.includeTemplates = 'true';
|
||||
// Do NOT include feature template roles - they belong to Feature-Rollen page
|
||||
// According to admin_ui_concept.md: Filter: featureCode=null AND featureInstanceId=null
|
||||
queryParams.includeTemplates = 'false';
|
||||
// Include mandate-specific roles for the selected mandate
|
||||
if (mandateId) {
|
||||
queryParams.mandateId = mandateId;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
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 { useUserMandates } 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';
|
||||
|
|
@ -22,7 +22,6 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
loading,
|
||||
error,
|
||||
fetchFeatures,
|
||||
fetchInstances,
|
||||
fetchInstanceUsers,
|
||||
addUserToInstance,
|
||||
removeUserFromInstance,
|
||||
|
|
@ -33,10 +32,19 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
const { fetchMandates } = useUserMandates();
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
// Combined instance option type
|
||||
interface CombinedInstanceOption {
|
||||
mandateId: string;
|
||||
instanceId: string;
|
||||
mandateName: string;
|
||||
instanceLabel: string;
|
||||
featureCode: string;
|
||||
combinedKey: string; // mandateId:instanceId for unique identification
|
||||
}
|
||||
|
||||
// State
|
||||
const [mandates, setMandates] = useState<Mandate[]>([]);
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
|
||||
const [selectedInstanceId, setSelectedInstanceId] = useState<string>('');
|
||||
const [combinedOptions, setCombinedOptions] = useState<CombinedInstanceOption[]>([]);
|
||||
const [selectedCombinedKey, setSelectedCombinedKey] = 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 }>>([]);
|
||||
|
|
@ -46,21 +54,62 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
const [usersLoading, setUsersLoading] = useState(false);
|
||||
const [usersPagination, setUsersPagination] = useState<PaginationMetadata | null>(null);
|
||||
|
||||
// Load mandates and features on mount
|
||||
// Extract mandateId and instanceId from combined key
|
||||
const selectedMandateId = useMemo(() => {
|
||||
if (!selectedCombinedKey) return '';
|
||||
return selectedCombinedKey.split(':')[0] || '';
|
||||
}, [selectedCombinedKey]);
|
||||
|
||||
const selectedInstanceId = useMemo(() => {
|
||||
if (!selectedCombinedKey) return '';
|
||||
return selectedCombinedKey.split(':')[1] || '';
|
||||
}, [selectedCombinedKey]);
|
||||
|
||||
// Load mandates and features on mount, then build combined options
|
||||
useEffect(() => {
|
||||
fetchFeatures();
|
||||
fetchMandates().then(setMandates);
|
||||
}, [fetchFeatures, fetchMandates]);
|
||||
const loadCombinedOptions = async () => {
|
||||
const loadedMandates = await fetchMandates();
|
||||
|
||||
// Load instances when mandate changes
|
||||
useEffect(() => {
|
||||
if (selectedMandateId) {
|
||||
fetchInstances(selectedMandateId);
|
||||
setSelectedInstanceId('');
|
||||
setInstanceUsers([]);
|
||||
setInstanceRoles([]);
|
||||
}
|
||||
}, [selectedMandateId, fetchInstances]);
|
||||
// Load instances for all mandates in parallel
|
||||
const allOptions: CombinedInstanceOption[] = [];
|
||||
|
||||
for (const mandate of loadedMandates) {
|
||||
try {
|
||||
const response = await api.get('/api/features/instances', {
|
||||
headers: { 'X-Mandate-Id': mandate.id }
|
||||
});
|
||||
const instanceList = response.data?.items || response.data || [];
|
||||
|
||||
if (Array.isArray(instanceList)) {
|
||||
for (const inst of instanceList) {
|
||||
allOptions.push({
|
||||
mandateId: mandate.id,
|
||||
instanceId: inst.id,
|
||||
mandateName: typeof mandate.name === 'string' ? mandate.name : (mandate.name?.de || mandate.name?.en || Object.values(mandate.name || {})[0] || mandate.id),
|
||||
instanceLabel: inst.label || inst.id,
|
||||
featureCode: inst.featureCode,
|
||||
combinedKey: `${mandate.id}:${inst.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error loading instances for mandate ${mandate.id}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by mandate name, then by instance label
|
||||
allOptions.sort((a, b) => {
|
||||
const mandateCompare = a.mandateName.localeCompare(b.mandateName);
|
||||
if (mandateCompare !== 0) return mandateCompare;
|
||||
return a.instanceLabel.localeCompare(b.instanceLabel);
|
||||
});
|
||||
|
||||
setCombinedOptions(allOptions);
|
||||
};
|
||||
|
||||
loadCombinedOptions();
|
||||
}, [fetchFeatures, fetchMandates]);
|
||||
|
||||
// Load users and roles when instance changes
|
||||
useEffect(() => {
|
||||
|
|
@ -297,14 +346,6 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
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);
|
||||
|
|
@ -316,12 +357,24 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
return code;
|
||||
};
|
||||
|
||||
// Get selected instance info
|
||||
// Get selected instance info from combined options
|
||||
const selectedInstance = useMemo(() => {
|
||||
return instances.find(i => i.id === selectedInstanceId);
|
||||
}, [instances, selectedInstanceId]);
|
||||
const option = combinedOptions.find(o => o.combinedKey === selectedCombinedKey);
|
||||
if (!option) return null;
|
||||
return instances.find(i => i.id === option.instanceId) || {
|
||||
id: option.instanceId,
|
||||
label: option.instanceLabel,
|
||||
featureCode: option.featureCode,
|
||||
mandateId: option.mandateId,
|
||||
};
|
||||
}, [combinedOptions, selectedCombinedKey, instances]);
|
||||
|
||||
if (error && !selectedMandateId) {
|
||||
// Get selected combined option for display
|
||||
const selectedOption = useMemo(() => {
|
||||
return combinedOptions.find(o => o.combinedKey === selectedCombinedKey);
|
||||
}, [combinedOptions, selectedCombinedKey]);
|
||||
|
||||
if (error && !selectedCombinedKey) {
|
||||
return (
|
||||
<div className={styles.adminPage}>
|
||||
<div className={styles.errorContainer}>
|
||||
|
|
@ -344,50 +397,44 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selectors */}
|
||||
{/* Combined Selector: Mandate + Feature Instance */}
|
||||
<div className={styles.filterSection}>
|
||||
<div className={styles.filterGroup}>
|
||||
<div className={styles.filterGroup} style={{ flex: 1, maxWidth: 500 }}>
|
||||
<label className={styles.filterLabel}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
Mandant:
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
Mandant / Feature-Instanz:
|
||||
</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
value={selectedCombinedKey}
|
||||
onChange={(e) => setSelectedCombinedKey(e.target.value)}
|
||||
disabled={loading || combinedOptions.length === 0}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
</option>
|
||||
))}
|
||||
<option value="">-- Mandant / Feature-Instanz wählen --</option>
|
||||
{/* Group options by mandate */}
|
||||
{(() => {
|
||||
const groupedByMandate: Record<string, CombinedInstanceOption[]> = {};
|
||||
combinedOptions.forEach(opt => {
|
||||
if (!groupedByMandate[opt.mandateName]) {
|
||||
groupedByMandate[opt.mandateName] = [];
|
||||
}
|
||||
groupedByMandate[opt.mandateName].push(opt);
|
||||
});
|
||||
|
||||
return Object.entries(groupedByMandate).map(([mandateName, options]) => (
|
||||
<optgroup key={mandateName} label={mandateName}>
|
||||
{options.map(opt => (
|
||||
<option key={opt.combinedKey} value={opt.combinedKey}>
|
||||
{opt.instanceLabel} ({getFeatureLabel(opt.featureCode)})
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
));
|
||||
})()}
|
||||
</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 && (
|
||||
{selectedCombinedKey && (
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
|
|
@ -408,9 +455,19 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
{/* Info box when instance is selected */}
|
||||
{selectedOption && (
|
||||
<div className={styles.infoBox}>
|
||||
<FaBuilding style={{ marginRight: 8 }} />
|
||||
<span>Mandant: <strong>{selectedOption.mandateName}</strong></span>
|
||||
<span style={{ margin: '0 16px', color: 'var(--color-border)' }}>|</span>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Instanz: <strong>{selectedOption.instanceLabel}</strong> ({selectedOption.featureCode})</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Roles info box */}
|
||||
{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}>
|
||||
|
|
@ -430,21 +487,13 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
)}
|
||||
|
||||
{/* 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 ? (
|
||||
{!selectedCombinedKey ? (
|
||||
<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.'
|
||||
{combinedOptions.length === 0
|
||||
? 'Es gibt noch keine Feature-Instanzen. Erstellen Sie zuerst Feature-Instanzen unter "Feature-Instanzen".'
|
||||
: 'Wählen Sie eine Feature-Instanz aus, um deren Benutzer zu verwalten.'}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@ import api from '../../api';
|
|||
import styles from './Admin.module.css';
|
||||
|
||||
interface Feature {
|
||||
id: string;
|
||||
featureCode: string;
|
||||
name: string | { [key: string]: string };
|
||||
id?: string;
|
||||
code: string; // Backend uses 'code' not 'featureCode'
|
||||
featureCode?: string; // Alias for backward compatibility
|
||||
label: string | { [key: string]: string }; // Backend uses 'label' not 'name'
|
||||
name?: string | { [key: string]: string }; // Alias for backward compatibility
|
||||
description?: string | { [key: string]: string };
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
interface FeatureRole {
|
||||
|
|
@ -49,13 +52,13 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const loadFeatures = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/features');
|
||||
const response = await api.get('/api/features/');
|
||||
const featureList = response.data?.items || response.data || [];
|
||||
setFeatures(Array.isArray(featureList) ? featureList : []);
|
||||
|
||||
// Auto-select first feature if available
|
||||
if (featureList.length > 0 && !selectedFeatureCode) {
|
||||
setSelectedFeatureCode(featureList[0].featureCode);
|
||||
setSelectedFeatureCode(featureList[0].code || featureList[0].featureCode || '');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error loading features:', err);
|
||||
|
|
@ -235,8 +238,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
setEditingRole(role);
|
||||
};
|
||||
|
||||
// Get feature name
|
||||
const getFeatureName = (feature: Feature) => getTextValue(feature.name);
|
||||
// Get feature name - Backend uses 'label' field
|
||||
const getFeatureName = (feature: Feature) => getTextValue(feature.label || feature.name);
|
||||
|
||||
if (error && !selectedFeatureCode) {
|
||||
return (
|
||||
|
|
@ -274,11 +277,14 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
onChange={(e) => setSelectedFeatureCode(e.target.value)}
|
||||
>
|
||||
<option value="">-- Feature wählen --</option>
|
||||
{features.map(f => (
|
||||
<option key={f.id} value={f.featureCode}>
|
||||
{getFeatureName(f)} ({f.featureCode})
|
||||
</option>
|
||||
))}
|
||||
{features.map(f => {
|
||||
const featureCode = f.code || f.featureCode || '';
|
||||
return (
|
||||
<option key={featureCode} value={featureCode}>
|
||||
{getFeatureName(f)} ({featureCode})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue