diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts
index 87944a9..e52336a 100644
--- a/backend/src/services/ausruestungsanfrage.service.ts
+++ b/backend/src/services/ausruestungsanfrage.service.ts
@@ -324,7 +324,13 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string })
COALESCE(u2.given_name || ' ' || u2.family_name, u2.name) AS bearbeitet_von_name,
a.fuer_benutzer_name,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count,
- (SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count
+ (SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count,
+ EXISTS(
+ SELECT 1 FROM ausruestung_anfrage_bestellung ab
+ JOIN bestellungen b ON b.id = ab.bestellung_id
+ WHERE ab.anfrage_id = a.id
+ AND b.status IN ('lieferung_pruefen', 'abgeschlossen')
+ ) AS im_haus
FROM ausruestung_anfragen a
LEFT JOIN users u ON u.id = a.anfrager_id
LEFT JOIN users u2 ON u2.id = a.bearbeitet_von
@@ -339,7 +345,13 @@ async function getMyRequests(userId: string) {
const result = await pool.query(
`SELECT a.*,
(SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id) AS positionen_count,
- (SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count
+ (SELECT COUNT(*)::int FROM ausruestung_anfrage_positionen p WHERE p.anfrage_id = a.id AND p.geliefert) AS geliefert_count,
+ EXISTS(
+ SELECT 1 FROM ausruestung_anfrage_bestellung ab
+ JOIN bestellungen b ON b.id = ab.bestellung_id
+ WHERE ab.anfrage_id = a.id
+ AND b.status IN ('lieferung_pruefen', 'abgeschlossen')
+ ) AS im_haus
FROM ausruestung_anfragen a
WHERE a.anfrager_id = $1
ORDER BY a.erstellt_am DESC`,
@@ -1100,7 +1112,7 @@ async function getUnassignedPositions() {
FROM ausruestung_anfrage_positionen p
JOIN ausruestung_anfragen a ON a.id = p.anfrage_id
LEFT JOIN users u ON u.id = a.anfrager_id
- WHERE p.geliefert = true AND p.zuweisung_typ IS NULL
+ WHERE p.geliefert = true AND (p.zuweisung_typ IS NULL OR p.zuweisung_typ = 'keine')
ORDER BY a.bestell_jahr DESC NULLS LAST, a.bestell_nummer DESC NULLS LAST, p.bezeichnung
`);
return result.rows;
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 8b71964..3e4d1b0 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -8,7 +8,7 @@ import App from './App';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
- staleTime: 5 * 60 * 1000, // 5 minutes
+ staleTime: 30 * 1000, // 30 seconds
gcTime: 10 * 60 * 1000, // keep cache 10 minutes
retry: 1,
refetchOnWindowFocus: false, // prevent refetch on every tab switch
diff --git a/frontend/src/pages/Ausruestungsanfrage.tsx b/frontend/src/pages/Ausruestungsanfrage.tsx
index bbd4b6a..e5ad503 100644
--- a/frontend/src/pages/Ausruestungsanfrage.tsx
+++ b/frontend/src/pages/Ausruestungsanfrage.tsx
@@ -96,6 +96,7 @@ function MeineAnfragenTab() {
Anfrage ID
Bezeichnung
Status
+ Im Haus
Positionen
Geliefert
Erstellt am
@@ -107,6 +108,7 @@ function MeineAnfragenTab() {
{formatOrderId(r)}
{r.bezeichnung || '-'}
+ {r.im_haus ? : null}
{r.positionen_count ?? r.items_count ?? '-'}
{r.positionen_count != null && r.positionen_count > 0
@@ -204,6 +206,7 @@ function AlleAnfragenTab() {
Bezeichnung
Anfrage für
Status
+ Im Haus
Positionen
Geliefert
Erstellt am
@@ -216,6 +219,7 @@ function AlleAnfragenTab() {
{r.bezeichnung || '-'}
{r.fuer_benutzer_name || r.anfrager_name || r.anfrager_id}
+ {r.im_haus ? : null}
{r.positionen_count ?? r.items_count ?? '-'}
{r.positionen_count != null && r.positionen_count > 0
diff --git a/frontend/src/pages/AusruestungsanfrageZuBestellung.tsx b/frontend/src/pages/AusruestungsanfrageZuBestellung.tsx
index 97b9fd7..e306fae 100644
--- a/frontend/src/pages/AusruestungsanfrageZuBestellung.tsx
+++ b/frontend/src/pages/AusruestungsanfrageZuBestellung.tsx
@@ -84,6 +84,7 @@ export default function AusruestungsanfrageZuBestellung() {
// ── State ──
const [assignments, setAssignments] = useState>({});
const [orderNames, setOrderNames] = useState>({});
+ const [quantities, setQuantities] = useState>({});
// New vendor dialog
const [newVendorDialog, setNewVendorDialog] = useState(false);
@@ -153,6 +154,8 @@ export default function AusruestungsanfrageZuBestellung() {
showSuccess(`${result.created_bestellungen.length} Bestellung(en) erfolgreich erstellt`);
queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', requestId] });
navigate('/bestellungen');
+ queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
},
onError: () => showError('Bestellungen konnten nicht erstellt werden'),
});
@@ -191,7 +194,7 @@ export default function AusruestungsanfrageZuBestellung() {
positionen: g.positionen.map(p => ({
position_id: p.id,
bezeichnung: p.bezeichnung,
- menge: p.menge,
+ menge: quantities[p.id] ?? p.menge,
einheit: p.einheit,
notizen: p.notizen,
artikel_id: p.artikel_id,
@@ -281,6 +284,7 @@ export default function AusruestungsanfrageZuBestellung() {
Artikel
Menge
+ Bestellmenge
Lieferant
@@ -301,6 +305,20 @@ export default function AusruestungsanfrageZuBestellung() {
{pos.menge} {pos.einheit ?? 'Stk'}
+
+ {
+ const val = Math.min(pos.menge, Math.max(1, Number(e.target.value)));
+ setQuantities(prev => ({ ...prev, [pos.id]: val }));
+ }}
+ inputProps={{ min: 1, max: pos.menge }}
+ helperText={pos.menge > 1 ? `max ${pos.menge}` : undefined}
+ />
+
diff --git a/frontend/src/pages/AusruestungsanfrageZuweisung.tsx b/frontend/src/pages/AusruestungsanfrageZuweisung.tsx
index c0bdf9c..d0b1c63 100644
--- a/frontend/src/pages/AusruestungsanfrageZuweisung.tsx
+++ b/frontend/src/pages/AusruestungsanfrageZuweisung.tsx
@@ -12,6 +12,7 @@ import { PageHeader } from '../components/templates';
import { useNotification } from '../contexts/NotificationContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
+import { personalEquipmentApi } from '../services/personalEquipment';
import { vehiclesApi } from '../services/vehicles';
import { membersService } from '../services/members';
import type { AusruestungAnfragePosition } from '../types/ausruestungsanfrage.types';
@@ -24,8 +25,6 @@ interface PositionAssignment {
standort?: string;
userId?: string;
benutzerName?: string;
- groesse?: string;
- kategorie?: string;
}
function getUnassignedPositions(positions: AusruestungAnfragePosition[]): AusruestungAnfragePosition[] {
@@ -92,6 +91,12 @@ export default function AusruestungsanfrageZuweisung() {
name: v.bezeichnung ?? v.kurzname,
}));
+ const { data: allPersonalItems = [] } = useQuery({
+ queryKey: ['persoenliche-ausruestung', 'all-for-count'],
+ queryFn: () => personalEquipmentApi.getAll(),
+ staleTime: 2 * 60 * 1000,
+ });
+
const [submitting, setSubmitting] = useState(false);
const updateAssignment = (posId: number, patch: Partial) => {
@@ -137,12 +142,12 @@ export default function AusruestungsanfrageZuweisung() {
standort: a.typ === 'ausruestung' ? a.standort : undefined,
userId: a.typ === 'persoenlich' ? a.userId : undefined,
benutzerName: a.typ === 'persoenlich' ? (a.benutzerName || anfrage.fuer_benutzer_name || anfrage.anfrager_name) : undefined,
- groesse: a.typ === 'persoenlich' ? a.groesse : undefined,
- kategorie: a.typ === 'persoenlich' ? a.kategorie : undefined,
}));
await ausruestungsanfrageApi.assignItems(anfrageId, payload);
showSuccess('Gegenstände zugewiesen');
navigate(`/ausruestungsanfrage/${id}`);
+ queryClient.invalidateQueries({ queryKey: ['persoenliche-ausruestung'] });
+ queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] });
} catch {
showError('Fehler beim Zuweisen');
} finally {
@@ -233,11 +238,10 @@ export default function AusruestungsanfrageZuweisung() {
size="small"
onChange={(_e, val) => val && updateAssignment(pos.id, { typ: val })}
sx={{ mb: 1.5 }}
- disabled={!pos.artikel_id}
>
- Ausrüstung
- Persönlich
- Nicht erfassen
+ Ausrüstung
+ Persönlich
+ Nicht verfolgt
{a.typ === 'ausruestung' && (
@@ -262,7 +266,7 @@ export default function AusruestungsanfrageZuweisung() {
)}
{a.typ === 'persoenlich' && (
-
+
- updateAssignment(pos.id, { groesse: e.target.value })}
- sx={{ minWidth: 100 }}
- />
- updateAssignment(pos.id, { kategorie: e.target.value })}
- sx={{ minWidth: 140 }}
- />
+ {(() => {
+ if (!a.userId || !pos.artikel_id) return null;
+ const count = allPersonalItems.filter(i => i.user_id === a.userId && i.artikel_id === pos.artikel_id).length;
+ if (count === 0) return null;
+ return Hat bereits {count} Stk.;
+ })()}
+ {pos.eigenschaften && pos.eigenschaften.length > 0 && (
+
+ {pos.eigenschaften.map(e => (
+
+ ))}
+
+ )}
)}
diff --git a/frontend/src/pages/BestellungDetail.tsx b/frontend/src/pages/BestellungDetail.tsx
index c72c2ee..6ecf1fa 100644
--- a/frontend/src/pages/BestellungDetail.tsx
+++ b/frontend/src/pages/BestellungDetail.tsx
@@ -214,6 +214,7 @@ export default function BestellungDetail() {
mutationFn: ({ status, force }: { status: string; force?: boolean }) => bestellungApi.updateStatus(orderId, status, force),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Status aktualisiert');
setStatusConfirmTarget(null);
setStatusForce(false);
@@ -225,6 +226,7 @@ export default function BestellungDetail() {
mutationFn: (data: BestellpositionFormData) => bestellungApi.addLineItem(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setNewItem({ ...emptyItem });
setSelectedKatalogItem(null);
setKatalogEigenschaften([]);
@@ -238,6 +240,7 @@ export default function BestellungDetail() {
mutationFn: (itemId: number) => bestellungApi.deleteLineItem(itemId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setDeleteItemTarget(null);
showSuccess('Position gelöscht');
},
@@ -249,6 +252,7 @@ export default function BestellungDetail() {
bestellungApi.updateReceivedQty(itemId, menge),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
@@ -257,6 +261,7 @@ export default function BestellungDetail() {
mutationFn: (file: File) => bestellungApi.uploadFile(orderId, file),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Datei hochgeladen');
},
onError: () => showError('Fehler beim Hochladen der Datei'),
@@ -266,6 +271,7 @@ export default function BestellungDetail() {
mutationFn: (fileId: number) => bestellungApi.deleteFile(fileId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setDeleteFileTarget(null);
showSuccess('Datei gelöscht');
},
@@ -276,6 +282,7 @@ export default function BestellungDetail() {
mutationFn: (data: ErinnerungFormData) => bestellungApi.addReminder(orderId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setReminderForm({ faellig_am: '', nachricht: '' });
setReminderFormOpen(false);
showSuccess('Erinnerung erstellt');
@@ -287,6 +294,7 @@ export default function BestellungDetail() {
mutationFn: (reminderId: number) => bestellungApi.markReminderDone(reminderId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
},
onError: () => showError('Fehler beim Aktualisieren'),
});
@@ -295,6 +303,7 @@ export default function BestellungDetail() {
mutationFn: (reminderId: number) => bestellungApi.deleteReminder(reminderId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
setDeleteReminderTarget(null);
showSuccess('Erinnerung gelöscht');
},
@@ -351,6 +360,7 @@ export default function BestellungDetail() {
}
}
await queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] });
+ queryClient.invalidateQueries({ queryKey: ['bestellungen'] });
showSuccess('Änderungen gespeichert');
setEditMode(false);
setEditItemsData({});
diff --git a/frontend/src/pages/PersoenlicheAusruestung.tsx b/frontend/src/pages/PersoenlicheAusruestung.tsx
index 33961b7..ab34762 100644
--- a/frontend/src/pages/PersoenlicheAusruestung.tsx
+++ b/frontend/src/pages/PersoenlicheAusruestung.tsx
@@ -102,7 +102,7 @@ function PersoenlicheAusruestungPage() {
setActiveTab(v)} sx={{ mb: 3 }}>
- {canApprove && }
+ {canApprove && }
{activeTab === 0 && (
diff --git a/frontend/src/pages/PersoenlicheAusruestungEdit.tsx b/frontend/src/pages/PersoenlicheAusruestungEdit.tsx
index 713c8fa..8c8e475 100644
--- a/frontend/src/pages/PersoenlicheAusruestungEdit.tsx
+++ b/frontend/src/pages/PersoenlicheAusruestungEdit.tsx
@@ -16,6 +16,7 @@ import { useParams, useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { personalEquipmentApi } from '../services/personalEquipment';
+import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage';
import { membersService } from '../services/members';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
@@ -64,10 +65,17 @@ export default function PersoenlicheAusruestungEdit() {
}));
}, [membersList]);
+ const { data: artikelEigenschaften = [] } = useQuery({
+ queryKey: ['ausruestungsanfrage', 'eigenschaften', item?.artikel_id],
+ queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(item!.artikel_id!),
+ enabled: !!item?.artikel_id,
+ staleTime: 5 * 60 * 1000,
+ });
+
+ const [catalogEigenschaftValues, setCatalogEigenschaftValues] = useState>({});
+
// Form state
const [bezeichnung, setBezeichnung] = useState('');
- const [kategorie, setKategorie] = useState('');
- const [groesse, setGroesse] = useState('');
const [seriennummer, setSeriennummer] = useState('');
const [inventarnummer, setInventarnummer] = useState('');
const [anschaffungDatum, setAnschaffungDatum] = useState('');
@@ -80,8 +88,6 @@ export default function PersoenlicheAusruestungEdit() {
useEffect(() => {
if (!item) return;
setBezeichnung(item.bezeichnung);
- setKategorie(item.kategorie ?? '');
- setGroesse(item.groesse ?? '');
setSeriennummer(item.seriennummer ?? '');
setInventarnummer(item.inventarnummer ?? '');
setAnschaffungDatum(item.anschaffung_datum ? item.anschaffung_datum.split('T')[0] : '');
@@ -95,6 +101,13 @@ export default function PersoenlicheAusruestungEdit() {
wert: e.wert,
})));
}
+ if (item.artikel_id && item.eigenschaften) {
+ const vals: Record = {};
+ item.eigenschaften.forEach(e => {
+ if (e.eigenschaft_id != null) vals[e.eigenschaft_id] = e.wert;
+ });
+ setCatalogEigenschaftValues(vals);
+ }
}, [item]);
// Set userId when item + memberOptions are ready
@@ -117,21 +130,27 @@ export default function PersoenlicheAusruestungEdit() {
});
const handleSave = () => {
- if (!bezeichnung.trim()) return;
+ if (!bezeichnung.trim() || !item) return;
const payload: UpdatePersoenlicheAusruestungPayload = {
bezeichnung: bezeichnung.trim(),
- kategorie: kategorie || null,
+ kategorie: null,
user_id: userId?.id || null,
- groesse: groesse || null,
+ groesse: null,
seriennummer: seriennummer || null,
inventarnummer: inventarnummer || null,
anschaffung_datum: anschaffungDatum || null,
zustand,
notizen: notizen || null,
- eigenschaften: eigenschaften
- .filter(e => e.name.trim() && e.wert.trim())
- .map(e => ({ eigenschaft_id: e.eigenschaft_id, name: e.name, wert: e.wert })),
+ eigenschaften: item.artikel_id
+ ? Object.entries(catalogEigenschaftValues)
+ .filter(([, v]) => v.trim())
+ .map(([id, wert]) => ({
+ eigenschaft_id: Number(id),
+ name: artikelEigenschaften.find(e => e.id === Number(id))?.name ?? '',
+ wert,
+ }))
+ : eigenschaften.filter(e => e.name.trim() && e.wert.trim()).map(e => ({ eigenschaft_id: e.eigenschaft_id, name: e.name, wert: e.wert })),
};
updateMutation.mutate(payload);
};
@@ -198,20 +217,6 @@ export default function PersoenlicheAusruestungEdit() {
/>
)}
- setKategorie(e.target.value)}
- />
-
- setGroesse(e.target.value)}
- />
-
{/* Eigenschaften */}
- Eigenschaften
- {eigenschaften.map((e, idx) => (
-
- updateEigenschaft(idx, 'name', ev.target.value)}
- sx={{ flex: 1 }}
- />
- updateEigenschaft(idx, 'wert', ev.target.value)}
- sx={{ flex: 1 }}
- />
- removeEigenschaft(idx)}>
-
-
-
- ))}
- } onClick={addEigenschaft}>
- Eigenschaft hinzufügen
-
+ {item.artikel_id ? (
+ <>
+ Eigenschaften
+ {artikelEigenschaften.map(e =>
+ e.typ === 'options' && e.optionen?.length ? (
+ setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
+ >
+
+ {e.optionen.map(opt => )}
+
+ ) : (
+ setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
+ />
+ )
+ )}
+ >
+ ) : (
+ <>
+ Eigenschaften
+ {eigenschaften.map((e, idx) => (
+
+ updateEigenschaft(idx, 'name', ev.target.value)}
+ sx={{ flex: 1 }}
+ />
+ updateEigenschaft(idx, 'wert', ev.target.value)}
+ sx={{ flex: 1 }}
+ />
+ removeEigenschaft(idx)}>
+
+
+
+ ))}
+ } onClick={addEigenschaft}>
+ Eigenschaft hinzufügen
+
+ >
+ )}
diff --git a/frontend/src/types/ausruestungsanfrage.types.ts b/frontend/src/types/ausruestungsanfrage.types.ts
index e2caf4b..95e9729 100644
--- a/frontend/src/types/ausruestungsanfrage.types.ts
+++ b/frontend/src/types/ausruestungsanfrage.types.ts
@@ -96,6 +96,7 @@ export interface AusruestungAnfrage {
positionen_count?: number;
geliefert_count?: number;
items_count?: number;
+ im_haus?: boolean;
}
export interface AusruestungAnfragePosition {