diff --git a/backend/src/controllers/bestellung.controller.ts b/backend/src/controllers/bestellung.controller.ts index 2a6bbdb..a7e93f7 100644 --- a/backend/src/controllers/bestellung.controller.ts +++ b/backend/src/controllers/bestellung.controller.ts @@ -8,6 +8,20 @@ import fs from 'fs'; const param = (req: Request, key: string): string => req.params[key] as string; class BestellungController { + // --------------------------------------------------------------------------- + // Members + // --------------------------------------------------------------------------- + + async listMembers(_req: Request, res: Response): Promise { + try { + const members = await bestellungService.getAllMembers(); + res.status(200).json({ success: true, data: members }); + } catch (error) { + logger.error('BestellungController.listMembers error', { error }); + res.status(500).json({ success: false, message: 'Mitglieder konnten nicht geladen werden' }); + } + } + // --------------------------------------------------------------------------- // Catalog (shared ausruestung_artikel) // --------------------------------------------------------------------------- diff --git a/backend/src/database/migrations/096_add_im_haus_to_positionen.sql b/backend/src/database/migrations/096_add_im_haus_to_positionen.sql new file mode 100644 index 0000000..82312a2 --- /dev/null +++ b/backend/src/database/migrations/096_add_im_haus_to_positionen.sql @@ -0,0 +1,11 @@ +-- Migration 096: Add im_haus to internal request positions and FK back from external order positions +-- +-- im_haus (ausruestung_anfrage_positionen): auto-set when external bestellposition is received +-- anfrage_position_id (bestellpositionen): explicit link so receipt can sync back + +ALTER TABLE ausruestung_anfrage_positionen + ADD COLUMN IF NOT EXISTS im_haus BOOLEAN DEFAULT false; + +ALTER TABLE bestellpositionen + ADD COLUMN IF NOT EXISTS anfrage_position_id INT + REFERENCES ausruestung_anfrage_positionen(id) ON DELETE SET NULL; diff --git a/backend/src/routes/bestellung.routes.ts b/backend/src/routes/bestellung.routes.ts index 82cc56e..d705b6d 100644 --- a/backend/src/routes/bestellung.routes.ts +++ b/backend/src/routes/bestellung.routes.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; import bestellungController from '../controllers/bestellung.controller'; import { authenticate } from '../middleware/auth.middleware'; -import { requirePermission } from '../middleware/rbac.middleware'; +import { requirePermission, requireAnyPermission } from '../middleware/rbac.middleware'; import { uploadBestellung } from '../middleware/upload'; const router = Router(); @@ -77,6 +77,17 @@ router.get( bestellungController.listKatalogKategorien.bind(bestellungController) ); +// --------------------------------------------------------------------------- +// Members (all active users for member selector) +// --------------------------------------------------------------------------- + +router.get( + '/members', + authenticate, + requirePermission('bestellungen:view'), + bestellungController.listMembers.bind(bestellungController) +); + // --------------------------------------------------------------------------- // Orders (Bestellungen) // --------------------------------------------------------------------------- @@ -159,8 +170,7 @@ router.delete( router.patch( '/items/:itemId/received', authenticate, - requirePermission('bestellungen:manage_orders'), - bestellungController.updateReceivedQuantity.bind(bestellungController) + requireAnyPermission('bestellungen:manage_orders', 'bestellungen:create'), bestellungController.updateReceivedQuantity.bind(bestellungController) ); // --------------------------------------------------------------------------- diff --git a/backend/src/services/ausruestungsanfrage.service.ts b/backend/src/services/ausruestungsanfrage.service.ts index c1543e3..00c4652 100644 --- a/backend/src/services/ausruestungsanfrage.service.ts +++ b/backend/src/services/ausruestungsanfrage.service.ts @@ -325,6 +325,7 @@ async function getRequests(filters?: { status?: string; anfrager_id?: string }) 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.im_haus) AS im_haus_count, EXISTS( SELECT 1 FROM ausruestung_anfrage_bestellung ab JOIN bestellungen b ON b.id = ab.bestellung_id @@ -346,6 +347,7 @@ async function getMyRequests(userId: string) { `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.im_haus) AS im_haus_count, EXISTS( SELECT 1 FROM ausruestung_anfrage_bestellung ab JOIN bestellungen b ON b.id = ab.bestellung_id @@ -859,9 +861,9 @@ async function createOrdersFromRequest( } await client.query( - `INSERT INTO bestellpositionen (bestellung_id, bezeichnung, menge, einheit, notizen, artikel_id, spezifikationen) - VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`, - [bestellung.id, pos.bezeichnung, pos.menge, pos.einheit || 'Stk', pos.notizen || null, artikelId, JSON.stringify(spezifikationen)] + `INSERT INTO bestellpositionen (bestellung_id, bezeichnung, menge, einheit, notizen, artikel_id, spezifikationen, anfrage_position_id) + VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)`, + [bestellung.id, pos.bezeichnung, pos.menge, pos.einheit || 'Stk', pos.notizen || null, artikelId, JSON.stringify(spezifikationen), pos.position_id || null] ); } diff --git a/backend/src/services/bestellung.service.ts b/backend/src/services/bestellung.service.ts index cdfb5f9..1c688d8 100644 --- a/backend/src/services/bestellung.service.ts +++ b/backend/src/services/bestellung.service.ts @@ -41,6 +41,25 @@ async function getKatalogKategorien() { } } +// --------------------------------------------------------------------------- +// Members (all active users for member selector) +// --------------------------------------------------------------------------- + +async function getAllMembers() { + try { + const result = await pool.query( + `SELECT id, COALESCE(given_name || ' ' || family_name, name) AS name + FROM users + WHERE is_active = true + ORDER BY name`, + ); + return result.rows; + } catch (error) { + logger.error('BestellungService.getAllMembers failed', { error }); + throw new Error('Mitglieder konnten nicht geladen werden'); + } +} + // --------------------------------------------------------------------------- // Vendors (Lieferanten) // --------------------------------------------------------------------------- @@ -574,6 +593,15 @@ async function updateReceivedQuantity(id: number, menge: number, userId: string) const item = result.rows[0]; await logAction(item.bestellung_id, 'Liefermenge aktualisiert', `"${item.bezeichnung}": ${menge} von ${item.menge} erhalten`, userId); + // Sync im_haus on linked internal request position + if (item.anfrage_position_id) { + const imHaus = Number(item.erhalten_menge) >= Number(item.menge); + await pool.query( + `UPDATE ausruestung_anfrage_positionen SET im_haus = $1 WHERE id = $2`, + [imHaus, item.anfrage_position_id] + ); + } + // Check if all items for this order are fully received const allItems = await pool.query( `SELECT menge, erhalten_menge FROM bestellpositionen WHERE bestellung_id = $1`, @@ -760,6 +788,8 @@ async function getHistory(bestellungId: number) { } export default { + // Members + getAllMembers, // Catalog getKatalogItems, getKatalogItem, diff --git a/frontend/src/pages/Ausruestungsanfrage.tsx b/frontend/src/pages/Ausruestungsanfrage.tsx index 3285a75..af1348a 100644 --- a/frontend/src/pages/Ausruestungsanfrage.tsx +++ b/frontend/src/pages/Ausruestungsanfrage.tsx @@ -109,9 +109,11 @@ function MeineAnfragenTab() { {r.bezeichnung || '-'} - {r.im_haus && r.geliefert_count != null && r.positionen_count != null && r.geliefert_count < r.positionen_count + {r.im_haus_count != null && r.positionen_count != null && r.im_haus_count > 0 && r.im_haus_count < r.positionen_count ? - : r.im_haus ? : null} + : r.im_haus_count != null && r.positionen_count != null && r.im_haus_count > 0 && r.im_haus_count >= r.positionen_count + ? + : r.im_haus ? : null} {r.positionen_count ?? r.items_count ?? '-'} @@ -224,9 +226,11 @@ function AlleAnfragenTab() { {r.fuer_benutzer_name || r.anfrager_name || r.anfrager_id} - {r.im_haus && r.geliefert_count != null && r.positionen_count != null && r.geliefert_count < r.positionen_count + {r.im_haus_count != null && r.positionen_count != null && r.im_haus_count > 0 && r.im_haus_count < r.positionen_count ? - : r.im_haus ? : null} + : r.im_haus_count != null && r.positionen_count != null && r.im_haus_count > 0 && r.im_haus_count >= r.positionen_count + ? + : r.im_haus ? : null} {r.positionen_count ?? r.items_count ?? '-'} diff --git a/frontend/src/pages/AusruestungsanfrageDetail.tsx b/frontend/src/pages/AusruestungsanfrageDetail.tsx index 954bde8..b976aa5 100644 --- a/frontend/src/pages/AusruestungsanfrageDetail.tsx +++ b/frontend/src/pages/AusruestungsanfrageDetail.tsx @@ -220,8 +220,8 @@ export default function AusruestungsanfrageDetail() { <> {detail?.im_haus && (() => { const total = detail.positionen.length; - const delivered = detail.positionen.filter(p => p.geliefert).length; - return total > 0 && delivered < total + const imHaus = detail.positionen.filter(p => p.im_haus).length; + return total > 0 && imHaus < total ? : ; })()} @@ -430,7 +430,7 @@ export default function AusruestungsanfrageDetail() { {p.ist_ersatz && ( )} - {p.geliefert && detail?.im_haus && ( + {p.im_haus && ( )} {p.geliefert && p.zuweisung_typ === 'keine' && ( diff --git a/frontend/src/pages/BestellungDetail.tsx b/frontend/src/pages/BestellungDetail.tsx index be68804..65d2915 100644 --- a/frontend/src/pages/BestellungDetail.tsx +++ b/frontend/src/pages/BestellungDetail.tsx @@ -107,8 +107,8 @@ const STATUS_TRANSITIONS: Record = { entwurf: ['wartet_auf_genehmigung'], wartet_auf_genehmigung: ['bereit_zur_bestellung', 'entwurf'], bereit_zur_bestellung: ['bestellt'], - bestellt: ['teillieferung', 'lieferung_pruefen'], - teillieferung: ['lieferung_pruefen'], + bestellt: [], // auto via received qty + teillieferung: [], // auto via received qty lieferung_pruefen: ['abgeschlossen'], abgeschlossen: [], }; @@ -547,7 +547,6 @@ export default function BestellungDetail() { const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : []; const allCostsEntered = positionen.length === 0 || positionen.every(p => p.einzelpreis != null && Number(p.einzelpreis) > 0); const allItemsReceived = positionen.length > 0 && positionen.every(p => Number(p.erhalten_menge) >= Number(p.menge)); - const anyItemReceived = positionen.some(p => Number(p.erhalten_menge) > 0); // All statuses except current, for force override const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen']; @@ -1216,8 +1215,7 @@ export default function BestellungDetail() { variant="contained" color={color as 'error' | 'info' | 'success'} disabled={ - (isAbgeschlossen && (!allCostsEntered || !allItemsReceived)) || - (s === 'teillieferung' && !anyItemReceived) + isAbgeschlossen && (!allCostsEntered || !allItemsReceived) } onClick={() => { setStatusForce(false); setStatusConfirmTarget(s); }} > @@ -1340,10 +1338,10 @@ export default function BestellungDetail() { {formatCurrency((editItemsData[p.id]?.einzelpreis ?? parseFloat(String(p.einzelpreis ?? 0))) * (editItemsData[p.id]?.menge ?? parseFloat(String(p.menge))))} - {canManageOrders ? ( + {(canManageOrders || canCreate) ? ( updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })} /> ) : p.erhalten_menge} @@ -1408,14 +1406,14 @@ export default function BestellungDetail() { {formatCurrency(p.einzelpreis)} {formatCurrency((p.einzelpreis ?? 0) * p.menge)} - {canManageOrders ? ( + {(canManageOrders || canCreate) ? ( updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })} /> ) : ( diff --git a/frontend/src/pages/BestellungNeu.tsx b/frontend/src/pages/BestellungNeu.tsx index 829b698..b3834d0 100644 --- a/frontend/src/pages/BestellungNeu.tsx +++ b/frontend/src/pages/BestellungNeu.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useCallback, useRef } from 'react'; import { Box, Typography, @@ -8,10 +8,13 @@ import { IconButton, Autocomplete, Tooltip, + Divider, + MenuItem, } from '@mui/material'; import { Add as AddIcon, RemoveCircleOutline as RemoveIcon, + Delete as DeleteIcon, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; @@ -20,12 +23,71 @@ 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'; +import type { BestellungFormData, LieferantFormData, Lieferant } from '../types/bestellung.types'; +import type { AusruestungArtikel, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types'; + +// ── EigenschaftFields (same pattern as AusruestungsanfrageNeu) ── + +interface EigenschaftFieldsProps { + eigenschaften: AusruestungEigenschaft[]; + values: Record; + onChange: (eigenschaftId: number, wert: string) => void; +} + +function EigenschaftFields({ eigenschaften, values, onChange }: EigenschaftFieldsProps) { + if (eigenschaften.length === 0) return null; + return ( + + {eigenschaften.map(e => ( + + {e.typ === 'options' && e.optionen && e.optionen.length > 0 ? ( + onChange(e.id, ev.target.value)} + required={e.pflicht} + sx={{ minWidth: 160 }} + > + + {e.optionen.map(opt => ( + {opt} + ))} + + ) : ( + onChange(e.id, ev.target.value)} + required={e.pflicht} + sx={{ minWidth: 160 }} + /> + )} + + ))} + + ); +} + +// ── Types for form state ── + +interface CatalogPositionForm { + artikel_id?: number; + bezeichnung: string; + menge: number; + einheit: string; +} + +interface FreePositionForm { + bezeichnung: string; + menge: number; + einheit: string; +} const emptyOrderForm: BestellungFormData = { bezeichnung: '', lieferant_id: undefined, besteller_id: '', mitglied_id: undefined, notizen: '', positionen: [] }; const emptyVendorForm: LieferantFormData = { name: '', kontakt_name: '', email: '', telefon: '', adresse: '', website: '', notizen: '' }; -const emptyPosition: BestellpositionFormData = { bezeichnung: '', menge: 1, einheit: 'Stk' }; export default function BestellungNeu() { const navigate = useNavigate(); @@ -36,6 +98,16 @@ export default function BestellungNeu() { const [inlineVendorOpen, setInlineVendorOpen] = useState(false); const [inlineVendorForm, setInlineVendorForm] = useState({ ...emptyVendorForm }); + // Position state (split into catalog + free text) + const [catalogItems, setCatalogItems] = useState([{ bezeichnung: '', menge: 1, einheit: 'Stk' }]); + const [freeItems, setFreeItems] = useState([]); + + // Eigenschaften state + const [itemEigenschaften, setItemEigenschaften] = useState>({}); + const itemEigenschaftenRef = useRef(itemEigenschaften); + itemEigenschaftenRef.current = itemEigenschaften; + const [itemEigenschaftValues, setItemEigenschaftValues] = useState>>({}); + // ── Queries ── const { data: vendors = [] } = useQuery({ queryKey: ['lieferanten'], @@ -47,6 +119,11 @@ export default function BestellungNeu() { queryFn: bestellungApi.getOrderUsers, }); + const { data: allMembers = [] } = useQuery({ + queryKey: ['bestellungen', 'all-members'], + queryFn: bestellungApi.getAllMembers, + }); + const { data: katalogItems = [] } = useQuery({ queryKey: ['katalog'], queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }), @@ -76,9 +153,68 @@ export default function BestellungNeu() { onError: () => showError('Fehler beim Erstellen des Lieferanten'), }); + // ── Eigenschaft loading ── + const loadEigenschaften = useCallback(async (artikelId: number) => { + if (itemEigenschaftenRef.current[artikelId]) return; + try { + const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(artikelId); + if (eigs && eigs.length > 0) { + setItemEigenschaften(prev => ({ ...prev, [artikelId]: eigs })); + } + } catch (err) { + console.warn('Failed to load eigenschaften for artikel', artikelId, err); + } + }, []); + + // ── Submit ── const handleSubmit = () => { if (!orderForm.bezeichnung.trim()) return; - createOrder.mutate(orderForm); + + // Build catalog positions with spezifikationen from Eigenschaft values + const catalogPositionen = catalogItems + .filter(i => i.bezeichnung.trim() && i.artikel_id) + .map((item, idx) => { + const vals = itemEigenschaftValues[idx] || {}; + const spezifikationen = Object.entries(vals) + .filter(([, wert]) => wert.trim()) + .map(([eid, wert]) => { + const eig = item.artikel_id ? itemEigenschaften[item.artikel_id]?.find(e => e.id === Number(eid)) : null; + return `${eig?.name || eid}: ${wert}`; + }); + return { + bezeichnung: item.bezeichnung, + menge: item.menge, + einheit: item.einheit, + artikel_id: item.artikel_id, + spezifikationen: spezifikationen.length > 0 ? spezifikationen : undefined, + }; + }); + + // Build free text positions + const freePositionen = freeItems + .filter(i => i.bezeichnung.trim()) + .map(i => ({ + bezeichnung: i.bezeichnung, + menge: i.menge, + einheit: i.einheit, + })); + + // Check required Eigenschaften for catalog items + for (let idx = 0; idx < catalogItems.length; idx++) { + const item = catalogItems[idx]; + if (!item.bezeichnung.trim() || !item.artikel_id) continue; + if (itemEigenschaften[item.artikel_id]) { + for (const e of itemEigenschaften[item.artikel_id]) { + if (e.pflicht && !(itemEigenschaftValues[idx]?.[e.id]?.trim())) { + showError(`Pflichtfeld "${e.name}" für "${item.bezeichnung}" fehlt`); + return; + } + } + } + } + + const allPositionen = [...catalogPositionen, ...freePositionen]; + createOrder.mutate({ ...orderForm, positionen: allPositionen }); }; return ( @@ -151,10 +287,10 @@ export default function BestellungNeu() { /> o.name} - value={orderUsers.find((u) => u.id === orderForm.mitglied_id) || null} - onChange={(_e, v) => setOrderForm((f) => ({ ...f, mitglied_id: v?.id || '' }))} + value={allMembers.find((u) => u.id === orderForm.mitglied_id) || null} + onChange={(_e, v) => setOrderForm((f) => ({ ...f, mitglied_id: v?.id }))} renderInput={(params) => } /> @@ -166,90 +302,136 @@ export default function BestellungNeu() { onChange={(e) => setOrderForm((f) => ({ ...f, notizen: e.target.value }))} /> - {/* ── Positionen ── */} - Positionen - {(orderForm.positionen || []).map((pos, idx) => ( - - - freeSolo - size="small" - options={katalogItems} - getOptionLabel={(o) => typeof o === 'string' ? o : o.bezeichnung} - value={pos.bezeichnung || ''} - onChange={(_, v) => { - const next = [...(orderForm.positionen || [])]; - 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) => } - renderOption={(props, option) => ( -
  • - - {option.bezeichnung} - {option.kategorie_name && {option.kategorie_name}} - -
  • - )} - isOptionEqualToValue={(o, v) => o.id === (typeof v === 'object' ? v.id : undefined)} - sx={{ flexGrow: 1 }} - /> - { - const next = [...(orderForm.positionen || [])]; - next[idx] = { ...next[idx], menge: Math.max(1, Number(e.target.value) || 1) }; - setOrderForm((f) => ({ ...f, positionen: next })); - }} - sx={{ width: 90 }} - inputProps={{ min: 1 }} - /> - { - const next = [...(orderForm.positionen || [])]; - next[idx] = { ...next[idx], einheit: e.target.value }; - setOrderForm((f) => ({ ...f, positionen: next })); - }} - sx={{ width: 100 }} - /> - { - const next = (orderForm.positionen || []).filter((_, i) => i !== idx); - setOrderForm((f) => ({ ...f, positionen: next })); - }} - > - - + {/* ── Positionen: Katalogartikel ── */} + + Aus Katalog + {catalogItems.map((item, idx) => ( + + + + options={katalogItems} + getOptionLabel={(o) => o.bezeichnung} + value={item.artikel_id ? katalogItems.find(c => c.id === item.artikel_id) || null : null} + onChange={(_, v) => { + if (v) { + setCatalogItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it)); + loadEigenschaften(v.id); + } else { + setCatalogItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: undefined, bezeichnung: '' } : it)); + setItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; }); + } + }} + renderInput={(params) => } + renderOption={(props, option) => ( +
  • + + {option.bezeichnung} + {option.kategorie_name && {option.kategorie_name}} + +
  • + )} + isOptionEqualToValue={(o, v) => o.id === v.id} + sx={{ flexGrow: 1 }} + /> + setCatalogItems(prev => prev.map((it, i) => i === idx ? { ...it, menge: Math.max(1, Number(e.target.value) || 1) } : it))} + sx={{ width: 90 }} + inputProps={{ min: 1 }} + /> + setCatalogItems(prev => prev.map((it, i) => i === idx ? { ...it, einheit: e.target.value } : it))} + sx={{ width: 100 }} + /> + { + setCatalogItems(prev => prev.filter((_, i) => i !== idx)); + setItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; }); + }} + disabled={catalogItems.length <= 1 && freeItems.length === 0} + > + + +
    + {item.artikel_id && itemEigenschaften[item.artikel_id] && itemEigenschaften[item.artikel_id].length > 0 && ( + setItemEigenschaftValues(prev => ({ + ...prev, + [idx]: { ...(prev[idx] || {}), [eid]: wert }, + }))} + /> + )}
    ))} + + {/* ── Positionen: Freitext ── */} + + Freitext-Positionen + {freeItems.length === 0 ? ( + Keine Freitext-Positionen. + ) : ( + freeItems.map((item, idx) => ( + + setFreeItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: e.target.value } : it))} + sx={{ flexGrow: 1 }} + /> + setFreeItems(prev => prev.map((it, i) => i === idx ? { ...it, menge: Math.max(1, Number(e.target.value) || 1) } : it))} + sx={{ width: 90 }} + inputProps={{ min: 1 }} + /> + setFreeItems(prev => prev.map((it, i) => i === idx ? { ...it, einheit: e.target.value } : it))} + sx={{ width: 100 }} + /> + setFreeItems(prev => prev.filter((_, i) => i !== idx))} + > + + + + )) + )} + {/* ── Submit ── */} diff --git a/frontend/src/services/bestellung.ts b/frontend/src/services/bestellung.ts index 98e4826..b039776 100644 --- a/frontend/src/services/bestellung.ts +++ b/frontend/src/services/bestellung.ts @@ -122,6 +122,12 @@ export const bestellungApi = { return r.data.data; }, + // ── All members (for "Für Mitglied" selector) ── + getAllMembers: async (): Promise> => { + const r = await api.get('/api/bestellungen/members'); + return r.data.data; + }, + // ── Catalog ── getKatalogItems: async (filters?: { search?: string; kategorie?: string }): Promise => { const params = new URLSearchParams(); diff --git a/frontend/src/types/ausruestungsanfrage.types.ts b/frontend/src/types/ausruestungsanfrage.types.ts index 95e9729..3ab08fe 100644 --- a/frontend/src/types/ausruestungsanfrage.types.ts +++ b/frontend/src/types/ausruestungsanfrage.types.ts @@ -97,6 +97,7 @@ export interface AusruestungAnfrage { geliefert_count?: number; items_count?: number; im_haus?: boolean; + im_haus_count?: number; } export interface AusruestungAnfragePosition { @@ -108,6 +109,7 @@ export interface AusruestungAnfragePosition { einheit?: string; notizen?: string; geliefert: boolean; + im_haus: boolean; erstellt_am: string; eigenschaften?: AusruestungPositionEigenschaft[]; ist_ersatz: boolean;