fix: Invitation Wizard Anpassungen fertig

This commit is contained in:
Ida Dittrich 2026-02-26 10:46:51 +01:00
parent b5e9599ef0
commit f312cd41b1
7 changed files with 579 additions and 548 deletions

View file

@ -98,6 +98,10 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ 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);

View file

@ -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;

View file

@ -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;

View file

@ -66,8 +66,11 @@ export const InvitePage: React.FC = () => {
if (result.valid && !isAuthenticated) {
localStorage.setItem(PENDING_INVITATION_KEY, token);
// 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
if (result.targetUsername) {
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 (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<h1>Einladung annehmen</h1>
<p>Sie wurden eingeladen, einem Mandanten beizutreten.</p>
<p>{introText}</p>
</div>
<div className={styles.inviteInfo}>
@ -214,10 +226,10 @@ export const InvitePage: React.FC = () => {
<span className={styles.infoLabel}>Status:</span>
<span className={styles.infoValue}>Angemeldet</span>
</div>
{validation.roleLabels && validation.roleLabels.length > 0 && (
{rolesValue && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Zugewiesene Rollen:</span>
<span className={styles.infoValue}>{validation.roleLabels.join(', ')}</span>
<span className={styles.infoLabel}>{rolesLabel}:</span>
<span className={styles.infoValue}>{rolesValue}</span>
</div>
)}
</div>
@ -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 (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.header}>
<h1>Einladung annehmen</h1>
<p>Sie wurden eingeladen, einem Mandanten beizutreten.</p>
<p>{introText}</p>
</div>
<div className={styles.inviteInfo}>
@ -273,21 +285,19 @@ export const InvitePage: React.FC = () => {
<span className={styles.infoValue}>{validation.mandateName}</span>
</div>
)}
{validation.roleLabels && validation.roleLabels.length > 0 && (
{rolesValue && (
<div className={styles.infoRow}>
<span className={styles.infoLabel}>Zugewiesene Rollen:</span>
<span className={styles.infoValue}>{validation.roleLabels.join(', ')}</span>
<span className={styles.infoLabel}>{rolesLabel}:</span>
<span className={styles.infoValue}>{rolesValue}</span>
</div>
)}
</div>
<div className={styles.authPrompt}>
<p>
{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.'}
</p>
</div>
@ -298,48 +308,26 @@ export const InvitePage: React.FC = () => {
)}
<div className={styles.authActions}>
{userExists === true ? (
<button
className={styles.primaryButton}
onClick={handleLoginRedirect}
>
<FaSignInAlt /> Anmelden
</button>
) : userExists === false ? (
<button
className={styles.primaryButton}
onClick={handleRegisterRedirect}
>
<FaUserPlus /> Konto erstellen
</button>
) : (
<>
<button
className={styles.primaryButton}
onClick={handleLoginRedirect}
>
<FaSignInAlt /> Anmelden
<FaUserPlus /> Neues Konto erstellen
</button>
<div className={styles.divider}>
<span>oder</span>
</div>
<button
className={styles.secondaryButton}
onClick={handleRegisterRedirect}
onClick={handleLoginRedirect}
>
<FaUserPlus /> Neues Konto erstellen
<FaSignInAlt /> Mit bestehendem Konto verlinken
</button>
</>
)}
</div>
<div className={styles.authInfo}>
<p>
{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.
</p>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -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,

View file

@ -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<FeatureProviderProps> = ({ 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
*/