/** * 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(null); // Step 1: Invite type const [inviteType, setInviteType] = useState(null); // Step 2: Selection const [mandates, setMandates] = useState([]); const [selectedMandate, setSelectedMandate] = useState(null); const [instances, setInstances] = useState([]); const [selectedInstance, setSelectedInstance] = useState(null); // Step 3: Invitees const [invitees, setInvitees] = useState([]); const [inviteeForm, setInviteeForm] = useState({ email: '', username: '', roleIds: [] as string[] }); const [addMode, setAddMode] = useState<'email' | 'existing'>('email'); const [selectedExistingUserId, setSelectedExistingUserId] = useState(''); const [allSystemUsers, setAllSystemUsers] = useState>([]); const [roles, setRoles] = useState([]); // Step 4: Dispatch const [dispatchResults, setDispatchResults] = useState(null); // Users who already have access (to filter from existing-user dropdown) const [existingAccessUserIds, setExistingAccessUserIds] = useState>(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 (

Einladungs-Wizard

Benutzer zu Mandant oder Feature-Instanz einladen

{error && (
{error}
)} {/* Step indicator */} {!dispatchResults && (
{stepLabels.map((label, idx) => { const s = idx + 1; return (
{ if (s < step) setStep(s); }} > {s < step ? '\u2713' : s} {label}
); })}
)} {/* ── STEP 1: Invite type ── */} {step === 1 && (

Wohin möchten Sie einladen?

)} {/* ── STEP 2: Select mandate / mandate + instance ── */} {step === 2 && (

{inviteType === 'mandate' ? 'Mandant auswählen' : 'Mandant und Feature-Instanz auswählen'}

{inviteType === 'featureInstance' && selectedMandate && (
{instances.length === 0 ? (

Keine Feature-Instanzen für diesen Mandanten vorhanden.

) : ( )}
)}
)} {/* ── STEP 3: Add invitees ── */} {step === 3 && selectedMandate && (

Einladungen hinzufügen

Für neue Benutzer: mindestens eine E-Mail oder 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.

{/* Add form: toggle email vs existing */}
{addMode === 'email' ? (
setInviteeForm(p => ({ ...p, email: e.target.value }))} placeholder="beispiel@firma.com" />
setInviteeForm(p => ({ ...p, username: e.target.value }))} placeholder="z. B. vorname.nachname" />

Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden.

{roles.length > 0 && (
{roles.map(r => ( ))}
)}
) : (
{availableExistingUsers.length === 0 &&

Keine weiteren Benutzer verfügbar.

}
{roles.length > 0 && (
{roles.map(r => ( ))}
)}
)} {/* Invitee list */} {invitees.length > 0 ? ( {invitees.map((inv, idx) => ( ))}
E-Mail / Benutzer Benutzername Rollen Typ Aktion
{inv.email || '—'} {inv.username || ''} {inv.roleIds.length > 0 ? roles.filter(r => inv.roleIds.includes(r.id)).map(r => r.roleLabel).join(', ') : '-'} {inv.isExisting ? 'Bestehend' : 'Neu (Einladung)'}
) : (

Noch keine Einladungen hinzugefügt.

)}
)} {/* ── STEP 4: Summary and send ── */} {step === 4 && selectedMandate && !dispatchResults && (

Zusammenfassung & Versand

Art: {inviteType === 'mandate' ? 'Einladung zum Mandanten' : 'Einladung zur Feature-Instanz'}
Mandant: {getMandateName(selectedMandate)}
{inviteType === 'featureInstance' && selectedInstance && (
Feature-Instanz: {selectedInstance.label || selectedInstance.featureCode}
)}
Einladungen ({invitees.length}):
    {invitees.map((inv, i) => (
  • {[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(', ')}`}
  • ))}
)} {/* ── Results ── */} {dispatchResults && (

Ergebnis

{dispatchResults.map((r, idx) => ( ))}
E-Mail / Benutzer Status E-Mail gesendet
{(r.email || '').trim() && r.username ? `${(r.email || '').trim()} (@${r.username})` : (r.email || '').trim() || (r.username ? `@${r.username}` : '—')} {r.success ? 'Erfolgreich' : r.error || 'Fehler'} {r.emailSent ? 'Ja' : '-'}
)}
); }; export default AdminInvitationWizardPage;