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

@@ -51,10 +51,12 @@ import GermanDateField from '../components/shared/GermanDateField';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { bestellungApi } from '../services/bestellung';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
import { configApi } from '../services/config';
import { addPdfHeader, addPdfFooter } from '../utils/pdfExport';
import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types';
import type { BestellungStatus, BestellpositionFormData, ErinnerungFormData } from '../types/bestellung.types';
import type { AusruestungArtikel, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types';
// ── Helpers ──
@@ -159,6 +161,11 @@ export default function BestellungDetail() {
const [reminderFormOpen, setReminderFormOpen] = useState(false);
const [deleteReminderTarget, setDeleteReminderTarget] = useState<number | null>(null);
// ── Catalog picker state ──
const [selectedKatalogItem, setSelectedKatalogItem] = useState<AusruestungArtikel | null>(null);
const [katalogEigenschaften, setKatalogEigenschaften] = useState<AusruestungEigenschaft[]>([]);
const [eigenschaftValues, setEigenschaftValues] = useState<Record<number, string>>({});
// ── Query ──
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['bestellung', orderId],
@@ -184,6 +191,13 @@ export default function BestellungDetail() {
enabled: editMode,
});
const { data: katalogItems = [] } = useQuery({
queryKey: ['katalogItems'],
queryFn: () => bestellungApi.getKatalogItems(),
enabled: editMode,
staleTime: 5 * 60 * 1000,
});
const canCreate = hasPermission('bestellungen:create');
const canDelete = hasPermission('bestellungen:delete');
const canManageReminders = hasPermission('bestellungen:manage_reminders');
@@ -214,6 +228,9 @@ export default function BestellungDetail() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
setNewItem({ ...emptyItem });
setSelectedKatalogItem(null);
setKatalogEigenschaften([]);
setEigenschaftValues({});
showSuccess('Position hinzugefügt');
},
onError: () => showError('Fehler beim Hinzufügen der Position'),
@@ -348,7 +365,21 @@ export default function BestellungDetail() {
function handleAddItem() {
if (!newItem.bezeichnung.trim()) return;
addItem.mutate(newItem);
// Merge characteristic values into spezifikationen
const charSpecs = Object.entries(eigenschaftValues)
.filter(([, v]) => v.trim())
.map(([eid, v]) => {
const eig = katalogEigenschaften.find(e => e.id === Number(eid));
return eig ? `${eig.name}: ${v}` : v;
});
const mergedSpecs = [...(newItem.spezifikationen || []), ...charSpecs];
addItem.mutate({
...newItem,
spezifikationen: mergedSpecs.length > 0 ? mergedSpecs : undefined,
});
setSelectedKatalogItem(null);
setKatalogEigenschaften([]);
setEigenschaftValues({});
}
function handleFileSelect(e: React.ChangeEvent<HTMLInputElement>) {
@@ -968,9 +999,52 @@ export default function BestellungDetail() {
{/* ── Add Item Row ── */}
{editMode && canCreate && (
<>
<TableRow>
<TableCell>
<TextField size="small" placeholder="Bezeichnung" value={newItem.bezeichnung} onChange={(e) => setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} />
<Autocomplete<AusruestungArtikel, false, false, true>
freeSolo
size="small"
options={katalogItems}
getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung}
value={selectedKatalogItem || newItem.bezeichnung || ''}
onChange={async (_, v) => {
if (typeof v === 'string') {
setNewItem((f) => ({ ...f, bezeichnung: v, artikel_id: undefined }));
setSelectedKatalogItem(null);
setKatalogEigenschaften([]);
setEigenschaftValues({});
} else if (v) {
setNewItem((f) => ({ ...f, bezeichnung: v.bezeichnung, artikel_id: v.id }));
setSelectedKatalogItem(v);
// Load eigenschaften
try {
const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(v.id);
setKatalogEigenschaften(eigs || []);
} catch { setKatalogEigenschaften([]); }
setEigenschaftValues({});
}
}}
onInputChange={(_, val, reason) => {
if (reason === 'input') {
setNewItem((f) => ({ ...f, bezeichnung: val, artikel_id: undefined }));
setSelectedKatalogItem(null);
setKatalogEigenschaften([]);
setEigenschaftValues({});
}
}}
renderInput={(params) => <TextField {...params} size="small" placeholder="Bezeichnung" />}
renderOption={(props, option) => (
<li {...props} key={option.id}>
<Box>
<Typography variant="body2">{option.bezeichnung}</Typography>
{option.kategorie_name && <Typography variant="caption" color="text.secondary">{option.kategorie_name}</Typography>}
</Box>
</li>
)}
isOptionEqualToValue={(o, v) => o.id === (typeof v === 'object' ? v.id : undefined)}
sx={{ minWidth: 200 }}
/>
</TableCell>
<TableCell>
<TextField size="small" placeholder="Artikelnr." value={newItem.artikelnummer || ''} onChange={(e) => setNewItem((f) => ({ ...f, artikelnummer: e.target.value }))} />
@@ -992,6 +1066,52 @@ export default function BestellungDetail() {
</IconButton>
</TableCell>
</TableRow>
{/* Characteristic fields when catalog item selected */}
{katalogEigenschaften.length > 0 && (
<TableRow>
<TableCell colSpan={8} sx={{ pt: 0, pb: 1, borderBottom: 'none' }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, ml: 1, pl: 1.5, borderLeft: '2px solid', borderColor: 'divider' }}>
{katalogEigenschaften.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 }))}
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={eigenschaftValues[e.id] || ''}
onChange={(ev) => setEigenschaftValues((prev) => ({ ...prev, [e.id]: ev.target.value }))}
sx={{ minWidth: 160 }}
/>
)
)}
{selectedKatalogItem?.bevorzugter_lieferant_name && !bestellung?.lieferant_id && (
<Chip
size="small"
label={`Bevorzugter Lieferant: ${selectedKatalogItem.bevorzugter_lieferant_name}`}
color="info"
variant="outlined"
sx={{ alignSelf: 'center' }}
/>
)}
</Box>
</TableCell>
</TableRow>
)}
</>
)}
{/* ── Totals Rows (Netto / MwSt / Brutto) ── */}