shared catalog in Bestellungen, catalog picker in line items, Ersatzbeschaffung flag, vendor detail flash fix

This commit is contained in:
Matthias Hochmeister
2026-03-27 14:50:31 +01:00
parent c704e2c173
commit 29d66e37a1
16 changed files with 506 additions and 32 deletions

View File

@@ -23,8 +23,10 @@ import {
FormGroup,
LinearProgress,
Divider,
TextField,
MenuItem,
} from '@mui/material';
import { Add as AddIcon, ExpandMore as ExpandMoreIcon, FilterList as FilterListIcon, PictureAsPdf as PdfIcon } from '@mui/icons-material';
import { Add as AddIcon, ExpandMore as ExpandMoreIcon, FilterList as FilterListIcon, PictureAsPdf as PdfIcon, Search as SearchIcon } from '@mui/icons-material';
import { useQuery } from '@tanstack/react-query';
import { useNavigate, useSearchParams } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
@@ -52,7 +54,7 @@ function TabPanel({ children, value, index }: TabPanelProps) {
return <Box sx={{ pt: 3 }}>{children}</Box>;
}
const TAB_COUNT = 2;
const TAB_COUNT = 3;
// ── Status options ──
@@ -85,6 +87,7 @@ export default function Bestellungen() {
const { hasPermission } = usePermissionContext();
const canManageVendors = hasPermission('bestellungen:manage_vendors');
const canExport = hasPermission('bestellungen:export');
const canManageCatalog = hasPermission('ausruestungsanfrage:manage_catalog');
// Tab from URL
const [tab, setTab] = useState(() => {
@@ -113,6 +116,26 @@ export default function Bestellungen() {
queryFn: bestellungApi.getVendors,
});
// ── Katalog state ──
const [katalogSearch, setKatalogSearch] = useState('');
const [katalogKategorie, setKatalogKategorie] = useState('');
const { data: katalogItems = [], isLoading: katalogLoading } = useQuery({
queryKey: ['katalogItems', katalogSearch, katalogKategorie],
queryFn: () => bestellungApi.getKatalogItems({
search: katalogSearch || undefined,
kategorie: katalogKategorie || undefined,
}),
enabled: tab === 2,
});
const { data: katalogKategorien = [] } = useQuery({
queryKey: ['katalogKategorien'],
queryFn: bestellungApi.getKatalogKategorien,
enabled: tab === 2,
staleTime: 5 * 60 * 1000,
});
// ── Derive unique filter values from data ──
const uniqueVendors = useMemo(() => {
const map = new Map<string, string>();
@@ -251,6 +274,7 @@ export default function Bestellungen() {
<Tabs value={tab} onChange={(_e, v) => { setTab(v); navigate(`/bestellungen?tab=${v}`, { replace: true }); }} variant="scrollable" scrollButtons="auto">
<Tab label="Bestellungen" />
{canManageVendors && <Tab label="Lieferanten" />}
<Tab label="Katalog" />
</Tabs>
</Box>
@@ -458,6 +482,82 @@ export default function Bestellungen() {
</ChatAwareFab>
</TabPanel>
)}
{/* ── Tab 2: Katalog ── */}
<TabPanel value={tab} index={canManageVendors ? 2 : 1}>
<Box sx={{ display: 'flex', gap: 2, mb: 3 }}>
<TextField
size="small"
placeholder="Suche..."
value={katalogSearch}
onChange={(e) => setKatalogSearch(e.target.value)}
InputProps={{ startAdornment: <SearchIcon fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} /> }}
sx={{ flex: 1, maxWidth: 400 }}
/>
<TextField
select
size="small"
label="Kategorie"
value={katalogKategorie}
onChange={(e) => setKatalogKategorie(e.target.value)}
sx={{ minWidth: 180 }}
>
<MenuItem value="">Alle Kategorien</MenuItem>
{katalogKategorien.map((k) => (
<MenuItem key={k} value={k}>{k}</MenuItem>
))}
</TextField>
</Box>
<TableContainer component={Paper}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Bezeichnung</TableCell>
<TableCell>Kategorie</TableCell>
<TableCell align="right">Geschätzter Preis</TableCell>
<TableCell>Bevorzugter Lieferant</TableCell>
<TableCell align="right">Eigenschaften</TableCell>
</TableRow>
</TableHead>
<TableBody>
{katalogLoading ? (
<TableRow><TableCell colSpan={5} align="center">Laden...</TableCell></TableRow>
) : katalogItems.length === 0 ? (
<TableRow><TableCell colSpan={5} align="center">Keine Artikel gefunden</TableCell></TableRow>
) : (
katalogItems.map((item) => (
<TableRow
key={item.id}
hover
sx={{ cursor: 'pointer' }}
onClick={() => navigate(`/ausruestungsanfrage/artikel/${item.id}`)}
>
<TableCell>
<Typography variant="body2" fontWeight={500}>{item.bezeichnung}</Typography>
{item.beschreibung && (
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', maxWidth: 300, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.beschreibung}
</Typography>
)}
</TableCell>
<TableCell>{item.kategorie_name || item.kategorie || ''}</TableCell>
<TableCell align="right">{item.geschaetzter_preis != null ? formatCurrency(item.geschaetzter_preis) : ''}</TableCell>
<TableCell>{item.bevorzugter_lieferant_name || ''}</TableCell>
<TableCell align="right">{item.eigenschaften_count ?? 0}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</TableContainer>
{canManageCatalog && (
<ChatAwareFab onClick={() => navigate('/ausruestungsanfrage/artikel/neu')} aria-label="Neuer Katalogartikel">
<AddIcon />
</ChatAwareFab>
)}
</TabPanel>
</DashboardLayout>
);
}