rework internal order system

This commit is contained in:
Matthias Hochmeister
2026-03-24 09:15:57 +01:00
parent 6ff5cc89ad
commit 39b8b30ca2
2 changed files with 166 additions and 88 deletions

View File

@@ -1,11 +1,26 @@
import pool from '../config/database'; import pool from '../config/database';
import logger from '../utils/logger'; import logger from '../utils/logger';
// Helper: check if a table exists (cached per process lifetime)
const existingTables = new Set<string>();
async function tableExists(tableName: string): Promise<boolean> {
if (existingTables.has(tableName)) return true;
try {
const r = await pool.query(
"SELECT EXISTS(SELECT 1 FROM information_schema.tables WHERE table_name = $1) AS ok",
[tableName],
);
if (r.rows[0]?.ok) { existingTables.add(tableName); return true; }
} catch { /* ignore */ }
return false;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Categories (ausruestung_kategorien_katalog) — hierarchical with parent_id // Categories (ausruestung_kategorien_katalog) — hierarchical with parent_id
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function getKategorien() { async function getKategorien() {
if (!(await tableExists('ausruestung_kategorien_katalog'))) return [];
const result = await pool.query( const result = await pool.query(
'SELECT * FROM ausruestung_kategorien_katalog ORDER BY parent_id NULLS FIRST, name', 'SELECT * FROM ausruestung_kategorien_katalog ORDER BY parent_id NULLS FIRST, name',
); );
@@ -13,6 +28,7 @@ async function getKategorien() {
} }
async function createKategorie(name: string, parentId?: number | null) { async function createKategorie(name: string, parentId?: number | null) {
if (!(await tableExists('ausruestung_kategorien_katalog'))) throw new Error('Migration 048 has not been applied yet');
const result = await pool.query( const result = await pool.query(
'INSERT INTO ausruestung_kategorien_katalog (name, parent_id) VALUES ($1, $2) RETURNING *', 'INSERT INTO ausruestung_kategorien_katalog (name, parent_id) VALUES ($1, $2) RETURNING *',
[name, parentId ?? null], [name, parentId ?? null],
@@ -69,32 +85,37 @@ async function getItems(filters?: { kategorie?: string; kategorie_id?: number; a
} }
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const hasKategorien = await tableExists('ausruestung_kategorien_katalog');
const hasEigenschaften = await tableExists('ausruestung_artikel_eigenschaften');
const result = await pool.query( const result = await pool.query(
`SELECT a.*, k.name AS kategorie_name, `SELECT a.*${hasKategorien ? ', k.name AS kategorie_name' : ''}${hasEigenschaften ? ',\n (SELECT COUNT(*)::int FROM ausruestung_artikel_eigenschaften e WHERE e.artikel_id = a.id) AS eigenschaften_count' : ''}
(SELECT COUNT(*)::int FROM ausruestung_artikel_eigenschaften e WHERE e.artikel_id = a.id) AS eigenschaften_count
FROM ausruestung_artikel a FROM ausruestung_artikel a
LEFT JOIN ausruestung_kategorien_katalog k ON k.id = a.kategorie_id ${hasKategorien ? 'LEFT JOIN ausruestung_kategorien_katalog k ON k.id = a.kategorie_id' : ''}
${where} ${where}
ORDER BY COALESCE(k.name, a.kategorie), a.bezeichnung`, ORDER BY ${hasKategorien ? 'COALESCE(k.name, a.kategorie)' : 'a.kategorie'}, a.bezeichnung`,
params, params,
); );
return result.rows; return result.rows;
} }
async function getItemById(id: number) { async function getItemById(id: number) {
const hasKategorien = await tableExists('ausruestung_kategorien_katalog');
const result = await pool.query( const result = await pool.query(
`SELECT a.*, k.name AS kategorie_name `SELECT a.*${hasKategorien ? ', k.name AS kategorie_name' : ''}
FROM ausruestung_artikel a FROM ausruestung_artikel a
LEFT JOIN ausruestung_kategorien_katalog k ON k.id = a.kategorie_id ${hasKategorien ? 'LEFT JOIN ausruestung_kategorien_katalog k ON k.id = a.kategorie_id' : ''}
WHERE a.id = $1`, WHERE a.id = $1`,
[id], [id],
); );
if (!result.rows[0]) return null; if (!result.rows[0]) return null;
const eigenschaften = await pool.query( let eigenschaften: { rows: unknown[] } = { rows: [] };
'SELECT * FROM ausruestung_artikel_eigenschaften WHERE artikel_id = $1 ORDER BY sort_order, id', if (await tableExists('ausruestung_artikel_eigenschaften')) {
[id], eigenschaften = await pool.query(
); 'SELECT * FROM ausruestung_artikel_eigenschaften WHERE artikel_id = $1 ORDER BY sort_order, id',
[id],
);
}
return { return {
...result.rows[0], ...result.rows[0],
@@ -193,6 +214,7 @@ async function getCategories() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function getArtikelEigenschaften(artikelId: number) { async function getArtikelEigenschaften(artikelId: number) {
if (!(await tableExists('ausruestung_artikel_eigenschaften'))) return [];
const result = await pool.query( const result = await pool.query(
'SELECT * FROM ausruestung_artikel_eigenschaften WHERE artikel_id = $1 ORDER BY sort_order, id', 'SELECT * FROM ausruestung_artikel_eigenschaften WHERE artikel_id = $1 ORDER BY sort_order, id',
[artikelId], [artikelId],
@@ -204,6 +226,7 @@ async function upsertArtikelEigenschaft(
artikelId: number, artikelId: number,
data: { id?: number; name: string; typ: string; optionen?: string[]; pflicht?: boolean; sort_order?: number }, data: { id?: number; name: string; typ: string; optionen?: string[]; pflicht?: boolean; sort_order?: number },
) { ) {
if (!(await tableExists('ausruestung_artikel_eigenschaften'))) throw new Error('Migration 048 has not been applied yet');
if (data.id) { if (data.id) {
const result = await pool.query( const result = await pool.query(
`UPDATE ausruestung_artikel_eigenschaften `UPDATE ausruestung_artikel_eigenschaften

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useEffect, useCallback } from 'react'; import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { import {
Box, Tab, Tabs, Typography, Grid, Button, Chip, Box, Tab, Tabs, Typography, Grid, Button, Chip,
Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper,
@@ -53,19 +53,20 @@ function EigenschaftFields({ eigenschaften, values, onChange }: EigenschaftField
{eigenschaften.map(e => ( {eigenschaften.map(e => (
<Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}> <Box key={e.id} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
{e.typ === 'options' && e.optionen && e.optionen.length > 0 ? ( {e.typ === 'options' && e.optionen && e.optionen.length > 0 ? (
<FormControl size="small" sx={{ minWidth: 160 }} required={e.pflicht}> <TextField
<InputLabel>{e.name}</InputLabel> select
<Select size="small"
value={values[e.id] || ''} label={e.name}
label={e.name} value={values[e.id] || ''}
onChange={ev => onChange(e.id, ev.target.value)} onChange={ev => onChange(e.id, ev.target.value)}
> required={e.pflicht}
<MenuItem value=""></MenuItem> sx={{ minWidth: 160 }}
{e.optionen.map(opt => ( >
<MenuItem key={opt} value={opt}>{opt}</MenuItem> <MenuItem value=""></MenuItem>
))} {e.optionen.map(opt => (
</Select> <MenuItem key={opt} value={opt}>{opt}</MenuItem>
</FormControl> ))}
</TextField>
) : ( ) : (
<TextField <TextField
size="small" size="small"
@@ -268,13 +269,17 @@ function EigenschaftenEditor({ artikelId }: EigenschaftenEditorProps) {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1, p: 1, border: '1px dashed', borderColor: 'divider', borderRadius: 1 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 1, p: 1, border: '1px dashed', borderColor: 'divider', borderRadius: 1 }}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField size="small" label="Name" value={newName} onChange={e => setNewName(e.target.value)} sx={{ flexGrow: 1 }} /> <TextField size="small" label="Name" value={newName} onChange={e => setNewName(e.target.value)} sx={{ flexGrow: 1 }} />
<FormControl size="small" sx={{ minWidth: 120 }}> <TextField
<InputLabel>Typ</InputLabel> select
<Select value={newTyp} label="Typ" onChange={e => setNewTyp(e.target.value as 'options' | 'freitext')}> size="small"
<MenuItem value="options">Auswahl</MenuItem> label="Typ"
<MenuItem value="freitext">Freitext</MenuItem> value={newTyp}
</Select> onChange={e => setNewTyp(e.target.value as 'options' | 'freitext')}
</FormControl> sx={{ minWidth: 120 }}
>
<MenuItem value="options">Auswahl</MenuItem>
<MenuItem value="freitext">Freitext</MenuItem>
</TextField>
<FormControlLabel <FormControlLabel
control={<Checkbox size="small" checked={newPflicht} onChange={e => setNewPflicht(e.target.checked)} />} control={<Checkbox size="small" checked={newPflicht} onChange={e => setNewPflicht(e.target.checked)} />}
label="Pflicht" label="Pflicht"
@@ -766,13 +771,17 @@ function KatalogTab() {
<Box> <Box>
{/* Filter */} {/* Filter */}
<Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center', flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 2, mb: 3, alignItems: 'center', flexWrap: 'wrap' }}>
<FormControl size="small" sx={{ minWidth: 200 }}> <TextField
<InputLabel>Kategorie</InputLabel> select
<Select value={filterKategorie} label="Kategorie" onChange={e => setFilterKategorie(e.target.value as number | '')}> size="small"
<MenuItem value="">Alle</MenuItem> label="Kategorie"
{kategorieOptions.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)} value={filterKategorie}
</Select> onChange={e => setFilterKategorie(e.target.value as number | '')}
</FormControl> sx={{ minWidth: 200 }}
>
<MenuItem value="">Alle</MenuItem>
{kategorieOptions.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
</TextField>
{canManageCategories && ( {canManageCategories && (
<Tooltip title="Kategorien verwalten"> <Tooltip title="Kategorien verwalten">
<IconButton size="small" onClick={() => setKategorieDialogOpen(true)}> <IconButton size="small" onClick={() => setKategorieDialogOpen(true)}>
@@ -832,17 +841,16 @@ function KatalogTab() {
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}> <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '20px !important' }}>
<TextField label="Bezeichnung" required value={artikelForm.bezeichnung} onChange={e => setArtikelForm(f => ({ ...f, bezeichnung: e.target.value }))} fullWidth /> <TextField label="Bezeichnung" required value={artikelForm.bezeichnung} onChange={e => setArtikelForm(f => ({ ...f, bezeichnung: e.target.value }))} fullWidth />
<TextField label="Beschreibung" multiline rows={2} value={artikelForm.beschreibung ?? ''} onChange={e => setArtikelForm(f => ({ ...f, beschreibung: e.target.value }))} /> <TextField label="Beschreibung" multiline rows={2} value={artikelForm.beschreibung ?? ''} onChange={e => setArtikelForm(f => ({ ...f, beschreibung: e.target.value }))} />
<FormControl fullWidth> <TextField
<InputLabel>Kategorie</InputLabel> select
<Select label="Kategorie"
value={artikelForm.kategorie_id ?? ''} value={artikelForm.kategorie_id ?? ''}
label="Kategorie" onChange={e => setArtikelForm(f => ({ ...f, kategorie_id: e.target.value ? Number(e.target.value) : null }))}
onChange={e => setArtikelForm(f => ({ ...f, kategorie_id: e.target.value ? Number(e.target.value) : null }))} fullWidth
> >
<MenuItem value="">Keine</MenuItem> <MenuItem value="">Keine</MenuItem>
{kategorieOptions.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)} {kategorieOptions.map(k => <MenuItem key={k.id} value={k.id}>{k.name}</MenuItem>)}
</Select> </TextField>
</FormControl>
{canManage && <EigenschaftenEditor artikelId={editArtikel?.id ?? null} />} {canManage && <EigenschaftenEditor artikelId={editArtikel?.id ?? null} />}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -887,8 +895,12 @@ function MeineAnfragenTab() {
const [newItems, setNewItems] = useState<AusruestungAnfrageFormItem[]>([{ bezeichnung: '', menge: 1 }]); const [newItems, setNewItems] = useState<AusruestungAnfrageFormItem[]>([{ bezeichnung: '', menge: 1 }]);
// Track loaded eigenschaften per item row (by artikel_id) // Track loaded eigenschaften per item row (by artikel_id)
const [itemEigenschaften, setItemEigenschaften] = useState<Record<number, AusruestungEigenschaft[]>>({}); const [itemEigenschaften, setItemEigenschaften] = useState<Record<number, AusruestungEigenschaft[]>>({});
const itemEigenschaftenRef = useRef(itemEigenschaften);
itemEigenschaftenRef.current = itemEigenschaften;
// Track eigenschaft values per item row index // Track eigenschaft values per item row index
const [itemEigenschaftValues, setItemEigenschaftValues] = useState<Record<number, Record<number, string>>>({}); const [itemEigenschaftValues, setItemEigenschaftValues] = useState<Record<number, Record<number, string>>>({});
// Separate free-text items
const [newFreeItems, setNewFreeItems] = useState<{ bezeichnung: string; menge: number }[]>([]);
const { data: requests = [], isLoading } = useQuery({ const { data: requests = [], isLoading } = useQuery({
queryKey: ['ausruestungsanfrage', 'myRequests'], queryKey: ['ausruestungsanfrage', 'myRequests'],
@@ -924,33 +936,42 @@ function MeineAnfragenTab() {
setNewNotizen(''); setNewNotizen('');
setNewFuerBenutzer(null); setNewFuerBenutzer(null);
setNewItems([{ bezeichnung: '', menge: 1 }]); setNewItems([{ bezeichnung: '', menge: 1 }]);
setNewFreeItems([]);
setItemEigenschaften({}); setItemEigenschaften({});
setItemEigenschaftValues({}); setItemEigenschaftValues({});
}; };
const loadEigenschaften = useCallback(async (artikelId: number) => { const loadEigenschaften = useCallback(async (artikelId: number) => {
if (itemEigenschaften[artikelId]) return; if (itemEigenschaftenRef.current[artikelId]) return;
try { try {
const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(artikelId); const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(artikelId);
setItemEigenschaften(prev => ({ ...prev, [artikelId]: eigs })); setItemEigenschaften(prev => ({ ...prev, [artikelId]: eigs }));
} catch { /* ignore */ } } catch { /* ignore */ }
}, [itemEigenschaften]); }, []);
const handleCreateSubmit = () => { const handleCreateSubmit = () => {
const validItems = newItems.filter(i => i.bezeichnung.trim()).map((item, idx) => { // Catalog items with eigenschaften
const catalogValidItems = newItems.filter(i => i.bezeichnung.trim() && i.artikel_id).map((item, idx) => {
const vals = itemEigenschaftValues[idx] || {}; const vals = itemEigenschaftValues[idx] || {};
const eigenschaften = Object.entries(vals) const eigenschaften = Object.entries(vals)
.filter(([, wert]) => wert.trim()) .filter(([, wert]) => wert.trim())
.map(([eid, wert]) => ({ eigenschaft_id: Number(eid), wert })); .map(([eid, wert]) => ({ eigenschaft_id: Number(eid), wert }));
return { ...item, eigenschaften: eigenschaften.length > 0 ? eigenschaften : undefined }; return { ...item, eigenschaften: eigenschaften.length > 0 ? eigenschaften : undefined };
}); });
if (validItems.length === 0) return;
// Check required eigenschaften // Free-text items
const freeValidItems = newFreeItems
.filter(i => i.bezeichnung.trim())
.map(i => ({ bezeichnung: i.bezeichnung, menge: i.menge }));
const allItems = [...catalogValidItems, ...freeValidItems];
if (allItems.length === 0) return;
// Check required eigenschaften for catalog items
for (let idx = 0; idx < newItems.length; idx++) { for (let idx = 0; idx < newItems.length; idx++) {
const item = newItems[idx]; const item = newItems[idx];
if (!item.bezeichnung.trim()) continue; if (!item.bezeichnung.trim() || !item.artikel_id) continue;
if (item.artikel_id && itemEigenschaften[item.artikel_id]) { if (itemEigenschaften[item.artikel_id]) {
for (const e of itemEigenschaften[item.artikel_id]) { for (const e of itemEigenschaften[item.artikel_id]) {
if (e.pflicht && !(itemEigenschaftValues[idx]?.[e.id]?.trim())) { if (e.pflicht && !(itemEigenschaftValues[idx]?.[e.id]?.trim())) {
showError(`Pflichtfeld "${e.name}" für "${item.bezeichnung}" fehlt`); showError(`Pflichtfeld "${e.name}" für "${item.bezeichnung}" fehlt`);
@@ -961,7 +982,7 @@ function MeineAnfragenTab() {
} }
createMut.mutate({ createMut.mutate({
items: validItems, items: allItems,
notizen: newNotizen || undefined, notizen: newNotizen || undefined,
bezeichnung: newBezeichnung || undefined, bezeichnung: newBezeichnung || undefined,
fuer_benutzer_id: newFuerBenutzer?.id, fuer_benutzer_id: newFuerBenutzer?.id,
@@ -994,16 +1015,20 @@ function MeineAnfragenTab() {
return ( return (
<Box> <Box>
<Box sx={{ display: 'flex', gap: 2, mb: 2, alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: 2, mb: 2, alignItems: 'center' }}>
<FormControl size="small" sx={{ minWidth: 200 }}> <TextField
<InputLabel>Status</InputLabel> select
<Select value={currentFilterValue} label="Status" onChange={e => handleStatusFilterChange(e.target.value)}> size="small"
label="Status"
value={currentFilterValue}
onChange={e => handleStatusFilterChange(e.target.value)}
sx={{ minWidth: 200 }}
>
<MenuItem value="active">Aktive Anfragen</MenuItem> <MenuItem value="active">Aktive Anfragen</MenuItem>
<MenuItem value="all">Alle</MenuItem> <MenuItem value="all">Alle</MenuItem>
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => ( {(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => (
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem> <MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
))} ))}
</Select> </TextField>
</FormControl>
</Box> </Box>
{filteredRequests.length === 0 ? ( {filteredRequests.length === 0 ? (
@@ -1072,31 +1097,24 @@ function MeineAnfragenTab() {
fullWidth fullWidth
/> />
<Divider /> <Divider />
<Typography variant="subtitle2">Positionen</Typography> <Typography variant="subtitle2">Aus Katalog</Typography>
{newItems.map((item, idx) => ( {newItems.map((item, idx) => (
<Box key={idx}> <Box key={`cat-${idx}`}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}> <Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Autocomplete <Autocomplete
freeSolo
options={catalogItems} options={catalogItems}
getOptionLabel={o => typeof o === 'string' ? o : o.bezeichnung} getOptionLabel={o => typeof o === 'string' ? o : o.bezeichnung}
value={item.artikel_id ? catalogItems.find(c => c.id === item.artikel_id) || item.bezeichnung : item.bezeichnung} value={item.artikel_id ? catalogItems.find(c => c.id === item.artikel_id) || null : null}
onChange={(_, v) => { onChange={(_, v) => {
if (typeof v === 'string') { if (v && typeof v !== 'string') {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: v, artikel_id: undefined } : it));
// Clear eigenschaften for this row
setItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; });
} else if (v) {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it)); setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it));
loadEigenschaften(v.id); loadEigenschaften(v.id);
} else {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: undefined, bezeichnung: '' } : it));
setItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; });
} }
}} }}
onInputChange={(_, val, reason) => { renderInput={params => <TextField {...params} label="Katalogartikel" size="small" />}
if (reason === 'input') {
setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: val, artikel_id: undefined } : it));
}
}}
renderInput={params => <TextField {...params} label="Artikel" size="small" />}
sx={{ flexGrow: 1 }} sx={{ flexGrow: 1 }}
/> />
<TextField <TextField
@@ -1126,7 +1144,40 @@ function MeineAnfragenTab() {
</Box> </Box>
))} ))}
<Button size="small" startIcon={<AddIcon />} onClick={() => setNewItems(prev => [...prev, { bezeichnung: '', menge: 1 }])}> <Button size="small" startIcon={<AddIcon />} onClick={() => setNewItems(prev => [...prev, { bezeichnung: '', menge: 1 }])}>
Position hinzufügen Katalogartikel hinzufügen
</Button>
<Divider />
<Typography variant="subtitle2">Freitext-Positionen</Typography>
{newFreeItems.length === 0 ? (
<Typography variant="body2" color="text.secondary">Keine Freitext-Positionen.</Typography>
) : (
newFreeItems.map((item, idx) => (
<Box key={`free-${idx}`} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<TextField
size="small"
label="Bezeichnung"
value={item.bezeichnung}
onChange={e => setNewFreeItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: e.target.value } : it))}
sx={{ flexGrow: 1 }}
/>
<TextField
size="small"
type="number"
label="Menge"
value={item.menge}
onChange={e => setNewFreeItems(prev => prev.map((it, i) => i === idx ? { ...it, menge: Math.max(1, Number(e.target.value)) } : it))}
sx={{ width: 90 }}
inputProps={{ min: 1 }}
/>
<IconButton size="small" onClick={() => setNewFreeItems(prev => prev.filter((_, i) => i !== idx))}>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
))
)}
<Button size="small" startIcon={<AddIcon />} onClick={() => setNewFreeItems(prev => [...prev, { bezeichnung: '', menge: 1 }])}>
Freitext-Position hinzufügen
</Button> </Button>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
@@ -1134,7 +1185,7 @@ function MeineAnfragenTab() {
<Button <Button
variant="contained" variant="contained"
onClick={handleCreateSubmit} onClick={handleCreateSubmit}
disabled={createMut.isPending || newItems.every(i => !i.bezeichnung.trim())} disabled={createMut.isPending || (newItems.every(i => !i.artikel_id) && newFreeItems.every(i => !i.bezeichnung.trim()))}
> >
Anfrage erstellen Anfrage erstellen
</Button> </Button>
@@ -1203,15 +1254,19 @@ function AlleAnfragenTab() {
</Grid> </Grid>
</Grid> </Grid>
<FormControl size="small" sx={{ minWidth: 200, mb: 2 }}> <TextField
<InputLabel>Status Filter</InputLabel> select
<Select value={statusFilter} label="Status Filter" onChange={e => setStatusFilter(e.target.value)}> size="small"
<MenuItem value="">Alle</MenuItem> label="Status Filter"
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => ( value={statusFilter}
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem> onChange={e => setStatusFilter(e.target.value)}
))} sx={{ minWidth: 200, mb: 2 }}
</Select> >
</FormControl> <MenuItem value="">Alle</MenuItem>
{(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => (
<MenuItem key={s} value={s}>{AUSRUESTUNG_STATUS_LABELS[s]}</MenuItem>
))}
</TextField>
{requests.length === 0 ? ( {requests.length === 0 ? (
<Typography color="text.secondary">Keine Anfragen vorhanden.</Typography> <Typography color="text.secondary">Keine Anfragen vorhanden.</Typography>