frontend_nyla/src/pages/admin/wizards/AdminMandateWizardPage.tsx

809 lines
34 KiB
TypeScript

/**
* AdminMandateWizardPage (v4.0 - poweron port)
*
* 4-step wizard for mandate management:
* 1. Select/Create Mandate
* 2. Manage Mandate Users (add/remove users to/from mandate)
* 3. Manage Feature Instances (CRUD)
* 4. Manage Users per Feature Instance (CRUD + Roles)
*/
import React, { useState, useEffect, useCallback } 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 api from '../../../api';
import styles from '../Admin.module.css';
const TOTAL_STEPS = 4;
const STEP_LABELS = ['Mandant', 'Benutzer', 'Instances', 'Feature-Benutzer'];
interface RoleOption {
id: string;
roleLabel: string;
}
export const AdminMandateWizardPage: React.FC = () => {
const { showSuccess } = useToast();
const {
fetchMandateUsers,
addUserToMandate,
removeUserFromMandate,
fetchMandates: fetchMandatesList,
fetchRoles: fetchMandateRolesList,
fetchAllUsers,
} = useUserMandates();
const {
fetchFeatures,
fetchInstances,
createInstance,
deleteInstance,
fetchInstanceUsers,
addUserToInstance,
removeUserFromInstance,
fetchInstanceRoles: fetchInstanceRolesList,
} = useFeatureAccess();
// Wizard state
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Step 1: Mandate
const [mandates, setMandates] = useState<Mandate[]>([]);
const [selectedMandate, setSelectedMandate] = useState<Record<string, any> | null>(null);
const [isCreatingMandate, setIsCreatingMandate] = useState(false);
const [mandateForm, setMandateForm] = useState({ name: '' });
// Step 2: Mandate Users
const [mandateUsers, setMandateUsers] = useState<MandateUser[]>([]);
const [allSystemUsers, setAllSystemUsers] = useState<Array<{ id: string; username: string; email?: string; fullName?: string }>>([]);
const [mandateRoles, setMandateRoles] = useState<RoleOption[]>([]);
const [isAddingMandateUser, setIsAddingMandateUser] = useState(false);
const [addMandateUserForm, setAddMandateUserForm] = useState({ userId: '', roleIds: [] as string[] });
// Step 3: Instances
const [features, setFeatures] = useState<Feature[]>([]);
const [instances, setInstances] = useState<FeatureInstance[]>([]);
const [selectedFeatureCode, setSelectedFeatureCode] = useState<string>('');
const [instanceForm, setInstanceForm] = useState({ label: '', enabled: true });
const [isCreatingInstance, setIsCreatingInstance] = useState(false);
// Step 4: Users per instance
const [selectedInstance, setSelectedInstance] = useState<FeatureInstance | null>(null);
const [instanceUsers, setInstanceUsers] = useState<FeatureAccessUser[]>([]);
const [instanceRoles, setInstanceRoles] = useState<RoleOption[]>([]);
const [isAddingInstanceUser, setIsAddingInstanceUser] = useState(false);
const [addInstanceUserForm, setAddInstanceUserForm] = useState({ userId: '', roleIds: [] as string[] });
// ─────────────────────────────────────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────────────────────────────────────
const getMandateName = (m: Mandate | Record<string, any>): 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;
};
const getFeatureLabel = (code: string): string => {
const f = features.find(feat => feat.code === code);
if (f) {
return typeof f.label === 'object'
? (f.label.de || f.label.en || code)
: (f.label || code);
}
return 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('Fehler beim Laden der Mandanten');
}
}, [fetchMandatesList]);
useEffect(() => { loadMandates(); }, [loadMandates]);
useEffect(() => {
fetchFeatures().then(setFeatures);
}, [fetchFeatures]);
// 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 () => {
if (!mandateForm.name.trim()) { setError('Name ist erforderlich'); return; }
setIsLoading(true);
setError(null);
try {
const response = await api.post('/api/mandates/', {
name: mandateForm.name,
enabled: true,
});
setSelectedMandate(response.data);
setIsCreatingMandate(false);
showSuccess('Erstellt', 'Mandant erstellt');
await loadMandates();
} catch (err: any) {
setError(err?.response?.data?.detail || err?.message || 'Fehler beim Erstellen');
} finally {
setIsLoading(false);
}
};
const handleAddMandateUser = async () => {
if (!selectedMandate || !addMandateUserForm.userId) return;
setIsLoading(true);
setError(null);
try {
const result = await addUserToMandate(selectedMandate.id, {
targetUserId: addMandateUserForm.userId,
roleIds: addMandateUserForm.roleIds,
});
if (result.success) {
setIsAddingMandateUser(false);
setAddMandateUserForm({ userId: '', roleIds: [] });
showSuccess('Hinzugefügt', 'Benutzer zum Mandanten hinzugefügt');
await loadMandateUsers();
} else {
setError(result.error || 'Fehler beim Hinzufügen');
}
} finally {
setIsLoading(false);
}
};
const handleRemoveMandateUser = async (userId: string) => {
if (!selectedMandate) return;
const result = await removeUserFromMandate(selectedMandate.id, userId);
if (result.success) {
showSuccess('Entfernt', 'Benutzer aus Mandant entfernt');
await loadMandateUsers();
} else {
setError(result.error || '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('Erstellt', 'Instance erstellt');
await loadInstances();
} else {
setError(result.error || 'Fehler beim Erstellen (Limit erreicht?)');
}
} finally {
setIsLoading(false);
}
};
const handleDeleteInstance = async (instanceId: string) => {
if (!selectedMandate) return;
const result = await deleteInstance(selectedMandate.id, instanceId);
if (result.success) {
showSuccess('Gelöscht', 'Instance gelöscht');
await loadInstances();
} else {
setError(result.error || 'Fehler beim Löschen');
}
};
const handleAddInstanceUser = async () => {
if (!selectedInstance || !selectedMandate || !addInstanceUserForm.userId) return;
setIsLoading(true);
setError(null);
try {
const result = await addUserToInstance(selectedMandate.id, selectedInstance.id, {
userId: addInstanceUserForm.userId,
roleIds: addInstanceUserForm.roleIds,
});
if (result.success) {
setIsAddingInstanceUser(false);
setAddInstanceUserForm({ userId: '', roleIds: [] });
showSuccess('Hinzugefügt', 'Benutzer zur Feature-Instanz hinzugefügt');
await loadInstanceUsers();
} else {
setError(result.error || 'Fehler beim Hinzufügen');
}
} finally {
setIsLoading(false);
}
};
const handleRemoveInstanceUser = async (userId: string) => {
if (!selectedInstance || !selectedMandate) return;
const result = await removeUserFromInstance(selectedMandate.id, selectedInstance.id, userId);
if (result.success) {
showSuccess('Entfernt', 'Benutzer aus Feature-Instanz entfernt');
await loadInstanceUsers();
} else {
setError(result.error || '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
// ─────────────────────────────────────────────────────────────────────────
const renderUserTable = (
users: Array<{ userId?: string; id?: string; username: string; email?: string | null; fullName?: string; firstname?: string | null; lastname?: string | null; enabled?: boolean; roleLabels?: string[] }>,
onRemove: (userId: string) => void,
) => (
users.length > 0 ? (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: 'var(--bg-secondary, #f8fafc)' }}>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>Benutzer</th>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>E-Mail</th>
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>Rollen</th>
<th style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px', fontWeight: 600 }}>Status</th>
<th style={{ padding: '8px 12px', textAlign: 'right', fontSize: '12px', fontWeight: 600 }}>Aktion</th>
</tr>
</thead>
<tbody>
{users.map(u => {
const uid = u.userId || u.id || '';
return (
<tr key={uid} style={{ borderBottom: '1px solid var(--border-color, #f1f5f9)' }}>
<td style={{ padding: '8px 12px', fontSize: '13px' }}>{getUserDisplayName(u as any)}</td>
<td style={{ padding: '8px 12px', fontSize: '13px', color: 'var(--text-secondary)' }}>{u.email || '-'}</td>
<td style={{ padding: '8px 12px', fontSize: '12px', color: 'var(--text-secondary)' }}>
{u.roleLabels?.join(', ') || '-'}
</td>
<td style={{ padding: '8px 12px', textAlign: 'center' }}>
<span className={styles.badge} style={{
background: u.enabled !== false ? '#dcfce7' : 'var(--bg-secondary)',
color: u.enabled !== false ? '#166534' : 'var(--text-secondary)',
}}>
{u.enabled !== false ? 'Aktiv' : 'Inaktiv'}
</span>
</td>
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
<button
style={{
padding: '3px 8px', fontSize: '11px', border: '1px solid #fecaca', borderRadius: '4px',
background: 'var(--surface-color, #fff)', color: '#dc2626', cursor: 'pointer',
}}
onClick={() => onRemove(uid)}
>
Entfernen
</button>
</td>
</tr>
);
})}
</tbody>
</table>
) : (
<div style={{ padding: '24px', textAlign: 'center', color: 'var(--text-secondary)', fontSize: '13px' }}>
Noch keine Benutzer zugewiesen
</div>
)
);
const renderAddUserForm = (
availableUsers: Array<{ id?: string; userId?: string; username: string; email?: string | null; fullName?: string; firstname?: string | null; lastname?: string | null }>,
roles: RoleOption[],
formValue: { userId: string; roleIds: string[] },
setFormValue: (fn: (prev: { userId: string; roleIds: string[] }) => { userId: string; roleIds: string[] }) => void,
onSubmit: () => void,
onCancel: () => void,
) => (
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
<div>
<label className={styles.formLabel}>Benutzer *</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={formValue.userId}
onChange={e => setFormValue(p => ({ ...p, userId: e.target.value }))}
>
<option value="">-- Benutzer wählen --</option>
{availableUsers.map(u => {
const uid = u.userId || u.id || '';
const name = getUserDisplayName(u as any);
return (
<option key={uid} value={uid}>
{u.username} {u.email ? `(${u.email})` : ''} {name !== u.username ? `- ${name}` : ''}
</option>
);
})}
</select>
</div>
{roles.length > 0 && (
<div>
<label className={styles.formLabel}>Rollen</label>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{roles.map(r => (
<label key={r.id} className={styles.checkboxLabel}>
<input
type="checkbox"
checked={formValue.roleIds.includes(r.id)}
onChange={e => {
setFormValue(p => ({
...p,
roleIds: e.target.checked
? [...p.roleIds, r.id]
: p.roleIds.filter(id => id !== r.id),
}));
}}
/>
{r.roleLabel}
</label>
))}
</div>
</div>
)}
<div style={{ display: 'flex', gap: '8px' }}>
<button className={styles.primaryButton} onClick={onSubmit} disabled={isLoading || !formValue.userId}>
{isLoading ? 'Hinzufügen...' : 'Hinzufügen'}
</button>
<button className={styles.secondaryButton} onClick={onCancel}>
Abbrechen
</button>
</div>
</div>
);
// ─────────────────────────────────────────────────────────────────────────
// STEP INDICATOR
// ─────────────────────────────────────────────────────────────────────────
const renderStepIndicator = () => (
<div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
{Array.from({ length: TOTAL_STEPS }, (_, i) => i + 1).map(s => (
<div
key={s}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 16px',
borderRadius: '8px',
background: s === step ? 'var(--primary-color, #f25843)' : s < step ? '#dcfce7' : 'var(--bg-secondary, #f1f5f9)',
color: s === step ? '#fff' : s < step ? '#166534' : 'var(--text-secondary)',
fontSize: '13px',
fontWeight: 600,
cursor: s < step ? 'pointer' : 'default',
transition: 'background 0.2s',
}}
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>
{STEP_LABELS[s - 1]}
</div>
))}
</div>
);
// ─────────────────────────────────────────────────────────────────────────
// 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 (
<div className={styles.adminPage} style={{ overflow: 'auto' }}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Mandanten-Verwaltung</h1>
<p className={styles.pageSubtitle}>Schritt-für-Schritt Wizard zur Mandanten-Konfiguration</p>
</div>
</div>
{error && (
<div style={{
padding: '12px 16px', background: 'var(--error-bg, #fef2f2)', color: 'var(--danger-color, #dc2626)',
borderRadius: '8px', marginBottom: '16px', fontSize: '13px', display: 'flex', justifyContent: 'space-between', alignItems: 'center',
border: '1px solid var(--danger-color, #fecaca)',
}}>
{error}
<button onClick={() => setError(null)} style={{ background: 'none', border: 'none', fontWeight: 600, cursor: 'pointer', color: 'inherit', fontSize: '16px' }}>&times;</button>
</div>
)}
{renderStepIndicator()}
{/* ── STEP 1: MANDATE ── */}
{step === 1 && (
<div style={cardStyle}>
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>Mandant auswählen oder erstellen</h3>
{!isCreatingMandate ? (
<>
<div style={{ display: 'grid', gap: '8px', marginBottom: '16px' }}>
{mandates.map(m => (
<button
key={m.id}
onClick={() => setSelectedMandate(m)}
style={{
padding: '12px 16px',
border: `2px solid ${selectedMandate?.id === m.id ? 'var(--primary-color, #f25843)' : 'var(--border-color, #e5e7eb)'}`,
borderRadius: '8px',
background: selectedMandate?.id === m.id ? 'var(--primary-bg, rgba(242,88,67,0.06))' : 'var(--surface-color, #fff)',
cursor: 'pointer', textAlign: 'left',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
color: 'var(--text-primary)',
}}
>
<div>
<div style={{ fontWeight: 600, fontSize: '14px' }}>{getMandateName(m)}</div>
</div>
{selectedMandate?.id === m.id && <span style={{ color: 'var(--primary-color)', fontWeight: 700, fontSize: '16px' }}>{'\u2713'}</span>}
</button>
))}
</div>
<button className={styles.secondaryButton} onClick={() => setIsCreatingMandate(true)}>
+ Neuen Mandanten erstellen
</button>
</>
) : (
<div style={{ display: 'grid', gap: '12px' }}>
<div className={styles.formGroup}>
<label className={`${styles.formLabel} ${styles.required}`}>Name</label>
<input
className={styles.formInput}
value={mandateForm.name}
onChange={e => setMandateForm(p => ({ ...p, name: e.target.value }))}
placeholder="z.B. Swiss Trust AG"
/>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={styles.primaryButton} onClick={handleCreateMandate} disabled={isLoading}>
{isLoading ? 'Erstellen...' : 'Mandant erstellen'}
</button>
<button className={styles.secondaryButton} onClick={() => setIsCreatingMandate(false)}>Abbrechen</button>
</div>
</div>
)}
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'flex-end' }}>
<button className={styles.primaryButton} disabled={!selectedMandate} onClick={() => setStep(2)}>
Weiter &rarr;
</button>
</div>
</div>
)}
{/* ── STEP 2: MANDATE USERS ── */}
{step === 2 && selectedMandate && (
<div style={cardStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, margin: 0 }}>
Benutzer von &laquo;{getMandateName(selectedMandate)}&raquo;
</h3>
<button
className={styles.primaryButton}
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setIsAddingMandateUser(true)}
disabled={availableUsersForMandate.length === 0}
>
+ Benutzer hinzufügen
</button>
</div>
<p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginBottom: '16px' }}>
Alle Systembenutzer können dem Mandanten zugewiesen werden.
</p>
{isAddingMandateUser && renderAddUserForm(
availableUsersForMandate,
mandateRoles,
addMandateUserForm,
setAddMandateUserForm,
handleAddMandateUser,
() => { setIsAddingMandateUser(false); setAddMandateUserForm({ userId: '', roleIds: [] }); },
)}
<div style={{ marginBottom: '16px' }}>
{renderUserTable(mandateUsers as any[], handleRemoveMandateUser)}
</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} onClick={() => setStep(3)}>
Weiter &rarr;
</button>
</div>
</div>
)}
{/* ── STEP 3: INSTANCES ── */}
{step === 3 && selectedMandate && (
<div style={cardStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, margin: 0 }}>
Feature-Instances für &laquo;{getMandateName(selectedMandate)}&raquo;
</h3>
<span style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
{instances.length} Instances
</span>
</div>
{/* Feature Filter */}
<div style={{ marginBottom: '16px' }}>
<label className={styles.formLabel}>Feature filtern:</label>
<select
className={styles.filterSelect}
value={selectedFeatureCode}
onChange={e => setSelectedFeatureCode(e.target.value)}
>
<option value="">Alle Features</option>
{features.map(f => (
<option key={f.code} value={f.code}>{getFeatureLabel(f.code)}</option>
))}
</select>
</div>
<div style={{ display: 'grid', gap: '8px', marginBottom: '16px' }}>
{instances.map(inst => (
<div key={inst.id} style={{
padding: '12px 16px',
border: '1px solid var(--border-color, #e5e7eb)',
borderRadius: '8px',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
}}>
<div>
<span style={{ fontWeight: 600 }}>{inst.label}</span>
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', marginLeft: '8px' }}>
{getFeatureLabel(inst.featureCode)} | {inst.enabled ? 'Aktiv' : 'Inaktiv'}
</span>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
className={styles.secondaryButton}
style={{ padding: '4px 8px', fontSize: '12px' }}
onClick={() => { setSelectedInstance(inst); setStep(4); }}
>
Benutzer
</button>
<button
style={{
padding: '4px 8px', fontSize: '12px', border: '1px solid #fecaca', borderRadius: '6px',
background: 'var(--surface-color, #fff)', color: '#dc2626', cursor: 'pointer',
}}
onClick={() => handleDeleteInstance(inst.id)}
>
Löschen
</button>
</div>
</div>
))}
</div>
{!isCreatingInstance ? (
<button className={styles.secondaryButton} onClick={() => setIsCreatingInstance(true)}>
+ Neue Instance erstellen
</button>
) : (
<div style={{ display: 'grid', gap: '12px', padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px' }}>
<div className={styles.formGroup}>
<label className={styles.formLabel}>Feature *</label>
<select
className={styles.filterSelect}
style={{ width: '100%' }}
value={selectedFeatureCode}
onChange={e => setSelectedFeatureCode(e.target.value)}
>
<option value="">-- Feature wählen --</option>
{features.map(f => (
<option key={f.code} value={f.code}>{getFeatureLabel(f.code)}</option>
))}
</select>
</div>
<div className={styles.formGroup}>
<label className={`${styles.formLabel} ${styles.required}`}>Bezeichnung</label>
<input
className={styles.formInput}
value={instanceForm.label}
onChange={e => setInstanceForm(p => ({ ...p, label: e.target.value }))}
placeholder="z.B. Kunde A"
/>
</div>
<label className={styles.checkboxLabel}>
<input
type="checkbox"
checked={instanceForm.enabled}
onChange={e => setInstanceForm(p => ({ ...p, enabled: e.target.checked }))}
/>
Instance aktiviert
</label>
<div style={{ display: 'flex', gap: '8px' }}>
<button className={styles.primaryButton} onClick={handleCreateInstance} disabled={isLoading || !selectedFeatureCode}>
{isLoading ? 'Erstellen...' : 'Erstellen'}
</button>
<button className={styles.secondaryButton} onClick={() => setIsCreatingInstance(false)}>Abbrechen</button>
</div>
</div>
)}
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(2)}>&larr; Zurück</button>
<button
className={styles.primaryButton}
onClick={() => { if (instances.length > 0) { setSelectedInstance(instances[0]); setStep(4); } }}
disabled={instances.length === 0}
>
Weiter &rarr;
</button>
</div>
</div>
)}
{/* ── STEP 4: FEATURE INSTANCE USERS ── */}
{step === 4 && selectedMandate && selectedInstance && (
<div style={cardStyle}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<h3 style={{ fontSize: '15px', fontWeight: 600, margin: 0 }}>
Feature-Benutzer für &laquo;{selectedInstance.label}&raquo;
</h3>
<button
className={styles.primaryButton}
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setIsAddingInstanceUser(true)}
disabled={availableUsersForInstance.length === 0 || instanceRoles.length === 0}
>
+ Benutzer hinzufügen
</button>
</div>
<p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginBottom: '16px' }}>
Mandant: {getMandateName(selectedMandate)} | Mitglieder des Mandanten können der Feature-Instanz zugewiesen werden.
</p>
{isAddingInstanceUser && renderAddUserForm(
availableUsersForInstance as any[],
instanceRoles,
addInstanceUserForm,
setAddInstanceUserForm,
handleAddInstanceUser,
() => { setIsAddingInstanceUser(false); setAddInstanceUserForm({ userId: '', roleIds: [] }); },
)}
<div style={{ marginBottom: '16px' }}>
{renderUserTable(
instanceUsers.map(u => ({ ...u, userId: u.userId || u.id })),
handleRemoveInstanceUser,
)}
</div>
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
<button className={styles.secondaryButton} onClick={() => setStep(3)}>&larr; Zurück</button>
<button className={styles.primaryButton} onClick={() => {
showSuccess('Fertig', 'Konfiguration abgeschlossen!');
setSelectedInstance(null);
setStep(1);
}}>
Fertig
</button>
</div>
</div>
)}
</div>
);
};
export default AdminMandateWizardPage;