new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 17:18:38 +01:00
parent da08948ca8
commit 269b797f42
2 changed files with 262 additions and 31 deletions

View File

@@ -94,16 +94,30 @@ const PERMISSION_SUB_GROUPS: Record<string, Record<string, string[]>> = {
'Buchungen': ['view_bookings', 'manage_bookings'],
},
bestellungen: {
'Bestellungen': ['view', 'create', 'delete', 'export'],
'Bestellungen': ['view', 'create', 'manage_orders', 'delete', 'export'],
'Lieferanten': ['manage_vendors'],
'Erinnerungen': ['manage_reminders'],
'Widget': ['widget'],
},
shop: {
'Katalog': ['view', 'manage_catalog'],
'Anfragen': ['create_request', 'approve_requests', 'link_orders'],
'Anfragen': ['create_request', 'approve_requests', 'link_orders', 'view_overview', 'order_for_user'],
'Widget': ['widget'],
},
admin: {
'Allgemein': ['view', 'write'],
'Services': ['view_services', 'edit_services'],
'System': ['view_system'],
'Benutzer': ['view_users', 'edit_users'],
'Broadcast': ['edit_broadcast'],
'Banner': ['view_banner', 'edit_banner'],
'Wartung': ['view_maintenance', 'edit_maintenance'],
'FDISK': ['view_fdisk', 'edit_fdisk'],
'Berechtigungen': ['view_permissions', 'edit_permissions'],
'Bestell-Admin': ['view_order_settings', 'edit_order_settings'],
'Datenverwaltung': ['view_data', 'edit_data'],
'Debug': ['view_debug'],
},
};
function getSubGroupLabel(featureGroupId: string, permId: string): string | null {
@@ -327,6 +341,7 @@ function PermissionMatrixTab() {
permissionDeps={permissionDeps}
allGroups={nonAdminGroups}
allPermissions={permissions}
featureGroups={featureGroups}
onSave={(config) => depConfigMutation.mutate(config)}
isSaving={depConfigMutation.isPending}
/>
@@ -512,14 +527,40 @@ interface DependencyEditorProps {
permissionDeps: Record<string, string[]>;
allGroups: string[];
allPermissions: Permission[];
featureGroups: FeatureGroup[];
onSave: (config: { groupHierarchy?: Record<string, string[]>; permissionDeps?: Record<string, string[]> }) => void;
isSaving: boolean;
}
function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermissions, onSave, isSaving }: DependencyEditorProps) {
function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermissions, featureGroups, onSave, isSaving }: DependencyEditorProps) {
const groupSet = useMemo(() => new Set(allGroups), [allGroups]);
const permIdSet = useMemo(() => new Set(allPermissions.map(p => p.id)), [allPermissions]);
// Build lookup: permId → feature_group_id
const permToFeatureGroup = useMemo(() => {
const map: Record<string, string> = {};
for (const p of allPermissions) map[p.id] = p.feature_group_id;
return map;
}, [allPermissions]);
// Build lookup: feature_group_id → Permission[]
const permsByFeatureGroup = useMemo(() => {
const map: Record<string, Permission[]> = {};
for (const fg of featureGroups) map[fg.id] = [];
for (const p of allPermissions) {
if (!map[p.feature_group_id]) map[p.feature_group_id] = [];
map[p.feature_group_id].push(p);
}
return map;
}, [allPermissions, featureGroups]);
// Format label as "feature:action (Label)"
const formatPermLabel = useCallback((permId: string) => {
const perm = allPermissions.find(p => p.id === permId);
if (!perm) return permId;
return `${permId} (${perm.label})`;
}, [allPermissions]);
// Filter saved config to only include groups/permissions that actually exist
const [editHierarchy, setEditHierarchy] = useState<Record<string, string[]>>(() => {
const filtered: Record<string, string[]> = {};
@@ -541,7 +582,6 @@ function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermis
});
const nonAdminGroups = allGroups.filter(g => g !== 'dashboard_admin');
const permOptions = allPermissions.map(p => p.id);
const handleHierarchyChange = (group: string, inheritors: string[]) => {
setEditHierarchy(prev => {
@@ -577,6 +617,9 @@ function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermis
const [newDepPerm, setNewDepPerm] = useState<string | null>(null);
// All permission options for "add new dependency" picker
const allPermOptions = allPermissions.map(p => p.id).filter(p => !editDeps[p]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, mt: 2 }}>
{/* Group Hierarchy Editor */}
@@ -610,39 +653,53 @@ function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermis
))}
</Box>
{/* Permission Dependency Editor */}
{/* Permission Dependency Editor — Grouped by Feature Group */}
<Box>
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 'bold' }}>
Berechtigungsabhängigkeiten
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Wenn eine Berechtigung gesetzt wird, werden die hier definierten Voraussetzungen automatisch mit aktiviert.
Abhängigkeiten können nur innerhalb derselben Feature-Gruppe gesetzt werden.
</Typography>
{Object.entries(editDeps).map(([permId, deps]) => {
const perm = allPermissions.find(p => p.id === permId);
{featureGroups.map(fg => {
// Get dependencies that belong to this feature group
const fgDeps = Object.entries(editDeps).filter(([permId]) => permToFeatureGroup[permId] === fg.id);
if (fgDeps.length === 0) return null;
// Options for dependency targets: only perms within same feature group
const sameGroupPermOptions = (permsByFeatureGroup[fg.id] || []).map(p => p.id);
return (
<Box key={permId} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5 }}>
<Typography variant="body2" sx={{ minWidth: 220, fontWeight: 500 }}>
{perm?.label ?? permId}
<Box key={fg.id} sx={{ mb: 2 }}>
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary', mb: 1, textTransform: 'uppercase', fontSize: '0.75rem', letterSpacing: 0.5 }}>
{fg.label}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mx: 1 }}>benötigt</Typography>
<Autocomplete
multiple size="small" sx={{ flex: 1 }}
options={permOptions.filter(p => p !== permId)}
getOptionLabel={(p) => allPermissions.find(pp => pp.id === p)?.label ?? p}
value={deps}
onChange={(_e, val) => handleDepChange(permId, val)}
renderInput={(params) => <TextField {...params} placeholder="Voraussetzungen..." size="small" />}
renderTags={(value, getTagProps) =>
value.map((p, index) => (
<Chip {...getTagProps({ index })} key={p}
label={allPermissions.find(pp => pp.id === p)?.label ?? p} size="small" />
))
}
/>
<IconButton size="small" onClick={() => handleRemoveDep(permId)} color="error">
<DeleteIcon fontSize="small" />
</IconButton>
{fgDeps.map(([permId, deps]) => (
<Box key={permId} sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1.5, ml: 2 }}>
<Typography variant="body2" sx={{ minWidth: 220, fontWeight: 500 }}>
{formatPermLabel(permId)}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mx: 1 }}>benötigt</Typography>
<Autocomplete
multiple size="small" sx={{ flex: 1 }}
options={sameGroupPermOptions.filter(p => p !== permId)}
getOptionLabel={formatPermLabel}
value={deps.filter(d => sameGroupPermOptions.includes(d))}
onChange={(_e, val) => handleDepChange(permId, val)}
renderInput={(params) => <TextField {...params} placeholder="Voraussetzungen..." size="small" />}
renderTags={(value, getTagProps) =>
value.map((p, index) => (
<Chip {...getTagProps({ index })} key={p} label={formatPermLabel(p)} size="small" />
))
}
/>
<IconButton size="small" onClick={() => handleRemoveDep(permId)} color="error">
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
))}
</Box>
);
})}
@@ -650,9 +707,13 @@ function DependencyEditor({ groupHierarchy, permissionDeps, allGroups, allPermis
{/* Add new dependency */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
<Autocomplete
size="small" sx={{ width: 300 }}
options={permOptions.filter(p => !editDeps[p])}
getOptionLabel={(p) => allPermissions.find(pp => pp.id === p)?.label ?? p}
size="small" sx={{ width: 350 }}
options={allPermOptions}
groupBy={(option) => {
const fg = featureGroups.find(f => f.id === permToFeatureGroup[option]);
return fg?.label ?? 'Sonstige';
}}
getOptionLabel={formatPermLabel}
value={newDepPerm}
onChange={(_e, val) => setNewDepPerm(val)}
renderInput={(params) => <TextField {...params} placeholder="Neue Abhängigkeit hinzufügen..." size="small" />}