feat(ausruestungsanfrage): add personal item tracking, catalog enforcement, and detail pages
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react';
|
||||
import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Container,
|
||||
MenuItem,
|
||||
@@ -16,6 +17,7 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { personalEquipmentApi } from '../services/personalEquipment';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import { membersService } from '../services/members';
|
||||
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||
import ChatAwareFab from '../components/shared/ChatAwareFab';
|
||||
@@ -35,6 +37,7 @@ function PersoenlicheAusruestungPage() {
|
||||
|
||||
const canViewAll = hasPermission('persoenliche_ausruestung:view_all');
|
||||
const canCreate = hasPermission('persoenliche_ausruestung:create');
|
||||
const canApprove = hasPermission('ausruestungsanfrage:approve');
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [filterZustand, setFilterZustand] = useState<string>('');
|
||||
@@ -55,6 +58,13 @@ function PersoenlicheAusruestungPage() {
|
||||
enabled: canViewAll,
|
||||
});
|
||||
|
||||
const { data: unassignedPositions, isLoading: unassignedLoading } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage', 'nicht-zugewiesen'],
|
||||
queryFn: () => ausruestungsanfrageApi.getUnassignedPositions(),
|
||||
staleTime: 2 * 60 * 1000,
|
||||
enabled: canApprove && activeTab === 2,
|
||||
});
|
||||
|
||||
const memberOptions = useMemo(() => {
|
||||
return (membersList?.items ?? []).map((m) => ({
|
||||
id: m.id,
|
||||
@@ -92,6 +102,7 @@ function PersoenlicheAusruestungPage() {
|
||||
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} sx={{ mb: 3 }}>
|
||||
<Tab label="Zuweisungen" />
|
||||
<Tab label="Katalog" />
|
||||
{canApprove && <Tab label="Nicht Zugewiesen" />}
|
||||
</Tabs>
|
||||
|
||||
{activeTab === 0 && (
|
||||
@@ -193,7 +204,7 @@ function PersoenlicheAusruestungPage() {
|
||||
</tr>
|
||||
) : (
|
||||
filtered.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<tr key={item.id} onClick={() => navigate(`/persoenliche-ausruestung/${item.id}`)}>
|
||||
<td>
|
||||
<Typography variant="body2" fontWeight={500}>
|
||||
{item.bezeichnung}
|
||||
@@ -203,6 +214,19 @@ function PersoenlicheAusruestungPage() {
|
||||
{item.artikel_bezeichnung}
|
||||
</Typography>
|
||||
)}
|
||||
{item.eigenschaften && item.eigenschaften.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mt: 0.5 }}>
|
||||
{item.eigenschaften.map((e) => (
|
||||
<Chip
|
||||
key={e.id}
|
||||
label={`${e.name}: ${e.wert}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ height: 20, fontSize: '0.7rem' }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Typography variant="body2">{item.kategorie ?? '—'}</Typography>
|
||||
@@ -242,6 +266,97 @@ function PersoenlicheAusruestungPage() {
|
||||
)}
|
||||
|
||||
{activeTab === 1 && <KatalogTab />}
|
||||
|
||||
{activeTab === 2 && canApprove && (
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<Box
|
||||
component="table"
|
||||
sx={{
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
'& th, & td': {
|
||||
textAlign: 'left',
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
},
|
||||
'& th': {
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
textTransform: 'uppercase',
|
||||
color: 'text.secondary',
|
||||
letterSpacing: '0.06em',
|
||||
},
|
||||
'& tbody tr': {
|
||||
'&:hover': { bgcolor: 'action.hover' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bezeichnung</th>
|
||||
<th>Anfrage</th>
|
||||
<th>Für wen</th>
|
||||
<th>Im Katalog</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{unassignedLoading ? (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<Typography color="text.secondary" sx={{ py: 4, textAlign: 'center' }}>
|
||||
Lade Daten…
|
||||
</Typography>
|
||||
</td>
|
||||
</tr>
|
||||
) : !unassignedPositions || unassignedPositions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', py: 6 }}>
|
||||
<CheckroomIcon sx={{ fontSize: 48, color: 'text.disabled', mb: 1 }} />
|
||||
<Typography color="text.secondary">
|
||||
Alle Positionen sind zugewiesen
|
||||
</Typography>
|
||||
</Box>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
unassignedPositions.map((pos) => (
|
||||
<tr key={pos.id}>
|
||||
<td>
|
||||
<Typography variant="body2" fontWeight={500}>{pos.bezeichnung}</Typography>
|
||||
<Chip label={`${pos.menge}x`} size="small" variant="outlined" sx={{ ml: 1, height: 20 }} />
|
||||
</td>
|
||||
<td>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ cursor: 'pointer', color: 'primary.main', textDecoration: 'underline' }}
|
||||
onClick={() => navigate(`/ausruestungsanfrage/${pos.anfrage_id}`)}
|
||||
>
|
||||
{pos.anfrage_bezeichnung || (pos.bestell_jahr && pos.bestell_nummer ? `${pos.bestell_jahr}/${String(pos.bestell_nummer).padStart(3, '0')}` : `#${pos.anfrage_id}`)}
|
||||
</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Typography variant="body2" fontWeight={500}>{pos.fuer_wen || '—'}</Typography>
|
||||
</td>
|
||||
<td>
|
||||
<Chip label={pos.artikel_id ? 'Ja' : 'Nein'} color={pos.artikel_id ? 'success' : 'warning'} size="small" variant="outlined" />
|
||||
</td>
|
||||
<td>
|
||||
<Button size="small" variant="outlined" onClick={() => navigate(`/ausruestungsanfrage/${pos.anfrage_id}/zuweisen`)}>
|
||||
Zuweisen
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
{/* FAB */}
|
||||
|
||||
Reference in New Issue
Block a user