catalog search/sort, edit-page characteristics, preferred vendor per article

This commit is contained in:
Matthias Hochmeister
2026-03-27 13:17:05 +01:00
parent eb82fe29b7
commit 35b3718e38
8 changed files with 277 additions and 29 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useCallback } from 'react';
import {
Box, Typography, Paper, Button, Chip, IconButton,
Table, TableBody, TableCell, TableHead, TableRow,
@@ -21,6 +21,7 @@ import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/a
import type {
AusruestungAnfrage, AusruestungAnfrageDetailResponse,
AusruestungAnfrageFormItem, AusruestungAnfrageStatus,
AusruestungEigenschaft,
} from '../types/ausruestungsanfrage.types';
// ── Helpers ──
@@ -57,6 +58,10 @@ export default function AusruestungsanfrageDetail() {
const [adminNotizen, setAdminNotizen] = useState('');
const [statusChangeValue, setStatusChangeValue] = useState('');
// Eigenschaften state for edit mode
const [editItemEigenschaften, setEditItemEigenschaften] = useState<Record<number, AusruestungEigenschaft[]>>({});
const [editItemEigenschaftValues, setEditItemEigenschaftValues] = useState<Record<number, Record<number, string>>>({});
// Permissions
const showAdminActions = hasPermission('ausruestungsanfrage:approve');
const canEditAny = hasPermission('ausruestungsanfrage:edit');
@@ -73,9 +78,17 @@ export default function AusruestungsanfrageDetail() {
const { data: catalogItems = [] } = useQuery({
queryKey: ['ausruestungsanfrage', 'items-for-edit'],
queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }),
enabled: editing,
staleTime: 5 * 60 * 1000,
});
const loadEigenschaftenForItem = useCallback(async (artikelId: number) => {
if (editItemEigenschaften[artikelId]) return;
try {
const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(artikelId);
if (eigs?.length > 0) setEditItemEigenschaften(prev => ({ ...prev, [artikelId]: eigs }));
} catch { /* ignore */ }
}, [editItemEigenschaften]);
// ── Mutations ──
const updateMut = useMutation({
mutationFn: (data: { bezeichnung?: string; notizen?: string; items?: AusruestungAnfrageFormItem[] }) =>
@@ -122,15 +135,31 @@ export default function AusruestungsanfrageDetail() {
notizen: p.notizen,
eigenschaften: p.eigenschaften?.map(e => ({ eigenschaft_id: e.eigenschaft_id, wert: e.wert })),
})));
const initVals: Record<number, Record<number, string>> = {};
detail.positionen.forEach((p, idx) => {
if (p.artikel_id) loadEigenschaftenForItem(p.artikel_id);
if (p.eigenschaften?.length) {
initVals[idx] = {};
p.eigenschaften.forEach(e => { initVals[idx][e.eigenschaft_id] = e.wert; });
}
});
setEditItemEigenschaftValues(initVals);
setEditing(true);
};
const handleSaveEdit = () => {
if (editItems.length === 0) return;
const items = editItems.map((item, idx) => {
const vals = editItemEigenschaftValues[idx] || {};
const eigenschaften = Object.entries(vals)
.filter(([, wert]) => wert.trim())
.map(([eid, wert]) => ({ eigenschaft_id: Number(eid), wert }));
return { ...item, eigenschaften: eigenschaften.length > 0 ? eigenschaften : undefined };
});
updateMut.mutate({
bezeichnung: editBezeichnung || undefined,
notizen: editNotizen || undefined,
items: editItems,
items,
});
};
@@ -197,7 +226,8 @@ export default function AusruestungsanfrageDetail() {
/>
<Typography variant="subtitle2" sx={{ mt: 1 }}>Positionen</Typography>
{editItems.map((item, idx) => (
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Box key={idx} sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<Autocomplete
freeSolo
options={catalogItems}
@@ -209,6 +239,8 @@ export default function AusruestungsanfrageDetail() {
updateEditItem(idx, 'artikel_id', undefined);
} else if (v) {
setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it));
loadEigenschaftenForItem(v.id);
setEditItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; });
}
}}
onInputChange={(_, val, reason) => {
@@ -231,6 +263,33 @@ export default function AusruestungsanfrageDetail() {
<IconButton size="small" onClick={() => removeEditItem(idx)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Box>
{item.artikel_id && editItemEigenschaften[item.artikel_id]?.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, ml: 1, pl: 1.5, borderLeft: '2px solid', borderColor: 'divider' }}>
{editItemEigenschaften[item.artikel_id].map(e => (
e.typ === 'options' && e.optionen?.length ? (
<TextField key={e.id} select size="small" label={e.name} required={e.pflicht}
value={editItemEigenschaftValues[idx]?.[e.id] || ''}
onChange={ev => setEditItemEigenschaftValues(prev => ({
...prev, [idx]: { ...(prev[idx] || {}), [e.id]: ev.target.value }
}))}
sx={{ minWidth: 140 }}
>
<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={editItemEigenschaftValues[idx]?.[e.id] || ''}
onChange={ev => setEditItemEigenschaftValues(prev => ({
...prev, [idx]: { ...(prev[idx] || {}), [e.id]: ev.target.value }
}))}
sx={{ minWidth: 160 }}
/>
)
))}
</Box>
)}
</Box>
))}
<Button size="small" startIcon={<AddIcon />} onClick={addEditItem}>