frontend_nyla/src/pages/admin/AdminFeatureAccessPage.tsx
ValueOn AG 6a406d885d fixes
2026-01-24 18:01:35 +01:00

438 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* AdminFeatureAccessPage
*
* Admin page for managing feature instances within mandates.
* Allows creating, viewing, and managing feature instances.
*/
import React, { useState, useEffect, useMemo } from 'react';
import { useFeatureAccess, type FeatureInstance } from '../../hooks/useFeatureAccess';
import { useUserMandates, type Mandate } from '../../hooks/useUserMandates';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaCube, FaBuilding, FaCogs, FaEdit } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import styles from './Admin.module.css';
export const AdminFeatureAccessPage: React.FC = () => {
const {
features,
instances,
instancesPagination,
loading,
error,
fetchFeatures,
fetchInstances,
createInstance,
updateInstance,
deleteInstance,
syncInstanceRoles,
} = useFeatureAccess();
const { fetchMandates } = useUserMandates();
const { showSuccess, showError } = useToast();
// State
const [mandates, setMandates] = useState<Mandate[]>([]);
const [selectedMandateId, setSelectedMandateId] = useState<string>('');
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [editingInstance, setEditingInstance] = useState<FeatureInstance | null>(null);
const [, setIsSubmitting] = useState(false);
const [syncingInstance, setSyncingInstance] = useState<string | null>(null);
const [backendAttributes, setBackendAttributes] = useState<AttributeDefinition[]>([]);
// Load features, mandates, and attributes on mount
useEffect(() => {
fetchFeatures();
fetchMandates().then(setMandates);
// Fetch FeatureInstance attributes from backend
api.get('/api/attributes/FeatureInstance').then(response => {
const attrs = response.data?.attributes || response.data || [];
setBackendAttributes(Array.isArray(attrs) ? attrs : []);
}).catch(() => setBackendAttributes([]));
}, [fetchFeatures, fetchMandates]);
// Load instances when mandate changes
useEffect(() => {
if (selectedMandateId) {
fetchInstances(selectedMandateId);
}
}, [selectedMandateId, fetchInstances]);
// Table columns
const columns = useMemo(() => [
{ key: 'label', label: 'Name', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 },
{ key: 'featureCode', label: 'Feature', type: 'string' as const, sortable: true, filterable: true, width: 150,
render: (value: string) => {
const feature = features.find(f => f.code === value);
if (feature) {
const label = typeof feature.label === 'object'
? (feature.label.de || feature.label.en || value)
: feature.label;
return label;
}
return value;
}
},
{ key: 'enabled', label: 'Aktiv', type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
], [features]);
// Form attributes from backend - merge with dynamic feature options
const createFields: AttributeDefinition[] = useMemo(() => {
const excludedFields = ['id', 'mandateId'];
const featureOptions = features.map(f => ({
value: f.code,
label: typeof f.label === 'object'
? (f.label.de || f.label.en || f.code)
: (f.label || f.code)
}));
return backendAttributes
.filter(attr => !excludedFields.includes(attr.name))
.map(attr => ({
...attr,
// Override featureCode: make editable for create and add dynamic options
readonly: attr.name === 'featureCode' ? false : attr.readonly,
editable: attr.name === 'featureCode' || attr.name === 'enabled' ? true : attr.editable,
options: attr.name === 'featureCode' ? featureOptions : attr.options,
})) as AttributeDefinition[];
}, [features, backendAttributes]);
// Handle create instance
const handleCreateInstance = async (data: { featureCode: string; label: string; enabled?: boolean; copyTemplateRoles?: boolean }) => {
if (!selectedMandateId) return;
setIsSubmitting(true);
try {
const result = await createInstance(selectedMandateId, {
featureCode: data.featureCode,
label: data.label,
enabled: data.enabled !== false,
copyTemplateRoles: data.copyTemplateRoles !== false
});
if (result.success) {
setShowCreateModal(false);
fetchInstances(selectedMandateId);
showSuccess('Feature-Instanz erstellt', `Die Instanz "${data.label}" wurde erfolgreich erstellt.`);
} else {
showError('Fehler', result.error || 'Fehler beim Erstellen der Feature-Instanz');
}
} finally {
setIsSubmitting(false);
}
};
// Handle edit click
const handleEditClick = (instance: FeatureInstance) => {
setEditingInstance(instance);
setShowEditModal(true);
};
// Handle update instance
const handleUpdateInstance = async (data: { label: string; enabled?: boolean }) => {
if (!selectedMandateId || !editingInstance) return;
setIsSubmitting(true);
try {
const result = await updateInstance(selectedMandateId, editingInstance.id, {
label: data.label,
enabled: data.enabled
});
if (result.success) {
setShowEditModal(false);
setEditingInstance(null);
fetchInstances(selectedMandateId);
showSuccess('Feature-Instanz aktualisiert', `Die Instanz "${data.label}" wurde erfolgreich aktualisiert.`);
} else {
showError('Fehler', result.error || 'Fehler beim Aktualisieren der Feature-Instanz');
}
} finally {
setIsSubmitting(false);
}
};
// Handle delete instance - called by DeleteActionButton with instanceId
const handleDeleteInstance = async (instanceId: string): Promise<boolean> => {
if (!selectedMandateId) return false;
const result = await deleteInstance(selectedMandateId, instanceId);
if (result.success) {
showSuccess('Instanz gelöscht', 'Die Feature-Instanz wurde gelöscht.');
return true;
} else {
showError('Fehler', result.error || 'Fehler beim Löschen der Feature-Instanz');
return false;
}
};
// Handle sync roles
const handleSyncRoles = async (instance: FeatureInstance) => {
if (!selectedMandateId) return;
setSyncingInstance(instance.id);
try {
const result = await syncInstanceRoles(selectedMandateId, instance.id, true);
if (result.success && result.data) {
showSuccess(
'Rollen synchronisiert',
`Hinzugefügt: ${result.data.added}\nEntfernt: ${result.data.removed}\nUnverändert: ${result.data.unchanged}`
);
} else {
showError('Synchronisierung fehlgeschlagen', result.error || 'Fehler beim Synchronisieren der Rollen');
}
} finally {
setSyncingInstance(null);
}
};
// Get mandate name
const getMandateName = (mandate: Mandate) => {
if (typeof mandate.name === 'object') {
return mandate.name.de || mandate.name.en || Object.values(mandate.name)[0] || mandate.id;
}
return mandate.name || mandate.id;
};
// Get feature label
const getFeatureLabel = (code: string) => {
const feature = features.find(f => f.code === code);
if (feature) {
return typeof feature.label === 'object'
? (feature.label.de || feature.label.en || code)
: (feature.label || code);
}
return code;
};
if (error && !selectedMandateId) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler: {error}</p>
<button className={styles.secondaryButton} onClick={() => fetchFeatures()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Feature-Instanzen</h1>
<p className={styles.pageSubtitle}>Verwalten Sie Feature-Instanzen für jeden Mandanten</p>
</div>
</div>
{/* Mandate Selector */}
<div className={styles.filterSection}>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>
<FaBuilding style={{ marginRight: 8 }} />
Mandant auswählen:
</label>
<select
className={styles.filterSelect}
value={selectedMandateId}
onChange={(e) => setSelectedMandateId(e.target.value)}
>
<option value="">-- Mandant wählen --</option>
{mandates.map(m => (
<option key={m.id} value={m.id}>
{getMandateName(m)}
</option>
))}
</select>
</div>
{selectedMandateId && (
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => fetchInstances(selectedMandateId)}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
disabled={features.length === 0}
>
<FaPlus /> Neue Instanz
</button>
</div>
)}
</div>
{/* Available Features Info */}
{features.length > 0 && (
<div className={styles.infoBox}>
<FaCube style={{ marginRight: 8 }} />
<span>Verfügbare Features: </span>
{features.map((f, i) => (
<span key={f.code}>
{i > 0 && ', '}
<strong>{getFeatureLabel(f.code)}</strong>
</span>
))}
</div>
)}
{/* Content */}
{!selectedMandateId ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
<p className={styles.emptyDescription}>
Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.
</p>
</div>
) : loading && instances.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Feature-Instanzen...</span>
</div>
) : instances.length === 0 ? (
<div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Feature-Instanzen</h3>
<p className={styles.emptyDescription}>
Für diesen Mandanten wurden noch keine Feature-Instanzen erstellt.
</p>
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
disabled={features.length === 0}
>
<FaPlus /> Erste Instanz erstellen
</button>
</div>
) : (
<div className={styles.tableContainer}>
<FormGeneratorTable
data={instances}
columns={columns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
{
type: 'delete' as const,
title: 'Instanz löschen',
}
]}
customActions={[
{
id: 'edit',
icon: <FaEdit />,
onClick: handleEditClick,
title: 'Instanz bearbeiten',
},
{
id: 'syncRoles',
icon: <FaCogs />,
onClick: handleSyncRoles,
title: 'Rollen synchronisieren',
loading: (row: FeatureInstance) => syncingInstance === row.id,
disabled: (row: FeatureInstance) => !row.enabled,
}
]}
hookData={{
refetch: fetchInstances,
pagination: instancesPagination,
handleDelete: handleDeleteInstance,
}}
emptyMessage="Keine Feature-Instanzen gefunden"
/>
</div>
)}
{/* Create Instance Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Neue Feature-Instanz erstellen</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
{features.length === 0 ? (
<p>Keine Features verfügbar. Bitte wenden Sie sich an den System-Administrator.</p>
) : createFields.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Formular...</span>
</div>
) : (
<FormGeneratorForm
attributes={createFields}
mode="create"
onSubmit={handleCreateInstance}
onCancel={() => setShowCreateModal(false)}
submitButtonText="Erstellen"
cancelButtonText="Abbrechen"
/>
)}
</div>
</div>
</div>
)}
{/* Edit Instance Modal */}
{showEditModal && editingInstance && (
<div className={styles.modalOverlay} onClick={() => { setShowEditModal(false); setEditingInstance(null); }}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Feature-Instanz bearbeiten</h2>
<button
className={styles.modalClose}
onClick={() => { setShowEditModal(false); setEditingInstance(null); }}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
attributes={[
{
name: 'label',
type: 'string' as const,
label: 'Bezeichnung',
required: true,
editable: true,
},
{
name: 'enabled',
type: 'boolean' as const,
label: 'Aktiviert',
required: false,
editable: true,
}
]}
data={editingInstance}
mode="edit"
onSubmit={handleUpdateInstance}
onCancel={() => { setShowEditModal(false); setEditingInstance(null); }}
submitButtonText="Speichern"
cancelButtonText="Abbrechen"
/>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminFeatureAccessPage;