fix permissions

This commit is contained in:
Matthias Hochmeister
2026-03-24 17:10:01 +01:00
parent a0d99dce8d
commit f9f54b7e07
7 changed files with 190 additions and 8 deletions

View File

@@ -0,0 +1,148 @@
import React, { useState } from 'react';
import {
Card,
CardContent,
Typography,
Box,
TextField,
Button,
MenuItem,
Select,
FormControl,
InputLabel,
Skeleton,
SelectChangeEvent,
} from '@mui/material';
import { BugReport } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { issuesApi } from '../../services/issues';
import { useNotification } from '../../contexts/NotificationContext';
const PRIO_OPTIONS = [
{ value: 'niedrig', label: 'Niedrig' },
{ value: 'mittel', label: 'Mittel' },
{ value: 'hoch', label: 'Hoch' },
];
const IssueQuickAddWidget: React.FC = () => {
const [titel, setTitel] = useState('');
const [typId, setTypId] = useState<number | ''>('');
const [prioritaet, setPrioritaet] = useState<string>('mittel');
const { showSuccess, showError } = useNotification();
const queryClient = useQueryClient();
const { data: types = [], isLoading: typesLoading } = useQuery({
queryKey: ['issue-types'],
queryFn: issuesApi.getTypes,
staleTime: 10 * 60 * 1000,
});
const activeTypes = types.filter((t) => t.aktiv);
const defaultTypId = activeTypes[0]?.id;
const mutation = useMutation({
mutationFn: () =>
issuesApi.createIssue({
titel: titel.trim(),
typ_id: typId !== '' ? (typId as number) : defaultTypId,
prioritaet,
}),
onSuccess: () => {
showSuccess('Issue erstellt');
setTitel('');
setTypId('');
setPrioritaet('mittel');
queryClient.invalidateQueries({ queryKey: ['issues'] });
},
onError: () => {
showError('Issue konnte nicht erstellt werden');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!titel.trim()) return;
mutation.mutate();
};
return (
<Card
sx={{
height: '100%',
transition: 'all 0.3s ease',
'&:hover': { boxShadow: 3 },
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<BugReport color="primary" />
<Typography variant="h6">Issue melden</Typography>
</Box>
{typesLoading ? (
<Box>
<Skeleton variant="rectangular" height={40} sx={{ mb: 1.5, borderRadius: 1 }} />
<Skeleton variant="rectangular" height={40} sx={{ mb: 1.5, borderRadius: 1 }} />
<Skeleton variant="rectangular" height={40} sx={{ borderRadius: 1 }} />
</Box>
) : (
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
<TextField
fullWidth
size="small"
label="Titel"
value={titel}
onChange={(e) => setTitel(e.target.value)}
inputProps={{ maxLength: 255 }}
autoComplete="off"
/>
<FormControl fullWidth size="small">
<InputLabel>Typ</InputLabel>
<Select
value={typId}
label="Typ"
onChange={(e: SelectChangeEvent<number | ''>) =>
setTypId(e.target.value as number | '')
}
>
{activeTypes.map((t) => (
<MenuItem key={t.id} value={t.id}>
{t.name}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl fullWidth size="small">
<InputLabel>Priorität</InputLabel>
<Select
value={prioritaet}
label="Priorität"
onChange={(e: SelectChangeEvent<string>) => setPrioritaet(e.target.value)}
>
{PRIO_OPTIONS.map((p) => (
<MenuItem key={p.value} value={p.value}>
{p.label}
</MenuItem>
))}
</Select>
</FormControl>
<Button
type="submit"
variant="contained"
size="small"
disabled={!titel.trim() || mutation.isPending}
fullWidth
>
{mutation.isPending ? 'Wird erstellt…' : 'Melden'}
</Button>
</Box>
)}
</CardContent>
</Card>
);
};
export default IssueQuickAddWidget;

View File

@@ -20,3 +20,4 @@ export { default as LinksWidget } from './LinksWidget';
export { default as WidgetGroup } from './WidgetGroup';
export { default as BestellungenWidget } from './BestellungenWidget';
export { default as AusruestungsanfrageWidget } from './AusruestungsanfrageWidget';
export { default as IssueQuickAddWidget } from './IssueQuickAddWidget';

View File

@@ -15,6 +15,7 @@ export const WIDGETS = [
{ key: 'links', label: 'Links', defaultVisible: true },
{ key: 'bestellungen', label: 'Bestellungen', defaultVisible: true },
{ key: 'ausruestungsanfragen', label: 'Interne Bestellungen', defaultVisible: true },
{ key: 'issueQuickAdd', label: 'Issue melden', defaultVisible: true },
] as const;
export type WidgetKey = typeof WIDGETS[number]['key'];

View File

@@ -30,6 +30,7 @@ import BannerWidget from '../components/dashboard/BannerWidget';
import WidgetGroup from '../components/dashboard/WidgetGroup';
import BestellungenWidget from '../components/dashboard/BestellungenWidget';
import AusruestungsanfrageWidget from '../components/dashboard/AusruestungsanfrageWidget';
import IssueQuickAddWidget from '../components/dashboard/IssueQuickAddWidget';
import { preferencesApi } from '../services/settings';
import { configApi } from '../services/config';
import { WidgetKey } from '../constants/widgets';
@@ -218,6 +219,14 @@ function Dashboard() {
</Box>
</Fade>
)}
{hasPermission('issues:widget') && widgetVisible('issueQuickAdd') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '760ms' }}>
<Box>
<IssueQuickAddWidget />
</Box>
</Fade>
)}
</WidgetGroup>
{/* Information Group */}

View File

@@ -27,6 +27,9 @@ import type { Issue, IssueComment, IssueTyp, CreateIssuePayload, UpdateIssuePayl
const formatDate = (iso?: string) =>
iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '-';
const formatIssueId = (issue: Issue) =>
`${new Date(issue.created_at).getFullYear()}/${issue.id}`;
const STATUS_COLORS: Record<Issue['status'], 'info' | 'warning' | 'success' | 'error'> = {
offen: 'info',
in_bearbeitung: 'warning',
@@ -217,7 +220,7 @@ function IssueRow({
sx={{ cursor: 'pointer', '& > *': { borderBottom: expanded ? 'unset' : undefined } }}
onClick={() => setExpanded(!expanded)}
>
<TableCell sx={{ width: 50 }}>#{issue.id}</TableCell>
<TableCell sx={{ width: 80 }}>{formatIssueId(issue)}</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{getTypIcon(issue.typ_icon, issue.typ_farbe)}
@@ -341,7 +344,7 @@ function IssueRow({
{/* Reopen Dialog */}
<Dialog open={reopenOpen} onClose={() => setReopenOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Issue wiedereröffnen</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
<TextField
label="Kommentar (Pflicht)"
required
@@ -685,7 +688,7 @@ function IssueTypeAdmin() {
{/* Create Type Dialog */}
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Neue Kategorie erstellen</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
<TextField label="Name" required fullWidth value={createData.name || ''} onChange={(e) => setCreateData({ ...createData, name: e.target.value })} autoFocus />
<FormControl fullWidth>
<InputLabel>Übergeordnete Kategorie</InputLabel>
@@ -876,7 +879,7 @@ export default function Issues() {
{/* Create Issue Dialog */}
<Dialog open={createOpen} onClose={() => setCreateOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Neues Issue erstellen</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
<TextField
label="Titel"
required
@@ -931,7 +934,7 @@ export default function Issues() {
</Dialog>
{/* FAB */}
{canCreate && (
{canCreate && activeTab === 'mine' && (
<ChatAwareFab
color="primary"
aria-label="Neues Issue"