shared catalog in Bestellungen, catalog picker in line items, Ersatzbeschaffung flag, vendor detail flash fix
This commit is contained in:
@@ -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) ── */}
|
||||
|
||||
Reference in New Issue
Block a user