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) { if (result) {
setActionSuccess(notification.id); 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 // Clear success state after animation
setTimeout(() => { setTimeout(() => {
setActionSuccess(null); setActionSuccess(null);

View file

@ -47,7 +47,9 @@ export interface Invitation {
} }
export interface InvitationCreate { 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; email?: string;
roleIds: string[]; roleIds: string[];
featureInstanceId?: string; featureInstanceId?: string;
@ -62,6 +64,7 @@ export interface InvitationValidation {
mandateId?: string; mandateId?: string;
mandateName?: string; mandateName?: string;
featureInstanceId?: string; featureInstanceId?: string;
featureInstanceName?: string;
roleIds: string[]; roleIds: string[];
roleLabels?: string[]; roleLabels?: string[];
targetUsername?: string; targetUsername?: string;

View file

@ -125,6 +125,8 @@
.infoRow { .infoRow {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline;
gap: 1rem;
padding: 0.5rem 0; padding: 0.5rem 0;
} }
@ -133,11 +135,15 @@
} }
.infoLabel { .infoLabel {
flex-shrink: 0;
min-width: 12rem;
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.875rem; font-size: 0.875rem;
} }
.infoValue { .infoValue {
flex: 1;
text-align: right;
color: var(--text-primary); color: var(--text-primary);
font-weight: 500; font-weight: 500;
font-size: 0.875rem; font-size: 0.875rem;

View file

@ -66,8 +66,11 @@ export const InvitePage: React.FC = () => {
if (result.valid && !isAuthenticated) { if (result.valid && !isAuthenticated) {
localStorage.setItem(PENDING_INVITATION_KEY, token); localStorage.setItem(PENDING_INVITATION_KEY, token);
// Check if the target username already has an account // No targetUsername = new-user invitation (email only) -> only show "Neues Konto erstellen"
if (result.targetUsername) { if (!result.targetUsername) {
setUserExists(false); // Treat as new user, show only register
} else {
// Check if the target username already has an account
try { try {
const resp = await api.get(`/api/local/available`, { const resp = await api.get(`/api/local/available`, {
params: { username: result.targetUsername } params: { username: result.targetUsername }
@ -188,13 +191,22 @@ export const InvitePage: React.FC = () => {
} }
// Already authenticated - show accept button // 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) { if (isAuthenticated) {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.card}> <div className={styles.card}>
<div className={styles.header}> <div className={styles.header}>
<h1>Einladung annehmen</h1> <h1>Einladung annehmen</h1>
<p>Sie wurden eingeladen, einem Mandanten beizutreten.</p> <p>{introText}</p>
</div> </div>
<div className={styles.inviteInfo}> <div className={styles.inviteInfo}>
@ -214,10 +226,10 @@ export const InvitePage: React.FC = () => {
<span className={styles.infoLabel}>Status:</span> <span className={styles.infoLabel}>Status:</span>
<span className={styles.infoValue}>Angemeldet</span> <span className={styles.infoValue}>Angemeldet</span>
</div> </div>
{validation.roleLabels && validation.roleLabels.length > 0 && ( {rolesValue && (
<div className={styles.infoRow}> <div className={styles.infoRow}>
<span className={styles.infoLabel}>Zugewiesene Rollen:</span> <span className={styles.infoLabel}>{rolesLabel}:</span>
<span className={styles.infoValue}>{validation.roleLabels.join(', ')}</span> <span className={styles.infoValue}>{rolesValue}</span>
</div> </div>
)} )}
</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 ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.card}> <div className={styles.card}>
<div className={styles.header}> <div className={styles.header}>
<h1>Einladung annehmen</h1> <h1>Einladung annehmen</h1>
<p>Sie wurden eingeladen, einem Mandanten beizutreten.</p> <p>{introText}</p>
</div> </div>
<div className={styles.inviteInfo}> <div className={styles.inviteInfo}>
@ -273,21 +285,19 @@ export const InvitePage: React.FC = () => {
<span className={styles.infoValue}>{validation.mandateName}</span> <span className={styles.infoValue}>{validation.mandateName}</span>
</div> </div>
)} )}
{validation.roleLabels && validation.roleLabels.length > 0 && ( {rolesValue && (
<div className={styles.infoRow}> <div className={styles.infoRow}>
<span className={styles.infoLabel}>Zugewiesene Rollen:</span> <span className={styles.infoLabel}>{rolesLabel}:</span>
<span className={styles.infoValue}>{validation.roleLabels.join(', ')}</span> <span className={styles.infoValue}>{rolesValue}</span>
</div> </div>
)} )}
</div> </div>
<div className={styles.authPrompt}> <div className={styles.authPrompt}>
<p> <p>
{userExists === true {userExists === true && validation.targetUsername
? `Bitte melden Sie sich als "${validation.targetUsername}" an, um die Einladung anzunehmen.` ? `Sie haben bereits ein Konto (${validation.targetUsername}). Melden Sie sich an oder erstellen Sie ein neues Konto.`
: userExists === false : 'Erstellen Sie ein neues Konto mit Ihrem Benutzernamen oder verlinken Sie die Einladung mit Ihrem bestehenden Account.'}
? 'Bitte erstellen Sie ein Konto, um die Einladung anzunehmen.'
: 'Bitte melden Sie sich an oder erstellen Sie ein Konto, um die Einladung anzunehmen.'}
</p> </p>
</div> </div>
@ -298,48 +308,26 @@ export const InvitePage: React.FC = () => {
)} )}
<div className={styles.authActions}> <div className={styles.authActions}>
{userExists === true ? ( <button
<button className={styles.primaryButton}
className={styles.primaryButton} onClick={handleRegisterRedirect}
onClick={handleLoginRedirect} >
> <FaUserPlus /> Neues Konto erstellen
<FaSignInAlt /> Anmelden </button>
</button> <div className={styles.divider}>
) : userExists === false ? ( <span>oder</span>
<button </div>
className={styles.primaryButton} <button
onClick={handleRegisterRedirect} className={styles.secondaryButton}
> onClick={handleLoginRedirect}
<FaUserPlus /> Konto erstellen >
</button> <FaSignInAlt /> Mit bestehendem Konto verlinken
) : ( </button>
<>
<button
className={styles.primaryButton}
onClick={handleLoginRedirect}
>
<FaSignInAlt /> Anmelden
</button>
<div className={styles.divider}>
<span>oder</span>
</div>
<button
className={styles.secondaryButton}
onClick={handleRegisterRedirect}
>
<FaUserPlus /> Neues Konto erstellen
</button>
</>
)}
</div> </div>
<div className={styles.authInfo}> <div className={styles.authInfo}>
<p> <p>
{userExists === true 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.
? '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.'}
</p> </p>
</div> </div>
</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 React, { useState, useEffect, useCallback } from 'react';
import { import {
useUserMandates, useUserMandates,

View file

@ -5,7 +5,7 @@
* Ein User gehört keinem Mandanten direkt an, sondern hat Zugriff auf Feature-Instanzen. * 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 { import type {
Mandate, Mandate,
MandateFeature, 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 * Holt einen Mandanten per ID
*/ */