fix(issues): allow priority+status change for assignees, dynamic owner transitions, kanban droppable columns; feat(persoenliche-ausruestung): configurable zustand
This commit is contained in:
@@ -116,18 +116,24 @@ class IssueController {
|
|||||||
updateData = { ...req.body };
|
updateData = { ...req.body };
|
||||||
// Explicit null for unassign is handled by 'zugewiesen_an' in data check in service
|
// Explicit null for unassign is handled by 'zugewiesen_an' in data check in service
|
||||||
} else if (canChangeStatus || isAssignee) {
|
} else if (canChangeStatus || isAssignee) {
|
||||||
// Can only change status (+ kommentar is handled separately)
|
// Can change status and priority (+ kommentar is handled separately)
|
||||||
updateData = {};
|
updateData = {};
|
||||||
if (req.body.status !== undefined) updateData.status = req.body.status;
|
if (req.body.status !== undefined) updateData.status = req.body.status;
|
||||||
|
if (req.body.prioritaet !== undefined) updateData.prioritaet = req.body.prioritaet;
|
||||||
} else if (isOwner) {
|
} else if (isOwner) {
|
||||||
// Owner without change_status: can only close own issue or reopen from erledigt
|
// Owner without change_status: can only close own issue or reopen from terminal status
|
||||||
updateData = {};
|
updateData = {};
|
||||||
if (req.body.status !== undefined) {
|
if (req.body.status !== undefined) {
|
||||||
const newStatus = req.body.status;
|
const newStatus = req.body.status;
|
||||||
if (newStatus === 'erledigt') {
|
const allStatuses = await issueService.getIssueStatuses();
|
||||||
updateData.status = 'erledigt';
|
const targetDef = allStatuses.find((s: any) => s.schluessel === newStatus);
|
||||||
} else if (newStatus === 'offen' && existing.status === 'erledigt') {
|
const currentDef = allStatuses.find((s: any) => s.schluessel === existing.status);
|
||||||
// Reopen: require kommentar
|
|
||||||
|
if (targetDef?.ist_abschluss) {
|
||||||
|
// Owner can close with any terminal status
|
||||||
|
updateData.status = newStatus;
|
||||||
|
} else if (targetDef?.ist_initial && currentDef?.ist_abschluss) {
|
||||||
|
// Owner can reopen from terminal → initial (requires kommentar)
|
||||||
if (!req.body.kommentar || typeof req.body.kommentar !== 'string' || req.body.kommentar.trim().length === 0) {
|
if (!req.body.kommentar || typeof req.body.kommentar !== 'string' || req.body.kommentar.trim().length === 0) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -135,7 +141,7 @@ class IssueController {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateData.status = 'offen';
|
updateData.status = newStatus;
|
||||||
} else {
|
} else {
|
||||||
res.status(403).json({ success: false, message: 'Keine Berechtigung für diese Statusänderung' });
|
res.status(403).json({ success: false, message: 'Keine Berechtigung für diese Statusänderung' });
|
||||||
return;
|
return;
|
||||||
@@ -192,13 +198,18 @@ class IssueController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle reopen comment (owner reopen flow)
|
// Handle reopen comment (owner reopen flow: terminal → initial)
|
||||||
if (isOwner && !canChangeStatus && req.body.status === 'offen' && existing.status === 'erledigt' && req.body.kommentar) {
|
if (isOwner && !canChangeStatus && updateData.status && req.body.kommentar) {
|
||||||
await issueService.addComment(id, userId, `[Wiedereröffnet] ${req.body.kommentar.trim()}`);
|
const allStatusesForComment = await issueService.getIssueStatuses();
|
||||||
}
|
const targetForComment = allStatusesForComment.find((s: any) => s.schluessel === updateData.status);
|
||||||
|
const currentForComment = allStatusesForComment.find((s: any) => s.schluessel === existing.status);
|
||||||
// If kommentar was provided alongside a status change (not the reopen flow above)
|
if (targetForComment?.ist_initial && currentForComment?.ist_abschluss) {
|
||||||
if (req.body.kommentar && updateData.status && !(isOwner && !canChangeStatus && req.body.status === 'offen' && existing.status === 'erledigt')) {
|
await issueService.addComment(id, userId, `[Wiedereröffnet] ${req.body.kommentar.trim()}`);
|
||||||
|
} else if (req.body.kommentar && updateData.status) {
|
||||||
|
await issueService.addComment(id, userId, req.body.kommentar.trim());
|
||||||
|
}
|
||||||
|
} else if (req.body.kommentar && updateData.status) {
|
||||||
|
// If kommentar was provided alongside a status change (non-owner flow)
|
||||||
await issueService.addComment(id, userId, req.body.kommentar.trim());
|
await issueService.addComment(id, userId, req.body.kommentar.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,11 +41,13 @@ const BASE_SELECT = `
|
|||||||
SELECT pa.*,
|
SELECT pa.*,
|
||||||
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS user_display_name,
|
COALESCE(u.given_name || ' ' || u.family_name, u.name) AS user_display_name,
|
||||||
aa.bezeichnung AS artikel_bezeichnung,
|
aa.bezeichnung AS artikel_bezeichnung,
|
||||||
akk.name AS artikel_kategorie_name
|
akk.name AS artikel_kategorie_name,
|
||||||
|
akk_parent.name AS artikel_kategorie_parent_name
|
||||||
FROM persoenliche_ausruestung pa
|
FROM persoenliche_ausruestung pa
|
||||||
LEFT JOIN users u ON u.id = pa.user_id
|
LEFT JOIN users u ON u.id = pa.user_id
|
||||||
LEFT JOIN ausruestung_artikel aa ON aa.id = pa.artikel_id
|
LEFT JOIN ausruestung_artikel aa ON aa.id = pa.artikel_id
|
||||||
LEFT JOIN ausruestung_kategorien_katalog akk ON akk.id = aa.kategorie_id
|
LEFT JOIN ausruestung_kategorien_katalog akk ON akk.id = aa.kategorie_id
|
||||||
|
LEFT JOIN ausruestung_kategorien_katalog akk_parent ON akk_parent.id = akk.parent_id
|
||||||
WHERE pa.geloescht_am IS NULL
|
WHERE pa.geloescht_am IS NULL
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ function PersoenlicheAusruestungWidget() {
|
|||||||
<ListItem key={item.id} disablePadding sx={{ py: 0.5 }}>
|
<ListItem key={item.id} disablePadding sx={{ py: 0.5 }}>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={item.bezeichnung}
|
primary={item.bezeichnung}
|
||||||
secondary={item.artikel_kategorie_name ?? item.kategorie ?? undefined}
|
secondary={item.artikel_kategorie_parent_name
|
||||||
|
? `${item.artikel_kategorie_parent_name} > ${item.artikel_kategorie_name}`
|
||||||
|
: item.artikel_kategorie_name ?? item.kategorie ?? undefined}
|
||||||
primaryTypographyProps={{ variant: 'body2', noWrap: true }}
|
primaryTypographyProps={{ variant: 'body2', noWrap: true }}
|
||||||
secondaryTypographyProps={{ variant: 'caption' }}
|
secondaryTypographyProps={{ variant: 'caption' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
closestCorners,
|
closestCorners,
|
||||||
|
useDroppable,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import type { DragStartEvent, DragEndEvent } from '@dnd-kit/core';
|
import type { DragStartEvent, DragEndEvent } from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
@@ -163,18 +164,21 @@ function KanbanColumn({
|
|||||||
onClick: (id: number) => void;
|
onClick: (id: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const chipColor = MUI_THEME_COLORS[statusDef.farbe] ?? statusDef.farbe ?? '#9e9e9e';
|
const chipColor = MUI_THEME_COLORS[statusDef.farbe] ?? statusDef.farbe ?? '#9e9e9e';
|
||||||
|
const { setNodeRef, isOver } = useDroppable({ id: statusDef.schluessel });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
ref={setNodeRef}
|
||||||
sx={{
|
sx={{
|
||||||
minWidth: 260,
|
minWidth: 260,
|
||||||
maxWidth: 320,
|
maxWidth: 320,
|
||||||
flex: '1 1 260px',
|
flex: '1 1 260px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
bgcolor: 'background.default',
|
bgcolor: isOver ? 'action.hover' : 'background.default',
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
transition: 'background-color 0.15s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Column header */}
|
{/* Column header */}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
Accordion,
|
Accordion,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
Delete,
|
Delete,
|
||||||
@@ -31,7 +33,7 @@ import {
|
|||||||
Checkroom as CheckroomIcon,
|
Checkroom as CheckroomIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
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 DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
@@ -39,6 +41,19 @@ import { settingsApi } from '../services/settings';
|
|||||||
import { personalEquipmentApi } from '../services/personalEquipment';
|
import { personalEquipmentApi } from '../services/personalEquipment';
|
||||||
import type { ZustandOption } from '../types/personalEquipment.types';
|
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 {
|
interface LinkCollection {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -65,6 +80,18 @@ const ADMIN_INTERVAL_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function AdminSettings() {
|
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 { hasPermission } = usePermissionContext();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -317,10 +344,23 @@ function AdminSettings() {
|
|||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<Container maxWidth="lg">
|
<Container maxWidth="lg">
|
||||||
<Typography variant="h4" gutterBottom sx={{ mb: 4 }}>
|
<Typography variant="h4" gutterBottom sx={{ mb: 2 }}>
|
||||||
Admin-Einstellungen
|
Admin-Einstellungen
|
||||||
</Typography>
|
</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}>
|
<Stack spacing={3}>
|
||||||
{/* Section 1: General Settings (App Logo) */}
|
{/* Section 1: General Settings (App Logo) */}
|
||||||
<Card>
|
<Card>
|
||||||
@@ -596,98 +636,7 @@ function AdminSettings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Section 5: Zustandsoptionen (Persönliche Ausrüstung) */}
|
{/* Section 5: Info */}
|
||||||
<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 */}
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
@@ -703,6 +652,102 @@ function AdminSettings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Stack>
|
</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>
|
</Container>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -423,7 +423,7 @@ export default function IssueDetail() {
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Priority control */}
|
{/* Priority control */}
|
||||||
{hasEdit && (
|
{(hasEdit || canChangeStatus) && (
|
||||||
<FormControl size="small" sx={{ minWidth: 140 }}>
|
<FormControl size="small" sx={{ minWidth: 140 }}>
|
||||||
<InputLabel>Priorität</InputLabel>
|
<InputLabel>Priorität</InputLabel>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -232,7 +232,9 @@ function PersoenlicheAusruestungPage() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<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>
|
</td>
|
||||||
{canSeeAll && (
|
{canSeeAll && (
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export default function PersoenlicheAusruestungDetail() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title={item.bezeichnung}
|
title={item.bezeichnung}
|
||||||
backTo="/persoenliche-ausruestung"
|
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 */}
|
{/* Status + actions row */}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface PersoenlicheAusruestung {
|
|||||||
artikel_id?: number;
|
artikel_id?: number;
|
||||||
artikel_bezeichnung?: string;
|
artikel_bezeichnung?: string;
|
||||||
artikel_kategorie_name?: string;
|
artikel_kategorie_name?: string;
|
||||||
|
artikel_kategorie_parent_name?: string;
|
||||||
user_id?: string;
|
user_id?: string;
|
||||||
user_display_name?: string;
|
user_display_name?: string;
|
||||||
benutzer_name?: string;
|
benutzer_name?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user