import { useState, useMemo } from 'react'; import { Box, Typography, Container, Button, Chip, TextField, Autocomplete, ToggleButton, ToggleButtonGroup, Stack, Divider, LinearProgress, MenuItem, } from '@mui/material'; import { Assignment as AssignmentIcon } from '@mui/icons-material'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; 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, AusruestungEigenschaft } from '../types/ausruestungsanfrage.types'; type AssignmentTyp = 'ausruestung' | 'persoenlich' | 'keine'; interface PositionAssignment { typ: AssignmentTyp; fahrzeugId?: string; standort?: string; userId?: string; benutzerName?: string; eigenschaften?: Record; // eigenschaft_id → wert } function getUnassignedPositions(positions: AusruestungAnfragePosition[]): AusruestungAnfragePosition[] { return positions.filter((p) => p.geliefert && (!p.zuweisung_typ || p.zuweisung_typ === 'keine')); } export default function AusruestungsanfrageZuweisung() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { showSuccess, showError } = useNotification(); const anfrageId = Number(id); const queryClient = useQueryClient(); const { hasPermission } = usePermissionContext(); const canManageCatalog = hasPermission('ausruestungsanfrage:manage_catalog'); const [createArtikelFor, setCreateArtikelFor] = useState(null); const [newArtikelBezeichnung, setNewArtikelBezeichnung] = useState(''); const [newArtikelSubmitting, setNewArtikelSubmitting] = useState(false); const { data: detail, isLoading, isError } = useQuery({ queryKey: ['ausruestungsanfrage', 'request', anfrageId], queryFn: () => ausruestungsanfrageApi.getRequest(anfrageId), enabled: !isNaN(anfrageId), retry: 1, }); const unassigned = useMemo(() => { if (!detail) return []; return getUnassignedPositions(detail.positionen); }, [detail]); const [assignments, setAssignments] = useState>({}); // Collect unique artikel_ids from unassigned positions to batch-load their eigenschaften const uniqueArtikelIds = useMemo( () => [...new Set(unassigned.filter(p => p.artikel_id).map(p => p.artikel_id!))], [unassigned], ); const { data: artikelEigenschaftenMap = {} } = useQuery>({ queryKey: ['ausruestungsanfrage', 'eigenschaften-batch', uniqueArtikelIds], queryFn: async () => { const results: Record = {}; await Promise.all( uniqueArtikelIds.map(async (aid) => { results[aid] = await ausruestungsanfrageApi.getArtikelEigenschaften(aid); }), ); return results; }, enabled: uniqueArtikelIds.length > 0, staleTime: 5 * 60 * 1000, }); // Initialize assignments when unassigned positions load useMemo(() => { if (unassigned.length > 0 && Object.keys(assignments).length === 0) { const init: Record = {}; for (const p of unassigned) { // Pre-fill eigenschaften from position values const prefilled: Record = {}; for (const e of p.eigenschaften ?? []) { prefilled[e.eigenschaft_id] = e.wert; } init[p.id] = { typ: 'persoenlich', eigenschaften: prefilled }; } setAssignments(init); } }, [unassigned]); const { data: vehicleList } = useQuery({ queryKey: ['vehicles', 'sidebar'], queryFn: () => vehiclesApi.getAll(), staleTime: 2 * 60 * 1000, }); const { data: membersList } = useQuery({ queryKey: ['members-list-compact'], queryFn: () => membersService.getMembers({ pageSize: 500 }), staleTime: 5 * 60 * 1000, }); const memberOptions = (membersList?.items ?? []).map((m) => ({ id: m.id, name: [m.given_name, m.family_name].filter(Boolean).join(' ') || m.email, })); const vehicleOptions = (vehicleList ?? []).map((v) => ({ id: v.id, 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) => { setAssignments((prev) => ({ ...prev, [posId]: { ...prev[posId], ...patch }, })); }; const handleSkipAll = () => { const updated: Record = {}; for (const p of unassigned) { updated[p.id] = { typ: 'keine' }; } setAssignments(updated); }; const handleCreateArtikel = async (posId: number) => { if (!newArtikelBezeichnung.trim()) return; setNewArtikelSubmitting(true); try { const newArtikel = await ausruestungsanfrageApi.createItem({ bezeichnung: newArtikelBezeichnung.trim(), aktiv: true }); await ausruestungsanfrageApi.linkPositionToArtikel(posId, newArtikel.id); queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage', 'request', anfrageId] }); setCreateArtikelFor(null); showSuccess('Katalogartikel erstellt und Position verknüpft'); } catch { showError('Fehler beim Erstellen des Katalogartikels'); } finally { setNewArtikelSubmitting(false); } }; const handleSubmit = async () => { if (!detail) return; setSubmitting(true); try { const anfrage = detail.anfrage; const posMap = Object.fromEntries(unassigned.map(p => [p.id, p])); const payload = Object.entries(assignments).map(([posId, a]) => { const pos = posMap[Number(posId)]; const artikelEigs: AusruestungEigenschaft[] = pos?.artikel_id ? (artikelEigenschaftenMap[pos.artikel_id] ?? []) : []; return { positionId: Number(posId), typ: a.typ, fahrzeugId: a.typ === 'ausruestung' ? a.fahrzeugId : undefined, 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, eigenschaften: a.typ === 'persoenlich' && a.eigenschaften ? Object.entries(a.eigenschaften) .filter(([, v]) => v.trim()) .map(([eid, wert]) => ({ eigenschaft_id: Number(eid), name: artikelEigs.find(e => e.id === Number(eid))?.name ?? '', wert, })) : 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 { setSubmitting(false); } }; const backPath = `/ausruestungsanfrage/${id}`; return ( {isLoading ? ( ) : isError || !detail ? ( Fehler beim Laden der Anfrage. ) : unassigned.length === 0 ? ( Keine unzugewiesenen Positionen vorhanden. ) : ( <> Wähle für jeden gelieferten Gegenstand, wie er erfasst werden soll. }> {unassigned.map((pos) => { const a = assignments[pos.id] ?? { typ: 'persoenlich' as const }; return ( {pos.bezeichnung} {!pos.artikel_id && ( {canManageCatalog && createArtikelFor !== pos.id && ( )} {createArtikelFor === pos.id && ( setNewArtikelBezeichnung(e.target.value)} sx={{ flex: 1 }} /> )} )} val && updateAssignment(pos.id, { typ: val })} sx={{ mb: 1.5 }} > Ausrüstung Persönlich Nicht zuweisen {a.typ === 'ausruestung' && ( o.name} value={vehicleOptions.find((v) => v.id === a.fahrzeugId) ?? null} onChange={(_e, v) => updateAssignment(pos.id, { fahrzeugId: v?.id })} renderInput={(params) => } sx={{ minWidth: 200, flex: 1 }} /> updateAssignment(pos.id, { standort: e.target.value })} sx={{ minWidth: 160, flex: 1 }} /> )} {a.typ === 'persoenlich' && ( o.name} value={memberOptions.find((m) => m.id === a.userId) ?? null} onChange={(_e, v) => updateAssignment(pos.id, { userId: v?.id, benutzerName: v?.name })} renderInput={(params) => ( )} sx={{ minWidth: 200, flex: 1 }} /> {(() => { 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.; })()} {/* Editable characteristic fields from article definitions */} {pos.artikel_id && (artikelEigenschaftenMap[pos.artikel_id] ?? []).map(e => e.typ === 'options' && e.optionen?.length ? ( updateAssignment(pos.id, { eigenschaften: { ...a.eigenschaften, [e.id]: ev.target.value }, })} > {e.optionen.map(opt => {opt})} ) : ( updateAssignment(pos.id, { eigenschaften: { ...a.eigenschaften, [e.id]: ev.target.value }, })} /> ) )} )} ); })} )} ); }