diff --git a/backend/src/controllers/ausruestungsanfrage.controller.ts b/backend/src/controllers/ausruestungsanfrage.controller.ts index 14e183f..425ad56 100644 --- a/backend/src/controllers/ausruestungsanfrage.controller.ts +++ b/backend/src/controllers/ausruestungsanfrage.controller.ts @@ -81,7 +81,8 @@ class AusruestungsanfrageController { const kategorie = req.query.kategorie as string | undefined; const kategorie_id = req.query.kategorie_id ? Number(req.query.kategorie_id) : undefined; const aktiv = req.query.aktiv !== undefined ? req.query.aktiv === 'true' : undefined; - const items = await ausruestungsanfrageService.getItems({ kategorie, kategorie_id, aktiv }); + const search = req.query.search as string | undefined; + const items = await ausruestungsanfrageService.getItems({ kategorie, kategorie_id, aktiv, search }); res.status(200).json({ success: true, data: items }); } catch (error) { logger.error('AusruestungsanfrageController.getItems error', { error }); diff --git a/frontend/src/pages/Ausruestungsanfrage.tsx b/frontend/src/pages/Ausruestungsanfrage.tsx index eeb82ee..b446a71 100644 --- a/frontend/src/pages/Ausruestungsanfrage.tsx +++ b/frontend/src/pages/Ausruestungsanfrage.tsx @@ -9,7 +9,7 @@ import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, Check as CheckIcon, Close as CloseIcon, Settings as SettingsIcon, } from '@mui/icons-material'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'; import { useSearchParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; @@ -170,12 +170,13 @@ function KatalogTab() { const [sortField, setSortField] = useState<'bezeichnung' | 'kategorie'>('bezeichnung'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc'); - const { data: items = [], isLoading } = useQuery({ + const { data: items = [], isLoading, isFetching } = useQuery({ queryKey: ['ausruestungsanfrage', 'items', filterKategorie, search], queryFn: () => ausruestungsanfrageApi.getItems({ ...(filterKategorie ? { kategorie_id: filterKategorie as number } : {}), ...(search.trim() ? { search: search.trim() } : {}), }), + placeholderData: keepPreviousData, }); const { data: kategorien = [] } = useQuery({ @@ -261,10 +262,13 @@ function KatalogTab() { {isLoading ? ( Lade Katalog... - ) : items.length === 0 ? ( + ) : items.length === 0 && !isFetching ? ( Keine Artikel vorhanden. ) : ( - + + Keine Artikel vorhanden. + ) : ( + diff --git a/frontend/src/pages/AusruestungsanfrageArtikelDetail.tsx b/frontend/src/pages/AusruestungsanfrageArtikelDetail.tsx index 4047b96..cf0019d 100644 --- a/frontend/src/pages/AusruestungsanfrageArtikelDetail.tsx +++ b/frontend/src/pages/AusruestungsanfrageArtikelDetail.tsx @@ -5,7 +5,7 @@ import { Autocomplete, } from '@mui/material'; import { - ArrowBack, Edit as EditIcon, Delete as DeleteIcon, + ArrowBack, Delete as DeleteIcon, Add as AddIcon, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; @@ -32,12 +32,8 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) { const [newOptionen, setNewOptionen] = useState(''); const [newPflicht, setNewPflicht] = useState(false); - // Inline edit state - const [editingId, setEditingId] = useState(null); - const [editName, setEditName] = useState(''); - const [editTyp, setEditTyp] = useState<'options' | 'freitext'>('options'); - const [editOptionen, setEditOptionen] = useState(''); - const [editPflicht, setEditPflicht] = useState(false); + // Per-eigenschaft edit state + const [rowState, setRowState] = useState>({}); const { data: eigenschaften = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId], @@ -45,6 +41,14 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) { enabled: artikelId != null, }); + const getRow = (e: AusruestungEigenschaft) => + rowState[e.id] ?? { name: e.name, typ: e.typ, optionen: e.optionen?.join(', ') || '', pflicht: e.pflicht }; + + const updateRow = (e: AusruestungEigenschaft, patch: Partial<{ name: string; typ: 'options' | 'freitext'; optionen: string; pflicht: boolean }>) => { + const current = rowState[e.id] ?? { name: e.name, typ: e.typ, optionen: e.optionen?.join(', ') || '', pflicht: e.pflicht }; + setRowState(prev => ({ ...prev, [e.id]: { ...current, ...patch } })); + }; + const upsertMut = useMutation({ mutationFn: (data: { eigenschaft_id?: number; name: string; typ: string; optionen?: string[]; pflicht?: boolean; sort_order?: number }) => ausruestungsanfrageApi.upsertArtikelEigenschaft(artikelId!, data), @@ -54,101 +58,76 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) { setNewName(''); setNewOptionen(''); setNewPflicht(false); - setEditingId(null); }, onError: () => showError('Fehler beim Speichern'), }); const deleteMut = useMutation({ mutationFn: (id: number) => ausruestungsanfrageApi.deleteArtikelEigenschaft(id), - onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId] }); showSuccess('Eigenschaft geloescht'); }, - onError: () => showError('Fehler beim Loeschen'), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'eigenschaften', artikelId] }); showSuccess('Eigenschaft gelöscht'); }, + onError: () => showError('Fehler beim Löschen'), }); const handleAdd = () => { if (!newName.trim()) return; const optionen = newTyp === 'options' ? newOptionen.split(',').map(s => s.trim()).filter(Boolean) : undefined; - upsertMut.mutate({ - name: newName.trim(), - typ: newTyp, - optionen, - pflicht: newPflicht, - sort_order: eigenschaften.length, - }); + upsertMut.mutate({ name: newName.trim(), typ: newTyp, optionen, pflicht: newPflicht, sort_order: eigenschaften.length }); }; - const startEditEigenschaft = (e: AusruestungEigenschaft) => { - setEditingId(e.id); - setEditName(e.name); - setEditTyp(e.typ); - setEditOptionen(e.optionen?.join(', ') || ''); - setEditPflicht(e.pflicht); - }; - - const handleSaveEdit = (e: AusruestungEigenschaft) => { - if (!editName.trim()) return; + const handleSaveRow = (e: AusruestungEigenschaft) => { + const row = getRow(e); + if (!row.name.trim()) return; upsertMut.mutate({ eigenschaft_id: e.id, - name: editName.trim(), - typ: editTyp, - optionen: editTyp === 'options' ? editOptionen.split(',').map(s => s.trim()).filter(Boolean) : undefined, - pflicht: editPflicht, + name: row.name.trim(), + typ: row.typ, + optionen: row.typ === 'options' ? row.optionen.split(',').map(s => s.trim()).filter(Boolean) : undefined, + pflicht: row.pflicht, sort_order: e.sort_order, }); }; - if (artikelId == null) return Bitte speichern Sie den Artikel zuerst, bevor Sie Eigenschaften hinzufuegen.; + if (artikelId == null) return Bitte speichern Sie den Artikel zuerst, bevor Sie Eigenschaften hinzufügen.; return ( Eigenschaften - {eigenschaften.map(e => ( - - {editingId === e.id ? ( - + {eigenschaften.map(e => { + const row = getRow(e); + return ( + + - setEditName(ev.target.value)} sx={{ flexGrow: 1 }} /> + updateRow(e, { name: ev.target.value })} sx={{ flexGrow: 1 }} /> setEditTyp(ev.target.value as 'options' | 'freitext')} + select size="small" label="Typ" value={row.typ} + onChange={ev => updateRow(e, { typ: ev.target.value as 'options' | 'freitext' })} sx={{ minWidth: 120 }} > Auswahl Freitext setEditPflicht(ev.target.checked)} />} + control={ updateRow(e, { pflicht: ev.target.checked })} />} label="Pflicht" /> + + deleteMut.mutate(e.id)}> - {editTyp === 'options' && ( - setEditOptionen(ev.target.value)} fullWidth /> + {row.typ === 'options' && ( + updateRow(e, { optionen: ev.target.value })} fullWidth /> )} - - - - - ) : ( - - - {e.name} ({e.typ === 'options' ? `Auswahl: ${e.optionen?.join(', ')}` : 'Freitext'}) - {e.pflicht && } - - startEditEigenschaft(e)}> - deleteMut.mutate(e.id)}> - - )} - - ))} + + ); + })} setNewName(e.target.value)} sx={{ flexGrow: 1 }} /> setNewTyp(e.target.value as 'options' | 'freitext')} sx={{ minWidth: 120 }} > @@ -161,22 +140,10 @@ function EigenschaftenEditor({ artikelId }: { artikelId: number | null }) { /> {newTyp === 'options' && ( - setNewOptionen(e.target.value)} - placeholder="S, M, L, XL" - fullWidth - /> + setNewOptionen(e.target.value)} placeholder="S, M, L, XL" fullWidth /> )} - @@ -369,7 +336,7 @@ export default function AusruestungsanfrageArtikelDetail() { setMainKat(val); if (val) { const subs = subKategorienOf(val as number); - setForm(f => ({ ...f, kategorie_id: subs.length === 0 ? (val as number) : null })); + setForm(f => ({ ...f, kategorie_id: val as number })); } else { setForm(f => ({ ...f, kategorie_id: null })); }