frontend_nyla/src/pages/admin/AdminFeatureRolesPage.tsx
2026-04-14 00:15:51 +02:00

502 lines
17 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.

/**
* AdminFeatureRolesPage
*
* Admin page for managing FEATURE TEMPLATE ROLES.
* These are roles that are copied to new feature instances.
*
* According to admin_ui_concept.md:
* - Filter: featureCode!=null AND mandateId=null AND featureInstanceId=null
* - View: Per feature (dropdown selection)
* - Actions: Create feature role, edit description, manage AccessRules, delete role
*/
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { AccessRulesEditor } from '../../components/AccessRules';
import { FaPlus, FaSync, FaUserShield, FaCube, FaShieldAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import api from '../../api';
import styles from './Admin.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
interface Feature {
id?: string;
code: string;
featureCode?: string;
label: string;
name?: string;
description?: string;
icon?: string;
}
interface FeatureRole {
id: string;
roleLabel: string;
description?: string;
featureCode: string;
mandateId?: string | null;
featureInstanceId?: string | null;
isSystemRole?: boolean;
}
export const AdminFeatureRolesPage: React.FC = () => {
const { t } = useLanguage();
const { showError } = useToast();
// State
const [features, setFeatures] = useState<Feature[]>([]);
const [selectedFeatureCode, setSelectedFeatureCode] = useState<string>('');
const [roles, setRoles] = useState<FeatureRole[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingRole, setEditingRole] = useState<FeatureRole | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [permissionsRole, setPermissionsRole] = useState<FeatureRole | null>(null);
// Load features on mount
useEffect(() => {
const loadFeatures = async () => {
try {
const response = await api.get('/api/features/');
const featureList = response.data?.items || response.data || [];
setFeatures(Array.isArray(featureList) ? featureList : []);
// Auto-select first feature if available
if (featureList.length > 0 && !selectedFeatureCode) {
setSelectedFeatureCode(featureList[0].code || featureList[0].featureCode || '');
}
} catch (err: any) {
console.error('Error loading features:', err);
setError(t('Fehler beim Laden der Features'));
}
};
loadFeatures();
}, []);
const [pagination, setPagination] = useState<any>(null);
// Load roles when feature changes
const fetchRoles = useCallback(async (params?: any) => {
if (!selectedFeatureCode) {
setRoles([]);
return;
}
setLoading(true);
setError(null);
try {
const requestParams: Record<string, string> = { featureCode: selectedFeatureCode };
if (params && typeof params === 'object') {
const paginationObj: any = {};
if (params.page !== undefined) paginationObj.page = params.page;
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
}
}
const response = await api.get(`/api/features/templates/roles`, { params: requestParams });
const data = response.data;
if (data && typeof data === 'object' && 'items' in data) {
setRoles(Array.isArray(data.items) ? data.items : []);
if (data.pagination) setPagination(data.pagination);
} else {
setRoles(Array.isArray(data) ? data : []);
setPagination(null);
}
} catch (err: any) {
console.error('Error loading feature roles:', err);
setError(t('Fehler beim Laden der Feature-Rollen'));
setRoles([]);
setPagination(null);
} finally {
setLoading(false);
}
}, [selectedFeatureCode, t]);
useEffect(() => {
fetchRoles();
}, [fetchRoles]);
const getTextValue = (value: any): string => {
if (!value) return '-';
if (typeof value === 'string') return value;
return String(value);
};
// Table columns
const columns = useMemo(() => [
{
key: 'roleLabel',
label: t('Rollen-Label'),
type: 'string' as const,
sortable: true,
filterable: true,
searchable: true,
width: 180
},
{
key: 'description',
label: t('Beschreibung'),
type: 'string' as const,
sortable: false,
width: 300,
formatter: (value: string) => getTextValue(value)
},
{
key: 'featureCode',
label: t('Feature'),
type: 'string' as const,
sortable: true,
filterable: true,
width: 120,
formatter: (value: string) => (
<span className={styles.badge} style={{ background: 'var(--primary-color, #4a5568)', color: 'white' }}>
<FaCube style={{ marginRight: 4 }} /> {value}
</span>
)
},
], [t]);
// Form attributes for create
const createFields: AttributeDefinition[] = useMemo(() => {
const fields: AttributeDefinition[] = [
{
name: 'roleLabel',
label: t('Rollen-Label'),
type: 'string',
required: true,
description: t('Rollen-Label Beschreibung')
},
{
name: 'description',
label: t('Beschreibung'),
type: 'textarea',
required: false,
description: t('Beschreibung der Rolle')
}
];
return fields;
}, [t]);
// Form attributes for edit
const editFields: AttributeDefinition[] = useMemo(() => {
return [
{
name: 'roleLabel',
label: t('Rollen-Label'),
type: 'string',
required: true,
readonly: true,
description: t('Rollen-Label (nur lesen)')
},
{
name: 'description',
label: t('Beschreibung'),
type: 'textarea',
required: false,
description: t('Beschreibung der Rolle')
}
];
}, [t]);
// Handle create role
const handleCreateRole = async (data: { roleLabel: string; description?: string }) => {
if (!selectedFeatureCode) return;
setIsSubmitting(true);
try {
const params = new URLSearchParams();
params.append('roleLabel', data.roleLabel);
params.append('featureCode', selectedFeatureCode);
await api.post(`/api/features/templates/roles?${params.toString()}`, data.description ?? '');
setShowCreateModal(false);
await fetchRoles();
} catch (err: any) {
console.error('Error creating role:', err);
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Erstellen der Rolle'));
} finally {
setIsSubmitting(false);
}
};
// Handle edit role
const handleEditRole = async (data: { roleLabel: string; description?: string }) => {
if (!editingRole) return;
setIsSubmitting(true);
try {
await api.put(`/api/rbac/roles/${editingRole.id}`, {
description: data.description
});
setEditingRole(null);
await fetchRoles();
} catch (err: any) {
console.error('Error updating role:', err);
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Aktualisieren der Rolle'));
} finally {
setIsSubmitting(false);
}
};
// Handle delete role (confirmation handled by DeleteActionButton)
const handleDeleteRole = async (role: FeatureRole) => {
try {
await api.delete(`/api/rbac/roles/${role.id}`);
await fetchRoles();
} catch (err: any) {
console.error('Error deleting role:', err);
showError(t('Fehler'), err.response?.data?.detail || t('Fehler beim Löschen der Rolle'));
}
};
// Handle edit click
const handleEditClick = (role: FeatureRole) => {
setEditingRole(role);
};
const getFeatureName = (feature: Feature) => {
return feature.label || feature.name || '-';
};
if (error && !selectedFeatureCode) {
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>{error}</p>
<button className={styles.secondaryButton} onClick={() => window.location.reload()}>
<FaSync /> {t('Erneut versuchen')}
</button>
</div>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>{t('Feature-Rollen-Rechte')}</h1>
<p className={styles.pageSubtitle}>{t('Template-Rollen und deren Berechtigungen für')}</p>
</div>
</div>
{/* Feature Selector */}
<div className={styles.filterSection}>
<div className={styles.filterGroup}>
<label className={styles.filterLabel}>
<FaCube style={{ marginRight: 8 }} />
{t('Feature:')}
</label>
<select
className={styles.filterSelect}
value={selectedFeatureCode}
onChange={(e) => setSelectedFeatureCode(e.target.value)}
>
<option value="">{t('Feature wählen')}</option>
{features.map(f => {
const featureCode = f.code || f.featureCode || '';
return (
<option key={featureCode} value={featureCode}>
{getFeatureName(f)} ({featureCode})
</option>
);
})}
</select>
</div>
{selectedFeatureCode && (
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => fetchRoles()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
</button>
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> {t('Neue Feature-Rolle')}
</button>
</div>
)}
</div>
{/* Info Box */}
{selectedFeatureCode && (
<div className={styles.infoBox}>
<FaUserShield style={{ marginRight: 8 }} />
<span>
<strong>{t('Feature-Template-Rollen')}</strong>{' '}
{t('werden bei der Erstellung neuer Feature-Instanzen automatisch kopiert. Änderungen an Template-Rollen wirken sich nicht auf bestehende Instanzen aus.')}
</span>
</div>
)}
{/* Content */}
{!selectedFeatureCode ? (
<div className={styles.emptyState}>
<FaCube className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>{t('Kein Feature ausgewählt')}</h3>
<p className={styles.emptyDescription}>
{t('Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.')}
</p>
</div>
) : (
<div className={styles.tableContainer}>
<FormGeneratorTable
data={roles}
columns={columns}
apiEndpoint="/api/features/templates/roles"
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={true}
actionButtons={[
{
type: 'edit' as const,
onAction: handleEditClick,
title: t('Rolle bearbeiten'),
},
{
type: 'delete' as const,
title: t('Rolle löschen'),
}
]}
customActions={[
{
id: 'permissions',
icon: <FaShieldAlt />,
onClick: (role: FeatureRole) => setPermissionsRole(role),
title: t('Berechtigungen verwalten'),
}
]}
onDelete={handleDeleteRole}
hookData={{
refetch: fetchRoles,
pagination,
handleDelete: handleDeleteRole,
}}
emptyMessage={t('Keine Feature-Rollen gefunden')}
/>
</div>
)}
{/* Create Role 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}>{t('Neue Feature-Rolle erstellen')}</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} />
<span>
{t('Feature')}: <strong>{selectedFeatureCode}</strong>
</span>
</div>
<FormGeneratorForm
attributes={createFields}
mode="create"
onSubmit={handleCreateRole}
onCancel={() => setShowCreateModal(false)}
submitButtonText={isSubmitting ? t('Erstelle…') : t('Rolle erstellen')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>
</div>
)}
{/* Edit Role Modal */}
{editingRole && (
<div className={styles.modalOverlay} onClick={() => setEditingRole(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>{t('Feature-Rolle bearbeiten')}</h2>
<button
className={styles.modalClose}
onClick={() => setEditingRole(null)}
>
</button>
</div>
<div className={styles.modalContent}>
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} />
<span>{t('Feature:')} <strong>{editingRole.featureCode}</strong></span>
</div>
<FormGeneratorForm
attributes={editFields}
data={editingRole}
mode="edit"
onSubmit={handleEditRole}
onCancel={() => setEditingRole(null)}
submitButtonText={isSubmitting ? t('Speichern') : t('Speichern')}
cancelButtonText={t('Abbrechen')}
/>
</div>
</div>
</div>
)}
{/* Permissions Modal */}
{permissionsRole && (
<div className={styles.modalOverlay} onClick={() => setPermissionsRole(null)}>
<div className={styles.modal} style={{ maxWidth: '900px', width: '90%' }} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>
<FaShieldAlt style={{ marginRight: 8 }} />
{t('Berechtigungen')}: {permissionsRole.roleLabel}
</h2>
<button
className={styles.modalClose}
onClick={() => setPermissionsRole(null)}
>
</button>
</div>
<div className={styles.modalContent}>
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
<FaCube style={{ marginRight: 8 }} />
<span>
{t('Feature')}: <strong>{permissionsRole.featureCode}</strong>
</span>
<span style={{ marginLeft: '1rem' }}>{t('Template-Rolle global')}</span>
</div>
<AccessRulesEditor
roleId={permissionsRole.id}
roleName={permissionsRole.roleLabel}
isTemplate={true}
onSave={() => setPermissionsRole(null)}
featureCode={permissionsRole.featureCode}
/>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminFeatureRolesPage;