diff --git a/src/components/NotificationBell/NotificationBell.tsx b/src/components/NotificationBell/NotificationBell.tsx index a88d9d5..baa33a1 100644 --- a/src/components/NotificationBell/NotificationBell.tsx +++ b/src/components/NotificationBell/NotificationBell.tsx @@ -98,6 +98,10 @@ export const NotificationBell: React.FC = ({ className }) if (result) { setActionSuccess(notification.id); + // Reload sidebar when accepting an invitation (grants new mandate/feature access) + if (actionId === 'accept' && notification.referenceType === 'Invitation') { + window.dispatchEvent(new CustomEvent('features-changed')); + } // Clear success state after animation setTimeout(() => { setActionSuccess(null); diff --git a/src/hooks/useInvitations.ts b/src/hooks/useInvitations.ts index 040d95b..68f505a 100644 --- a/src/hooks/useInvitations.ts +++ b/src/hooks/useInvitations.ts @@ -47,7 +47,9 @@ export interface Invitation { } export interface InvitationCreate { - targetUsername: string; + /** Username of the user to invite (optional when email is provided) */ + targetUsername?: string; + /** Email address to send invitation link (required for new users) */ email?: string; roleIds: string[]; featureInstanceId?: string; @@ -62,6 +64,7 @@ export interface InvitationValidation { mandateId?: string; mandateName?: string; featureInstanceId?: string; + featureInstanceName?: string; roleIds: string[]; roleLabels?: string[]; targetUsername?: string; diff --git a/src/pages/InvitePage.module.css b/src/pages/InvitePage.module.css index d4be4fe..cbd7432 100644 --- a/src/pages/InvitePage.module.css +++ b/src/pages/InvitePage.module.css @@ -125,6 +125,8 @@ .infoRow { display: flex; justify-content: space-between; + align-items: baseline; + gap: 1rem; padding: 0.5rem 0; } @@ -133,11 +135,15 @@ } .infoLabel { + flex-shrink: 0; + min-width: 12rem; color: var(--text-secondary); font-size: 0.875rem; } .infoValue { + flex: 1; + text-align: right; color: var(--text-primary); font-weight: 500; font-size: 0.875rem; diff --git a/src/pages/InvitePage.tsx b/src/pages/InvitePage.tsx index 85c83bc..c0bdfbd 100644 --- a/src/pages/InvitePage.tsx +++ b/src/pages/InvitePage.tsx @@ -66,8 +66,11 @@ export const InvitePage: React.FC = () => { if (result.valid && !isAuthenticated) { localStorage.setItem(PENDING_INVITATION_KEY, token); - // Check if the target username already has an account - if (result.targetUsername) { + // No targetUsername = new-user invitation (email only) -> only show "Neues Konto erstellen" + if (!result.targetUsername) { + setUserExists(false); // Treat as new user, show only register + } else { + // Check if the target username already has an account try { const resp = await api.get(`/api/local/available`, { params: { username: result.targetUsername } @@ -188,13 +191,22 @@ export const InvitePage: React.FC = () => { } // Already authenticated - show accept button + const isFeatureInvite = !!validation.featureInstanceId; + const introText = isFeatureInvite + ? 'Sie wurden eingeladen, einem Mandanten und einem Feature beizutreten.' + : 'Sie wurden eingeladen, einem Mandanten beizutreten.'; + const rolesLabel = isFeatureInvite ? 'Features mit zugewiesenen Rollen' : 'Zugewiesene Rollen'; + const rolesValue = validation.featureInstanceName && validation.roleLabels?.length + ? `${validation.featureInstanceName} (${validation.roleLabels.join(', ')})` + : validation.roleLabels?.join(', ') || ''; + if (isAuthenticated) { return (

Einladung annehmen

-

Sie wurden eingeladen, einem Mandanten beizutreten.

+

{introText}

@@ -214,10 +226,10 @@ export const InvitePage: React.FC = () => { Status: Angemeldet
- {validation.roleLabels && validation.roleLabels.length > 0 && ( + {rolesValue && (
- Zugewiesene Rollen: - {validation.roleLabels.join(', ')} + {rolesLabel}: + {rolesValue}
)}
@@ -251,13 +263,13 @@ export const InvitePage: React.FC = () => { ); } - // Not authenticated - show appropriate options based on whether user account exists + // Not authenticated - show create account / link to existing return (

Einladung annehmen

-

Sie wurden eingeladen, einem Mandanten beizutreten.

+

{introText}

@@ -273,21 +285,19 @@ export const InvitePage: React.FC = () => { {validation.mandateName}
)} - {validation.roleLabels && validation.roleLabels.length > 0 && ( + {rolesValue && (
- Zugewiesene Rollen: - {validation.roleLabels.join(', ')} + {rolesLabel}: + {rolesValue}
)}

- {userExists === true - ? `Bitte melden Sie sich als "${validation.targetUsername}" an, um die Einladung anzunehmen.` - : userExists === false - ? 'Bitte erstellen Sie ein Konto, um die Einladung anzunehmen.' - : 'Bitte melden Sie sich an oder erstellen Sie ein Konto, um die Einladung anzunehmen.'} + {userExists === true && validation.targetUsername + ? `Sie haben bereits ein Konto (${validation.targetUsername}). Melden Sie sich an oder erstellen Sie ein neues Konto.` + : 'Erstellen Sie ein neues Konto mit Ihrem Benutzernamen oder verlinken Sie die Einladung mit Ihrem bestehenden Account.'}

@@ -298,48 +308,26 @@ export const InvitePage: React.FC = () => { )}
- {userExists === true ? ( - - ) : userExists === false ? ( - - ) : ( - <> - -
- oder -
- - - )} + +
+ oder +
+

- {userExists === true - ? 'Melden Sie sich mit Ihrem bestehenden Konto an. Die Einladung wird automatisch nach der Anmeldung akzeptiert.' - : userExists === false - ? 'Erstellen Sie ein neues Konto. Die Einladung wird automatisch nach der Registrierung akzeptiert.' - : 'Die Einladung wird automatisch nach der Anmeldung akzeptiert.'} + Neues Konto: E-Mail wird vorausgefüllt, Benutzername legen Sie selbst fest. Bestehendes Konto: Die Einladung wird nach der Anmeldung automatisch an Ihr Konto verknüpft.

diff --git a/src/pages/admin/wizards/AdminInvitationWizardPage.tsx b/src/pages/admin/wizards/AdminInvitationWizardPage.tsx index 17b0f21..79fe0a5 100644 --- a/src/pages/admin/wizards/AdminInvitationWizardPage.tsx +++ b/src/pages/admin/wizards/AdminInvitationWizardPage.tsx @@ -1,49 +1,48 @@ /** * 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 + * 4-step invitation wizard: + * 1. Choose invite type: "Invite to mandate" OR "Invite to feature instance" + * 2. Select mandate (and feature instance if applicable) + * 3. Add invitees (email required, username optional; existing users; role per invitee) + * 4. Summary and send */ import React, { useState, useEffect } from 'react'; -import { useInvitations, type InvitationCreate } from '../../../hooks/useInvitations'; +import { useInvitations } 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 -// ============================================================================= +type InviteType = 'mandate' | 'featureInstance'; -interface InviteeEntry { - targetUsername: string; - email: string; - roleIds: string[]; +interface RoleOption { + id: string; + roleLabel: string; } -type RoleInfo = { id: string; roleLabel: string }; +interface InviteeEntry { + email: string; + username?: string; + roleIds: string[]; + isExisting: boolean; + userId?: string; +} interface DispatchResult { - targetUsername: string; + email: string; + username?: string; success: boolean; error?: string; emailSent?: boolean; } -interface DispatchResults { - successful: number; - failed: number; - total: number; - results: DispatchResult[]; -} +// ============================================================================= +// WIZARD-STYLE CONSTANTS +// ============================================================================= -// ============================================================================= -// WIZARD-SPECIFIC INLINE STYLES -// ============================================================================= +const EXPIRES_IN_HOURS = 72; const _stepStyle = (stepNum: number, currentStep: number) => ({ display: 'flex', @@ -75,39 +74,49 @@ const _cardStyle: React.CSSProperties = { // ============================================================================= export const AdminInvitationWizardPage: React.FC = () => { + const { showSuccess } = useToast(); const { createInvitation } = useInvitations(); - const { fetchMandates, fetchRoles } = useUserMandates(); - const { fetchInstances, fetchInstanceRoles } = useFeatureAccess(); + const { fetchMandates, fetchRoles, fetchAllUsers, fetchMandateUsers } = useUserMandates(); + const { fetchInstances, fetchInstanceRoles, fetchInstanceUsers } = useFeatureAccess(); const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); - // Step 1: Mandate + // Step 1: Invite type + const [inviteType, setInviteType] = useState(null); + + // Step 2: Selection 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[] }); + // Step 3: Invitees + const [invitees, setInvitees] = useState([]); + const [inviteeForm, setInviteeForm] = useState({ email: '', username: '', roleIds: [] as string[] }); + const [addMode, setAddMode] = useState<'email' | 'existing'>('email'); + const [selectedExistingUserId, setSelectedExistingUserId] = useState(''); + const [allSystemUsers, setAllSystemUsers] = useState>([]); + const [roles, setRoles] = useState([]); - // Dispatch options - const [expiresInHours, setExpiresInHours] = useState(72); + // Step 4: Dispatch + const [dispatchResults, setDispatchResults] = useState(null); - // Dispatch results - const [dispatchResults, setDispatchResults] = useState(null); + // Users who already have access (to filter from existing-user dropdown) + const [existingAccessUserIds, setExistingAccessUserIds] = useState>(new Set()); + + // ========================================================================== + // HELPERS + // ========================================================================== + + const getMandateName = (m: Mandate): 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; + }; // ========================================================================== // DATA LOADING @@ -118,306 +127,183 @@ export const AdminInvitationWizardPage: React.FC = () => { }, [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]); + if (inviteType === 'featureInstance' && selectedMandate) { + fetchInstances(selectedMandate.id).then(data => + setInstances((data || []).filter((i: FeatureInstance) => i.enabled)) + ); + } else { + setInstances([]); + setSelectedInstance(null); + } + }, [inviteType, selectedMandate, 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; + if (step === 3 && selectedMandate) { + if (inviteType === 'mandate') { + fetchRoles(selectedMandate.id).then((r: Role[]) => { + const mandateRoles = r.filter((role: Role) => !role.featureInstanceId); + setRoles(mandateRoles.map((role: Role) => ({ id: role.id, roleLabel: role.roleLabel }))); + }); + fetchMandateUsers(selectedMandate.id).then((users: { userId: string }[]) => { + setExistingAccessUserIds(new Set(users.map(u => u.userId))); + }).catch(() => setExistingAccessUserIds(new Set())); + } else if (inviteType === 'featureInstance' && selectedInstance) { + fetchInstanceRoles(selectedMandate.id, selectedInstance.id).then((r: FeatureInstanceRole[]) => { + setRoles(r.map(role => ({ id: role.id, roleLabel: role.roleLabel }))); + }); + fetchInstanceUsers(selectedMandate.id, selectedInstance.id).then((users: { userId: string }[]) => { + setExistingAccessUserIds(new Set(users.map(u => u.userId))); + }).catch(() => setExistingAccessUserIds(new Set())); + } else { + setRoles([]); + setExistingAccessUserIds(new Set()); + } + fetchAllUsers().then(setAllSystemUsers); } - return m.name || m.id; - }; + }, [step, selectedMandate, selectedInstance, inviteType, fetchRoles, fetchInstanceRoles, fetchAllUsers, fetchMandateUsers, fetchInstanceUsers]); // ========================================================================== - // INVITEE MANAGEMENT + // INVITEE HANDLERS // ========================================================================== - const _addMandateInvitee = () => { - if (!mandateInviteeForm.username.trim()) { - setError('Benutzername ist erforderlich'); + const addInviteeByEmail = () => { + const email = inviteeForm.email.trim(); + if (!email) { + setError('E-Mail 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], + setInvitees(prev => [...prev, { + email, + username: undefined, + roleIds: [...inviteeForm.roleIds], + isExisting: false, }]); - setMandateInviteeForm({ username: '', email: '', roleIds: [] }); + setInviteeForm({ email: '', username: '', 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'); + const addInviteeExisting = () => { + if (!selectedExistingUserId) { + setError('Bitte wählen Sie einen Benutzer'); return; } - if (instanceInvitees.some(i => i.targetUsername === instanceInviteeForm.username.trim())) { - setError('Benutzer bereits in der Liste'); + const user = allSystemUsers.find(u => u.id === selectedExistingUserId); + if (!user) return; + const email = (user.email || '').trim(); + if (!email) { + setError('Dieser Benutzer hat keine E-Mail-Adresse'); return; } - setInstanceInvitees(prev => [...prev, { - targetUsername: instanceInviteeForm.username.trim(), - email: instanceInviteeForm.email.trim(), - roleIds: [...instanceInviteeForm.roleIds], + if (invitees.some(i => i.userId === user.id)) { + setError('Dieser Benutzer ist bereits in der Liste'); + return; + } + setInvitees(prev => [...prev, { + email, + username: user.username, + roleIds: inviteeForm.roleIds, + isExisting: true, + userId: user.id, }]); - setInstanceInviteeForm({ username: '', email: '', roleIds: [] }); + setInviteeForm({ email: '', username: '', roleIds: [] }); + setSelectedExistingUserId(''); setError(null); }; - const _removeInstanceInvitee = (username: string) => { - setInstanceInvitees(prev => prev.filter(i => i.targetUsername !== username)); + const removeInvitee = (index: number) => { + setInvitees(prev => prev.filter((_, i) => i !== index)); }; - 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], - })); - }; + const availableExistingUsers = allSystemUsers.filter( + u => !invitees.some(i => i.userId === u.id) && !existingAccessUserIds.has(u.id) + ); // ========================================================================== // 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); + const handleSend = async () => { + if (!selectedMandate || invitees.length === 0) return; + if (inviteType === 'featureInstance' && !selectedInstance) { + setError('Bitte wählen Sie eine Feature-Instanz aus.'); return; } - - let successful = 0; - let failed = 0; + setIsLoading(true); + setError(null); 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 }); + try { + for (const inv of invitees) { + const payload = { + email: inv.email, + targetUsername: inv.username || undefined, + roleIds: inv.roleIds, + expiresInHours: EXPIRES_IN_HOURS, + ...(inviteType === 'featureInstance' && selectedInstance?.id + ? { featureInstanceId: selectedInstance.id } + : {}), + }; + const result = await createInvitation(selectedMandate.id, payload); + if (result.success) { + results.push({ + email: inv.email, + username: inv.username, + success: true, + emailSent: result.data?.emailSent, + }); + } else { + results.push({ + email: inv.email, + username: inv.username, + success: false, + error: result.error, + }); + } } + setDispatchResults(results); + const ok = results.filter(r => r.success).length; + showSuccess('Versand', `${ok} von ${results.length} Einladungen erfolgreich versendet`); + } catch (err: any) { + setError(err?.response?.data?.detail || err?.message || 'Fehler beim Versand'); + } finally { + setIsLoading(false); } + }; - const dr: DispatchResults = { successful, failed, total: allInvitations.length, results }; - setDispatchResults(dr); - setSuccess(`${successful} von ${dr.total} Einladungen erfolgreich versendet.`); - setStep(5); - setIsLoading(false); + const resetWizard = () => { + setStep(1); + setInviteType(null); + setSelectedMandate(null); + setSelectedInstance(null); + setInvitees([]); + setInviteeForm({ email: '', username: '', roleIds: [] }); + setDispatchResults(null); + setError(null); }; // ========================================================================== - // STEP INDICATOR + // STEP LABELS // ========================================================================== - const _renderStepIndicator = () => { - const visibleSteps = skipInstance ? [1, 2, 3] : [1, 2, 3, 4]; - const labels = skipInstance - ? ['Mandant', 'Benutzer', 'Versand'] - : ['Mandant', 'Benutzer', 'Feature Instanz', 'Instanz-Benutzer']; + const stepLabels = inviteType === 'featureInstance' + ? ['Art', 'Mandant & Instanz', 'Einladungen', 'Zusammenfassung'] + : ['Art', 'Mandant', 'Einladungen', 'Zusammenfassung']; - 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 => ( - - ))} -
-
- )} -
- -
-
- ); + const canProceedStep3 = inviteType === 'mandate' + ? selectedMandate !== null + : selectedMandate !== null && selectedInstance !== null && instances.length > 0; // ========================================================================== // RENDER // ========================================================================== - const mandateName = selectedMandate ? getMandateName(selectedMandate) : ''; - return (

Einladungs-Wizard

-

Erstellen Sie mehrere Einladungen in einem Schritt

+

+ Benutzer zu Mandant oder Feature-Instanz einladen +

@@ -430,247 +316,392 @@ export const AdminInvitationWizardPage: React.FC = () => {
)} - {success && ( -
- {success} - + + {/* Step indicator */} + {!dispatchResults && ( +
+ {stepLabels.map((label, idx) => { + const s = idx + 1; + return ( +
{ if (s < step) setStep(s); }} + > + + {s < step ? '\u2713' : s} + + {label} +
+ ); + })}
)} - {step < 5 && _renderStepIndicator()} - - {/* ── STEP 1: SELECT MANDATE ── */} + {/* ── STEP 1: Invite type ── */} {step === 1 && (
-

Schritt 1: Mandant auswaehlen

- -
+

Wohin möchten Sie einladen?

+
+ +
+
+
)} - {/* ── STEP 2: MANDATE INVITEES ── */} + {/* ── STEP 2: Select mandate / mandate + instance ── */} {step === 2 && ( -
-
-

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

-

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

- {_renderInviteeList(mandateInvitees, mandateRoles, _removeMandateInvitee)} +
+

+ {inviteType === 'mandate' ? 'Mandant auswählen' : 'Mandant und Feature-Instanz auswählen'} +

+
+ +
- - {_renderAddForm(mandateInviteeForm, setMandateInviteeForm, mandateRoles, 'mandate', _addMandateInvitee)} - -
- -
- - + {inviteType === 'featureInstance' && selectedMandate && ( +
+ + {instances.length === 0 ? ( +

Keine aktiven Feature-Instanzen für diesen Mandanten vorhanden.

+ ) : ( + + )}
+ )} +
+ +
)} - {/* ── STEP 3: FEATURE INSTANCE (optional) ── */} - {step === 3 && !skipInstance && ( + {/* ── STEP 3: Add invitees ── */} + {step === 3 && selectedMandate && (
-

Schritt 3: Feature Instanz auswaehlen (optional)

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

Keine Feature-Instanzen fuer diesen Mandanten verfuegbar.

- ) : ( - + Per E-Mail (neue Benutzer) + + +
+ + {addMode === 'email' ? ( +
+
+ + setInviteeForm(p => ({ ...p, email: e.target.value }))} + placeholder="beispiel@firma.com" + /> +

+ Der Benutzername wird vom eingeladenen Benutzer beim Annehmen der Einladung festgelegt. +

+
+ {roles.length > 0 && ( +
+ +
+ {roles.map(r => ( + + ))} +
+
+ )} + +
+ ) : ( +
+
+ + + {availableExistingUsers.length === 0 &&

Keine weiteren Benutzer verfügbar.

} +
+ {roles.length > 0 && ( +
+ +
+ {roles.map(r => ( + + ))} +
+
+ )} + +
+ )} + + {/* Invitee list */} + {invitees.length > 0 ? ( + + + + + + + + + + + + {invitees.map((inv, idx) => ( + + + + + + + + ))} + +
E-MailBenutzernameRollenTypAktion
{inv.email}{inv.isExisting ? inv.username : ''} + {inv.roleIds.length > 0 + ? roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ') + : '-'} + {inv.isExisting ? 'Bestehend' : 'Neu'} + +
+ ) : ( +

Noch keine Einladungen hinzugefügt.

)}
+
- +
)} - {/* ── 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)) && ( + {/* ── STEP 4: Summary and send ── */} + {step === 4 && selectedMandate && !dispatchResults && (
-

Zusammenfassung & Versand

- +

Zusammenfassung & Versand

- Mandant: {mandateName} + Art: {inviteType === 'mandate' ? 'Einladung zum Mandanten' : 'Einladung zur Feature-Instanz'}
- - {mandateInvitees.length > 0 && ( +
+ Mandant: {getMandateName(selectedMandate)} +
+ {inviteType === 'featureInstance' && selectedInstance && (
-

Mandant-Einladungen ({mandateInvitees.length})

- {_renderInviteeList(mandateInvitees, mandateRoles, () => {})} + Feature-Instanz: {selectedInstance.label || selectedInstance.featureCode}
)} - - {!skipInstance && selectedInstance && instanceInvitees.length > 0 && ( -
-

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

- {_renderInviteeList(instanceInvitees, instRoles, () => {})} -
- )} -
- - setExpiresInHours(Number(e.target.value))} - /> + Einladungen ({invitees.length}): +
    + {invitees.map((inv, i) => ( +
  • + {inv.email}{inv.isExisting && inv.username ? ` (${inv.username})` : ''} + {inv.roleIds.length > 0 && ` – ${roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ')}`} +
  • + ))} +
- -
- +
+
)} - {/* ── RESULTS ── */} - {step === 5 && dispatchResults && ( + {/* ── Results ── */} + {dispatchResults && (

Ergebnis

-
-
-
{dispatchResults.successful}
-
Erfolgreich
-
-
-
{dispatchResults.failed}
-
Fehlgeschlagen
-
-
-
{dispatchResults.total}
-
Gesamt
-
-
- - {dispatchResults.results.length > 0 && ( - - - - - - +
BenutzerStatusE-Mail
+ + + + + + + + + {dispatchResults.map((r, idx) => ( + + + + - - - {dispatchResults.results.map((r, idx) => ( - - - - - - ))} - -
E-MailStatusE-Mail gesendet
{r.email}{r.username ? ` (${r.username})` : ''} + + {r.success ? 'Erfolgreich' : r.error || 'Fehler'} + + {r.emailSent ? 'Ja' : '-'}
{r.targetUsername} - - {r.success ? 'Erfolgreich' : r.error || 'Fehler'} - - - {r.emailSent ? 'Versendet' : '-'} -
- )} - + ))} + +
-
diff --git a/src/pages/admin/wizards/AdminMandateWizardPage.tsx b/src/pages/admin/wizards/AdminMandateWizardPage.tsx index c44b90a..8e66add 100644 --- a/src/pages/admin/wizards/AdminMandateWizardPage.tsx +++ b/src/pages/admin/wizards/AdminMandateWizardPage.tsx @@ -1,13 +1,3 @@ -/** - * 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, diff --git a/src/stores/featureStore.tsx b/src/stores/featureStore.tsx index 40ea54a..6d49c16 100644 --- a/src/stores/featureStore.tsx +++ b/src/stores/featureStore.tsx @@ -5,7 +5,7 @@ * Ein User gehört keinem Mandanten direkt an, sondern hat Zugriff auf Feature-Instanzen. */ -import React, { createContext, useContext, useState, useCallback, useRef, ReactNode } from 'react'; +import React, { createContext, useContext, useState, useCallback, useRef, useEffect, ReactNode } from 'react'; import type { Mandate, MandateFeature, @@ -169,6 +169,15 @@ export const FeatureProvider: React.FC = ({ children }) => }); }, []); + // Reload features when access changes (e.g. after accepting an invitation) + useEffect(() => { + const onFeaturesChanged = () => { + loadFeatures(); + }; + window.addEventListener('features-changed', onFeaturesChanged); + return () => window.removeEventListener('features-changed', onFeaturesChanged); + }, [loadFeatures]); + /** * Holt einen Mandanten per ID */