feat(ausruestungsanfrage): add personal item tracking, catalog enforcement, and detail pages

This commit is contained in:
Matthias Hochmeister
2026-04-14 16:49:20 +02:00
parent e6b6639fe9
commit 633a75cb0b
15 changed files with 1031 additions and 26 deletions

View File

@@ -0,0 +1,166 @@
import { useState } from 'react';
import {
Box, Typography, Container, Chip, Button, Paper, Divider,
Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions,
LinearProgress,
} from '@mui/material';
import { Edit as EditIcon, Delete as DeleteIcon } from '@mui/icons-material';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { PageHeader } from '../components/templates';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { personalEquipmentApi } from '../services/personalEquipment';
import { ZUSTAND_LABELS, ZUSTAND_COLORS } from '../types/personalEquipment.types';
import type { PersoenlicheAusruestungZustand } from '../types/personalEquipment.types';
export default function PersoenlicheAusruestungDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { hasPermission } = usePermissionContext();
const { showSuccess, showError } = useNotification();
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const { data: item, isLoading, isError } = useQuery({
queryKey: ['persoenliche-ausruestung', 'detail', id],
queryFn: () => personalEquipmentApi.getById(id!),
enabled: !!id,
});
const canEdit = hasPermission('persoenliche_ausruestung:edit');
const canDelete = hasPermission('persoenliche_ausruestung:delete');
const handleDelete = async () => {
if (!id) return;
setDeleting(true);
try {
await personalEquipmentApi.delete(id);
queryClient.invalidateQueries({ queryKey: ['persoenliche-ausruestung'] });
showSuccess('Persönliche Ausrüstung gelöscht');
navigate('/persoenliche-ausruestung');
} catch {
showError('Fehler beim Löschen');
} finally {
setDeleting(false);
setDeleteOpen(false);
}
};
return (
<DashboardLayout>
<Container maxWidth="md">
{isLoading ? (
<LinearProgress />
) : isError || !item ? (
<Typography color="error">Fehler beim Laden.</Typography>
) : (
<>
<PageHeader
title={item.bezeichnung}
backTo="/persoenliche-ausruestung"
subtitle={item.kategorie || undefined}
/>
{/* Status + actions row */}
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', mb: 3 }}>
<Chip
label={ZUSTAND_LABELS[item.zustand as PersoenlicheAusruestungZustand]}
color={ZUSTAND_COLORS[item.zustand as PersoenlicheAusruestungZustand]}
variant="outlined"
/>
<Box sx={{ flex: 1 }} />
{canEdit && (
<Button startIcon={<EditIcon />} variant="outlined" size="small"
onClick={() => navigate(`/persoenliche-ausruestung/${id}/edit`)}>
Bearbeiten
</Button>
)}
{canDelete && (
<Button startIcon={<DeleteIcon />} variant="outlined" color="error" size="small"
onClick={() => setDeleteOpen(true)}>
Löschen
</Button>
)}
</Box>
{/* Info card */}
<Paper sx={{ p: 2.5, mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>Details</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 1.5 }}>
{([
['Benutzer', item.user_display_name || item.benutzer_name],
['Größe', item.groesse],
['Seriennummer', item.seriennummer],
['Inventarnummer', item.inventarnummer],
['Anschaffungsdatum', item.anschaffung_datum ? new Date(item.anschaffung_datum).toLocaleDateString('de-AT') : null],
['Erstellt am', new Date(item.erstellt_am).toLocaleDateString('de-AT')],
] as [string, string | null | undefined][]).map(([label, value]) => value ? (
<Box key={label}>
<Typography variant="caption" color="text.secondary">{label}</Typography>
<Typography variant="body2">{value}</Typography>
</Box>
) : null)}
</Box>
{item.anfrage_id && (
<>
<Divider sx={{ my: 1.5 }} />
<Typography variant="caption" color="text.secondary">Aus Anfrage</Typography>
<Typography
variant="body2"
sx={{ cursor: 'pointer', color: 'primary.main' }}
onClick={() => navigate(`/ausruestungsanfrage/${item.anfrage_id}`)}
>
Anfrage #{item.anfrage_id}
</Typography>
</>
)}
</Paper>
{/* Eigenschaften */}
{item.eigenschaften && item.eigenschaften.length > 0 && (
<Paper sx={{ p: 2.5, mb: 2 }}>
<Typography variant="subtitle2" gutterBottom>Eigenschaften</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{item.eigenschaften.map((e) => (
<Box key={e.id}>
<Typography variant="caption" color="text.secondary">{e.name}</Typography>
<Typography variant="body2" fontWeight={500}>{e.wert}</Typography>
</Box>
))}
</Box>
</Paper>
)}
{/* Notizen */}
{item.notizen && (
<Paper sx={{ p: 2.5 }}>
<Typography variant="subtitle2" gutterBottom>Notizen</Typography>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>{item.notizen}</Typography>
</Paper>
)}
</>
)}
</Container>
{/* Delete confirmation */}
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)}>
<DialogTitle>Persönliche Ausrüstung löschen?</DialogTitle>
<DialogContent>
<DialogContentText>
Dieser Eintrag wird dauerhaft gelöscht und kann nicht wiederhergestellt werden.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteOpen(false)}>Abbrechen</Button>
<Button onClick={handleDelete} color="error" disabled={deleting}>
Löschen
</Button>
</DialogActions>
</Dialog>
</DashboardLayout>
);
}