frontend_nyla/src/pages/admin/wizards/AdminInvitationWizardPage.tsx
2026-04-02 23:53:24 +02:00

746 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* AdminInvitationWizardPage
*
* 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 (mindestens E-Mail oder Benutzername für neue Benutzer; bestehende Benutzer; Rolle pro Einladung)
* 4. Summary and send
*/
import React, { useState, useEffect } from 'react';
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';
type InviteType = 'mandate' | 'featureInstance';
interface RoleOption {
id: string;
roleLabel: string;
}
interface InviteeEntry {
email: string;
username?: string;
roleIds: string[];
isExisting: boolean;
userId?: string;
}
interface DispatchResult {
email: string;
username?: string;
success: boolean;
error?: string;
emailSent?: boolean;
}
// =============================================================================
// WIZARD-STYLE CONSTANTS
// =============================================================================
const EXPIRES_IN_HOURS = 72;
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 { showSuccess } = useToast();
const { createInvitation } = useInvitations();
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<string | null>(null);
// Step 1: Invite type
const [inviteType, setInviteType] = useState<InviteType | null>(null);
// Step 2: Selection
const [mandates, setMandates] = useState<Mandate[]>([]);
const [selectedMandate, setSelectedMandate] = useState<Mandate | null>(null);
const [instances, setInstances] = useState<FeatureInstance[]>([]);
const [selectedInstance, setSelectedInstance] = useState<FeatureInstance | null>(null);
// Step 3: Invitees
const [invitees, setInvitees] = useState<InviteeEntry[]>([]);
const [inviteeForm, setInviteeForm] = useState({ email: '', username: '', roleIds: [] as string[] });
const [addMode, setAddMode] = useState<'email' | 'existing'>('email');
const [selectedExistingUserId, setSelectedExistingUserId] = useState('');
const [allSystemUsers, setAllSystemUsers] = useState<Array<{ id: string; username: string; email?: string; fullName?: string }>>([]);
const [roles, setRoles] = useState<RoleOption[]>([]);
// Step 4: Dispatch
const [dispatchResults, setDispatchResults] = useState<DispatchResult[] | null>(null);
// Users who already have access (to filter from existing-user dropdown)
const [existingAccessUserIds, setExistingAccessUserIds] = useState<Set<string>>(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
// ==========================================================================
useEffect(() => {
fetchMandates().then(setMandates);
}, [fetchMandates]);
useEffect(() => {
if (inviteType === 'featureInstance' && selectedMandate) {
// Show all instances for the mandate (including disabled). Previously only `enabled` instances were listed, which hid deactivated instances from admins.
fetchInstances(selectedMandate.id).then(data => setInstances(data || []));
} else {
setInstances([]);
setSelectedInstance(null);
}
}, [inviteType, selectedMandate, fetchInstances]);
useEffect(() => {
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);
}
}, [step, selectedMandate, selectedInstance, inviteType, fetchRoles, fetchInstanceRoles, fetchAllUsers, fetchMandateUsers, fetchInstanceUsers]);
// ==========================================================================
// INVITEE HANDLERS
// ==========================================================================
const addInviteeByEmail = () => {
const email = inviteeForm.email.trim();
const username = inviteeForm.username.trim();
if (!email && !username) {
setError('Bitte mindestens eine E-Mail-Adresse oder einen Benutzernamen angeben.');
return;
}
const emailLower = email.toLowerCase();
const userLower = username.toLowerCase();
if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) {
setError('Diese E-Mail ist bereits in der Liste');
return;
}
if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) {
setError('Dieser Benutzername ist bereits in der Liste');
return;
}
setInvitees(prev => [...prev, {
email,
username: username || undefined,
roleIds: [...inviteeForm.roleIds],
isExisting: false,
}]);
setInviteeForm({ email: '', username: '', roleIds: [] });
setError(null);
};
const addInviteeExisting = () => {
if (!selectedExistingUserId) {
setError('Bitte wählen Sie einen Benutzer');
return;
}
const user = allSystemUsers.find(u => u.id === selectedExistingUserId);
if (!user) return;
const email = (user.email || '').trim();
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,
}]);
setInviteeForm({ email: '', username: '', roleIds: [] });
setSelectedExistingUserId('');
setError(null);
};
const removeInvitee = (index: number) => {
setInvitees(prev => prev.filter((_, i) => i !== index));
};
const availableExistingUsers = allSystemUsers.filter(
u => !invitees.some(i => i.userId === u.id) && !existingAccessUserIds.has(u.id)
);
// ==========================================================================
// DISPATCH
// ==========================================================================
const handleSend = async () => {
if (!selectedMandate || invitees.length === 0) return;
if (inviteType === 'featureInstance' && !selectedInstance) {
setError('Bitte wählen Sie eine Feature-Instanz aus.');
return;
}
setIsLoading(true);
setError(null);
const results: DispatchResult[] = [];
try {
for (const inv of invitees) {
const emailTrim = (inv.email || '').trim();
const payload = {
...(emailTrim ? { email: emailTrim } : {}),
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: emailTrim,
username: inv.username,
success: true,
emailSent: result.data?.emailSent,
});
} else {
results.push({
email: emailTrim,
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 resetWizard = () => {
setStep(1);
setInviteType(null);
setSelectedMandate(null);
setSelectedInstance(null);
setInvitees([]);
setInviteeForm({ email: '', username: '', roleIds: [] });
setDispatchResults(null);
setError(null);
};
// ==========================================================================
// STEP LABELS
// ==========================================================================
const stepLabels = inviteType === 'featureInstance'
? ['Art', 'Mandant & Instanz', 'Einladungen', 'Zusammenfassung']
: ['Art', 'Mandant', 'Einladungen', 'Zusammenfassung'];
const canProceedStep3 = inviteType === 'mandate'
? selectedMandate !== null
: selectedMandate !== null && selectedInstance !== null && instances.length > 0;
// ==========================================================================
// RENDER
// ==========================================================================
return (
<div className={styles.adminPage} style={{ overflow: 'auto' }}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Einladungs-Wizard</h1>
<p className={styles.pageSubtitle}>
Benutzer zu Mandant oder Feature-Instanz einladen
</p>
</div>
</div>
{error && (
<div className={styles.errorContainer} style={{
flexDirection: 'row', padding: '12px 16px', marginBottom: '16px',
background: '#fef2f2', borderRadius: '8px', fontSize: '13px', justifyContent: 'flex-start',
}}>
{error}
<button onClick={() => setError(null)} style={{ marginLeft: '8px', cursor: 'pointer', background: 'none', border: 'none', fontWeight: 600 }}>&times;</button>
</div>
)}
{/* Step indicator */}
{!dispatchResults && (
<div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
{stepLabels.map((label, idx) => {
const s = idx + 1;
return (
<div
key={s}
style={_stepStyle(s, step)}
onClick={() => { if (s < step) setStep(s); }}
>
<span style={{
width: '24px', height: '24px', borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: s === step ? 'rgba(255,255,255,0.2)' : 'transparent',
fontSize: '12px',
}}>
{s < step ? '\u2713' : s}
</span>
{label}
</div>
);
})}
</div>
)}
{/* ── STEP 1: Invite type ── */}
{step === 1 && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Wohin möchten Sie einladen?</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
<button
onClick={() => setInviteType('mandate')}
style={{
padding: '20px', border: `2px solid ${inviteType === 'mandate' ? 'var(--primary-color, #f25843)' : 'var(--border-color, #e5e7eb)'}`,
borderRadius: '8px', background: inviteType === 'mandate' ? 'var(--primary-bg, rgba(242,88,67,0.06))' : 'var(--surface-color, #fff)',
cursor: 'pointer', textAlign: 'left',
}}
>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>Zum Mandanten</div>
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
Einladung zum Mandanten ohne spezifische Feature-Instanz
</div>
</button>
<button
onClick={() => setInviteType('featureInstance')}
style={{
padding: '20px', border: `2px solid ${inviteType === 'featureInstance' ? 'var(--primary-color, #f25843)' : 'var(--border-color, #e5e7eb)'}`,
borderRadius: '8px', background: inviteType === 'featureInstance' ? 'var(--primary-bg, rgba(242,88,67,0.06))' : 'var(--surface-color, #fff)',
cursor: 'pointer', textAlign: 'left',
}}
>
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>Zur Feature-Instanz</div>
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
Einladung zu einer bestimmten Feature-Instanz mit Rolle
</div>
</button>
</div>
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'flex-end' }}>
<button className={styles.primaryButton} disabled={!inviteType} onClick={() => setStep(2)}>
Weiter &rarr;
</button>
</div>
</div>
)}
{/* ── STEP 2: Select mandate / mandate + instance ── */}
{step === 2 && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>
{inviteType === 'mandate' ? 'Mandant auswählen' : 'Mandant und Feature-Instanz auswählen'}
</h3>
<div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>Mandant *</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={selectedMandate?.id || ''}
onChange={e => {
const m = mandates.find(x => x.id === e.target.value);
setSelectedMandate(m || null);
setSelectedInstance(null);
}}
>
<option value="">-- Mandant wählen --</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>{getMandateName(m)}</option>
))}
</select>
</div>
{inviteType === 'featureInstance' && selectedMandate && (
<div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>Feature-Instanz *</label>
{instances.length === 0 ? (
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Keine Feature-Instanzen für diesen Mandanten vorhanden.</p>
) : (
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={selectedInstance?.id || ''}
onChange={e => {
const inst = instances.find(i => i.id === e.target.value);
setSelectedInstance(inst || null);
}}
>
<option value="">-- Feature-Instanz wählen --</option>
{instances.map(inst => {
const baseLabel = inst.label || inst.featureCode;
const suffix = inst.enabled === false ? ' (deaktiviert)' : '';
return (
<option key={inst.id} value={inst.id}>{`${baseLabel}${suffix}`}</option>
);
})}
</select>
)}
</div>
)}
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(1)}>&larr; Zurück</button>
<button
className={styles.primaryButton}
disabled={!canProceedStep3}
onClick={() => setStep(3)}
>
Weiter &rarr;
</button>
</div>
</div>
)}
{/* ── STEP 3: Add invitees ── */}
{step === 3 && selectedMandate && (
<div>
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>Einladungen hinzufügen</h3>
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', margin: '0 0 16px 0' }}>
Für neue Benutzer: mindestens eine E-Mail <em>oder</em> ein Benutzername (vorgegeben). Ohne E-Mail wird kein Link per Mail versendet der Einladungslink kann manuell geteilt werden. Bestehende Benutzer wählen Sie im zweiten Tab.
</p>
{/* Add form: toggle email vs existing */}
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
<button
className={addMode === 'email' ? styles.primaryButton : styles.secondaryButton}
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setAddMode('email')}
>
Neue Benutzer (E-Mail und/oder Benutzername)
</button>
<button
className={addMode === 'existing' ? styles.primaryButton : styles.secondaryButton}
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setAddMode('existing')}
>
Bestehende Benutzer
</button>
</div>
{addMode === 'email' ? (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div>
<label className={styles.formLabel}>E-Mail (optional)</label>
<input
className={styles.formInput}
type="email"
value={inviteeForm.email}
onChange={e => setInviteeForm(p => ({ ...p, email: e.target.value }))}
placeholder="beispiel@firma.com"
/>
</div>
<div>
<label className={styles.formLabel}>Benutzername (optional)</label>
<input
className={styles.formInput}
type="text"
autoComplete="off"
value={inviteeForm.username}
onChange={e => setInviteeForm(p => ({ ...p, username: e.target.value }))}
placeholder="z. B. vorname.nachname"
/>
<p style={{ fontSize: '11px', color: 'var(--text-secondary)', marginTop: '4px' }}>
Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden.
</p>
</div>
{roles.length > 0 && (
<div>
<label className={styles.formLabel}>Rolle{inviteType === 'featureInstance' ? ' (pro Instanz)' : ''} *</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{roles.map(r => (
<label key={r.id} className={styles.checkboxLabel} style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '6px 12px', borderRadius: '6px',
background: inviteeForm.roleIds.includes(r.id) ? '#dbeafe' : 'var(--bg-secondary, #f8fafc)',
border: `1px solid ${inviteeForm.roleIds.includes(r.id) ? 'var(--primary-color, #F25843)' : 'var(--border-color, #e2e8f0)'}`,
fontSize: '12px', cursor: 'pointer',
}}>
<input
type="checkbox"
checked={inviteeForm.roleIds.includes(r.id)}
onChange={e => {
setInviteeForm(p => ({
...p,
roleIds: e.target.checked ? [...p.roleIds, r.id] : p.roleIds.filter(id => id !== r.id),
}));
}}
/>
{r.roleLabel}
</label>
))}
</div>
</div>
)}
<button
className={styles.primaryButton}
onClick={addInviteeByEmail}
disabled={
(!inviteeForm.email.trim() && !inviteeForm.username.trim())
|| (roles.length > 0 && inviteeForm.roleIds.length === 0)
}
>
Hinzufügen
</button>
</div>
) : (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div>
<label className={styles.formLabel}>Bestehender Benutzer *</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={selectedExistingUserId}
onChange={e => setSelectedExistingUserId(e.target.value)}
>
<option value="">-- Benutzer wählen --</option>
{availableExistingUsers.map(u => (
<option key={u.id} value={u.id}>
{u.username} {u.email ? `(${u.email})` : ''}
</option>
))}
</select>
{availableExistingUsers.length === 0 && <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '4px' }}>Keine weiteren Benutzer verfügbar.</p>}
</div>
{roles.length > 0 && (
<div>
<label className={styles.formLabel}>Rolle *</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{roles.map(r => (
<label key={r.id} className={styles.checkboxLabel} style={{
display: 'flex', alignItems: 'center', gap: '6px',
padding: '6px 12px', borderRadius: '6px',
background: inviteeForm.roleIds.includes(r.id) ? '#dbeafe' : 'var(--bg-secondary, #f8fafc)',
border: `1px solid ${inviteeForm.roleIds.includes(r.id) ? 'var(--primary-color, #F25843)' : 'var(--border-color, #e2e8f0)'}`,
fontSize: '12px', cursor: 'pointer',
}}>
<input
type="checkbox"
checked={inviteeForm.roleIds.includes(r.id)}
onChange={e => {
setInviteeForm(p => ({
...p,
roleIds: e.target.checked ? [...p.roleIds, r.id] : p.roleIds.filter(id => id !== r.id),
}));
}}
/>
{r.roleLabel}
</label>
))}
</div>
</div>
)}
<button
className={styles.primaryButton}
onClick={addInviteeExisting}
disabled={!selectedExistingUserId || (roles.length > 0 && inviteeForm.roleIds.length === 0)}
>
Hinzufügen
</button>
</div>
)}
{/* Invitee list */}
{invitees.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail / Benutzer</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Benutzername</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Rollen</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Typ</th>
<th style={{ textAlign: 'right', padding: '8px' }}>Aktion</th>
</tr>
</thead>
<tbody>
{invitees.map((inv, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--bg-secondary, #f1f5f9)' }}>
<td style={{ padding: '8px' }}>{inv.email || '—'}</td>
<td style={{ padding: '8px', color: 'var(--text-secondary)' }}>
{inv.username || ''}
</td>
<td style={{ padding: '8px' }}>
{inv.roleIds.length > 0
? roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ')
: '-'}
</td>
<td style={{ padding: '8px', fontSize: '12px' }}>{inv.isExisting ? 'Bestehend' : 'Neu (Einladung)'}</td>
<td style={{ padding: '8px', textAlign: 'right' }}>
<button
onClick={() => removeInvitee(idx)}
style={{ background: 'none', border: 'none', color: 'var(--danger-color)', cursor: 'pointer', fontWeight: 600 }}
>
Entfernen
</button>
</td>
</tr>
))}
</tbody>
</table>
) : (
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', marginBottom: '16px' }}>Noch keine Einladungen hinzugefügt.</p>
)}
</div>
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(2)}>&larr; Zurück</button>
<button
className={styles.primaryButton}
disabled={invitees.length === 0}
onClick={() => setStep(4)}
>
Zur Zusammenfassung &rarr;
</button>
</div>
</div>
)}
{/* ── STEP 4: Summary and send ── */}
{step === 4 && selectedMandate && !dispatchResults && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Zusammenfassung & Versand</h3>
<div style={{ marginBottom: '16px' }}>
<strong>Art:</strong> {inviteType === 'mandate' ? 'Einladung zum Mandanten' : 'Einladung zur Feature-Instanz'}
</div>
<div style={{ marginBottom: '16px' }}>
<strong>Mandant:</strong> {getMandateName(selectedMandate)}
</div>
{inviteType === 'featureInstance' && selectedInstance && (
<div style={{ marginBottom: '16px' }}>
<strong>Feature-Instanz:</strong> {selectedInstance.label || selectedInstance.featureCode}
</div>
)}
<div style={{ marginBottom: '16px' }}>
<strong>Einladungen ({invitees.length}):</strong>
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
{invitees.map((inv, i) => (
<li key={i} style={{ marginBottom: '4px' }}>
{[inv.email || null, inv.username ? `@${inv.username}` : null].filter(Boolean).join(' · ')
|| '—'}
{inv.roleIds.length > 0 && ` ${roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ')}`}
</li>
))}
</ul>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
<button className={styles.secondaryButton} onClick={() => setStep(3)}>&larr; Zurück</button>
<button
className={styles.primaryButton}
disabled={isLoading}
onClick={handleSend}
>
{isLoading ? 'Wird versendet...' : `${invitees.length} Einladungen versenden`}
</button>
</div>
</div>
)}
{/* ── Results ── */}
{dispatchResults && (
<div style={_cardStyle}>
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Ergebnis</h3>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail / Benutzer</th>
<th style={{ textAlign: 'left', padding: '8px' }}>Status</th>
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail gesendet</th>
</tr>
</thead>
<tbody>
{dispatchResults.map((r, idx) => (
<tr key={idx} style={{ borderBottom: '1px solid var(--bg-secondary, #f1f5f9)' }}>
<td style={{ padding: '8px' }}>
{(r.email || '').trim() && r.username
? `${(r.email || '').trim()} (@${r.username})`
: (r.email || '').trim() || (r.username ? `@${r.username}` : '—')}
</td>
<td style={{ padding: '8px' }}>
<span style={{
padding: '2px 8px', borderRadius: '4px', fontSize: '12px',
background: r.success ? '#dcfce7' : '#fef2f2',
color: r.success ? '#166534' : '#991b1b',
}}>
{r.success ? 'Erfolgreich' : r.error || 'Fehler'}
</span>
</td>
<td style={{ padding: '8px' }}>{r.emailSent ? 'Ja' : '-'}</td>
</tr>
))}
</tbody>
</table>
<div style={{ marginTop: '16px' }}>
<button className={styles.primaryButton} onClick={resetWizard}>
Neuen Wizard starten
</button>
</div>
</div>
)}
</div>
);
};
export default AdminInvitationWizardPage;