frontend_nyla/src/pages/admin/wizards/FeatureInstanceWizard.tsx
2026-04-11 22:23:35 +02:00

290 lines
11 KiB
TypeScript

/**
* FeatureInstanceWizard
*
* Guided flow: Create instance → Sync roles → Add users (optional).
*/
import React, { useState, useMemo } from 'react';
import { useFeatureAccess } from '../../../hooks/useFeatureAccess';
import { FormGeneratorForm, type AttributeDefinition } from '../../../components/FormGenerator/FormGeneratorForm';
import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api';
import type { Mandate } from '../../../hooks/useUserMandates';
import type { Feature } from '../../../hooks/useFeatureAccess';
import styles from '../Admin.module.css';
import wizardStyles from './FeatureInstanceWizard.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
function getMandateName(m: Mandate): string {
return m.label || m.name || m.id;
}
export interface FeatureInstanceWizardProps {
mandateId: string;
mandates: Mandate[];
features: Feature[];
onClose: () => void;
onComplete: () => void;
}
export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ mandateId: initialMandateId,
mandates,
features,
onClose,
onComplete,
}) => {
const { createInstance, addUserToInstance, fetchInstanceRoles } = useFeatureAccess();
const { showSuccess, showError } = useToast();
const { t } = useLanguage();
const steps = useMemo(() => [
{ id: 'create' as const, title: t('Instanz erstellen') },
{ id: 'roles' as const, title: t('Rollen') },
{ id: 'users' as const, title: t('Benutzer (optional)') },
], [t]);
const [step, setStep] = useState(0);
const [mandateId, setMandateId] = useState(initialMandateId || '');
const [featureCode, setFeatureCode] = useState('');
const [label, setLabel] = useState('');
const [enabled, setEnabled] = useState(true);
const [copyTemplateRoles, setCopyTemplateRoles] = useState(true);
const [createdInstanceId, setCreatedInstanceId] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [mandateUsers, setMandateUsers] = useState<Array<{ id: string; username: string; email?: string }>>([]);
const [instanceRoles, setInstanceRoles] = useState<Array<{ id: string; roleLabel: string }>>([]);
const [selectedUserRoles, setSelectedUserRoles] = useState<Array<{ userId: string; roleIds: string[] }>>([]);
const featureOptions = useMemo(
() => features.map((f) => ({ value: f.code, label: f.label || f.code })),
[features]
);
const mandateOptions = useMemo(
() => mandates.map((m) => ({ value: m.id, label: getMandateName(m) })),
[mandates]
);
const createFields: AttributeDefinition[] = useMemo(
() => [
{ name: 'mandateId', label: t('Mandant'), type: 'enum' as const, required: true, options: mandateOptions },
{ name: 'featureCode', label: t('Feature'), type: 'enum' as const, required: true, options: featureOptions },
{ name: 'label', label: t('Bezeichnung'), type: 'string' as const, required: true, editable: true },
{ name: 'enabled', label: t('Aktiv'), type: 'boolean' as const, required: false, editable: true },
],
[mandateOptions, featureOptions]
);
const handleStep1Submit = async (data: {
mandateId: string;
featureCode: string;
label: string;
enabled?: boolean;
}) => {
setSubmitting(true);
try {
const result = await createInstance(data.mandateId, {
featureCode: data.featureCode,
label: data.label,
enabled: data.enabled !== false,
copyTemplateRoles: copyTemplateRoles,
});
if (result.success && result.data) {
setMandateId(data.mandateId);
setFeatureCode(data.featureCode);
setLabel(data.label);
setEnabled(data.enabled !== false);
setCreatedInstanceId(result.data.id);
setStep(1);
} else {
showError(t('Fehler'), result.error || t('Instanz konnte nicht erstellt werden'));
}
} finally {
setSubmitting(false);
}
};
const handleStep2Next = async () => {
if (createdInstanceId && mandateId) {
setSubmitting(true);
try {
const [roleList, usersRes] = await Promise.all([
fetchInstanceRoles(mandateId, createdInstanceId),
api.get(`/api/mandates/${mandateId}/users`),
]);
setInstanceRoles(Array.isArray(roleList) ? roleList : []);
const data = usersRes.data?.items || usersRes.data || [];
setMandateUsers(
Array.isArray(data)
? data.map((u: { userId: string; username: string; email?: string }) => ({
id: u.userId,
username: u.username,
email: u.email,
}))
: []
);
} catch {
setInstanceRoles([]);
setMandateUsers([]);
} finally {
setSubmitting(false);
}
}
setStep(2);
};
const handleStep3Complete = async () => {
if (!createdInstanceId || !mandateId) {
onComplete();
return;
}
setSubmitting(true);
try {
for (const { userId, roleIds } of selectedUserRoles) {
if (roleIds.length > 0) {
await addUserToInstance(mandateId, createdInstanceId, { userId, roleIds });
}
}
showSuccess(t('Fertig'), t('Feature-Instanz wurde erstellt und Benutzer zugewiesen.'));
onComplete();
} catch {
showError(t('Fehler'), t('Einige Benutzer konnten nicht zugewiesen werden.'));
} finally {
setSubmitting(false);
}
};
const handleAddUserRole = (userId: string, roleIds: string[]) => {
setSelectedUserRoles((prev) => {
const rest = prev.filter((p) => p.userId !== userId);
if (roleIds.length === 0) return rest;
return [...rest, { userId, roleIds }];
});
};
const currentStepId = steps[step]?.id;
return (
<div className={styles.modalOverlay} onClick={onClose}>
<div className={`${styles.modal} ${wizardStyles.modal}`} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Neue Feature-Instanz')}</h2>
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('Schließen')}>
</button>
</div>
<div className={wizardStyles.steps}>
{steps.map((s, i) => (
<div
key={s.id}
className={`${wizardStyles.stepDot} ${i <= step ? wizardStyles.stepDotActive : ''}`}
title={s.title}
>
{i + 1}
</div>
))}
</div>
<div className={styles.modalContent}>
{currentStepId === 'create' && (
<div className={wizardStyles.stepContent}>
<FormGeneratorForm
attributes={createFields}
mode="create"
data={{
mandateId: mandateId || (mandates[0]?.id ?? ''),
featureCode: featureCode || (features[0]?.code ?? ''),
label,
enabled,
}}
onSubmit={handleStep1Submit}
onCancel={onClose}
submitButtonText={t('Weiter')}
cancelButtonText={t('Abbrechen')}
/>
<label className={wizardStyles.checkLabel}>
<input
type="checkbox"
checked={copyTemplateRoles}
onChange={(e) => setCopyTemplateRoles(e.target.checked)}
/>
{t('Rollen von Feature-Vorlage übernehmen (empfohlen)')}
</label>
</div>
)}
{currentStepId === 'roles' && (
<div className={wizardStyles.stepContent}>
<p className={wizardStyles.stepText}>
{t('Die Rollen wurden beim Erstellen der Instanz übernommen. Sie können später unter „Benutzer verwalten“ weitere Rollen synchronisieren.')}
</p>
<div className={wizardStyles.stepActions}>
<button type="button" className={styles.secondaryButton} onClick={() => setStep(0)}>
{t('← Zurück')}
</button>
<button type="button" className={styles.primaryButton} onClick={handleStep2Next}>
{t('Weiter →')}
</button>
</div>
</div>
)}
{currentStepId === 'users' && (
<div className={wizardStyles.stepContent}>
<p className={wizardStyles.stepText}>
{t('Optional: Weisen Sie Benutzern Rollen zu. Sie können dies auch später in der Zugriffsverwaltung tun.')}
</p>
{mandateUsers.length === 0 ? (
<p className={wizardStyles.stepText}>{t('Keine Mandantenbenutzer vorhanden')}</p>
) : (
<div className={wizardStyles.userList}>
{mandateUsers.map((u) => {
const selected = selectedUserRoles.find((s) => s.userId === u.id);
const roleIds = selected?.roleIds ?? [];
return (
<div key={u.id} className={wizardStyles.userRow}>
<span className={wizardStyles.userName}>{u.username}</span>
<select
className={wizardStyles.roleSelect}
value={roleIds[0] ?? ''}
onChange={(e) => {
const roleId = e.target.value;
const rids = roleId ? [roleId] : [];
handleAddUserRole(u.id, rids);
}}
>
<option value="">{t('Keine Rolle')}</option>
{instanceRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.roleLabel}
</option>
))}
</select>
</div>
);
})}
</div>
)}
<div className={wizardStyles.stepActions}>
<button type="button" className={styles.secondaryButton} onClick={() => setStep(1)}>
{t('← Zurück')}
</button>
<button
type="button"
className={styles.primaryButton}
onClick={handleStep3Complete}
disabled={submitting}
>
{submitting ? t('Speichern…') : t('Erstellen')}
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default FeatureInstanceWizard;