ui-nyla/src/pages/admin/wizards/AdminInvitationWizardPage.tsx

717 lines
30 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 (email required, username optional; existing users; role per invitee)
* 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();
if (!email) {
setError('E-Mail ist erforderlich');
return;
}
setInvitees(prev => [...prev, {
email,
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 (!email) {
setError('Dieser Benutzer hat keine E-Mail-Adresse');
return;
}
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 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 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' }}>
E-Mail ist erforderlich. Neue Benutzer legen ihren Benutzernamen beim Annehmen der Einladung selbst fest. Sie können neue Benutzer per E-Mail oder bestehende Benutzer hinzufügen.
</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')}
>
Per E-Mail (neue Benutzer)
</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} ${styles.required}`}>E-Mail *</label>
<input
className={styles.formInput}
type="email"
value={inviteeForm.email}
onChange={e => setInviteeForm(p => ({ ...p, email: e.target.value }))}
placeholder="beispiel@firma.com"
/>
<p style={{ fontSize: '11px', color: 'var(--text-secondary)', marginTop: '4px' }}>
Der Benutzername wird vom eingeladenen Benutzer beim Annehmen der Einladung festgelegt.
</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, #3b82f6)' : '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() || (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, #3b82f6)' : '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</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.isExisting ? 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'}</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}{inv.isExisting && inv.username ? ` (${inv.username})` : ''}
{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</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}{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;