fix permissions
This commit is contained in:
@@ -530,226 +530,48 @@ function FilterBar({
|
||||
);
|
||||
}
|
||||
|
||||
// ── Issue Type Admin ──
|
||||
// ── Shared color picker helpers ──
|
||||
|
||||
function IssueTypeAdmin() {
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [editData, setEditData] = useState<Partial<IssueTyp>>({});
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createData, setCreateData] = useState<Partial<IssueTyp>>({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true });
|
||||
|
||||
const { data: types = [], isLoading } = useQuery({
|
||||
queryKey: ['issue-types'],
|
||||
queryFn: issuesApi.getTypes,
|
||||
});
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (data: Partial<IssueTyp>) => issuesApi.createType(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issue-types'] });
|
||||
showSuccess('Kategorie erstellt');
|
||||
setCreateOpen(false);
|
||||
setCreateData({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true });
|
||||
},
|
||||
onError: () => showError('Fehler beim Erstellen'),
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<IssueTyp> }) => issuesApi.updateType(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issue-types'] });
|
||||
showSuccess('Kategorie aktualisiert');
|
||||
setEditId(null);
|
||||
},
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: (id: number) => issuesApi.deleteType(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['issue-types'] });
|
||||
showSuccess('Kategorie gelöscht');
|
||||
},
|
||||
onError: () => showError('Fehler beim Löschen'),
|
||||
});
|
||||
|
||||
const startEdit = (t: IssueTyp) => {
|
||||
setEditId(t.id);
|
||||
setEditData({ name: t.name, icon: t.icon, farbe: t.farbe, erlaubt_abgelehnt: t.erlaubt_abgelehnt, sort_order: t.sort_order, aktiv: t.aktiv, parent_id: t.parent_id });
|
||||
};
|
||||
|
||||
// Flatten types with children indented
|
||||
const flatTypes = useMemo(() => {
|
||||
const roots = types.filter(t => !t.parent_id);
|
||||
const result: { type: IssueTyp; indent: boolean }[] = [];
|
||||
for (const root of roots) {
|
||||
result.push({ type: root, indent: false });
|
||||
const children = types.filter(t => t.parent_id === root.id);
|
||||
for (const child of children) {
|
||||
result.push({ type: child, indent: true });
|
||||
}
|
||||
}
|
||||
// Add orphans (parent_id set but parent missing)
|
||||
const listed = new Set(result.map(r => r.type.id));
|
||||
for (const t of types) {
|
||||
if (!listed.has(t.id)) result.push({ type: t, indent: false });
|
||||
}
|
||||
return result;
|
||||
}, [types]);
|
||||
|
||||
if (isLoading) return <CircularProgress />;
|
||||
const MUI_CHIP_COLORS = ['default', 'primary', 'secondary', 'error', 'info', 'success', 'warning'] as const;
|
||||
const MUI_THEME_COLORS: Record<string, string> = { default: '#9e9e9e', primary: '#1976d2', secondary: '#9c27b0', error: '#d32f2f', info: '#0288d1', success: '#2e7d32', warning: '#ed6c02', action: '#757575' };
|
||||
const ICON_COLORS = ['action', 'error', 'info', 'success', 'warning', 'primary', 'secondary'] as const;
|
||||
|
||||
function ColorSwatch({ colors, value, onChange }: { colors: readonly string[]; value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">Issue-Kategorien</Typography>
|
||||
<Button startIcon={<AddIcon />} variant="contained" size="small" onClick={() => setCreateOpen(true)}>
|
||||
Neue Kategorie
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Icon</TableCell>
|
||||
<TableCell>Farbe</TableCell>
|
||||
<TableCell>Abgelehnt erlaubt</TableCell>
|
||||
<TableCell>Sort</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell>Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{flatTypes.map(({ type: t, indent }) => (
|
||||
<TableRow key={t.id} sx={indent ? { bgcolor: 'action.hover' } : undefined}>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{indent && <DragIndicator fontSize="small" sx={{ opacity: 0.3, ml: 2 }} />}
|
||||
{editId === t.id ? (
|
||||
<TextField size="small" value={editData.name || ''} onChange={(e) => setEditData({ ...editData, name: e.target.value })} />
|
||||
) : (
|
||||
<Typography variant="body2">{t.name}</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editId === t.id ? (
|
||||
<Select size="small" value={editData.icon || 'HelpOutline'} onChange={(e) => setEditData({ ...editData, icon: e.target.value })}>
|
||||
<MenuItem value="BugReport">BugReport</MenuItem>
|
||||
<MenuItem value="FiberNew">FiberNew</MenuItem>
|
||||
<MenuItem value="HelpOutline">HelpOutline</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<Box sx={{ display: 'inline-flex' }}>{getTypIcon(t.icon, t.farbe)}</Box>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editId === t.id ? (
|
||||
<Select size="small" value={editData.farbe || 'action'} onChange={(e) => setEditData({ ...editData, farbe: e.target.value })}>
|
||||
<MenuItem value="error">error</MenuItem>
|
||||
<MenuItem value="info">info</MenuItem>
|
||||
<MenuItem value="action">action</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<Chip label={t.farbe || 'action'} size="small" variant="outlined" />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editId === t.id ? (
|
||||
<Switch checked={editData.erlaubt_abgelehnt ?? true} onChange={(e) => setEditData({ ...editData, erlaubt_abgelehnt: e.target.checked })} size="small" />
|
||||
) : (
|
||||
<Typography variant="body2">{t.erlaubt_abgelehnt ? 'Ja' : 'Nein'}</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editId === t.id ? (
|
||||
<TextField size="small" type="number" sx={{ width: 70 }} value={editData.sort_order ?? 0} onChange={(e) => setEditData({ ...editData, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
) : (
|
||||
t.sort_order
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editId === t.id ? (
|
||||
<Switch checked={editData.aktiv ?? true} onChange={(e) => setEditData({ ...editData, aktiv: e.target.checked })} size="small" />
|
||||
) : (
|
||||
<Chip label={t.aktiv ? 'Aktiv' : 'Inaktiv'} size="small" color={t.aktiv ? 'success' : 'default'} />
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{editId === t.id ? (
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Button size="small" variant="contained" onClick={() => updateMut.mutate({ id: t.id, data: editData })} disabled={updateMut.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button size="small" onClick={() => setEditId(null)}>Abbrechen</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<IconButton size="small" onClick={() => startEdit(t)}><EditIcon fontSize="small" /></IconButton>
|
||||
<IconButton size="small" color="error" onClick={() => deleteMut.mutate(t.id)}><DeleteIcon fontSize="small" /></IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{/* 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, 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>
|
||||
<Select value={createData.parent_id ?? ''} label="Übergeordnete Kategorie" onChange={(e) => setCreateData({ ...createData, parent_id: e.target.value ? Number(e.target.value) : null })}>
|
||||
<MenuItem value="">Keine</MenuItem>
|
||||
{types.filter(t => !t.parent_id).map(t => (
|
||||
<MenuItem key={t.id} value={t.id}>{t.name}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Icon</InputLabel>
|
||||
<Select value={createData.icon || 'HelpOutline'} label="Icon" onChange={(e) => setCreateData({ ...createData, icon: e.target.value })}>
|
||||
<MenuItem value="BugReport">BugReport</MenuItem>
|
||||
<MenuItem value="FiberNew">FiberNew</MenuItem>
|
||||
<MenuItem value="HelpOutline">HelpOutline</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Farbe</InputLabel>
|
||||
<Select value={createData.farbe || 'action'} label="Farbe" onChange={(e) => setCreateData({ ...createData, farbe: e.target.value })}>
|
||||
<MenuItem value="error">error</MenuItem>
|
||||
<MenuItem value="info">info</MenuItem>
|
||||
<MenuItem value="action">action</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={createData.erlaubt_abgelehnt ?? true} onChange={(e) => setCreateData({ ...createData, erlaubt_abgelehnt: e.target.checked })} />}
|
||||
label="Abgelehnt erlaubt"
|
||||
/>
|
||||
<TextField label="Sortierung" type="number" value={createData.sort_order ?? 0} onChange={(e) => setCreateData({ ...createData, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setCreateOpen(false)}>Abbrechen</Button>
|
||||
<Button variant="contained" disabled={!createData.name?.trim() || createMut.isPending} onClick={() => createMut.mutate(createData)}>
|
||||
Erstellen
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
{colors.map((c) => (
|
||||
<Box
|
||||
key={c}
|
||||
onClick={() => onChange(c)}
|
||||
sx={{
|
||||
width: 22, height: 22, borderRadius: '50%', cursor: 'pointer',
|
||||
bgcolor: MUI_THEME_COLORS[c] ?? c,
|
||||
border: value === c ? '2.5px solid' : '2px solid transparent',
|
||||
borderColor: value === c ? 'text.primary' : 'transparent',
|
||||
'&:hover': { opacity: 0.8 },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Issue Settings (Status + Prioritäten + Statusmeldungen + Kategorien) ──
|
||||
function HexColorInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box
|
||||
component="input"
|
||||
type="color"
|
||||
value={value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
|
||||
sx={{ width: 28, height: 28, border: 'none', borderRadius: 1, cursor: 'pointer', p: 0, bgcolor: 'transparent' }}
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">{value}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const MUI_CHIP_COLORS = ['default', 'primary', 'secondary', 'error', 'info', 'success', 'warning'];
|
||||
// ── Issue Settings (consolidated: Status + Prioritäten + Kategorien) ──
|
||||
|
||||
function IssueSettings() {
|
||||
const queryClient = useQueryClient();
|
||||
@@ -767,17 +589,18 @@ function IssueSettings() {
|
||||
const [prioEditId, setPrioEditId] = useState<number | null>(null);
|
||||
const [prioEditData, setPrioEditData] = useState<Partial<IssuePriorityDef>>({});
|
||||
|
||||
const { data: issueStatuses = [], isLoading: statusLoading } = useQuery({
|
||||
queryKey: ['issue-statuses'],
|
||||
queryFn: issuesApi.getStatuses,
|
||||
});
|
||||
// ── Kategorien state ──
|
||||
const [typeCreateOpen, setTypeCreateOpen] = useState(false);
|
||||
const [typeCreateData, setTypeCreateData] = useState<Partial<IssueTyp>>({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true });
|
||||
const [typeEditId, setTypeEditId] = useState<number | null>(null);
|
||||
const [typeEditData, setTypeEditData] = useState<Partial<IssueTyp>>({});
|
||||
|
||||
const { data: issuePriorities = [], isLoading: prioLoading } = useQuery({
|
||||
queryKey: ['issue-priorities'],
|
||||
queryFn: issuesApi.getPriorities,
|
||||
});
|
||||
// ── Queries ──
|
||||
const { data: issueStatuses = [], isLoading: statusLoading } = useQuery({ queryKey: ['issue-statuses'], queryFn: issuesApi.getStatuses });
|
||||
const { data: issuePriorities = [], isLoading: prioLoading } = useQuery({ queryKey: ['issue-priorities'], queryFn: issuesApi.getPriorities });
|
||||
const { data: types = [], isLoading: typesLoading } = useQuery({ queryKey: ['issue-types'], queryFn: issuesApi.getTypes });
|
||||
|
||||
// Status mutations
|
||||
// ── Status mutations ──
|
||||
const createStatusMut = useMutation({
|
||||
mutationFn: (data: Partial<IssueStatusDef>) => issuesApi.createStatus(data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-statuses'] }); showSuccess('Status erstellt'); setStatusCreateOpen(false); setStatusCreateData({ schluessel: '', bezeichnung: '', farbe: 'default', ist_abschluss: false, ist_initial: false, benoetigt_typ_freigabe: false, sort_order: 0 }); },
|
||||
@@ -794,7 +617,7 @@ function IssueSettings() {
|
||||
onError: () => showError('Fehler beim Deaktivieren'),
|
||||
});
|
||||
|
||||
// Priority mutations
|
||||
// ── Priority mutations ──
|
||||
const createPrioMut = useMutation({
|
||||
mutationFn: (data: Partial<IssuePriorityDef>) => issuesApi.createPriority(data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-priorities'] }); showSuccess('Priorität erstellt'); setPrioCreateOpen(false); setPrioCreateData({ schluessel: '', bezeichnung: '', farbe: '#9e9e9e', sort_order: 0 }); },
|
||||
@@ -811,10 +634,39 @@ function IssueSettings() {
|
||||
onError: () => showError('Fehler beim Deaktivieren'),
|
||||
});
|
||||
|
||||
// ── Type mutations ──
|
||||
const createTypeMut = useMutation({
|
||||
mutationFn: (data: Partial<IssueTyp>) => issuesApi.createType(data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie erstellt'); setTypeCreateOpen(false); setTypeCreateData({ name: '', icon: 'HelpOutline', farbe: 'action', erlaubt_abgelehnt: true, sort_order: 0, aktiv: true }); },
|
||||
onError: () => showError('Fehler beim Erstellen'),
|
||||
});
|
||||
const updateTypeMut = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: Partial<IssueTyp> }) => issuesApi.updateType(id, data),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie aktualisiert'); setTypeEditId(null); },
|
||||
onError: () => showError('Fehler beim Aktualisieren'),
|
||||
});
|
||||
const deleteTypeMut = useMutation({
|
||||
mutationFn: (id: number) => issuesApi.deleteType(id),
|
||||
onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['issue-types'] }); showSuccess('Kategorie deaktiviert'); },
|
||||
onError: () => showError('Fehler beim Deaktivieren'),
|
||||
});
|
||||
|
||||
const flatTypes = useMemo(() => {
|
||||
const roots = types.filter(t => !t.parent_id);
|
||||
const result: { type: IssueTyp; indent: boolean }[] = [];
|
||||
for (const root of roots) {
|
||||
result.push({ type: root, indent: false });
|
||||
for (const child of types.filter(t => t.parent_id === root.id)) result.push({ type: child, indent: true });
|
||||
}
|
||||
const listed = new Set(result.map(r => r.type.id));
|
||||
for (const t of types) { if (!listed.has(t.id)) result.push({ type: t, indent: false }); }
|
||||
return result;
|
||||
}, [types]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
|
||||
{/* Section 1: Status */}
|
||||
{/* ──── Section 1: Status ──── */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">Status</Typography>
|
||||
@@ -832,47 +684,33 @@ function IssueSettings() {
|
||||
<TableCell>Initial</TableCell>
|
||||
<TableCell>Sort</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell>Aktionen</TableCell>
|
||||
<TableCell sx={{ width: 80 }}>Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{issueStatuses.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={8} sx={{ textAlign: 'center', color: 'text.secondary' }}>Keine Status</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={8} sx={{ textAlign: 'center', color: 'text.secondary', py: 3 }}>Keine Status vorhanden</TableCell></TableRow>
|
||||
) : issueStatuses.map((s) => (
|
||||
<TableRow key={s.id}>
|
||||
{statusEditId === s.id ? (
|
||||
<>
|
||||
<TableCell><TextField size="small" value={statusEditData.bezeichnung ?? s.bezeichnung} onChange={(e) => setStatusEditData({ ...statusEditData, bezeichnung: e.target.value })} /></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{s.schluessel}</Typography></TableCell>
|
||||
<TableCell>
|
||||
<Select size="small" value={statusEditData.farbe ?? s.farbe} onChange={(e) => setStatusEditData({ ...statusEditData, farbe: e.target.value })}>
|
||||
{MUI_CHIP_COLORS.map(c => <MenuItem key={c} value={c}>{c}</MenuItem>)}
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell><Switch checked={statusEditData.ist_abschluss ?? s.ist_abschluss} onChange={(e) => setStatusEditData({ ...statusEditData, ist_abschluss: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell><Switch checked={statusEditData.ist_initial ?? s.ist_initial} onChange={(e) => setStatusEditData({ ...statusEditData, ist_initial: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell><TextField size="small" type="number" sx={{ width: 60 }} value={statusEditData.sort_order ?? s.sort_order} onChange={(e) => setStatusEditData({ ...statusEditData, sort_order: parseInt(e.target.value) || 0 })} /></TableCell>
|
||||
<TableCell><Switch checked={statusEditData.aktiv ?? s.aktiv} onChange={(e) => setStatusEditData({ ...statusEditData, aktiv: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => updateStatusMut.mutate({ id: s.id, data: statusEditData })}><CheckIcon /></IconButton>
|
||||
<IconButton size="small" onClick={() => setStatusEditId(null)}><CloseIcon /></IconButton>
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell><Chip label={s.bezeichnung} color={s.farbe as any} size="small" /></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{s.schluessel}</Typography></TableCell>
|
||||
<TableCell><Typography variant="body2">{s.farbe}</Typography></TableCell>
|
||||
<TableCell>{s.ist_abschluss ? '✓' : '-'}</TableCell>
|
||||
<TableCell>{s.ist_initial ? '✓' : '-'}</TableCell>
|
||||
<TableCell>{s.sort_order}</TableCell>
|
||||
<TableCell><Switch checked={s.aktiv} onChange={(e) => updateStatusMut.mutate({ id: s.id, data: { aktiv: e.target.checked } })} size="small" /></TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => { setStatusEditId(s.id); setStatusEditData({ bezeichnung: s.bezeichnung, farbe: s.farbe, ist_abschluss: s.ist_abschluss, ist_initial: s.ist_initial, benoetigt_typ_freigabe: s.benoetigt_typ_freigabe, sort_order: s.sort_order, aktiv: s.aktiv }); }}><EditIcon /></IconButton>
|
||||
<IconButton size="small" onClick={() => deleteStatusMut.mutate(s.id)}><DeleteIcon /></IconButton>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{statusEditId === s.id ? (<>
|
||||
<TableCell><TextField size="small" value={statusEditData.bezeichnung ?? s.bezeichnung} onChange={(e) => setStatusEditData({ ...statusEditData, bezeichnung: e.target.value })} /></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{s.schluessel}</Typography></TableCell>
|
||||
<TableCell><ColorSwatch colors={MUI_CHIP_COLORS} value={statusEditData.farbe ?? s.farbe} onChange={(v) => setStatusEditData({ ...statusEditData, farbe: v })} /></TableCell>
|
||||
<TableCell><Switch checked={statusEditData.ist_abschluss ?? s.ist_abschluss} onChange={(e) => setStatusEditData({ ...statusEditData, ist_abschluss: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell><Switch checked={statusEditData.ist_initial ?? s.ist_initial} onChange={(e) => setStatusEditData({ ...statusEditData, ist_initial: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell><TextField size="small" type="number" sx={{ width: 60 }} value={statusEditData.sort_order ?? s.sort_order} onChange={(e) => setStatusEditData({ ...statusEditData, sort_order: parseInt(e.target.value) || 0 })} /></TableCell>
|
||||
<TableCell><Switch checked={statusEditData.aktiv ?? s.aktiv} onChange={(e) => setStatusEditData({ ...statusEditData, aktiv: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell><Box sx={{ display: 'flex', gap: 0.5 }}><IconButton size="small" color="primary" onClick={() => updateStatusMut.mutate({ id: s.id, data: statusEditData })}><CheckIcon fontSize="small" /></IconButton><IconButton size="small" onClick={() => setStatusEditId(null)}><CloseIcon fontSize="small" /></IconButton></Box></TableCell>
|
||||
</>) : (<>
|
||||
<TableCell><Chip label={s.bezeichnung} color={s.farbe as any} size="small" /></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{s.schluessel}</Typography></TableCell>
|
||||
<TableCell><Box sx={{ width: 16, height: 16, borderRadius: '50%', bgcolor: MUI_THEME_COLORS[s.farbe] ?? s.farbe }} /></TableCell>
|
||||
<TableCell>{s.ist_abschluss ? '✓' : '-'}</TableCell>
|
||||
<TableCell>{s.ist_initial ? '✓' : '-'}</TableCell>
|
||||
<TableCell>{s.sort_order}</TableCell>
|
||||
<TableCell><Switch checked={s.aktiv} onChange={(e) => updateStatusMut.mutate({ id: s.id, data: { aktiv: e.target.checked } })} size="small" /></TableCell>
|
||||
<TableCell><Box sx={{ display: 'flex', gap: 0.5 }}><IconButton size="small" onClick={() => { setStatusEditId(s.id); setStatusEditData({ bezeichnung: s.bezeichnung, farbe: s.farbe, ist_abschluss: s.ist_abschluss, ist_initial: s.ist_initial, benoetigt_typ_freigabe: s.benoetigt_typ_freigabe, sort_order: s.sort_order, aktiv: s.aktiv }); }}><EditIcon fontSize="small" /></IconButton><IconButton size="small" color="error" onClick={() => deleteStatusMut.mutate(s.id)}><DeleteIcon fontSize="small" /></IconButton></Box></TableCell>
|
||||
</>)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -881,7 +719,7 @@ function IssueSettings() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Section 2: Prioritäten */}
|
||||
{/* ──── Section 2: Prioritäten ──── */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">Prioritäten</Typography>
|
||||
@@ -897,44 +735,29 @@ function IssueSettings() {
|
||||
<TableCell>Farbe</TableCell>
|
||||
<TableCell>Sort</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell>Aktionen</TableCell>
|
||||
<TableCell sx={{ width: 80 }}>Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{issuePriorities.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={6} sx={{ textAlign: 'center', color: 'text.secondary' }}>Keine Prioritäten</TableCell></TableRow>
|
||||
<TableRow><TableCell colSpan={6} sx={{ textAlign: 'center', color: 'text.secondary', py: 3 }}>Keine Prioritäten vorhanden</TableCell></TableRow>
|
||||
) : issuePriorities.map((p) => (
|
||||
<TableRow key={p.id}>
|
||||
{prioEditId === p.id ? (
|
||||
<>
|
||||
<TableCell><TextField size="small" value={prioEditData.bezeichnung ?? p.bezeichnung} onChange={(e) => setPrioEditData({ ...prioEditData, bezeichnung: e.target.value })} /></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{p.schluessel}</Typography></TableCell>
|
||||
<TableCell><TextField size="small" value={prioEditData.farbe ?? p.farbe} onChange={(e) => setPrioEditData({ ...prioEditData, farbe: e.target.value })} placeholder="#hex" sx={{ width: 90 }} /></TableCell>
|
||||
<TableCell><TextField size="small" type="number" sx={{ width: 60 }} value={prioEditData.sort_order ?? p.sort_order} onChange={(e) => setPrioEditData({ ...prioEditData, sort_order: parseInt(e.target.value) || 0 })} /></TableCell>
|
||||
<TableCell><Switch checked={prioEditData.aktiv ?? p.aktiv} onChange={(e) => setPrioEditData({ ...prioEditData, aktiv: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => updatePrioMut.mutate({ id: p.id, data: prioEditData })}><CheckIcon /></IconButton>
|
||||
<IconButton size="small" onClick={() => setPrioEditId(null)}><CloseIcon /></IconButton>
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<CircleIcon sx={{ fontSize: 12, color: p.farbe }} />
|
||||
<Typography variant="body2">{p.bezeichnung}</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{p.schluessel}</Typography></TableCell>
|
||||
<TableCell><Typography variant="body2">{p.farbe}</Typography></TableCell>
|
||||
<TableCell>{p.sort_order}</TableCell>
|
||||
<TableCell><Switch checked={p.aktiv} onChange={(e) => updatePrioMut.mutate({ id: p.id, data: { aktiv: e.target.checked } })} size="small" /></TableCell>
|
||||
<TableCell>
|
||||
<IconButton size="small" onClick={() => { setPrioEditId(p.id); setPrioEditData({ bezeichnung: p.bezeichnung, farbe: p.farbe, sort_order: p.sort_order, aktiv: p.aktiv }); }}><EditIcon /></IconButton>
|
||||
<IconButton size="small" onClick={() => deletePrioMut.mutate(p.id)}><DeleteIcon /></IconButton>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{prioEditId === p.id ? (<>
|
||||
<TableCell><TextField size="small" value={prioEditData.bezeichnung ?? p.bezeichnung} onChange={(e) => setPrioEditData({ ...prioEditData, bezeichnung: e.target.value })} /></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{p.schluessel}</Typography></TableCell>
|
||||
<TableCell><HexColorInput value={prioEditData.farbe ?? p.farbe} onChange={(v) => setPrioEditData({ ...prioEditData, farbe: v })} /></TableCell>
|
||||
<TableCell><TextField size="small" type="number" sx={{ width: 60 }} value={prioEditData.sort_order ?? p.sort_order} onChange={(e) => setPrioEditData({ ...prioEditData, sort_order: parseInt(e.target.value) || 0 })} /></TableCell>
|
||||
<TableCell><Switch checked={prioEditData.aktiv ?? p.aktiv} onChange={(e) => setPrioEditData({ ...prioEditData, aktiv: e.target.checked })} size="small" /></TableCell>
|
||||
<TableCell><Box sx={{ display: 'flex', gap: 0.5 }}><IconButton size="small" color="primary" onClick={() => updatePrioMut.mutate({ id: p.id, data: prioEditData })}><CheckIcon fontSize="small" /></IconButton><IconButton size="small" onClick={() => setPrioEditId(null)}><CloseIcon fontSize="small" /></IconButton></Box></TableCell>
|
||||
</>) : (<>
|
||||
<TableCell><Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}><CircleIcon sx={{ fontSize: 12, color: p.farbe }} /><Typography variant="body2">{p.bezeichnung}</Typography></Box></TableCell>
|
||||
<TableCell><Typography variant="body2" color="text.secondary">{p.schluessel}</Typography></TableCell>
|
||||
<TableCell><Box sx={{ width: 16, height: 16, borderRadius: '50%', bgcolor: p.farbe, border: '1px solid', borderColor: 'divider' }} /></TableCell>
|
||||
<TableCell>{p.sort_order}</TableCell>
|
||||
<TableCell><Switch checked={p.aktiv} onChange={(e) => updatePrioMut.mutate({ id: p.id, data: { aktiv: e.target.checked } })} size="small" /></TableCell>
|
||||
<TableCell><Box sx={{ display: 'flex', gap: 0.5 }}><IconButton size="small" onClick={() => { setPrioEditId(p.id); setPrioEditData({ bezeichnung: p.bezeichnung, farbe: p.farbe, sort_order: p.sort_order, aktiv: p.aktiv }); }}><EditIcon fontSize="small" /></IconButton><IconButton size="small" color="error" onClick={() => deletePrioMut.mutate(p.id)}><DeleteIcon fontSize="small" /></IconButton></Box></TableCell>
|
||||
</>)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -943,53 +766,90 @@ function IssueSettings() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Section 3: Kategorien */}
|
||||
{/* ──── Section 3: Kategorien ──── */}
|
||||
<Box>
|
||||
<IssueTypeAdmin />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6">Kategorien</Typography>
|
||||
<Button startIcon={<AddIcon />} variant="contained" size="small" onClick={() => setTypeCreateOpen(true)}>Neue Kategorie</Button>
|
||||
</Box>
|
||||
{typesLoading ? <CircularProgress /> : (
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Icon</TableCell>
|
||||
<TableCell>Farbe</TableCell>
|
||||
<TableCell>Abgelehnt</TableCell>
|
||||
<TableCell>Sort</TableCell>
|
||||
<TableCell>Aktiv</TableCell>
|
||||
<TableCell sx={{ width: 80 }}>Aktionen</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{flatTypes.length === 0 ? (
|
||||
<TableRow><TableCell colSpan={7} sx={{ textAlign: 'center', color: 'text.secondary', py: 3 }}>Keine Kategorien vorhanden</TableCell></TableRow>
|
||||
) : flatTypes.map(({ type: t, indent }) => (
|
||||
<TableRow key={t.id} sx={indent ? { bgcolor: 'action.hover' } : undefined}>
|
||||
<TableCell><Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>{indent && <DragIndicator fontSize="small" sx={{ opacity: 0.3, ml: 2 }} />}{typeEditId === t.id ? <TextField size="small" value={typeEditData.name || ''} onChange={(e) => setTypeEditData({ ...typeEditData, name: e.target.value })} /> : <Typography variant="body2">{t.name}</Typography>}</Box></TableCell>
|
||||
<TableCell>{typeEditId === t.id ? (<Select size="small" value={typeEditData.icon || 'HelpOutline'} onChange={(e) => setTypeEditData({ ...typeEditData, icon: e.target.value })}><MenuItem value="BugReport">BugReport</MenuItem><MenuItem value="FiberNew">FiberNew</MenuItem><MenuItem value="HelpOutline">HelpOutline</MenuItem></Select>) : <Box sx={{ display: 'inline-flex' }}>{getTypIcon(t.icon, t.farbe)}</Box>}</TableCell>
|
||||
<TableCell>{typeEditId === t.id ? <ColorSwatch colors={ICON_COLORS} value={typeEditData.farbe || 'action'} onChange={(v) => setTypeEditData({ ...typeEditData, farbe: v })} /> : <Box sx={{ width: 16, height: 16, borderRadius: '50%', bgcolor: MUI_THEME_COLORS[t.farbe || 'action'] ?? '#757575' }} />}</TableCell>
|
||||
<TableCell>{typeEditId === t.id ? <Switch checked={typeEditData.erlaubt_abgelehnt ?? true} onChange={(e) => setTypeEditData({ ...typeEditData, erlaubt_abgelehnt: e.target.checked })} size="small" /> : (t.erlaubt_abgelehnt ? '✓' : '-')}</TableCell>
|
||||
<TableCell>{typeEditId === t.id ? <TextField size="small" type="number" sx={{ width: 60 }} value={typeEditData.sort_order ?? 0} onChange={(e) => setTypeEditData({ ...typeEditData, sort_order: parseInt(e.target.value) || 0 })} /> : t.sort_order}</TableCell>
|
||||
<TableCell><Switch checked={typeEditId === t.id ? (typeEditData.aktiv ?? t.aktiv) : t.aktiv} onChange={(e) => { if (typeEditId === t.id) setTypeEditData({ ...typeEditData, aktiv: e.target.checked }); else updateTypeMut.mutate({ id: t.id, data: { aktiv: e.target.checked } }); }} size="small" /></TableCell>
|
||||
<TableCell>{typeEditId === t.id ? (<Box sx={{ display: 'flex', gap: 0.5 }}><IconButton size="small" color="primary" onClick={() => updateTypeMut.mutate({ id: t.id, data: typeEditData })}><CheckIcon fontSize="small" /></IconButton><IconButton size="small" onClick={() => setTypeEditId(null)}><CloseIcon fontSize="small" /></IconButton></Box>) : (<Box sx={{ display: 'flex', gap: 0.5 }}><IconButton size="small" onClick={() => { setTypeEditId(t.id); setTypeEditData({ name: t.name, icon: t.icon, farbe: t.farbe, erlaubt_abgelehnt: t.erlaubt_abgelehnt, sort_order: t.sort_order, aktiv: t.aktiv, parent_id: t.parent_id }); }}><EditIcon fontSize="small" /></IconButton><IconButton size="small" color="error" onClick={() => deleteTypeMut.mutate(t.id)}><DeleteIcon fontSize="small" /></IconButton></Box>)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Create Status Dialog */}
|
||||
{/* ──── Create Status Dialog ──── */}
|
||||
<Dialog open={statusCreateOpen} onClose={() => setStatusCreateOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Neuer Status</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField label="Schlüssel" required fullWidth value={statusCreateData.schluessel || ''} onChange={(e) => setStatusCreateData({ ...statusCreateData, schluessel: e.target.value })} helperText="Maschinenwert, z.B. 'in_pruefung' (nicht änderbar)" autoFocus />
|
||||
<TextField label="Bezeichnung" required fullWidth value={statusCreateData.bezeichnung || ''} onChange={(e) => setStatusCreateData({ ...statusCreateData, bezeichnung: e.target.value })} />
|
||||
<FormControl fullWidth size="small">
|
||||
<InputLabel>Farbe</InputLabel>
|
||||
<Select label="Farbe" value={statusCreateData.farbe || 'default'} onChange={(e) => setStatusCreateData({ ...statusCreateData, farbe: e.target.value })}>
|
||||
{MUI_CHIP_COLORS.map(c => <MenuItem key={c} value={c}><Chip label={c} color={c as any} size="small" /></MenuItem>)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box><Typography variant="body2" sx={{ mb: 0.5 }}>Farbe</Typography><ColorSwatch colors={MUI_CHIP_COLORS} value={statusCreateData.farbe || 'default'} onChange={(v) => setStatusCreateData({ ...statusCreateData, farbe: v })} /></Box>
|
||||
<TextField label="Sortierung" type="number" value={statusCreateData.sort_order ?? 0} onChange={(e) => setStatusCreateData({ ...statusCreateData, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
<FormControlLabel control={<Switch checked={statusCreateData.ist_abschluss ?? false} onChange={(e) => setStatusCreateData({ ...statusCreateData, ist_abschluss: e.target.checked })} />} label="Abschluss-Status (erledigte Issues)" />
|
||||
<FormControlLabel control={<Switch checked={statusCreateData.ist_initial ?? false} onChange={(e) => setStatusCreateData({ ...statusCreateData, ist_initial: e.target.checked })} />} label="Initial-Status (Ziel beim Wiedereröffnen)" />
|
||||
<FormControlLabel control={<Switch checked={statusCreateData.benoetigt_typ_freigabe ?? false} onChange={(e) => setStatusCreateData({ ...statusCreateData, benoetigt_typ_freigabe: e.target.checked })} />} label="Nur bei Typ-Freigabe erlaubt" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setStatusCreateOpen(false)}>Abbrechen</Button>
|
||||
<Button variant="contained" onClick={() => createStatusMut.mutate(statusCreateData)} disabled={!statusCreateData.schluessel?.trim() || !statusCreateData.bezeichnung?.trim() || createStatusMut.isPending}>Erstellen</Button>
|
||||
</DialogActions>
|
||||
<DialogActions><Button onClick={() => setStatusCreateOpen(false)}>Abbrechen</Button><Button variant="contained" onClick={() => createStatusMut.mutate(statusCreateData)} disabled={!statusCreateData.schluessel?.trim() || !statusCreateData.bezeichnung?.trim() || createStatusMut.isPending}>Erstellen</Button></DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Create Priority Dialog */}
|
||||
{/* ──── Create Priority Dialog ──── */}
|
||||
<Dialog open={prioCreateOpen} onClose={() => setPrioCreateOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Neue Priorität</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField label="Schlüssel" required fullWidth value={prioCreateData.schluessel || ''} onChange={(e) => setPrioCreateData({ ...prioCreateData, schluessel: e.target.value })} helperText="Maschinenwert, z.B. 'kritisch'" autoFocus />
|
||||
<TextField label="Bezeichnung" required fullWidth value={prioCreateData.bezeichnung || ''} onChange={(e) => setPrioCreateData({ ...prioCreateData, bezeichnung: e.target.value })} />
|
||||
<TextField label="Farbe (Hex)" fullWidth value={prioCreateData.farbe || '#9e9e9e'} onChange={(e) => setPrioCreateData({ ...prioCreateData, farbe: e.target.value })} placeholder="#d32f2f" />
|
||||
<Box><Typography variant="body2" sx={{ mb: 0.5 }}>Farbe</Typography><HexColorInput value={prioCreateData.farbe || '#9e9e9e'} onChange={(v) => setPrioCreateData({ ...prioCreateData, farbe: v })} /></Box>
|
||||
<TextField label="Sortierung" type="number" value={prioCreateData.sort_order ?? 0} onChange={(e) => setPrioCreateData({ ...prioCreateData, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setPrioCreateOpen(false)}>Abbrechen</Button>
|
||||
<Button variant="contained" onClick={() => createPrioMut.mutate(prioCreateData)} disabled={!prioCreateData.schluessel?.trim() || !prioCreateData.bezeichnung?.trim() || createPrioMut.isPending}>Erstellen</Button>
|
||||
</DialogActions>
|
||||
<DialogActions><Button onClick={() => setPrioCreateOpen(false)}>Abbrechen</Button><Button variant="contained" onClick={() => createPrioMut.mutate(prioCreateData)} disabled={!prioCreateData.schluessel?.trim() || !prioCreateData.bezeichnung?.trim() || createPrioMut.isPending}>Erstellen</Button></DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* ──── Create Kategorie Dialog ──── */}
|
||||
<Dialog open={typeCreateOpen} onClose={() => setTypeCreateOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Neue Kategorie</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
|
||||
<TextField label="Name" required fullWidth value={typeCreateData.name || ''} onChange={(e) => setTypeCreateData({ ...typeCreateData, name: e.target.value })} autoFocus />
|
||||
<FormControl fullWidth><InputLabel>Übergeordnete Kategorie</InputLabel><Select value={typeCreateData.parent_id ?? ''} label="Übergeordnete Kategorie" onChange={(e) => setTypeCreateData({ ...typeCreateData, parent_id: e.target.value ? Number(e.target.value) : null })}><MenuItem value="">Keine</MenuItem>{types.filter(t => !t.parent_id).map(t => <MenuItem key={t.id} value={t.id}>{t.name}</MenuItem>)}</Select></FormControl>
|
||||
<FormControl fullWidth><InputLabel>Icon</InputLabel><Select value={typeCreateData.icon || 'HelpOutline'} label="Icon" onChange={(e) => setTypeCreateData({ ...typeCreateData, icon: e.target.value })}><MenuItem value="BugReport">BugReport</MenuItem><MenuItem value="FiberNew">FiberNew</MenuItem><MenuItem value="HelpOutline">HelpOutline</MenuItem></Select></FormControl>
|
||||
<Box><Typography variant="body2" sx={{ mb: 0.5 }}>Farbe</Typography><ColorSwatch colors={ICON_COLORS} value={typeCreateData.farbe || 'action'} onChange={(v) => setTypeCreateData({ ...typeCreateData, farbe: v })} /></Box>
|
||||
<FormControlLabel control={<Switch checked={typeCreateData.erlaubt_abgelehnt ?? true} onChange={(e) => setTypeCreateData({ ...typeCreateData, erlaubt_abgelehnt: e.target.checked })} />} label="Abgelehnt erlaubt" />
|
||||
<TextField label="Sortierung" type="number" value={typeCreateData.sort_order ?? 0} onChange={(e) => setTypeCreateData({ ...typeCreateData, sort_order: parseInt(e.target.value) || 0 })} />
|
||||
</DialogContent>
|
||||
<DialogActions><Button onClick={() => setTypeCreateOpen(false)}>Abbrechen</Button><Button variant="contained" disabled={!typeCreateData.name?.trim() || createTypeMut.isPending} onClick={() => createTypeMut.mutate(typeCreateData)}>Erstellen</Button></DialogActions>
|
||||
</Dialog>
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Main Page ──
|
||||
|
||||
export default function Issues() {
|
||||
|
||||
Reference in New Issue
Block a user