feat(ausruestung): catalog-driven item tracking, im_haus in overview, order quantity override, fix stale queries
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -96,6 +96,7 @@ function MeineAnfragenTab() {
|
||||
<TableCell>Anfrage ID</TableCell>
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Im Haus</TableCell>
|
||||
<TableCell>Positionen</TableCell>
|
||||
<TableCell>Geliefert</TableCell>
|
||||
<TableCell>Erstellt am</TableCell>
|
||||
@@ -107,6 +108,7 @@ function MeineAnfragenTab() {
|
||||
<TableCell>{formatOrderId(r)}</TableCell>
|
||||
<TableCell>{r.bezeichnung || '-'}</TableCell>
|
||||
<TableCell><Chip label={AUSRUESTUNG_STATUS_LABELS[r.status]} color={AUSRUESTUNG_STATUS_COLORS[r.status]} size="small" /></TableCell>
|
||||
<TableCell>{r.im_haus ? <Chip label="Im Haus" color="success" size="small" /> : null}</TableCell>
|
||||
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
{r.positionen_count != null && r.positionen_count > 0
|
||||
@@ -204,6 +206,7 @@ function AlleAnfragenTab() {
|
||||
<TableCell>Bezeichnung</TableCell>
|
||||
<TableCell>Anfrage für</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Im Haus</TableCell>
|
||||
<TableCell>Positionen</TableCell>
|
||||
<TableCell>Geliefert</TableCell>
|
||||
<TableCell>Erstellt am</TableCell>
|
||||
@@ -216,6 +219,7 @@ function AlleAnfragenTab() {
|
||||
<TableCell>{r.bezeichnung || '-'}</TableCell>
|
||||
<TableCell>{r.fuer_benutzer_name || r.anfrager_name || r.anfrager_id}</TableCell>
|
||||
<TableCell><Chip label={AUSRUESTUNG_STATUS_LABELS[r.status]} color={AUSRUESTUNG_STATUS_COLORS[r.status]} size="small" /></TableCell>
|
||||
<TableCell>{r.im_haus ? <Chip label="Im Haus" color="success" size="small" /> : null}</TableCell>
|
||||
<TableCell>{r.positionen_count ?? r.items_count ?? '-'}</TableCell>
|
||||
<TableCell>
|
||||
{r.positionen_count != null && r.positionen_count > 0
|
||||
|
||||
@@ -84,6 +84,7 @@ export default function AusruestungsanfrageZuBestellung() {
|
||||
// ── State ──
|
||||
const [assignments, setAssignments] = useState<Record<number, VendorAssignment | null>>({});
|
||||
const [orderNames, setOrderNames] = useState<Record<number, string>>({});
|
||||
const [quantities, setQuantities] = useState<Record<number, number>>({});
|
||||
|
||||
// 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() {
|
||||
<TableRow>
|
||||
<TableCell>Artikel</TableCell>
|
||||
<TableCell align="right" sx={{ width: 80 }}>Menge</TableCell>
|
||||
<TableCell align="right" sx={{ width: 140 }}>Bestellmenge</TableCell>
|
||||
<TableCell sx={{ width: 320 }}>Lieferant</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -301,6 +305,20 @@ export default function AusruestungsanfrageZuBestellung() {
|
||||
<TableCell align="right">
|
||||
<Typography variant="body2">{pos.menge} {pos.einheit ?? 'Stk'}</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
sx={{ width: 80 }}
|
||||
value={quantities[pos.id] ?? pos.menge}
|
||||
onChange={e => {
|
||||
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}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Autocomplete<Lieferant>
|
||||
|
||||
@@ -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<PositionAssignment>) => {
|
||||
@@ -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}
|
||||
>
|
||||
<ToggleButton value="ausruestung">Ausrüstung</ToggleButton>
|
||||
<ToggleButton value="persoenlich">Persönlich</ToggleButton>
|
||||
<ToggleButton value="keine">Nicht erfassen</ToggleButton>
|
||||
<ToggleButton value="ausruestung" disabled={!pos.artikel_id}>Ausrüstung</ToggleButton>
|
||||
<ToggleButton value="persoenlich" disabled={!pos.artikel_id}>Persönlich</ToggleButton>
|
||||
<ToggleButton value="keine">Nicht verfolgt</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{a.typ === 'ausruestung' && (
|
||||
@@ -262,7 +266,7 @@ export default function AusruestungsanfrageZuweisung() {
|
||||
)}
|
||||
|
||||
{a.typ === 'persoenlich' && (
|
||||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap' }}>
|
||||
<Box sx={{ display: 'flex', gap: 1.5, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Autocomplete
|
||||
size="small"
|
||||
options={memberOptions}
|
||||
@@ -278,20 +282,19 @@ export default function AusruestungsanfrageZuweisung() {
|
||||
)}
|
||||
sx={{ minWidth: 200, flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Größe"
|
||||
value={a.groesse ?? ''}
|
||||
onChange={(e) => updateAssignment(pos.id, { groesse: e.target.value })}
|
||||
sx={{ minWidth: 100 }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Kategorie"
|
||||
value={a.kategorie ?? ''}
|
||||
onChange={(e) => 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 <Typography variant="caption" color="text.secondary">Hat bereits {count} Stk.</Typography>;
|
||||
})()}
|
||||
{pos.eigenschaften && pos.eigenschaften.length > 0 && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1 }}>
|
||||
{pos.eigenschaften.map(e => (
|
||||
<Chip key={e.eigenschaft_id} label={`${e.eigenschaft_name}: ${e.wert}`} size="small" variant="outlined" />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -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({});
|
||||
|
||||
@@ -102,7 +102,7 @@ function PersoenlicheAusruestungPage() {
|
||||
<Tabs value={activeTab} onChange={(_e, v) => setActiveTab(v)} sx={{ mb: 3 }}>
|
||||
<Tab label="Zuweisungen" />
|
||||
<Tab label="Katalog" />
|
||||
{canApprove && <Tab label="Nicht Zugewiesen" />}
|
||||
{canApprove && <Tab label="Nicht verfolgt" />}
|
||||
</Tabs>
|
||||
|
||||
{activeTab === 0 && (
|
||||
|
||||
@@ -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<Record<number, string>>({});
|
||||
|
||||
// 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<number, string> = {};
|
||||
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() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Kategorie"
|
||||
size="small"
|
||||
value={kategorie}
|
||||
onChange={(e) => setKategorie(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Größe"
|
||||
size="small"
|
||||
value={groesse}
|
||||
onChange={(e) => setGroesse(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Seriennummer"
|
||||
size="small"
|
||||
@@ -257,6 +262,28 @@ export default function PersoenlicheAusruestungEdit() {
|
||||
/>
|
||||
|
||||
{/* Eigenschaften */}
|
||||
{item.artikel_id ? (
|
||||
<>
|
||||
<Typography variant="subtitle2">Eigenschaften</Typography>
|
||||
{artikelEigenschaften.map(e =>
|
||||
e.typ === 'options' && e.optionen?.length ? (
|
||||
<TextField key={e.id} select size="small" label={e.name} required={e.pflicht}
|
||||
value={catalogEigenschaftValues[e.id] ?? ''}
|
||||
onChange={ev => setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
|
||||
>
|
||||
<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={catalogEigenschaftValues[e.id] ?? ''}
|
||||
onChange={ev => setCatalogEigenschaftValues(prev => ({ ...prev, [e.id]: ev.target.value }))}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="subtitle2">Eigenschaften</Typography>
|
||||
{eigenschaften.map((e, idx) => (
|
||||
<Box key={idx} sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
@@ -282,6 +309,8 @@ export default function PersoenlicheAusruestungEdit() {
|
||||
<Button size="small" startIcon={<AddIcon />} onClick={addEigenschaft}>
|
||||
Eigenschaft hinzufügen
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end', mt: 1 }}>
|
||||
<Button onClick={() => navigate(`/persoenliche-ausruestung/${id}`)}>
|
||||
|
||||
@@ -34,13 +34,12 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
|
||||
const canViewAll = hasPermission('persoenliche_ausruestung:view_all');
|
||||
|
||||
const [formBezeichnung, setFormBezeichnung] = useState<string | AusruestungArtikel | null>(null);
|
||||
const [formKategorie, setFormKategorie] = useState('');
|
||||
const [formArtikel, setFormArtikel] = useState<AusruestungArtikel | null>(null);
|
||||
const [formUserId, setFormUserId] = useState<{ id: string; name: string } | null>(null);
|
||||
const [formBenutzerName, setFormBenutzerName] = useState('');
|
||||
const [formGroesse, setFormGroesse] = useState('');
|
||||
const [formZustand, setFormZustand] = useState<PersoenlicheAusruestungZustand>('gut');
|
||||
const [formNotizen, setFormNotizen] = useState('');
|
||||
const [eigenschaftValues, setEigenschaftValues] = useState<Record<number, string>>({});
|
||||
|
||||
const { data: catalogItems } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage-items-catalog'],
|
||||
@@ -48,6 +47,13 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
staleTime: 10 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: artikelEigenschaften = [] } = useQuery({
|
||||
queryKey: ['ausruestungsanfrage', 'eigenschaften', formArtikel?.id],
|
||||
queryFn: () => ausruestungsanfrageApi.getArtikelEigenschaften(formArtikel!.id),
|
||||
enabled: !!formArtikel?.id,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: membersList } = useQuery({
|
||||
queryKey: ['members-list-compact'],
|
||||
queryFn: () => membersService.getMembers({ pageSize: 500 }),
|
||||
@@ -75,20 +81,23 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
const bezeichnung = typeof formBezeichnung === 'string'
|
||||
? formBezeichnung
|
||||
: formBezeichnung?.bezeichnung ?? '';
|
||||
const bezeichnung = formArtikel?.bezeichnung ?? '';
|
||||
if (!bezeichnung.trim()) return;
|
||||
|
||||
const payload: CreatePersoenlicheAusruestungPayload = {
|
||||
bezeichnung: bezeichnung.trim(),
|
||||
kategorie: formKategorie || undefined,
|
||||
artikel_id: typeof formBezeichnung === 'object' && formBezeichnung ? formBezeichnung.id : undefined,
|
||||
artikel_id: formArtikel?.id,
|
||||
user_id: formUserId?.id || undefined,
|
||||
benutzer_name: formBenutzerName || undefined,
|
||||
groesse: formGroesse || undefined,
|
||||
zustand: formZustand,
|
||||
notizen: formNotizen || undefined,
|
||||
eigenschaften: Object.entries(eigenschaftValues)
|
||||
.filter(([, v]) => v.trim())
|
||||
.map(([id, wert]) => ({
|
||||
eigenschaft_id: Number(id),
|
||||
name: artikelEigenschaften.find(e => e.id === Number(id))?.name ?? '',
|
||||
wert,
|
||||
})),
|
||||
};
|
||||
createMutation.mutate(payload);
|
||||
};
|
||||
@@ -103,20 +112,12 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
|
||||
<Stack spacing={2}>
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
options={catalogItems ?? []}
|
||||
getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung}
|
||||
value={formBezeichnung}
|
||||
getOptionLabel={(o) => o.bezeichnung}
|
||||
value={formArtikel}
|
||||
onChange={(_e, v) => {
|
||||
setFormBezeichnung(v);
|
||||
if (v && typeof v !== 'string' && v.kategorie) {
|
||||
setFormKategorie(v.kategorie);
|
||||
}
|
||||
}}
|
||||
onInputChange={(_e, v) => {
|
||||
if (typeof formBezeichnung === 'string' || formBezeichnung === null) {
|
||||
setFormBezeichnung(v);
|
||||
}
|
||||
setFormArtikel(v);
|
||||
setEigenschaftValues({});
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField {...params} label="Bezeichnung" required size="small" />
|
||||
@@ -124,6 +125,23 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
size="small"
|
||||
/>
|
||||
|
||||
{artikelEigenschaften.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 }))}
|
||||
>
|
||||
<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 }))}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{canViewAll && (
|
||||
<Autocomplete
|
||||
options={memberOptions}
|
||||
@@ -146,20 +164,6 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
label="Kategorie"
|
||||
size="small"
|
||||
value={formKategorie}
|
||||
onChange={(e) => setFormKategorie(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Größe"
|
||||
size="small"
|
||||
value={formGroesse}
|
||||
onChange={(e) => setFormGroesse(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Zustand"
|
||||
select
|
||||
@@ -188,7 +192,7 @@ export default function PersoenlicheAusruestungNeu() {
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleCreate}
|
||||
disabled={createMutation.isPending || !(typeof formBezeichnung === 'string' ? formBezeichnung.trim() : formBezeichnung?.bezeichnung?.trim())}
|
||||
disabled={createMutation.isPending || !formArtikel}
|
||||
>
|
||||
Erstellen
|
||||
</Button>
|
||||
|
||||
@@ -96,6 +96,7 @@ export interface AusruestungAnfrage {
|
||||
positionen_count?: number;
|
||||
geliefert_count?: number;
|
||||
items_count?: number;
|
||||
im_haus?: boolean;
|
||||
}
|
||||
|
||||
export interface AusruestungAnfragePosition {
|
||||
|
||||
Reference in New Issue
Block a user