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 }}
>
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 />
)}
- }
- onClick={handleAdd}
- disabled={!newName.trim() || upsertMut.isPending}
- >
- Eigenschaft hinzufuegen
+ } onClick={handleAdd} disabled={!newName.trim() || upsertMut.isPending}>
+ Eigenschaft hinzufügen
@@ -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 }));
}