fix(issues): allow priority+status change for assignees, dynamic owner transitions, kanban droppable columns; feat(persoenliche-ausruestung): configurable zustand
This commit is contained in:
@@ -18,6 +18,8 @@ import {
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Tabs,
|
||||
Tab,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Delete,
|
||||
@@ -31,7 +33,7 @@ import {
|
||||
Checkroom as CheckroomIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
@@ -39,6 +41,19 @@ import { settingsApi } from '../services/settings';
|
||||
import { personalEquipmentApi } from '../services/personalEquipment';
|
||||
import type { ZustandOption } from '../types/personalEquipment.types';
|
||||
|
||||
interface TabPanelProps {
|
||||
children: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel({ children, value, index }: TabPanelProps) {
|
||||
if (value !== index) return null;
|
||||
return <Box sx={{ pt: 3 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
const SETTINGS_TAB_COUNT = 2;
|
||||
|
||||
interface LinkCollection {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -65,6 +80,18 @@ const ADMIN_INTERVAL_OPTIONS = [
|
||||
];
|
||||
|
||||
function AdminSettings() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [tab, setTab] = useState(() => {
|
||||
const t = Number(searchParams.get('tab'));
|
||||
return t >= 0 && t < SETTINGS_TAB_COUNT ? t : 0;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const t = Number(searchParams.get('tab'));
|
||||
if (t >= 0 && t < SETTINGS_TAB_COUNT) setTab(t);
|
||||
}, [searchParams]);
|
||||
|
||||
const { hasPermission } = usePermissionContext();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -317,10 +344,23 @@ function AdminSettings() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
<Typography variant="h4" gutterBottom sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" gutterBottom sx={{ mb: 2 }}>
|
||||
Admin-Einstellungen
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 1 }}>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onChange={(_e, v) => { setTab(v); navigate(`/admin/settings?tab=${v}`, { replace: true }); }}
|
||||
variant="scrollable"
|
||||
scrollButtons="auto"
|
||||
>
|
||||
<Tab label="Allgemein" />
|
||||
<Tab label="Werkzeuge" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
<TabPanel value={tab} index={0}>
|
||||
<Stack spacing={3}>
|
||||
{/* Section 1: General Settings (App Logo) */}
|
||||
<Card>
|
||||
@@ -596,98 +636,7 @@ function AdminSettings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 5: Zustandsoptionen (Persönliche Ausrüstung) */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CheckroomIcon color="primary" sx={{ mr: 2 }} />
|
||||
<Typography variant="h6">Zustandsoptionen — Persönliche Ausrüstung</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Konfigurierbare Zustandswerte für die persönliche Ausrüstung. Schlüssel wird intern gespeichert, Label wird angezeigt.
|
||||
</Typography>
|
||||
<Stack spacing={2}>
|
||||
{zustandOptions.map((opt, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Schlüssel"
|
||||
value={opt.key}
|
||||
onChange={(e) =>
|
||||
setZustandOptions((prev) =>
|
||||
prev.map((o, i) => (i === idx ? { ...o, key: e.target.value } : o))
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Label"
|
||||
value={opt.label}
|
||||
onChange={(e) =>
|
||||
setZustandOptions((prev) =>
|
||||
prev.map((o, i) => (i === idx ? { ...o, label: e.target.value } : o))
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<FormControl size="small" sx={{ minWidth: 130 }}>
|
||||
<InputLabel>Farbe</InputLabel>
|
||||
<Select
|
||||
value={opt.color}
|
||||
label="Farbe"
|
||||
onChange={(e) =>
|
||||
setZustandOptions((prev) =>
|
||||
prev.map((o, i) => (i === idx ? { ...o, color: e.target.value } : o))
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuItem value="success">Grün</MenuItem>
|
||||
<MenuItem value="warning">Gelb</MenuItem>
|
||||
<MenuItem value="error">Rot</MenuItem>
|
||||
<MenuItem value="default">Grau</MenuItem>
|
||||
<MenuItem value="info">Blau</MenuItem>
|
||||
<MenuItem value="primary">Primär</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() =>
|
||||
setZustandOptions((prev) => prev.filter((_, i) => i !== idx))
|
||||
}
|
||||
aria-label="Option entfernen"
|
||||
size="small"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
onClick={() =>
|
||||
setZustandOptions((prev) => [...prev, { key: '', label: '', color: 'default' }])
|
||||
}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Option hinzufügen
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => zustandMutation.mutate(zustandOptions)}
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={zustandMutation.isPending}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Section 6: Info */}
|
||||
{/* Section 5: Info */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
@@ -703,6 +652,102 @@ function AdminSettings() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tab} index={1}>
|
||||
<Stack spacing={3}>
|
||||
{/* Zustandsoptionen (Persönliche Ausrüstung) */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<CheckroomIcon color="primary" sx={{ mr: 2 }} />
|
||||
<Typography variant="h6">Zustandsoptionen — Persönliche Ausrüstung</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Konfigurierbare Zustandswerte für die persönliche Ausrüstung. Schlüssel wird intern gespeichert, Label wird angezeigt.
|
||||
</Typography>
|
||||
<Stack spacing={2}>
|
||||
{zustandOptions.map((opt, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Schlüssel"
|
||||
value={opt.key}
|
||||
onChange={(e) =>
|
||||
setZustandOptions((prev) =>
|
||||
prev.map((o, i) => (i === idx ? { ...o, key: e.target.value } : o))
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Label"
|
||||
value={opt.label}
|
||||
onChange={(e) =>
|
||||
setZustandOptions((prev) =>
|
||||
prev.map((o, i) => (i === idx ? { ...o, label: e.target.value } : o))
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
sx={{ flex: 1 }}
|
||||
/>
|
||||
<FormControl size="small" sx={{ minWidth: 130 }}>
|
||||
<InputLabel>Farbe</InputLabel>
|
||||
<Select
|
||||
value={opt.color}
|
||||
label="Farbe"
|
||||
onChange={(e) =>
|
||||
setZustandOptions((prev) =>
|
||||
prev.map((o, i) => (i === idx ? { ...o, color: e.target.value } : o))
|
||||
)
|
||||
}
|
||||
>
|
||||
<MenuItem value="success">Grün</MenuItem>
|
||||
<MenuItem value="warning">Gelb</MenuItem>
|
||||
<MenuItem value="error">Rot</MenuItem>
|
||||
<MenuItem value="default">Grau</MenuItem>
|
||||
<MenuItem value="info">Blau</MenuItem>
|
||||
<MenuItem value="primary">Primär</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<IconButton
|
||||
color="error"
|
||||
onClick={() =>
|
||||
setZustandOptions((prev) => prev.filter((_, i) => i !== idx))
|
||||
}
|
||||
aria-label="Option entfernen"
|
||||
size="small"
|
||||
>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button
|
||||
startIcon={<Add />}
|
||||
onClick={() =>
|
||||
setZustandOptions((prev) => [...prev, { key: '', label: '', color: 'default' }])
|
||||
}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
>
|
||||
Option hinzufügen
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => zustandMutation.mutate(zustandOptions)}
|
||||
variant="contained"
|
||||
size="small"
|
||||
disabled={zustandMutation.isPending}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</TabPanel>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
|
||||
@@ -423,7 +423,7 @@ export default function IssueDetail() {
|
||||
) : null}
|
||||
|
||||
{/* Priority control */}
|
||||
{hasEdit && (
|
||||
{(hasEdit || canChangeStatus) && (
|
||||
<FormControl size="small" sx={{ minWidth: 140 }}>
|
||||
<InputLabel>Priorität</InputLabel>
|
||||
<Select
|
||||
|
||||
@@ -232,7 +232,9 @@ function PersoenlicheAusruestungPage() {
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Typography variant="body2">{item.artikel_kategorie_name ?? item.kategorie ?? '—'}</Typography>
|
||||
<Typography variant="body2">{item.artikel_kategorie_parent_name
|
||||
? `${item.artikel_kategorie_parent_name} > ${item.artikel_kategorie_name}`
|
||||
: item.artikel_kategorie_name ?? item.kategorie ?? '—'}</Typography>
|
||||
</td>
|
||||
{canSeeAll && (
|
||||
<td>
|
||||
|
||||
@@ -69,7 +69,9 @@ export default function PersoenlicheAusruestungDetail() {
|
||||
<PageHeader
|
||||
title={item.bezeichnung}
|
||||
backTo="/persoenliche-ausruestung"
|
||||
subtitle={item.artikel_kategorie_name || item.kategorie || undefined}
|
||||
subtitle={item.artikel_kategorie_parent_name
|
||||
? `${item.artikel_kategorie_parent_name} > ${item.artikel_kategorie_name}`
|
||||
: item.artikel_kategorie_name || item.kategorie || undefined}
|
||||
/>
|
||||
|
||||
{/* Status + actions row */}
|
||||
|
||||
Reference in New Issue
Block a user