fix(multi): FDISK sync, order UX, Ausbildungen display, untracked items
FDISK sync: - fix(sync): strip 'KFZ-Führerschein / ' prefix from license class select option text before whitelist validation - fix(sync): fix navigateAndGetTableRows to pick date column with most matches (prevents sidebar tables from hijacking dateColIdx for Beförderungen) - fix(sync): input.value fallback now falls through to textContent when value is empty - feat(sync): expand Ausbildungen to capture Kursnummer, Kurz, Kurs (full name), Erfolgscode from FDISK table; add migration 086 External orders (Bestellungen): - fix(bestellungen): allow erhalten_menge editing in lieferung_pruefen status (resolves deadlock preventing order completion) - fix(bestellungen): show cost/received warnings for bestellt/teillieferung/lieferung_pruefen, not just when abgeschlossen is next - feat(bestellungen): rename status labels to Eingereicht, Genehmigt, Teilweise geliefert, Vollständig geliefert - fix(bestellungen): remove duplicate Bestelldatum from PDF export - feat(bestellungen): add catalog item autocomplete to creation form (auto-fills bezeichnung + artikelnummer) Internal orders (Ausruestungsanfrage): - fix(ausruestung): untracked items with zuweisung_typ='keine' now appear in Nicht-zugewiesen tab (frontend filter was too strict) - feat(ausruestung): load user-specific personal items when ordering for another user - feat(ausruestung): auto-set ist_ersatz=true for items from personal equipment list; add toggle for catalog/free-text items - feat(ausruestung): load item eigenschaften when personal item with artikel_id is checked Ausbildungen display: - feat(mitglieder): show kursname (full), kurs_kurzbezeichnung chip, erfolgscode chip (color-coded) per Ausbildung entry Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,9 @@ import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import { PageHeader, FormLayout } from '../components/templates';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
import { bestellungApi } from '../services/bestellung';
|
||||
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
|
||||
import type { BestellungFormData, BestellpositionFormData, LieferantFormData, Lieferant } from '../types/bestellung.types';
|
||||
import type { AusruestungArtikel } from '../types/ausruestungsanfrage.types';
|
||||
|
||||
const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', notizen: '', positionen: [] };
|
||||
const emptyVendorForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' };
|
||||
@@ -45,6 +47,12 @@ export default function BestellungNeu() {
|
||||
queryFn: bestellungApi.getOrderUsers,
|
||||
});
|
||||
|
||||
const { data: katalogItems = [] } = useQuery({
|
||||
queryKey: ['katalog'],
|
||||
queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
// ── Mutations ──
|
||||
const createOrder = useMutation({
|
||||
mutationFn: (data: BestellungFormData) => bestellungApi.createOrder(data),
|
||||
@@ -154,17 +162,40 @@ export default function BestellungNeu() {
|
||||
<Typography variant="subtitle2" sx={{ mt: 1 }}>Positionen</Typography>
|
||||
{(orderForm.positionen || []).map((pos, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Bezeichnung"
|
||||
<Autocomplete<AusruestungArtikel, false, false, true>
|
||||
freeSolo
|
||||
size="small"
|
||||
required
|
||||
InputLabelProps={{ shrink: true }}
|
||||
value={pos.bezeichnung}
|
||||
onChange={(e) => {
|
||||
options={katalogItems}
|
||||
getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung}
|
||||
value={pos.bezeichnung || ''}
|
||||
onChange={(_, v) => {
|
||||
const next = [...(orderForm.positionen || [])];
|
||||
next[idx] = { ...next[idx], bezeichnung: e.target.value };
|
||||
if (v && typeof v !== 'string') {
|
||||
next[idx] = { ...next[idx], bezeichnung: v.bezeichnung, artikel_id: v.id };
|
||||
} else if (typeof v === 'string') {
|
||||
next[idx] = { ...next[idx], bezeichnung: v, artikel_id: undefined };
|
||||
} else {
|
||||
next[idx] = { ...next[idx], bezeichnung: '', artikel_id: undefined };
|
||||
}
|
||||
setOrderForm((f) => ({ ...f, positionen: next }));
|
||||
}}
|
||||
onInputChange={(_, val, reason) => {
|
||||
if (reason === 'input') {
|
||||
const next = [...(orderForm.positionen || [])];
|
||||
next[idx] = { ...next[idx], bezeichnung: val, artikel_id: undefined };
|
||||
setOrderForm((f) => ({ ...f, positionen: next }));
|
||||
}
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} label="Bezeichnung" size="small" required InputLabelProps={{ shrink: true }} />}
|
||||
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={{ flexGrow: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
|
||||
Reference in New Issue
Block a user