feat(ausruestung): catalog-driven item tracking, im_haus in overview, order quantity override, fix stale queries

This commit is contained in:
Matthias Hochmeister
2026-04-15 10:20:36 +02:00
parent 633a75cb0b
commit 279cc03b6b
10 changed files with 195 additions and 114 deletions

View File

@@ -324,7 +324,13 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name,
a.fuer_benutzer_name,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count,
EXISTS(
SELECT 1 FROM ausruestung_anfrage_bestellung ab
JOIN bestellungen b ON b.id = ab.bestellung_id
WHERE ab.anfrage_id = a.id
AND b.status IN ('lieferung_pruefen', 'abgeschlossen')
) AS im_haus
FROM ausruestung_anfragen a
LEFT JOIN users u ON u.id = a.anfrager_id
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
@@ -339,7 +345,13 @@ async function getMyRequests(userId: string) {
const result = await pool.query(
`SELECT a.*,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count,
EXISTS(
SELECT 1 FROM ausruestung_anfrage_bestellung ab
JOIN bestellungen b ON b.id = ab.bestellung_id
WHERE ab.anfrage_id = a.id
AND b.status IN ('lieferung_pruefen', 'abgeschlossen')
) AS im_haus
FROM ausruestung_anfragen a
WHERE a.anfrager_id = $1
ORDER BY a.erstellt_am DESC`,
@@ -1100,7 +1112,7 @@ async function getUnassignedPositions() {
FROM ausruestung_anfrage_positionen p
JOIN ausruestung_anfragen a ON a.id = p.anfrage_id
LEFT JOIN users u ON u.id = a.anfrager_id
WHERE p.geliefert = true AND p.zuweisung_typ IS NULL
WHERE p.geliefert = true AND (p.zuweisung_typ IS NULL OR p.zuweisung_typ = 'keine')
ORDER BY a.bestell_jahr DESC NULLS LAST, a.bestell_nummer DESC NULLS LAST, p.bezeichnung
`);
return result.rows;

View File

@@ -8,7 +8,7 @@ import App from './App';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
staleTime: 30 * 1000, // 30 seconds
gcTime: 10 * 60 * 1000, // keep cache 10 minutes
retry: 1,
refetchOnWindowFocus: false, // prevent refetch on every tab switch

View File

@@ -96,6 +96,7 @@ function MeineAnfragenTab() {
<TableCell>Anfrage ID</TableCell>
<TableCell>Bezeichnung</TableCell>
<TableCell>Status</TableCell>
<TableCell>Im Haus</TableCell>
<TableCell>Positionen</TableCell>
<TableCell>Geliefert</TableCell>
<TableCell>Erstellt am</TableCell>
@@ -107,6 +108,7 @@ function MeineAnfragenTab() {
<TableCell>{formatOrderId(r)}</TableCell>
<TableCell>{r.bezeichnung || '-'}</TableCell>
<TableCell><Chip label={AUSRUESTUNG_STATUS_LABELS[r.status]} color={AUSRUESTUNG_STATUS_COLORS[r.status]} size="small" /></TableCell>
<TableCell>{r.im_haus ? <Chip label="Im Haus" color="success" size="small" /> : null}</TableCell>
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
<TableCell>
{r.positionen_count != null && r.positionen_count > 0
@@ -204,6 +206,7 @@ function AlleAnfragenTab() {
<TableCell>Bezeichnung</TableCell>
<TableCell>Anfrage für</TableCell>
<TableCell>Status</TableCell>
<TableCell>Im Haus</TableCell>
<TableCell>Positionen</TableCell>
<TableCell>Geliefert</TableCell>
<TableCell>Erstellt am</TableCell>
@@ -216,6 +219,7 @@ function AlleAnfragenTab() {
<TableCell>{r.bezeichnung || '-'}</TableCell>
<TableCell>{r.fuer_benutzer_name || r.anfrager_name || r.anfrager_id}</TableCell>
<TableCell><Chip label={AUSRUESTUNG_STATUS_LABELS[r.status]} color={AUSRUESTUNG_STATUS_COLORS[r.status]} size="small" /></TableCell>
<TableCell>{r.im_haus ? <Chip label="Im Haus" color="success" size="small" /> : null}</TableCell>
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
<TableCell>
{r.positionen_count != null && r.positionen_count > 0

View File

@@ -84,6 +84,7 @@ export default function AusruestungsanfrageZuBestellung() {
// ── State ──
const [assignments, setAssignments] = useState<Record<number, VendorAssignment | null>>({});
const [orderNames, setOrderNames] = useState<Record<number, string>>({});
const [quantities, setQuantities] = useState<Record<number, number>>({});
// New vendor dialog
const [newVendorDialog, setNewVendorDialog] = useState(false);
@@ -153,6 +154,8 @@ export default function AusruestungsanfrageZuBestellung() {
showSuccess(`${result.created_bestellungen.length} Bestellung(en) erfolgreich erstellt`);
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', requestId] });
navigate('/bestellungen');
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
},
onError: () => showError('Bestellungen konnten nicht erstellt werden'),
});
@@ -191,7 +194,7 @@ export default function AusruestungsanfrageZuBestellung() {
positionen: g.positionen.map(p => ({
position_id: p.id,
bezeichnung: p.bezeichnung,
menge: p.menge,
menge: quantities[p.id] ?? p.menge,
einheit: p.einheit,
notizen: p.notizen,
artikel_id: p.artikel_id,
@@ -281,6 +284,7 @@ export default function AusruestungsanfrageZuBestellung() {
<TableRow>
<TableCell>Artikel</TableCell>
<TableCell align="right" sx={{ width: 80 }}>Menge</TableCell>
<TableCell align="right" sx={{ width: 140 }}>Bestellmenge</TableCell>
<TableCell sx={{ width: 320 }}>Lieferant</TableCell>
</TableRow>
</TableHead>
@@ -301,6 +305,20 @@ export default function AusruestungsanfrageZuBestellung() {
<TableCell align="right">
<Typography variant="body2">{pos.menge} {pos.einheit ?? 'Stk'}</Typography>
</TableCell>
<TableCell align="right">
<TextField
type="number"
size="small"
sx={{ width: 80 }}
value={quantities[pos.id] ?? pos.menge}
onChange={e => {
const val = Math.min(pos.menge, Math.max(1, Number(e.target.value)));
setQuantities(prev => ({ ...prev, [pos.id]: val }));
}}
inputProps={{ min: 1, max: pos.menge }}
helperText={pos.menge > 1 ? `max ${pos.menge}` : undefined}
/>
</TableCell>
<TableCell>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Autocomplete<Lieferant>

View File

@@ -12,6 +12,7 @@ import { PageHeader } from '../components/templates';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
import { personalEquipmentApi } from '../services/personalEquipment';
import { vehiclesApi } from '../services/vehicles';
import { membersService } from '../services/members';
import type { AusruestungAnfragePosition } from '../types/ausruestungsanfrage.types';
@@ -24,8 +25,6 @@ interface PositionAssignment {
standort?: string;
userId?: string;
benutzerName?: string;
groesse?: string;
kategorie?: string;
}
function getUnassignedPositions(positions: AusruestungAnfragePosition[]): AusruestungAnfragePosition[] {
@@ -92,6 +91,12 @@ export default function AusruestungsanfrageZuweisung() {
name: v.bezeichnung ?? v.kurzname,
}));
const { data: allPersonalItems = [] } = useQuery({
queryKey: ['persoenliche-ausruestung', 'all-for-count'],
queryFn: () => personalEquipmentApi.getAll(),
staleTime: 2 * 60 * 1000,
});
const [submitting, setSubmitting] = useState(false);
const updateAssignment = (posId: number, patch: Partial<PositionAssignment>) => {
@@ -137,12 +142,12 @@ export default function AusruestungsanfrageZuweisung() {
standort: a.typ === 'ausruestung' ? a.standort : undefined,
userId: a.typ === 'persoenlich' ? a.userId : undefined,
benutzerName: a.typ === 'persoenlich' ? (a.benutzerName || anfrage.fuer_benutzer_name || anfrage.anfrager_name) : undefined,
groesse: a.typ === 'persoenlich' ? a.groesse : undefined,
kategorie: a.typ === 'persoenlich' ? a.kategorie : undefined,
}));
await ausruestungsanfrageApi.assignItems(anfrageId, payload);
showSuccess('Gegenstände zugewiesen');
navigate(`/ausruestungsanfrage/${id}`);
queryClient.invalidateQueries({ queryKey: ['persoenliche-ausruestung'] });
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
} catch {
showError('Fehler beim Zuweisen');
} finally {
@@ -233,11 +238,10 @@ export default function AusruestungsanfrageZuweisung() {
size="small"
onChange={(_e, val) => val && updateAssignment(pos.id, { typ: val })}
sx={{ mb: 1.5 }}
disabled={!pos.artikel_id}
>
<ToggleButton value="ausruestung">Ausrüstung</ToggleButton>
<ToggleButton value="persoenlich">Persönlich</ToggleButton>
<ToggleButton value="keine">Nicht erfassen</ToggleButton>
<ToggleButton value="ausruestung" disabled={!pos.artikel_id}>Ausrüstung</ToggleButton>
<ToggleButton value="persoenlich" disabled={!pos.artikel_id}>Persönlich</ToggleButton>
<ToggleButton value="keine">Nicht verfolgt</ToggleButton>
</ToggleButtonGroup>
{a.typ === 'ausruestung' && (
@@ -262,7 +266,7 @@ export default function AusruestungsanfrageZuweisung() {
)}
{a.typ === 'persoenlich' && (
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
<Autocomplete
size="small"
options={memberOptions}
@@ -278,20 +282,19 @@ export default function AusruestungsanfrageZuweisung() {
)}
sx={{ minWidth: 200, flex: 1 }}
/>
<TextField
size="small"
label="Größe"
value={a.groesse ?? ''}
onChange={(e) => updateAssignment(pos.id, { groesse: e.target.value })}
sx={{ minWidth: 100 }}
/>
<TextField
size="small"
label="Kategorie"
value={a.kategorie ?? ''}
onChange={(e) => updateAssignment(pos.id, { kategorie: e.target.value })}
sx={{ minWidth: 140 }}
/>
{(() => {
if (!a.userId || !pos.artikel_id) return null;
const count = allPersonalItems.filter(i => i.user_id === a.userId && i.artikel_id === pos.artikel_id).length;
if (count === 0) return null;
return <Typography variant="caption" color="text.secondary">Hat bereits {count} Stk.</Typography>;
})()}
{pos.eigenschaften && pos.eigenschaften.length > 0 && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}>
{pos.eigenschaften.map(e => (
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
))}
</Box>
)}
</Box>
)}
</Box>

View File

@@ -214,6 +214,7 @@ export default function BestellungDetail() {
mutationFn: ({ status, force }: { status: string; force?: boolean }) => bestellungApi.updateStatus(orderId, status, force),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Status aktualisiert');
setStatusConfirmTarget(null);
setStatusForce(false);
@@ -225,6 +226,7 @@ export default function BestellungDetail() {
mutationFn: (data: BestellpositionFormData) => bestellungApi.addLineItem(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setNewItem({ ...emptyItem });
setSelectedKatalogItem(null);
setKatalogEigenschaften([]);
@@ -238,6 +240,7 @@ export default function BestellungDetail() {
mutationFn: (itemId: number) => bestellungApi.deleteLineItem(itemId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setDeleteItemTarget(null);
showSuccess('Position gelöscht');
},
@@ -249,6 +252,7 @@ export default function BestellungDetail() {
bestellungApi.updateReceivedQty(itemId, menge),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
@@ -257,6 +261,7 @@ export default function BestellungDetail() {
mutationFn: (file: File) => bestellungApi.uploadFile(orderId, file),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Datei hochgeladen');
},
onError: () => showError('Fehler beim Hochladen der Datei'),
@@ -266,6 +271,7 @@ export default function BestellungDetail() {
mutationFn: (fileId: number) => bestellungApi.deleteFile(fileId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setDeleteFileTarget(null);
showSuccess('Datei gelöscht');
},
@@ -276,6 +282,7 @@ export default function BestellungDetail() {
mutationFn: (data: ErinnerungFormData) => bestellungApi.addReminder(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setReminderForm({ faellig_am: '', nachricht: '' });
setReminderFormOpen(false);
showSuccess('Erinnerung erstellt');
@@ -287,6 +294,7 @@ export default function BestellungDetail() {
mutationFn: (reminderId: number) => bestellungApi.markReminderDone(reminderId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
@@ -295,6 +303,7 @@ export default function BestellungDetail() {
mutationFn: (reminderId: number) => bestellungApi.deleteReminder(reminderId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setDeleteReminderTarget(null);
showSuccess('Erinnerung gelöscht');
},
@@ -351,6 +360,7 @@ export default function BestellungDetail() {
}
}
await queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Änderungen gespeichert');
setEditMode(false);
setEditItemsData({});

View File

@@ -102,7 +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" />}
{canApprove && <Tab label="Nicht verfolgt" />}
</Tabs>
{activeTab === 0 && (

View File

@@ -16,6 +16,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
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 { useNotification } from '../contexts/NotificationContext';
@@ -64,10 +65,17 @@ export default function PersoenlicheAusruestungEdit() {
}));
}, [membersList]);
const { data: artikelEigenschaften = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'eigenschaften', item?.artikel_id],
queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(item!.artikel_id!),
enabled: !!item?.artikel_id,
staleTime: 5 * 60 * 1000,
});
const [catalogEigenschaftValues, setCatalogEigenschaftValues] = useState<Record<number, string>>({});
// Form state
const [bezeichnung, setBezeichnung] = useState('');
const [kategorie, setKategorie] = useState('');
const [groesse, setGroesse] = useState('');
const [seriennummer, setSeriennummer] = useState('');
const [inventarnummer, setInventarnummer] = useState('');
const [anschaffungDatum, setAnschaffungDatum] = useState('');
@@ -80,8 +88,6 @@ export default function PersoenlicheAusruestungEdit() {
useEffect(() => {
if (!item) return;
setBezeichnung(item.bezeichnung);
setKategorie(item.kategorie ?? '');
setGroesse(item.groesse ?? '');
setSeriennummer(item.seriennummer ?? '');
setInventarnummer(item.inventarnummer ?? '');
setAnschaffungDatum(item.anschaffung_datum ? item.anschaffung_datum.split('T')[0] : '');
@@ -95,6 +101,13 @@ export default function PersoenlicheAusruestungEdit() {
wert: e.wert,
})));
}
if (item.artikel_id && item.eigenschaften) {
const vals: Record<number, string> = {};
item.eigenschaften.forEach(e => {
if (e.eigenschaft_id != null) vals[e.eigenschaft_id] = e.wert;
});
setCatalogEigenschaftValues(vals);
}
}, [item]);
// Set userId when item + memberOptions are ready
@@ -117,21 +130,27 @@ export default function PersoenlicheAusruestungEdit() {
});
const handleSave = () => {
if (!bezeichnung.trim()) return;
if (!bezeichnung.trim() || !item) return;
const payload: UpdatePersoenlicheAusruestungPayload = {
bezeichnung: bezeichnung.trim(),
kategorie: kategorie || null,
kategorie: null,
user_id: userId?.id || null,
groesse: groesse || null,
groesse: null,
seriennummer: seriennummer || null,
inventarnummer: inventarnummer || null,
anschaffung_datum: anschaffungDatum || null,
zustand,
notizen: notizen || null,
eigenschaften: eigenschaften
.filter(e => e.name.trim() && e.wert.trim())
.map(e => ({ eigenschaft_id: e.eigenschaft_id, name: e.name, wert: e.wert })),
eigenschaften: item.artikel_id
? Object.entries(catalogEigenschaftValues)
.filter(([, v]) => v.trim())
.map(([id, wert]) => ({
eigenschaft_id: Number(id),
name: artikelEigenschaften.find(e => e.id === Number(id))?.name ?? '',
wert,
}))
: eigenschaften.filter(e => e.name.trim() && e.wert.trim()).map(e => ({ eigenschaft_id: e.eigenschaft_id, name: e.name, wert: e.wert })),
};
updateMutation.mutate(payload);
};
@@ -198,20 +217,6 @@ export default function PersoenlicheAusruestungEdit() {
/>
)}
<TextField
label="Kategorie"
size="small"
value={kategorie}
onChange={(e) => setKategorie(e.target.value)}
/>
<TextField
label="Größe"
size="small"
value={groesse}
onChange={(e) => setGroesse(e.target.value)}
/>
<TextField
label="Seriennummer"
size="small"
@@ -257,6 +262,28 @@ export default function PersoenlicheAusruestungEdit() {
/>
{/* Eigenschaften */}
{item.artikel_id ? (
<>
<Typography variant="subtitle2">Eigenschaften</Typography>
{artikelEigenschaften.map(e =>
e.typ === 'options' && e.optionen?.length ? (
<TextField key={e.id} select size="small" label={e.name} required={e.pflicht}
value={catalogEigenschaftValues[e.id] ?? ''}
onChange={ev => setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
>
<MenuItem value=""></MenuItem>
{e.optionen.map(opt => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)}
</TextField>
) : (
<TextField key={e.id} size="small" label={e.name} required={e.pflicht}
value={catalogEigenschaftValues[e.id] ?? ''}
onChange={ev => setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
/>
)
)}
</>
) : (
<>
<Typography variant="subtitle2">Eigenschaften</Typography>
{eigenschaften.map((e, idx) => (
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
@@ -282,6 +309,8 @@ export default function PersoenlicheAusruestungEdit() {
<Button size="small" startIcon={<AddIcon />} onClick={addEigenschaft}>
Eigenschaft hinzufügen
</Button>
</>
)}
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end', mt: 1 }}>
<Button onClick={() => navigate(`/persoenliche-ausruestung/${id}`)}>

View File

@@ -34,13 +34,12 @@ export default function PersoenlicheAusruestungNeu() {
const canViewAll = hasPermission('persoenliche_ausruestung:view_all');
const [formBezeichnung, setFormBezeichnung] = useState<string | AusruestungArtikel | null>(null);
const [formKategorie, setFormKategorie] = useState('');
const [formArtikel, setFormArtikel] = useState<AusruestungArtikel | null>(null);
const [formUserId, setFormUserId] = useState<{ id: string; name: string } | null>(null);
const [formBenutzerName, setFormBenutzerName] = useState('');
const [formGroesse, setFormGroesse] = useState('');
const [formZustand, setFormZustand] = useState<PersoenlicheAusruestungZustand>('gut');
const [formNotizen, setFormNotizen] = useState('');
const [eigenschaftValues, setEigenschaftValues] = useState<Record<number, string>>({});
const { data: catalogItems } = useQuery({
queryKey: ['ausruestungsanfrage-items-catalog'],
@@ -48,6 +47,13 @@ export default function PersoenlicheAusruestungNeu() {
staleTime: 10 * 60 * 1000,
});
const { data: artikelEigenschaften = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'eigenschaften', formArtikel?.id],
queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(formArtikel!.id),
enabled: !!formArtikel?.id,
staleTime: 5 * 60 * 1000,
});
const { data: membersList } = useQuery({
queryKey: ['members-list-compact'],
queryFn: () => membersService.getMembers({ pageSize: 500 }),
@@ -75,20 +81,23 @@ export default function PersoenlicheAusruestungNeu() {
});
const handleCreate = () => {
const bezeichnung = typeof formBezeichnung === 'string'
? formBezeichnung
: formBezeichnung?.bezeichnung ?? '';
const bezeichnung = formArtikel?.bezeichnung ?? '';
if (!bezeichnung.trim()) return;
const payload: CreatePersoenlicheAusruestungPayload = {
bezeichnung: bezeichnung.trim(),
kategorie: formKategorie || undefined,
artikel_id: typeof formBezeichnung === 'object' && formBezeichnung ? formBezeichnung.id : undefined,
artikel_id: formArtikel?.id,
user_id: formUserId?.id || undefined,
benutzer_name: formBenutzerName || undefined,
groesse: formGroesse || undefined,
zustand: formZustand,
notizen: formNotizen || undefined,
eigenschaften: Object.entries(eigenschaftValues)
.filter(([, v]) => v.trim())
.map(([id, wert]) => ({
eigenschaft_id: Number(id),
name: artikelEigenschaften.find(e => e.id === Number(id))?.name ?? '',
wert,
})),
};
createMutation.mutate(payload);
};
@@ -103,20 +112,12 @@ export default function PersoenlicheAusruestungNeu() {
<Stack spacing={2}>
<Autocomplete
freeSolo
options={catalogItems ?? []}
getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung}
value={formBezeichnung}
getOptionLabel={(o) => o.bezeichnung}
value={formArtikel}
onChange={(_e, v) => {
setFormBezeichnung(v);
if (v && typeof v !== 'string' && v.kategorie) {
setFormKategorie(v.kategorie);
}
}}
onInputChange={(_e, v) => {
if (typeof formBezeichnung === 'string' || formBezeichnung === null) {
setFormBezeichnung(v);
}
setFormArtikel(v);
setEigenschaftValues({});
}}
renderInput={(params) => (
<TextField {...params} label="Bezeichnung" required size="small" />
@@ -124,6 +125,23 @@ export default function PersoenlicheAusruestungNeu() {
size="small"
/>
{artikelEigenschaften.map(e =>
e.typ === 'options' && e.optionen?.length ? (
<TextField key={e.id} select size="small" label={e.name} required={e.pflicht}
value={eigenschaftValues[e.id] ?? ''}
onChange={ev => setEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
>
<MenuItem value=""></MenuItem>
{e.optionen.map(opt => <MenuItem key={opt} value={opt}>{opt}</MenuItem>)}
</TextField>
) : (
<TextField key={e.id} size="small" label={e.name} required={e.pflicht}
value={eigenschaftValues[e.id] ?? ''}
onChange={ev => setEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
/>
)
)}
{canViewAll && (
<Autocomplete
options={memberOptions}
@@ -146,20 +164,6 @@ export default function PersoenlicheAusruestungNeu() {
/>
)}
<TextField
label="Kategorie"
size="small"
value={formKategorie}
onChange={(e) => setFormKategorie(e.target.value)}
/>
<TextField
label="Größe"
size="small"
value={formGroesse}
onChange={(e) => setFormGroesse(e.target.value)}
/>
<TextField
label="Zustand"
select
@@ -188,7 +192,7 @@ export default function PersoenlicheAusruestungNeu() {
<Button
variant="contained"
onClick={handleCreate}
disabled={createMutation.isPending || !(typeof formBezeichnung === 'string' ? formBezeichnung.trim() : formBezeichnung?.bezeichnung?.trim())}
disabled={createMutation.isPending || !formArtikel}
>
Erstellen
</Button>

View File

@@ -96,6 +96,7 @@ export interface AusruestungAnfrage {
positionen_count?: number;
geliefert_count?: number;
items_count?: number;
im_haus?: boolean;
}
export interface AusruestungAnfragePosition {