/** * 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 */ import React, { useState, useEffect } from 'react'; import { useInvitations, type InvitationCreate } from '../../hooks/useInvitations'; import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates'; import { useFeatureAccess, type FeatureInstance, type FeatureInstanceRole } from '../../hooks/useFeatureAccess'; import styles from './Admin.module.css'; // ============================================================================= // TYPES // ============================================================================= interface InviteeEntry { targetUsername: string; email: string; roleIds: string[]; } type RoleInfo = { id: string; roleLabel: string }; interface DispatchResult { targetUsername: string; success: boolean; error?: string; emailSent?: boolean; } interface DispatchResults { successful: number; failed: number; total: number; results: DispatchResult[]; } // ============================================================================= // WIZARD-SPECIFIC INLINE STYLES // ============================================================================= 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 { createInvitation } = useInvitations(); const { fetchMandates, fetchRoles } = useUserMandates(); const { fetchInstances, fetchInstanceRoles } = useFeatureAccess(); const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); // Step 1: Mandate 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[] }); // Dispatch options const [expiresInHours, setExpiresInHours] = useState(72); // Dispatch results const [dispatchResults, setDispatchResults] = useState(null); // ========================================================================== // DATA LOADING // ========================================================================== useEffect(() => { fetchMandates().then(setMandates); }, [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]); 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; } return m.name || m.id; }; // ========================================================================== // INVITEE MANAGEMENT // ========================================================================== const _addMandateInvitee = () => { if (!mandateInviteeForm.username.trim()) { setError('Benutzername 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], }]); setMandateInviteeForm({ username: '', email: '', 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'); return; } if (instanceInvitees.some(i => i.targetUsername === instanceInviteeForm.username.trim())) { setError('Benutzer bereits in der Liste'); return; } setInstanceInvitees(prev => [...prev, { targetUsername: instanceInviteeForm.username.trim(), email: instanceInviteeForm.email.trim(), roleIds: [...instanceInviteeForm.roleIds], }]); setInstanceInviteeForm({ username: '', email: '', roleIds: [] }); setError(null); }; const _removeInstanceInvitee = (username: string) => { setInstanceInvitees(prev => prev.filter(i => i.targetUsername !== username)); }; 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], })); }; // ========================================================================== // 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); return; } let successful = 0; let failed = 0; 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 }); } } const dr: DispatchResults = { successful, failed, total: allInvitations.length, results }; setDispatchResults(dr); setSuccess(`${successful} von ${dr.total} Einladungen erfolgreich versendet.`); setStep(5); setIsLoading(false); }; // ========================================================================== // STEP INDICATOR // ========================================================================== const _renderStepIndicator = () => { const visibleSteps = skipInstance ? [1, 2, 3] : [1, 2, 3, 4]; const labels = skipInstance ? ['Mandant', 'Benutzer', 'Versand'] : ['Mandant', 'Benutzer', 'Feature Instanz', 'Instanz-Benutzer']; 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 => ( ))}
Benutzername E-Mail Rollen Aktion
{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 => ( ))}
)}
); // ========================================================================== // RENDER // ========================================================================== const mandateName = selectedMandate ? getMandateName(selectedMandate) : ''; return (

Einladungs-Wizard

Erstellen Sie mehrere Einladungen in einem Schritt

{error && (
{error}
)} {success && (
{success}
)} {step < 5 && _renderStepIndicator()} {/* ═══ STEP 1: SELECT MANDATE ═══ */} {step === 1 && (

Schritt 1: Mandant auswaehlen

)} {/* ═══ STEP 2: MANDATE INVITEES ═══ */} {step === 2 && (

Schritt 2: Benutzer fuer Mandant “{mandateName}”

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

{_renderInviteeList(mandateInvitees, mandateRoles, _removeMandateInvitee)}
{_renderAddForm(mandateInviteeForm, setMandateInviteeForm, mandateRoles, 'mandate', _addMandateInvitee)}
)} {/* ═══ STEP 3: FEATURE INSTANCE (optional) ═══ */} {step === 3 && !skipInstance && (

Schritt 3: Feature Instanz auswaehlen (optional)

{instances.length === 0 ? (

Keine Feature-Instanzen fuer diesen Mandanten verfuegbar.

) : ( )}
)} {/* ═══ 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)) && (

Zusammenfassung & Versand

Mandant: {mandateName}
{mandateInvitees.length > 0 && (

Mandant-Einladungen ({mandateInvitees.length})

{_renderInviteeList(mandateInvitees, mandateRoles, () => {})}
)} {!skipInstance && selectedInstance && instanceInvitees.length > 0 && (

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

{_renderInviteeList(instanceInvitees, instRoles, () => {})}
)}
setExpiresInHours(Number(e.target.value))} />
)} {/* ═══ RESULTS ═══ */} {step === 5 && dispatchResults && (

Ergebnis

{dispatchResults.successful}
Erfolgreich
{dispatchResults.failed}
Fehlgeschlagen
{dispatchResults.total}
Gesamt
{dispatchResults.results.length > 0 && ( {dispatchResults.results.map((r, idx) => ( ))}
Benutzer Status E-Mail
{r.targetUsername} {r.success ? 'Erfolgreich' : r.error || 'Fehler'} {r.emailSent ? 'Versendet' : '-'}
)}
)}
); }; export default AdminInvitationWizardPage;