fix permissions

This commit is contained in:
Matthias Hochmeister
2026-03-24 17:54:36 +01:00
parent e6ddf67d95
commit f228dd67ba
11 changed files with 521 additions and 7 deletions

View File

@@ -10,7 +10,7 @@ import {
Add as AddIcon, Delete as DeleteIcon, ExpandMore, ExpandLess,
BugReport, FiberNew, HelpOutline, Send as SendIcon,
Circle as CircleIcon, Edit as EditIcon, Refresh as RefreshIcon,
DragIndicator,
DragIndicator, Check as CheckIcon, Close as CloseIcon,
} from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';
@@ -20,7 +20,7 @@ import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useAuth } from '../contexts/AuthContext';
import { issuesApi } from '../services/issues';
import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayload, IssueFilters, AssignableMember } from '../types/issue.types';
import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayload, IssueFilters, AssignableMember, IssueStatusmeldung } from '../types/issue.types';
// ── Helpers ──
@@ -732,6 +732,156 @@ function IssueTypeAdmin() {
);
}
// ── Issue Settings (Statusmeldungen + Kategorien) ──
function IssueSettings() {
const queryClient = useQueryClient();
const { showSuccess, showError } = useNotification();
const [createOpen, setCreateOpen] = useState(false);
const [createData, setCreateData] = useState<{ titel: string; inhalt: string; schwere: 'info' | 'warnung' | 'fehler' }>({ titel: '', inhalt: '', schwere: 'info' });
const [editId, setEditId] = useState<number | null>(null);
const [editData, setEditData] = useState<Partial<IssueStatusmeldung>>({});
const { data: statusmeldungen = [], isLoading: smLoading } = useQuery({
queryKey: ['issue-statusmeldungen'],
queryFn: issuesApi.getStatusmeldungen,
});
const createSmMut = useMutation({
mutationFn: (data: { titel: string; inhalt?: string; schwere?: string }) => issuesApi.createStatusmeldung(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issue-statusmeldungen'] });
showSuccess('Statusmeldung erstellt');
setCreateOpen(false);
setCreateData({ titel: '', inhalt: '', schwere: 'info' });
},
onError: () => showError('Fehler beim Erstellen'),
});
const updateSmMut = useMutation({
mutationFn: ({ id, data }: { id: number; data: Partial<IssueStatusmeldung> }) => issuesApi.updateStatusmeldung(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issue-statusmeldungen'] });
showSuccess('Statusmeldung aktualisiert');
setEditId(null);
},
onError: () => showError('Fehler beim Aktualisieren'),
});
const deleteSmMut = useMutation({
mutationFn: (id: number) => issuesApi.deleteStatusmeldung(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['issue-statusmeldungen'] });
showSuccess('Statusmeldung gelöscht');
},
onError: () => showError('Fehler beim Löschen'),
});
const schwereColors: Record<string, 'info' | 'warning' | 'error'> = { info: 'info', warnung: 'warning', fehler: 'error' };
const schwereLabels: Record<string, string> = { info: 'Info', warnung: 'Warnung', fehler: 'Fehler' };
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{/* Section 1: Statusmeldungen */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6">Statusmeldungen</Typography>
<Button startIcon={<AddIcon />} variant="contained" size="small" onClick={() => setCreateOpen(true)}>
Neue Meldung
</Button>
</Box>
{smLoading ? <CircularProgress /> : (
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Titel</TableCell>
<TableCell>Schwere</TableCell>
<TableCell>Aktiv</TableCell>
<TableCell>Aktionen</TableCell>
</TableRow>
</TableHead>
<TableBody>
{statusmeldungen.length === 0 ? (
<TableRow>
<TableCell colSpan={4} sx={{ textAlign: 'center', color: 'text.secondary' }}>
Keine Statusmeldungen
</TableCell>
</TableRow>
) : statusmeldungen.map((sm) => (
<TableRow key={sm.id}>
{editId === sm.id ? (
<>
<TableCell>
<TextField size="small" value={editData.titel ?? sm.titel} onChange={(e) => setEditData({ ...editData, titel: e.target.value })} />
</TableCell>
<TableCell>
<Select size="small" value={editData.schwere ?? sm.schwere} onChange={(e) => setEditData({ ...editData, schwere: e.target.value as 'info' | 'warnung' | 'fehler' })}>
<MenuItem value="info">Info</MenuItem>
<MenuItem value="warnung">Warnung</MenuItem>
<MenuItem value="fehler">Fehler</MenuItem>
</Select>
</TableCell>
<TableCell>
<Switch checked={editData.aktiv ?? sm.aktiv} onChange={(e) => setEditData({ ...editData, aktiv: e.target.checked })} size="small" />
</TableCell>
<TableCell>
<IconButton size="small" onClick={() => updateSmMut.mutate({ id: sm.id, data: editData })}><CheckIcon /></IconButton>
<IconButton size="small" onClick={() => setEditId(null)}><CloseIcon /></IconButton>
</TableCell>
</>
) : (
<>
<TableCell>{sm.titel}</TableCell>
<TableCell><Chip label={schwereLabels[sm.schwere]} color={schwereColors[sm.schwere]} size="small" /></TableCell>
<TableCell>
<Switch checked={sm.aktiv} onChange={(e) => updateSmMut.mutate({ id: sm.id, data: { aktiv: e.target.checked } })} size="small" />
</TableCell>
<TableCell>
<IconButton size="small" onClick={() => { setEditId(sm.id); setEditData({ titel: sm.titel, schwere: sm.schwere, aktiv: sm.aktiv, inhalt: sm.inhalt ?? '' }); }}><EditIcon /></IconButton>
<IconButton size="small" onClick={() => deleteSmMut.mutate(sm.id)}><DeleteIcon /></IconButton>
</TableCell>
</>
)}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</Box>
{/* Section 2: Kategorien */}
<Box>
<IssueTypeAdmin />
</Box>
{/* Create Statusmeldung Dialog */}
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Neue Statusmeldung</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
<TextField label="Titel" required fullWidth value={createData.titel} onChange={(e) => setCreateData({ ...createData, titel: e.target.value })} autoFocus />
<TextField label="Inhalt" fullWidth multiline rows={3} value={createData.inhalt} onChange={(e) => setCreateData({ ...createData, inhalt: e.target.value })} />
<FormControl fullWidth size="small">
<InputLabel>Schwere</InputLabel>
<Select label="Schwere" value={createData.schwere} onChange={(e) => setCreateData({ ...createData, schwere: e.target.value as 'info' | 'warnung' | 'fehler' })}>
<MenuItem value="info">Info</MenuItem>
<MenuItem value="warnung">Warnung</MenuItem>
<MenuItem value="fehler">Fehler</MenuItem>
</Select>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => setCreateOpen(false)}>Abbrechen</Button>
<Button variant="contained" onClick={() => createSmMut.mutate(createData)} disabled={!createData.titel.trim() || createSmMut.isPending}>
Erstellen
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
// ── Main Page ──
export default function Issues() {
@@ -756,7 +906,7 @@ export default function Issues() {
{ label: 'Zugewiesene Issues', key: 'assigned' },
];
if (canViewAll) t.push({ label: 'Alle Issues', key: 'all' });
if (hasEditSettings) t.push({ label: 'Kategorien', key: 'types' });
if (hasEditSettings) t.push({ label: 'Einstellungen', key: 'settings' });
return t;
}, [canViewAll, hasEditSettings]);
@@ -868,10 +1018,10 @@ export default function Issues() {
</TabPanel>
)}
{/* Tab 3: Kategorien (conditional) */}
{/* Tab: Einstellungen (conditional) */}
{hasEditSettings && (
<TabPanel value={tab} index={tabs.findIndex(t => t.key === 'types')}>
<IssueTypeAdmin />
<TabPanel value={tab} index={tabs.findIndex(t => t.key === 'settings')}>
<IssueSettings />
</TabPanel>
)}
</Box>