import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useUserMandates, type MandateUser, type Mandate, type Role, } from '../../../hooks/useUserMandates'; import { useFeatureAccess, type FeatureInstance, type FeatureAccessUser, type FeatureInstanceRole, type Feature, } from '../../../hooks/useFeatureAccess'; import { useToast } from '../../../contexts/ToastContext'; import { useApiRequest } from '../../../hooks/useApi'; import { useMandateFormAttributes } from '../../../hooks/useMandates'; import { createMandate, type MandateCreateData } from '../../../api/mandateApi'; import { updateSettingsAdmin } from '../../../api/billingApi'; import { splitMandateAndBillingFromForm } from '../../../utils/mandateBillingFormMerge'; import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm'; import styles from '../Admin.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; import { mandateDisplayLabel } from '../../../utils/mandateDisplayUtils'; const TOTAL_STEPS = 4; interface RoleOption { id: string; roleLabel: string; } export const AdminMandateWizardPage: React.FC = () => { const { t } = useLanguage(); const { showSuccess, showWarning, showError } = useToast(); const { request } = useApiRequest(); const { createFormAttributes, createFormAttributesWithBilling, loading: mandateAttrLoading, } = useMandateFormAttributes(); const { fetchMandateUsers, addUserToMandate, removeUserFromMandate, updateUserRoles, fetchMandates: fetchMandatesList, fetchRoles: fetchMandateRolesList, fetchAllUsers, } = useUserMandates(); const { fetchFeatures, fetchInstances, createInstance, deleteInstance, fetchInstanceUsers, addUserToInstance, removeUserFromInstance, updateInstanceUserRoles, fetchInstanceRoles: fetchInstanceRolesList, } = useFeatureAccess(); // Wizard state const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); // Step 1: Mandate const [mandates, setMandates] = useState([]); const [selectedMandate, setSelectedMandate] = useState | null>(null); const [isCreatingMandate, setIsCreatingMandate] = useState(false); // Step 2: Mandate Users const [mandateUsers, setMandateUsers] = useState([]); const [allSystemUsers, setAllSystemUsers] = useState>([]); const [mandateRoles, setMandateRoles] = useState([]); const [isAddingMandateUser, setIsAddingMandateUser] = useState(false); const [addMandateUserForm, setAddMandateUserForm] = useState({ userIds: [] as string[], roleIds: [] as string[] }); // Step 3: Instances const [features, setFeatures] = useState([]); const [instances, setInstances] = useState([]); const [selectedFeatureCode, setSelectedFeatureCode] = useState(''); const [instanceForm, setInstanceForm] = useState({ label: '', enabled: true }); const [isCreatingInstance, setIsCreatingInstance] = useState(false); // Step 4: Users per instance const [selectedInstance, setSelectedInstance] = useState(null); const [instanceUsers, setInstanceUsers] = useState([]); const [instanceRoles, setInstanceRoles] = useState([]); const [isAddingInstanceUser, setIsAddingInstanceUser] = useState(false); const [addInstanceUserForm, setAddInstanceUserForm] = useState({ userIds: [] as string[], roleIds: [] as string[] }); const [roleEditContext, setRoleEditContext] = useState< null | { scope: 'mandate' | 'instance'; userId: string } >(null); const [roleEditDraft, setRoleEditDraft] = useState([]); // ───────────────────────────────────────────────────────────────────────── // HELPERS // ───────────────────────────────────────────────────────────────────────── const getMandateName = (m: Mandate | Record): string => mandateDisplayLabel(m as { label?: string | null; name?: string | null; id?: string }); const getFeatureLabel = (code: string): string => { const f = features.find(feat => feat.code === code); return f ? (f.label || code) : code; }; const getUserDisplayName = (u: { fullName?: string; firstname?: string | null; lastname?: string | null; username: string }): string => { if (u.fullName) return u.fullName; const parts = [u.firstname, u.lastname].filter(Boolean); return parts.length > 0 ? parts.join(' ') : u.username; }; // ───────────────────────────────────────────────────────────────────────── // DATA LOADING // ───────────────────────────────────────────────────────────────────────── const loadMandates = useCallback(async () => { try { const data = await fetchMandatesList(); setMandates(data); } catch { setError(t('Fehler beim Laden der Mandanten')); } }, [fetchMandatesList, t]); useEffect(() => { loadMandates(); }, [loadMandates]); const stepLabels = useMemo(() => [ t('Mandant'), t('Benutzer'), t('Feature-Instanzen'), t('Feature-Benutzer'), ], [t]); useEffect(() => { fetchFeatures().then(setFeatures); }, [fetchFeatures]); useEffect(() => { setRoleEditContext(null); setRoleEditDraft([]); }, [step]); // Step 2 const loadMandateUsers = useCallback(async () => { if (!selectedMandate) return; const data = await fetchMandateUsers(selectedMandate.id); setMandateUsers(data); }, [selectedMandate, fetchMandateUsers]); const loadAllSystemUsers = useCallback(async () => { const data = await fetchAllUsers(); setAllSystemUsers(data); }, [fetchAllUsers]); const loadMandateRoles = useCallback(async () => { if (!selectedMandate) return; const data = await fetchMandateRolesList(selectedMandate.id); setMandateRoles(data.map((r: Role) => ({ id: r.id, roleLabel: r.roleLabel }))); }, [selectedMandate, fetchMandateRolesList]); useEffect(() => { if (step === 2 && selectedMandate) { loadMandateUsers(); loadAllSystemUsers(); loadMandateRoles(); } }, [step, selectedMandate, loadMandateUsers, loadAllSystemUsers, loadMandateRoles]); // Step 3 const loadInstances = useCallback(async () => { if (!selectedMandate) return; const data = await fetchInstances(selectedMandate.id, selectedFeatureCode || undefined); setInstances(data); }, [selectedMandate, selectedFeatureCode, fetchInstances]); useEffect(() => { if (step === 3 && selectedMandate) loadInstances(); }, [step, selectedMandate, loadInstances]); // Step 4 const loadInstanceUsers = useCallback(async () => { if (!selectedInstance || !selectedMandate) return; const data = await fetchInstanceUsers(selectedMandate.id, selectedInstance.id); setInstanceUsers(data); }, [selectedInstance, selectedMandate, fetchInstanceUsers]); const loadInstanceRoles = useCallback(async () => { if (!selectedInstance || !selectedMandate) return; const data = await fetchInstanceRolesList(selectedMandate.id, selectedInstance.id); setInstanceRoles(data.map((r: FeatureInstanceRole) => ({ id: r.id, roleLabel: r.roleLabel }))); }, [selectedInstance, selectedMandate, fetchInstanceRolesList]); useEffect(() => { if (step === 4 && selectedInstance) { loadInstanceUsers(); loadInstanceRoles(); loadMandateUsers(); } }, [step, selectedInstance, loadInstanceUsers, loadInstanceRoles, loadMandateUsers]); // ───────────────────────────────────────────────────────────────────────── // HANDLERS // ───────────────────────────────────────────────────────────────────────── const handleCreateMandate = async (data: Record) => { setIsLoading(true); setError(null); try { const { mandatePayload, billingUpdate } = splitMandateAndBillingFromForm(data); const body: MandateCreateData = { ...(mandatePayload as Record), label: String(mandatePayload.label ?? '').trim(), enabled: typeof mandatePayload.enabled === 'boolean' ? mandatePayload.enabled : true, }; const created = await createMandate(request, body); let billingSaved = false; try { await updateSettingsAdmin(request, String(created.id), billingUpdate); billingSaved = true; } catch (billingErr: unknown) { console.error(billingErr); showWarning( t('Mandant erstellt'), t('Abrechnungseinstellungen konnten nicht gespeichert werden. Bitte unter Administration → Abrechnung nachpflegen.'), ); } setSelectedMandate(created as Record); setIsCreatingMandate(false); if (billingSaved) { showSuccess(t('Erstellt'), t('Mandant inkl. Abrechnung gespeichert')); } window.dispatchEvent(new CustomEvent('features-changed')); await loadMandates(); } catch (err: unknown) { const e = err as { response?: { data?: { detail?: string } }; message?: string }; setError(e?.response?.data?.detail || e?.message || t('Fehler beim Erstellen des Mandanten')); } finally { setIsLoading(false); } }; const handleAddMandateUser = async () => { if (!selectedMandate || addMandateUserForm.userIds.length === 0) return; setIsLoading(true); setError(null); const failures: string[] = []; let ok = 0; try { for (const uid of addMandateUserForm.userIds) { const result = await addUserToMandate(selectedMandate.id, { targetUserId: uid, roleIds: addMandateUserForm.roleIds, }); if (result.success) ok += 1; else failures.push(`${uid}: ${result.error || t('Fehler')}`); } if (ok > 0) { setIsAddingMandateUser(false); setAddMandateUserForm({ userIds: [], roleIds: [] }); showSuccess(t('Hinzugefügt'), t('{count} Benutzer zum Mandanten hinzugefügt', { count: ok })); await loadMandateUsers(); } if (failures.length > 0) { showWarning( t('Teilweise fehlgeschlagen'), failures.slice(0, 5).join('; ') + (failures.length > 5 ? '…' : ''), ); if (ok === 0) setError(failures.join('; ')); else await loadMandateUsers(); } } finally { setIsLoading(false); } }; const handleRemoveMandateUser = async (userId: string) => { if (!selectedMandate) return; const result = await removeUserFromMandate(selectedMandate.id, userId); if (result.success) { showSuccess(t('Entfernt'), t('Benutzer aus Mandant entfernt')); await loadMandateUsers(); } else { setError(result.error || t('Fehler beim Entfernen')); } }; const handleCreateInstance = async () => { if (!instanceForm.label.trim() || !selectedMandate || !selectedFeatureCode) return; setIsLoading(true); setError(null); try { const result = await createInstance(selectedMandate.id, { featureCode: selectedFeatureCode, label: instanceForm.label, enabled: instanceForm.enabled, }); if (result.success) { setIsCreatingInstance(false); setInstanceForm({ label: '', enabled: true }); showSuccess(t('Erstellt'), t('Feature-Instanz erstellt')); await loadInstances(); } else { setError(result.error || t('Fehler beim Erstellen der Instanz (Limit erreicht?)')); } } finally { setIsLoading(false); } }; const handleDeleteInstance = async (instanceId: string) => { if (!selectedMandate) return; const result = await deleteInstance(selectedMandate.id, instanceId); if (result.success) { showSuccess(t('Gelöscht'), t('Feature-Instanz gelöscht')); await loadInstances(); } else { setError(result.error || t('Fehler beim Löschen der Instanz')); } }; const handleAddInstanceUser = async () => { if (!selectedInstance || !selectedMandate || addInstanceUserForm.userIds.length === 0) return; setIsLoading(true); setError(null); const failures: string[] = []; let ok = 0; try { for (const uid of addInstanceUserForm.userIds) { const result = await addUserToInstance(selectedMandate.id, selectedInstance.id, { userId: uid, roleIds: addInstanceUserForm.roleIds, }); if (result.success) ok += 1; else failures.push(`${uid}: ${result.error || t('Fehler')}`); } if (ok > 0) { setIsAddingInstanceUser(false); setAddInstanceUserForm({ userIds: [], roleIds: [] }); showSuccess(t('Hinzugefügt'), t('{count} Benutzer zur Feature-Instanz hinzugefügt', { count: ok })); await loadInstanceUsers(); } if (failures.length > 0) { showWarning( t('Teilweise fehlgeschlagen'), failures.slice(0, 5).join('; ') + (failures.length > 5 ? '…' : ''), ); if (ok === 0) setError(failures.join('; ')); else await loadInstanceUsers(); } } finally { setIsLoading(false); } }; const _saveMandateRoleEdit = async () => { if (!selectedMandate || roleEditContext?.scope !== 'mandate') return; setIsLoading(true); setError(null); const r = await updateUserRoles(selectedMandate.id, roleEditContext.userId, roleEditDraft); if (r.success) { showSuccess(t('Gespeichert'), t('Rollen aktualisiert')); setRoleEditContext(null); setRoleEditDraft([]); await loadMandateUsers(); } else { showError(t('Fehler'), r.error || t('Rollen konnten nicht gespeichert werden')); } setIsLoading(false); }; const _saveInstanceRoleEdit = async () => { if (!selectedMandate || !selectedInstance || roleEditContext?.scope !== 'instance') return; setIsLoading(true); setError(null); const r = await updateInstanceUserRoles( selectedMandate.id, selectedInstance.id, roleEditContext.userId, { roleIds: roleEditDraft }, ); if (r.success) { showSuccess(t('Gespeichert'), t('Rollen aktualisiert')); setRoleEditContext(null); setRoleEditDraft([]); await loadInstanceUsers(); } else { showError(t('Fehler'), r.error || t('Rollen konnten nicht gespeichert werden')); } setIsLoading(false); }; const handleRemoveInstanceUser = async (userId: string) => { if (!selectedInstance || !selectedMandate) return; const result = await removeUserFromInstance(selectedMandate.id, selectedInstance.id, userId); if (result.success) { showSuccess(t('Entfernt'), t('Benutzer aus Feature-Instanz entfernt')); await loadInstanceUsers(); } else { setError(result.error || t('Fehler beim Entfernen')); } }; // ───────────────────────────────────────────────────────────────────────── // COMPUTED // ───────────────────────────────────────────────────────────────────────── const availableUsersForMandate = allSystemUsers.filter( u => !mandateUsers.some(mu => mu.userId === u.id) ); const availableUsersForInstance = mandateUsers.filter( mu => !instanceUsers.some(iu => iu.userId === mu.userId) ); // ───────────────────────────────────────────────────────────────────────── // SHARED UI // ───────────────────────────────────────────────────────────────────────── type WizardUserRow = { userId?: string; id?: string; username: string; email?: string | null; fullName?: string; firstname?: string | null; lastname?: string | null; enabled?: boolean; roleIds?: string[]; roleLabels?: string[]; }; const _roleTextForRow = (u: WizardUserRow, roleLookup: RoleOption[]) => { if (u.roleLabels && u.roleLabels.length > 0) return u.roleLabels.join(', '); if (u.roleIds && u.roleIds.length > 0) { return u.roleIds .map(rid => roleLookup.find(r => r.id === rid)?.roleLabel || rid) .join(', '); } return '-'; }; const renderUserTable = ( users: WizardUserRow[], roleLookup: RoleOption[], onRemove: (userId: string) => void, options?: { onEditRoles?: (userId: string, currentRoleIds: string[]) => void }, ) => ( users.length > 0 ? ( {users.map(u => { const uid = u.userId || u.id || ''; const ids = u.roleIds || []; return ( ); })}
{t('Benutzer')} {t('E-Mail')} {t('Rollen')} {t('Status')} {t('Aktion')}
{getUserDisplayName(u as any)} {u.email || '-'} {_roleTextForRow(u, roleLookup)} {options?.onEditRoles && roleLookup.length > 0 && ( )} {u.enabled !== false ? t('Aktiv') : t('Inaktiv')}
) : (
{t('Noch keine Benutzer zugewiesen')}
) ); const renderAddUserForm = ( availableUsers: Array<{ id?: string; userId?: string; username: string; email?: string | null; fullName?: string; firstname?: string | null; lastname?: string | null }>, roles: RoleOption[], formValue: { userIds: string[]; roleIds: string[] }, setFormValue: (fn: (prev: { userIds: string[]; roleIds: string[] }) => { userIds: string[]; roleIds: string[] }) => void, onSubmit: () => void, onCancel: () => void, ) => (
{availableUsers.map(u => { const uid = u.userId || u.id || ''; const name = getUserDisplayName(u as any); return ( ); })}
{roles.length > 0 && (
{roles.map(r => ( ))}
)}
); // ───────────────────────────────────────────────────────────────────────── // STEP INDICATOR // ───────────────────────────────────────────────────────────────────────── const renderStepIndicator = () => (
{Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map(s => (
{ if (s < step) setStep(s); }} > {s < step ? '\u2713' : s} {stepLabels[s - 1]}
))}
); // ───────────────────────────────────────────────────────────────────────── // CARD WRAPPER (reusable section container matching poweron theme) // ───────────────────────────────────────────────────────────────────────── const cardStyle: React.CSSProperties = { background: 'var(--surface-color, #fff)', border: '1px solid var(--border-color, #e5e7eb)', borderRadius: '12px', padding: '24px', }; // ───────────────────────────────────────────────────────────────────────── // RENDER // ───────────────────────────────────────────────────────────────────────── return (

{t('Mandanten-Verwaltung')}

{t('Schritt-für-Schritt-Wizard zur Mandantenkonfiguration')}

{error && (
{error}
)} {renderStepIndicator()} {/* ── STEP 1: MANDATE ── */} {step === 1 && (

{t('Mandant auswählen oder erstellen')}

{!isCreatingMandate ? ( <>
{mandates.map(m => ( ))}
) : (
{mandateAttrLoading || createFormAttributes.length === 0 ? (
{t('Formular wird geladen')}
) : ( setIsCreatingMandate(false)} submitButtonText={isLoading ? t('Erstellen') : t('Mandant erstellen')} cancelButtonText={t('Abbrechen')} /> )}
)}
)} {/* ── STEP 2: MANDATE USERS ── */} {step === 2 && selectedMandate && (

{t('Benutzer von «{name}»', { name: getMandateName(selectedMandate) })}

{t('Alle Systembenutzer können dem Mandanten zugewiesen werden.')}

{isAddingMandateUser && renderAddUserForm( availableUsersForMandate, mandateRoles, addMandateUserForm, setAddMandateUserForm, handleAddMandateUser, () => { setIsAddingMandateUser(false); setAddMandateUserForm({ userIds: [], roleIds: [] }); }, )} {roleEditContext?.scope === 'mandate' && (
{t('Rollen bearbeiten')}
{mandateRoles.map(r => ( ))}
)}
{renderUserTable(mandateUsers as WizardUserRow[], mandateRoles, handleRemoveMandateUser, { onEditRoles: (userId, ids) => { setError(null); setRoleEditContext({ scope: 'mandate', userId }); setRoleEditDraft([...ids]); }, })}
)} {/* ── STEP 3: INSTANCES ── */} {step === 3 && selectedMandate && (

{t('Feature-Instanzen für «{name}»', { name: getMandateName(selectedMandate) })}

{t('{count} Instanzen', { count: instances.length })}
{/* Feature Filter */}
{instances.map(inst => (
{inst.label} {getFeatureLabel(inst.featureCode)} | {inst.enabled ? t('Aktiv') : t('Inaktiv')}
))}
{!isCreatingInstance ? ( ) : (
setInstanceForm(p => ({ ...p, label: e.target.value }))} placeholder={t('z.B. Kunde A')} />
)}
)} {/* ── STEP 4: FEATURE INSTANCE USERS ── */} {step === 4 && selectedMandate && selectedInstance && (

{t('Feature-Benutzer für «{label}»', { label: selectedInstance.label })}

{t('Mandant: {mandate} | Mitglieder des Mandanten können der Feature-Instanz zugewiesen werden.', { mandate: getMandateName(selectedMandate) })}

{isAddingInstanceUser && renderAddUserForm( availableUsersForInstance as any[], instanceRoles, addInstanceUserForm, setAddInstanceUserForm, handleAddInstanceUser, () => { setIsAddingInstanceUser(false); setAddInstanceUserForm({ userIds: [], roleIds: [] }); }, )} {roleEditContext?.scope === 'instance' && (
{t('Rollen bearbeiten (Featureinstanz)')}
{instanceRoles.map(r => ( ))}
)}
{renderUserTable( instanceUsers.map(u => ({ ...u, userId: u.userId || u.id })) as WizardUserRow[], instanceRoles, handleRemoveInstanceUser, { onEditRoles: (userId, ids) => { setError(null); setRoleEditContext({ scope: 'instance', userId }); setRoleEditDraft([...ids]); }, }, )}
)}
); }; export default AdminMandateWizardPage;