From d4b2cb1dd64099325e653015b2959f14bdd828b5 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Thu, 19 Feb 2026 00:31:37 +0100 Subject: [PATCH] sync features a and p --- src/App.tsx | 4 +- .../FormGeneratorTable.module.css | 1 + src/config/pageRegistry.tsx | 4 +- src/pages/admin/AdminInvitationWizardPage.tsx | 685 ++++++++++++++ src/pages/admin/AdminMandateWizardPage.tsx | 838 ++++++++++++++++++ src/pages/admin/index.ts | 4 +- 6 files changed, 1533 insertions(+), 3 deletions(-) create mode 100644 src/pages/admin/AdminInvitationWizardPage.tsx create mode 100644 src/pages/admin/AdminMandateWizardPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 53c8b61..f505a0d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -37,7 +37,7 @@ import { DashboardPage } from './pages/Dashboard'; import { SettingsPage } from './pages/Settings'; import { GDPRPage } from './pages/GDPR'; import { FeatureViewPage } from './pages/FeatureView'; -import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage } from './pages/admin'; +import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage, AdminAutomationEventsPage, AdminLogsPage, AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin'; import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows'; import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; import { BillingDataView, BillingAdmin } from './pages/billing'; @@ -188,6 +188,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css index d5cae60..6e4a873 100644 --- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css +++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.module.css @@ -918,6 +918,7 @@ tbody .actionsColumn { @keyframes booleanPulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } +} /* Grouping */ .groupHeader { diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx index a737c49..487f248 100644 --- a/src/config/pageRegistry.tsx +++ b/src/config/pageRegistry.tsx @@ -21,7 +21,7 @@ import { FaLightbulb, FaRegFileAlt, FaLink, FaComments, FaListAlt, FaCogs, FaChartLine, FaFileAlt, FaUserShield, FaDatabase, FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock, - FaHeadset, FaVideo, + FaHeadset, FaVideo, FaHatWizard, } from 'react-icons/fa'; // ============================================================================= @@ -68,6 +68,8 @@ export const PAGE_ICONS: Record = { 'page.admin.automationEvents': , 'page.admin.automation-events': , 'page.admin.logs': , + 'page.admin.mandate-wizard': , + 'page.admin.invitation-wizard': , // Feature pages - Trustee 'page.feature.trustee.dashboard': , diff --git a/src/pages/admin/AdminInvitationWizardPage.tsx b/src/pages/admin/AdminInvitationWizardPage.tsx new file mode 100644 index 0000000..9a74f30 --- /dev/null +++ b/src/pages/admin/AdminInvitationWizardPage.tsx @@ -0,0 +1,685 @@ +/** + * AdminInvitationWizardPage + * + * 5-step wizard for batch user invitations: + * 1. Select Mandate + * 2. Add users with roles for the mandate + * 3. Optionally select a Feature Instance + * 4. Add users with roles for the feature instance + * 5. Review & batch dispatch + */ + +import React, { useState, useEffect } from 'react'; +import { useInvitations, type InvitationCreate } from '../../hooks/useInvitations'; +import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates'; +import { useFeatureAccess, type FeatureInstance, type FeatureInstanceRole } from '../../hooks/useFeatureAccess'; +import { useToast } from '../../contexts/ToastContext'; +import styles from './Admin.module.css'; + +// ============================================================================= +// TYPES +// ============================================================================= + +interface InviteeEntry { + targetUsername: string; + email: string; + roleIds: string[]; +} + +type RoleInfo = { id: string; roleLabel: string }; + +interface DispatchResult { + targetUsername: string; + success: boolean; + error?: string; + emailSent?: boolean; +} + +interface DispatchResults { + successful: number; + failed: number; + total: number; + results: DispatchResult[]; +} + +// ============================================================================= +// WIZARD-SPECIFIC INLINE STYLES +// ============================================================================= + +const _stepStyle = (stepNum: number, currentStep: number) => ({ + display: 'flex', + alignItems: 'center', + gap: '8px', + padding: '8px 16px', + borderRadius: '8px', + background: stepNum === currentStep + ? 'var(--primary-color, #f25843)' + : stepNum < currentStep ? '#dcfce7' : 'var(--bg-secondary, #f1f5f9)', + color: stepNum === currentStep + ? '#fff' + : stepNum < currentStep ? '#166534' : 'var(--text-secondary, #64748b)', + fontSize: '13px', + fontWeight: 600 as const, + cursor: stepNum < currentStep ? 'pointer' as const : 'default' as const, +}); + +const _cardStyle: React.CSSProperties = { + background: 'var(--surface-color, #fff)', + borderRadius: '12px', + border: '1px solid var(--border-color, #C5D9E8)', + padding: '20px', + marginBottom: '16px', +}; + +// ============================================================================= +// COMPONENT +// ============================================================================= + +export const AdminInvitationWizardPage: React.FC = () => { + const { createInvitation } = useInvitations(); + const { fetchMandates, fetchRoles } = useUserMandates(); + const { fetchInstances, fetchInstanceRoles } = useFeatureAccess(); + const { showError: showToastError } = useToast(); + + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + // Step 1: Mandate + const [mandates, setMandates] = useState([]); + const [selectedMandate, setSelectedMandate] = useState(null); + + // Step 2: Mandate invitees + const [mandateRoles, setMandateRoles] = useState([]); + const [mandateInvitees, setMandateInvitees] = useState([]); + const [mandateInviteeForm, setMandateInviteeForm] = useState({ username: '', email: '', roleIds: [] as string[] }); + + // Step 3: Feature instance (optional) + const [instances, setInstances] = useState([]); + const [selectedInstance, setSelectedInstance] = useState(null); + const [skipInstance, setSkipInstance] = useState(false); + + // Step 4: Instance invitees + const [instRoles, setInstRoles] = useState([]); + const [instanceInvitees, setInstanceInvitees] = useState([]); + const [instanceInviteeForm, setInstanceInviteeForm] = useState({ username: '', email: '', roleIds: [] as string[] }); + + // Dispatch options + const [expiresInHours, setExpiresInHours] = useState(72); + + // Dispatch results + const [dispatchResults, setDispatchResults] = useState(null); + + // ========================================================================== + // DATA LOADING + // ========================================================================== + + useEffect(() => { + fetchMandates().then(setMandates); + }, [fetchMandates]); + + useEffect(() => { + if (!selectedMandate) return; + fetchRoles(selectedMandate.id).then(roles => { + setMandateRoles(roles.filter(r => !r.featureInstanceId)); + }); + fetchInstances(selectedMandate.id).then(data => { + setInstances(data.filter(i => i.enabled)); + }); + }, [selectedMandate, fetchRoles, fetchInstances]); + + useEffect(() => { + if (!selectedMandate || !selectedInstance) return; + fetchInstanceRoles(selectedMandate.id, selectedInstance.id).then(setInstRoles); + }, [selectedMandate, selectedInstance, fetchInstanceRoles]); + + // ========================================================================== + // HELPERS + // ========================================================================== + + const getMandateName = (m: Mandate) => { + if (m.label) return m.label; + if (typeof m.name === 'object') { + return m.name.de || m.name.en || Object.values(m.name)[0] || m.id; + } + return m.name || m.id; + }; + + // ========================================================================== + // INVITEE MANAGEMENT + // ========================================================================== + + const _addMandateInvitee = () => { + if (!mandateInviteeForm.username.trim()) { + setError('Benutzername ist erforderlich'); + return; + } + if (mandateInvitees.some(i => i.targetUsername === mandateInviteeForm.username.trim())) { + setError('Benutzer bereits in der Liste'); + return; + } + setMandateInvitees(prev => [...prev, { + targetUsername: mandateInviteeForm.username.trim(), + email: mandateInviteeForm.email.trim(), + roleIds: [...mandateInviteeForm.roleIds], + }]); + setMandateInviteeForm({ username: '', email: '', roleIds: [] }); + setError(null); + }; + + const _removeMandateInvitee = (username: string) => { + setMandateInvitees(prev => prev.filter(i => i.targetUsername !== username)); + }; + + const _addInstanceInvitee = () => { + if (!instanceInviteeForm.username.trim()) { + setError('Benutzername ist erforderlich'); + return; + } + if (instanceInvitees.some(i => i.targetUsername === instanceInviteeForm.username.trim())) { + setError('Benutzer bereits in der Liste'); + return; + } + setInstanceInvitees(prev => [...prev, { + targetUsername: instanceInviteeForm.username.trim(), + email: instanceInviteeForm.email.trim(), + roleIds: [...instanceInviteeForm.roleIds], + }]); + setInstanceInviteeForm({ username: '', email: '', roleIds: [] }); + setError(null); + }; + + const _removeInstanceInvitee = (username: string) => { + setInstanceInvitees(prev => prev.filter(i => i.targetUsername !== username)); + }; + + const _toggleRole = (roleId: string, target: 'mandate' | 'instance') => { + const setter = target === 'mandate' ? setMandateInviteeForm : setInstanceInviteeForm; + setter(prev => ({ + ...prev, + roleIds: prev.roleIds.includes(roleId) + ? prev.roleIds.filter(r => r !== roleId) + : [...prev.roleIds, roleId], + })); + }; + + // ========================================================================== + // DISPATCH + // ========================================================================== + + const _handleDispatch = async () => { + if (!selectedMandate) return; + setIsLoading(true); + setError(null); + + const allInvitations: InvitationCreate[] = []; + + for (const inv of mandateInvitees) { + allInvitations.push({ + targetUsername: inv.targetUsername, + email: inv.email || undefined, + roleIds: inv.roleIds, + expiresInHours, + }); + } + + if (selectedInstance && !skipInstance) { + for (const inv of instanceInvitees) { + allInvitations.push({ + targetUsername: inv.targetUsername, + email: inv.email || undefined, + roleIds: inv.roleIds, + featureInstanceId: selectedInstance.id, + expiresInHours, + }); + } + } + + if (allInvitations.length === 0) { + setError('Keine Einladungen zu versenden'); + setIsLoading(false); + return; + } + + let successful = 0; + let failed = 0; + const results: DispatchResult[] = []; + + for (const inv of allInvitations) { + const result = await createInvitation(selectedMandate.id, inv); + if (result.success) { + successful++; + results.push({ targetUsername: inv.targetUsername, success: true, emailSent: result.data?.emailSent }); + } else { + failed++; + results.push({ targetUsername: inv.targetUsername, success: false, error: result.error }); + } + } + + const dr: DispatchResults = { successful, failed, total: allInvitations.length, results }; + setDispatchResults(dr); + setSuccess(`${successful} von ${dr.total} Einladungen erfolgreich versendet.`); + setStep(5); + setIsLoading(false); + }; + + // ========================================================================== + // STEP INDICATOR + // ========================================================================== + + const _renderStepIndicator = () => { + const visibleSteps = skipInstance ? [1, 2, 3] : [1, 2, 3, 4]; + const labels = skipInstance + ? ['Mandant', 'Benutzer', 'Versand'] + : ['Mandant', 'Benutzer', 'Feature Instanz', 'Instanz-Benutzer']; + + return ( +
+ {visibleSteps.map((s, idx) => ( +
{ if (s < step) setStep(s); }} + > + + {s < step ? '\u2713' : idx + 1} + + {labels[idx]} +
+ ))} +
+ ); + }; + + // ========================================================================== + // RENDER: INVITEE TABLE + // ========================================================================== + + const _renderInviteeList = ( + invitees: InviteeEntry[], + roles: RoleInfo[], + onRemove: (u: string) => void, + ) => { + if (invitees.length === 0) { + return

Noch keine Benutzer hinzugefuegt.

; + } + return ( + + + + + + + + + + + {invitees.map(inv => ( + + + + + + + ))} + +
BenutzernameE-MailRollenAktion
{inv.targetUsername}{inv.email || '-'} + {inv.roleIds.length > 0 + ? inv.roleIds.map(rid => roles.find(r => r.id === rid)?.roleLabel || rid).join(', ') + : Keine} + + +
+ ); + }; + + // ========================================================================== + // RENDER: ADD INVITEE FORM + // ========================================================================== + + const _renderAddForm = ( + form: { username: string; email: string; roleIds: string[] }, + setForm: React.Dispatch>, + roles: RoleInfo[], + target: 'mandate' | 'instance', + onAdd: () => void, + ) => ( +
+

Benutzer hinzufuegen

+
+
+ + setForm(prev => ({ ...prev, username: e.target.value }))} + placeholder="z.B. hans.muster" + /> +
+
+ + setForm(prev => ({ ...prev, email: e.target.value }))} + placeholder="hans.muster@example.com" + /> +
+
+ {roles.length > 0 && ( +
+ +
+ {roles.map(role => ( + + ))} +
+
+ )} +
+ +
+
+ ); + + // ========================================================================== + // RENDER + // ========================================================================== + + const mandateName = selectedMandate ? getMandateName(selectedMandate) : ''; + + return ( +
+
+
+

Einladungs-Wizard

+

Erstellen Sie mehrere Einladungen in einem Schritt

+
+
+ + {error && ( +
+ {error} + +
+ )} + {success && ( +
+ {success} + +
+ )} + + {step < 5 && _renderStepIndicator()} + + {/* ═══ STEP 1: SELECT MANDATE ═══ */} + {step === 1 && ( +
+

Schritt 1: Mandant auswaehlen

+ +
+ +
+
+ )} + + {/* ═══ STEP 2: MANDATE INVITEES ═══ */} + {step === 2 && ( +
+
+

+ Schritt 2: Benutzer fuer Mandant “{mandateName}” +

+

+ Fuegen Sie Benutzer hinzu, die zum Mandanten eingeladen werden sollen. +

+ {_renderInviteeList(mandateInvitees, mandateRoles, _removeMandateInvitee)} +
+ + {_renderAddForm(mandateInviteeForm, setMandateInviteeForm, mandateRoles, 'mandate', _addMandateInvitee)} + +
+ +
+ + +
+
+
+ )} + + {/* ═══ STEP 3: FEATURE INSTANCE (optional) ═══ */} + {step === 3 && !skipInstance && ( +
+
+

Schritt 3: Feature Instanz auswaehlen (optional)

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

Keine Feature-Instanzen fuer diesen Mandanten verfuegbar.

+ ) : ( + + )} +
+
+ + +
+
+ )} + + {/* ═══ STEP 4: INSTANCE INVITEES ═══ */} + {step === 4 && !skipInstance && selectedInstance && ( +
+
+

+ Schritt 4: Benutzer fuer Feature Instanz “{selectedInstance.label}” +

+

+ Fuegen Sie Benutzer hinzu, die zur Feature Instanz eingeladen werden sollen. +

+ {_renderInviteeList(instanceInvitees, instRoles, _removeInstanceInvitee)} +
+ + {_renderAddForm(instanceInviteeForm, setInstanceInviteeForm, instRoles, 'instance', _addInstanceInvitee)} + +
+ + +
+
+ )} + + {/* ═══ DISPATCH / SUMMARY STEP ═══ */} + {((step === 3 && skipInstance) || (step === 5 && !dispatchResults)) && ( +
+

Zusammenfassung & Versand

+ +
+ Mandant: {mandateName} +
+ + {mandateInvitees.length > 0 && ( +
+

Mandant-Einladungen ({mandateInvitees.length})

+ {_renderInviteeList(mandateInvitees, mandateRoles, () => {})} +
+ )} + + {!skipInstance && selectedInstance && instanceInvitees.length > 0 && ( +
+

+ Feature Instanz “{selectedInstance.label}” Einladungen ({instanceInvitees.length}) +

+ {_renderInviteeList(instanceInvitees, instRoles, () => {})} +
+ )} + +
+ + setExpiresInHours(Number(e.target.value))} + /> +
+ +
+ + +
+
+ )} + + {/* ═══ RESULTS ═══ */} + {step === 5 && dispatchResults && ( +
+

Ergebnis

+
+
+
{dispatchResults.successful}
+
Erfolgreich
+
+
+
{dispatchResults.failed}
+
Fehlgeschlagen
+
+
+
{dispatchResults.total}
+
Gesamt
+
+
+ + {dispatchResults.results.length > 0 && ( + + + + + + + + + + {dispatchResults.results.map((r, idx) => ( + + + + + + ))} + +
BenutzerStatusE-Mail
{r.targetUsername} + + {r.success ? 'Erfolgreich' : r.error || 'Fehler'} + + + {r.emailSent ? 'Versendet' : '-'} +
+ )} + +
+ +
+
+ )} +
+ ); +}; + +export default AdminInvitationWizardPage; diff --git a/src/pages/admin/AdminMandateWizardPage.tsx b/src/pages/admin/AdminMandateWizardPage.tsx new file mode 100644 index 0000000..c02dfdb --- /dev/null +++ b/src/pages/admin/AdminMandateWizardPage.tsx @@ -0,0 +1,838 @@ +/** + * AdminMandateWizardPage (v4.0 - poweron port) + * + * 4-step wizard for mandate management: + * 1. Select/Create Mandate + * 2. Manage Mandate Users (add/remove users to/from mandate) + * 3. Manage Feature Instances (CRUD) + * 4. Manage Users per Feature Instance (CRUD + Roles) + */ + +import React, { useState, useEffect, useCallback } from 'react'; +import { + useUserMandates, + type MandateUser, + type Mandate, + type Role, +} from '../../hooks/useUserMandates'; +import { + useFeatureAccess, + type FeatureInstance, + type FeatureAccessUser, + type FeatureInstanceRole, + type Feature, +} from '../../hooks/useFeatureAccess'; +import { useToast } from '../../contexts/ToastContext'; +import api from '../../api'; +import styles from './Admin.module.css'; + +const TOTAL_STEPS = 4; +const STEP_LABELS = ['Mandant', 'Benutzer', 'Instances', 'Feature-Benutzer']; + +interface RoleOption { + id: string; + roleLabel: string; +} + +export const AdminMandateWizardPage: React.FC = () => { + const { showSuccess, showError } = useToast(); + + const { + fetchMandateUsers, + addUserToMandate, + removeUserFromMandate, + fetchMandates: fetchMandatesList, + fetchRoles: fetchMandateRolesList, + fetchAllUsers, + } = useUserMandates(); + + const { + fetchFeatures, + fetchInstances, + createInstance, + deleteInstance, + fetchInstanceUsers, + addUserToInstance, + removeUserFromInstance, + fetchInstanceRoles: fetchInstanceRolesList, + } = useFeatureAccess(); + + // Wizard state + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Step 1: Mandate + const [mandates, setMandates] = useState([]); + const [selectedMandate, setSelectedMandate] = useState | null>(null); + const [isCreatingMandate, setIsCreatingMandate] = useState(false); + const [mandateForm, setMandateForm] = useState({ name: '', maxInstances: 1, quotaNamesPerYear: 100 }); + + // Step 2: Mandate Users + const [mandateUsers, setMandateUsers] = useState([]); + const [allSystemUsers, setAllSystemUsers] = useState>([]); + const [mandateRoles, setMandateRoles] = useState([]); + const [isAddingMandateUser, setIsAddingMandateUser] = useState(false); + const [addMandateUserForm, setAddMandateUserForm] = useState({ userId: '', roleIds: [] as string[] }); + + // Step 3: Instances + const [features, setFeatures] = useState([]); + const [instances, setInstances] = useState([]); + const [selectedFeatureCode, setSelectedFeatureCode] = useState(''); + const [instanceForm, setInstanceForm] = useState({ label: '', enabled: true }); + const [isCreatingInstance, setIsCreatingInstance] = useState(false); + + // Step 4: Users per instance + const [selectedInstance, setSelectedInstance] = useState(null); + const [instanceUsers, setInstanceUsers] = useState([]); + const [instanceRoles, setInstanceRoles] = useState([]); + const [isAddingInstanceUser, setIsAddingInstanceUser] = useState(false); + const [addInstanceUserForm, setAddInstanceUserForm] = useState({ userId: '', roleIds: [] as string[] }); + + // ═══════════════════════════════════════════════════════════════════════════ + // HELPERS + // ═══════════════════════════════════════════════════════════════════════════ + + const getMandateName = (m: Mandate | Record): string => { + if (m.label) return m.label; + if (typeof m.name === 'object') { + return m.name.de || m.name.en || Object.values(m.name)[0] || m.id; + } + return m.name || m.id; + }; + + const getFeatureLabel = (code: string): string => { + const f = features.find(feat => feat.code === code); + if (f) { + return typeof f.label === 'object' + ? (f.label.de || f.label.en || code) + : (f.label || code); + } + return code; + }; + + const getUserDisplayName = (u: { fullName?: string; firstname?: string | null; lastname?: string | null; username: string }): string => { + if (u.fullName) return u.fullName; + const parts = [u.firstname, u.lastname].filter(Boolean); + return parts.length > 0 ? parts.join(' ') : u.username; + }; + + // ═══════════════════════════════════════════════════════════════════════════ + // DATA LOADING + // ═══════════════════════════════════════════════════════════════════════════ + + const loadMandates = useCallback(async () => { + try { + const data = await fetchMandatesList(); + setMandates(data); + } catch { + setError('Fehler beim Laden der Mandanten'); + } + }, [fetchMandatesList]); + + useEffect(() => { loadMandates(); }, [loadMandates]); + + useEffect(() => { + fetchFeatures().then(setFeatures); + }, [fetchFeatures]); + + // Step 2 + const loadMandateUsers = useCallback(async () => { + if (!selectedMandate) return; + const data = await fetchMandateUsers(selectedMandate.id); + setMandateUsers(data); + }, [selectedMandate, fetchMandateUsers]); + + const loadAllSystemUsers = useCallback(async () => { + const data = await fetchAllUsers(); + setAllSystemUsers(data); + }, [fetchAllUsers]); + + const loadMandateRoles = useCallback(async () => { + if (!selectedMandate) return; + const data = await fetchMandateRolesList(selectedMandate.id); + setMandateRoles(data.map((r: Role) => ({ id: r.id, roleLabel: r.roleLabel }))); + }, [selectedMandate, fetchMandateRolesList]); + + useEffect(() => { + if (step === 2 && selectedMandate) { + loadMandateUsers(); + loadAllSystemUsers(); + loadMandateRoles(); + } + }, [step, selectedMandate, loadMandateUsers, loadAllSystemUsers, loadMandateRoles]); + + // Step 3 + const loadInstances = useCallback(async () => { + if (!selectedMandate) return; + const data = await fetchInstances(selectedMandate.id, selectedFeatureCode || undefined); + setInstances(data); + }, [selectedMandate, selectedFeatureCode, fetchInstances]); + + useEffect(() => { + if (step === 3 && selectedMandate) loadInstances(); + }, [step, selectedMandate, loadInstances]); + + // Step 4 + const loadInstanceUsers = useCallback(async () => { + if (!selectedInstance || !selectedMandate) return; + const data = await fetchInstanceUsers(selectedMandate.id, selectedInstance.id); + setInstanceUsers(data); + }, [selectedInstance, selectedMandate, fetchInstanceUsers]); + + const loadInstanceRoles = useCallback(async () => { + if (!selectedInstance || !selectedMandate) return; + const data = await fetchInstanceRolesList(selectedMandate.id, selectedInstance.id); + setInstanceRoles(data.map((r: FeatureInstanceRole) => ({ id: r.id, roleLabel: r.roleLabel }))); + }, [selectedInstance, selectedMandate, fetchInstanceRolesList]); + + useEffect(() => { + if (step === 4 && selectedInstance) { + loadInstanceUsers(); + loadInstanceRoles(); + loadMandateUsers(); + } + }, [step, selectedInstance, loadInstanceUsers, loadInstanceRoles, loadMandateUsers]); + + // ═══════════════════════════════════════════════════════════════════════════ + // HANDLERS + // ═══════════════════════════════════════════════════════════════════════════ + + const handleCreateMandate = async () => { + if (!mandateForm.name.trim()) { setError('Name ist erforderlich'); return; } + setIsLoading(true); + setError(null); + try { + const response = await api.post('/api/mandates/', { + name: mandateForm.name, + maxInstances: mandateForm.maxInstances, + quotaNamesPerYear: mandateForm.quotaNamesPerYear, + enabled: true, + }); + setSelectedMandate(response.data); + setIsCreatingMandate(false); + showSuccess('Erstellt', 'Mandant erstellt'); + await loadMandates(); + } catch (err: any) { + setError(err?.response?.data?.detail || err?.message || 'Fehler beim Erstellen'); + } finally { + setIsLoading(false); + } + }; + + const handleAddMandateUser = async () => { + if (!selectedMandate || !addMandateUserForm.userId) return; + setIsLoading(true); + setError(null); + try { + const result = await addUserToMandate(selectedMandate.id, { + targetUserId: addMandateUserForm.userId, + roleIds: addMandateUserForm.roleIds, + }); + if (result.success) { + setIsAddingMandateUser(false); + setAddMandateUserForm({ userId: '', roleIds: [] }); + showSuccess('Hinzugefügt', 'Benutzer zum Mandanten hinzugefügt'); + await loadMandateUsers(); + } else { + setError(result.error || 'Fehler beim Hinzufügen'); + } + } finally { + setIsLoading(false); + } + }; + + const handleRemoveMandateUser = async (userId: string) => { + if (!selectedMandate) return; + const result = await removeUserFromMandate(selectedMandate.id, userId); + if (result.success) { + showSuccess('Entfernt', 'Benutzer aus Mandant entfernt'); + await loadMandateUsers(); + } else { + setError(result.error || 'Fehler beim Entfernen'); + } + }; + + const handleCreateInstance = async () => { + if (!instanceForm.label.trim() || !selectedMandate || !selectedFeatureCode) return; + setIsLoading(true); + setError(null); + try { + const result = await createInstance(selectedMandate.id, { + featureCode: selectedFeatureCode, + label: instanceForm.label, + enabled: instanceForm.enabled, + }); + if (result.success) { + setIsCreatingInstance(false); + setInstanceForm({ label: '', enabled: true }); + showSuccess('Erstellt', 'Instance erstellt'); + await loadInstances(); + } else { + setError(result.error || 'Fehler beim Erstellen (Limit erreicht?)'); + } + } finally { + setIsLoading(false); + } + }; + + const handleDeleteInstance = async (instanceId: string) => { + if (!selectedMandate) return; + const result = await deleteInstance(selectedMandate.id, instanceId); + if (result.success) { + showSuccess('Gelöscht', 'Instance gelöscht'); + await loadInstances(); + } else { + setError(result.error || 'Fehler beim Löschen'); + } + }; + + const handleAddInstanceUser = async () => { + if (!selectedInstance || !selectedMandate || !addInstanceUserForm.userId) return; + setIsLoading(true); + setError(null); + try { + const result = await addUserToInstance(selectedMandate.id, selectedInstance.id, { + userId: addInstanceUserForm.userId, + roleIds: addInstanceUserForm.roleIds, + }); + if (result.success) { + setIsAddingInstanceUser(false); + setAddInstanceUserForm({ userId: '', roleIds: [] }); + showSuccess('Hinzugefügt', 'Benutzer zur Feature-Instanz hinzugefügt'); + await loadInstanceUsers(); + } else { + setError(result.error || 'Fehler beim Hinzufügen'); + } + } finally { + setIsLoading(false); + } + }; + + const handleRemoveInstanceUser = async (userId: string) => { + if (!selectedInstance || !selectedMandate) return; + const result = await removeUserFromInstance(selectedMandate.id, selectedInstance.id, userId); + if (result.success) { + showSuccess('Entfernt', 'Benutzer aus Feature-Instanz entfernt'); + await loadInstanceUsers(); + } else { + setError(result.error || 'Fehler beim Entfernen'); + } + }; + + // ═══════════════════════════════════════════════════════════════════════════ + // COMPUTED + // ═══════════════════════════════════════════════════════════════════════════ + + const availableUsersForMandate = allSystemUsers.filter( + u => !mandateUsers.some(mu => mu.userId === u.id) + ); + + const availableUsersForInstance = mandateUsers.filter( + mu => !instanceUsers.some(iu => iu.userId === mu.userId) + ); + + // ═══════════════════════════════════════════════════════════════════════════ + // SHARED UI + // ═══════════════════════════════════════════════════════════════════════════ + + const renderUserTable = ( + users: Array<{ userId?: string; id?: string; username: string; email?: string | null; fullName?: string; firstname?: string | null; lastname?: string | null; enabled?: boolean; roleLabels?: string[] }>, + onRemove: (userId: string) => void, + ) => ( + users.length > 0 ? ( + + + + + + + + + + + + {users.map(u => { + const uid = u.userId || u.id || ''; + return ( + + + + + + + + ); + })} + +
BenutzerE-MailRollenStatusAktion
{getUserDisplayName(u as any)}{u.email || '-'} + {u.roleLabels?.join(', ') || '-'} + + + {u.enabled !== false ? 'Aktiv' : 'Inaktiv'} + + + +
+ ) : ( +
+ Noch keine Benutzer zugewiesen +
+ ) + ); + + const renderAddUserForm = ( + availableUsers: Array<{ id?: string; userId?: string; username: string; email?: string | null; fullName?: string; firstname?: string | null; lastname?: string | null }>, + roles: RoleOption[], + formValue: { userId: string; roleIds: string[] }, + setFormValue: (fn: (prev: { userId: string; roleIds: string[] }) => { userId: string; roleIds: string[] }) => void, + onSubmit: () => void, + onCancel: () => void, + ) => ( +
+
+ + +
+ {roles.length > 0 && ( +
+ +
+ {roles.map(r => ( + + ))} +
+
+ )} +
+ + +
+
+ ); + + // ═══════════════════════════════════════════════════════════════════════════ + // STEP INDICATOR + // ═══════════════════════════════════════════════════════════════════════════ + + const renderStepIndicator = () => ( +
+ {Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map(s => ( +
{ if (s < step) setStep(s); }} + > + + {s < step ? '\u2713' : s} + + {STEP_LABELS[s - 1]} +
+ ))} +
+ ); + + // ═══════════════════════════════════════════════════════════════════════════ + // CARD WRAPPER (reusable section container matching poweron theme) + // ═══════════════════════════════════════════════════════════════════════════ + + const cardStyle: React.CSSProperties = { + background: 'var(--surface-color, #fff)', + border: '1px solid var(--border-color, #e5e7eb)', + borderRadius: '12px', + padding: '24px', + }; + + // ═══════════════════════════════════════════════════════════════════════════ + // RENDER + // ═══════════════════════════════════════════════════════════════════════════ + + return ( +
+
+
+

Mandanten-Verwaltung

+

Schritt-für-Schritt Wizard zur Mandanten-Konfiguration

+
+
+ + {error && ( +
+ {error} + +
+ )} + + {renderStepIndicator()} + + {/* ═══ STEP 1: MANDATE ═══ */} + {step === 1 && ( +
+

Mandant auswählen oder erstellen

+ + {!isCreatingMandate ? ( + <> +
+ {mandates.map(m => ( + + ))} +
+ + + ) : ( +
+
+ + setMandateForm(p => ({ ...p, name: e.target.value }))} + placeholder="z.B. Swiss Trust AG" + /> +
+
+
+ + setMandateForm(p => ({ ...p, maxInstances: parseInt(e.target.value) || 1 }))} + /> + 1 = Einzelkunde, >1 = Service Provider +
+
+ + setMandateForm(p => ({ ...p, quotaNamesPerYear: parseInt(e.target.value) || 0 }))} + /> +
+
+
+ + +
+
+ )} + +
+ +
+
+ )} + + {/* ═══ STEP 2: MANDATE USERS ═══ */} + {step === 2 && selectedMandate && ( +
+
+

+ Benutzer von «{getMandateName(selectedMandate)}» +

+ +
+

+ Alle Systembenutzer können dem Mandanten zugewiesen werden. +

+ + {isAddingMandateUser && renderAddUserForm( + availableUsersForMandate, + mandateRoles, + addMandateUserForm, + setAddMandateUserForm, + handleAddMandateUser, + () => { setIsAddingMandateUser(false); setAddMandateUserForm({ userId: '', roleIds: [] }); }, + )} + +
+ {renderUserTable(mandateUsers as any[], handleRemoveMandateUser)} +
+ +
+ + +
+
+ )} + + {/* ═══ STEP 3: INSTANCES ═══ */} + {step === 3 && selectedMandate && ( +
+
+

+ Feature-Instances für «{getMandateName(selectedMandate)}» +

+ + {instances.length} / {(selectedMandate as any).maxInstances || '?'} Instances + +
+ + {/* Feature Filter */} +
+ + +
+ +
+ {instances.map(inst => ( +
+
+ {inst.label} + + {getFeatureLabel(inst.featureCode)} | {inst.enabled ? 'Aktiv' : 'Inaktiv'} + +
+
+ + +
+
+ ))} +
+ + {!isCreatingInstance ? ( + + ) : ( +
+
+ + +
+
+ + setInstanceForm(p => ({ ...p, label: e.target.value }))} + placeholder="z.B. Kunde A" + /> +
+ +
+ + +
+
+ )} + +
+ + +
+
+ )} + + {/* ═══ STEP 4: FEATURE INSTANCE USERS ═══ */} + {step === 4 && selectedMandate && selectedInstance && ( +
+
+

+ Feature-Benutzer für «{selectedInstance.label}» +

+ +
+

+ Mandant: {getMandateName(selectedMandate)} | Mitglieder des Mandanten können der Feature-Instanz zugewiesen werden. +

+ + {isAddingInstanceUser && renderAddUserForm( + availableUsersForInstance as any[], + instanceRoles, + addInstanceUserForm, + setAddInstanceUserForm, + handleAddInstanceUser, + () => { setIsAddingInstanceUser(false); setAddInstanceUserForm({ userId: '', roleIds: [] }); }, + )} + +
+ {renderUserTable( + instanceUsers.map(u => ({ ...u, userId: u.userId || u.id })), + handleRemoveInstanceUser, + )} +
+ +
+ + +
+
+ )} +
+ ); +}; + +export default AdminMandateWizardPage; + diff --git a/src/pages/admin/index.ts b/src/pages/admin/index.ts index ae3b0b5..4c27ffd 100644 --- a/src/pages/admin/index.ts +++ b/src/pages/admin/index.ts @@ -16,4 +16,6 @@ export { AdminFeatureInstanceUsersPage } from './AdminFeatureInstanceUsersPage'; export { AdminMandateRolePermissionsPage } from './AdminMandateRolePermissionsPage'; export { AdminUserAccessOverviewPage } from './AdminUserAccessOverviewPage'; export { AdminAutomationEventsPage } from './AdminAutomationEventsPage'; -export { AdminLogsPage } from './AdminLogsPage'; \ No newline at end of file +export { AdminLogsPage } from './AdminLogsPage'; +export { default as AdminMandateWizardPage } from './AdminMandateWizardPage'; +export { default as AdminInvitationWizardPage } from './AdminInvitationWizardPage';