import { useState, useCallback } from 'react'; import { Box, Typography, Paper, Button, Chip, IconButton, Table, TableBody, TableCell, TableHead, TableRow, Dialog, DialogTitle, DialogContent, DialogActions, TextField, MenuItem, Select, FormControl, InputLabel, Autocomplete, Checkbox, LinearProgress, Switch, FormControlLabel, Alert, } from '@mui/material'; import { ArrowBack, Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, Check as CheckIcon, Close as CloseIcon, ShoppingCart as ShoppingCartIcon, Assignment as AssignmentIcon, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { useNotification } from '../contexts/NotificationContext'; import { usePermissionContext } from '../contexts/PermissionContext'; import { useAuth } from '../contexts/AuthContext'; import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage'; import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/ausruestungsanfrage.types'; import type { AusruestungAnfrage, AusruestungAnfrageDetailResponse, AusruestungAnfrageFormItem, AusruestungAnfrageStatus, AusruestungAnfragePosition, AusruestungEigenschaft, } from '../types/ausruestungsanfrage.types'; // ── Helpers ── function formatOrderId(r: AusruestungAnfrage): string { if (r.bestell_jahr && r.bestell_nummer) { return `${r.bestell_jahr}/${String(r.bestell_nummer).padStart(3, '0')}`; } return `#${r.id}`; } // ── Helpers ── function getUnassignedPositions(positions: AusruestungAnfragePosition[]): AusruestungAnfragePosition[] { return positions.filter((p) => p.geliefert && (!p.zuweisung_typ || p.zuweisung_typ === 'keine')); } // ══════════════════════════════════════════════════════════════════════════════ // Component // ══════════════════════════════════════════════════════════════════════════════ export default function AusruestungsanfrageDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const { user } = useAuth(); const requestId = Number(id); // ── State ── const [editing, setEditing] = useState(false); const [editBezeichnung, setEditBezeichnung] = useState(''); const [editNotizen, setEditNotizen] = useState(''); const [editItems, setEditItems] = useState([]); // Admin action state const [actionDialog, setActionDialog] = useState<{ action: 'genehmigt' | 'abgelehnt' } | null>(null); const [adminNotizen, setAdminNotizen] = useState(''); const [statusChangeValue, setStatusChangeValue] = useState(''); // Eigenschaften state for edit mode const [editItemEigenschaften, setEditItemEigenschaften] = useState>({}); const [editItemEigenschaftValues, setEditItemEigenschaftValues] = useState>>({}); // Permissions const showAdminActions = hasPermission('ausruestungsanfrage:approve'); const canEditAny = hasPermission('ausruestungsanfrage:edit'); const canLink = hasPermission('ausruestungsanfrage:link_orders'); // ── Queries ── const { data: detail, isLoading, isError } = useQuery({ queryKey: ['ausruestungsanfrage', 'request', requestId], queryFn: () => ausruestungsanfrageApi.getRequest(requestId), enabled: !isNaN(requestId), retry: 1, }); const { data: catalogItems = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'items-for-edit'], queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }), staleTime: 5 * 60 * 1000, }); const loadEigenschaftenForItem = useCallback(async (artikelId: number) => { if (editItemEigenschaften[artikelId]) return; try { const eigs = await ausruestungsanfrageApi.getArtikelEigenschaften(artikelId); if (eigs?.length > 0) setEditItemEigenschaften(prev => ({ ...prev, [artikelId]: eigs })); } catch { /* ignore */ } }, [editItemEigenschaften]); // ── Mutations ── const updateMut = useMutation({ mutationFn: (data: { bezeichnung?: string; notizen?: string; items?: AusruestungAnfrageFormItem[] }) => ausruestungsanfrageApi.updateRequest(requestId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Anfrage aktualisiert'); setEditing(false); }, onError: () => showError('Fehler beim Aktualisieren'), }); const statusMut = useMutation({ mutationFn: ({ status, notes }: { status: string; notes?: string }) => ausruestungsanfrageApi.updateRequestStatus(requestId, status, notes), onSuccess: (_data, variables) => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Status aktualisiert'); setActionDialog(null); setAdminNotizen(''); setStatusChangeValue(''); // Auto-navigate to assignment page when status changes to 'erledigt' and unassigned positions exist if (variables.status === 'erledigt' && detail) { const unassigned = getUnassignedPositions(detail.positionen); if (unassigned.length > 0) { navigate(`/ausruestungsanfrage/${requestId}/zuweisung`); } } }, onError: () => showError('Fehler beim Aktualisieren'), }); const geliefertMut = useMutation({ mutationFn: ({ positionId, geliefert }: { positionId: number; geliefert: boolean }) => ausruestungsanfrageApi.updatePositionGeliefert(positionId, geliefert), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); }, onError: () => showError('Fehler beim Aktualisieren'), }); const zurueckgegebenMut = useMutation({ mutationFn: ({ positionId, zurueckgegeben }: { positionId: number; zurueckgegeben: boolean }) => ausruestungsanfrageApi.updatePositionZurueckgegeben(positionId, zurueckgegeben), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); }, onError: () => showError('Fehler beim Aktualisieren'), }); // ── Edit helpers ── const startEditing = () => { if (!detail) return; setEditBezeichnung(detail.anfrage.bezeichnung || ''); setEditNotizen(detail.anfrage.notizen || ''); setEditItems(detail.positionen.map(p => ({ artikel_id: p.artikel_id, bezeichnung: p.bezeichnung, menge: p.menge, notizen: p.notizen, eigenschaften: p.eigenschaften?.map(e => ({ eigenschaft_id: e.eigenschaft_id, wert: e.wert })), ist_ersatz: p.ist_ersatz || false, }))); const initVals: Record> = {}; detail.positionen.forEach((p, idx) => { if (p.artikel_id) loadEigenschaftenForItem(p.artikel_id); if (p.eigenschaften?.length) { initVals[idx] = {}; p.eigenschaften.forEach(e => { initVals[idx][e.eigenschaft_id] = e.wert; }); } }); setEditItemEigenschaftValues(initVals); setEditing(true); }; const handleSaveEdit = () => { if (editItems.length === 0) return; const items = editItems.map((item, idx) => { const vals = editItemEigenschaftValues[idx] || {}; const eigenschaften = Object.entries(vals) .filter(([, wert]) => wert.trim()) .map(([eid, wert]) => ({ eigenschaft_id: Number(eid), wert })); return { ...item, eigenschaften: eigenschaften.length > 0 ? eigenschaften : undefined }; }); updateMut.mutate({ bezeichnung: editBezeichnung || undefined, notizen: editNotizen || undefined, items, }); }; const addEditItem = () => { setEditItems(prev => [...prev, { bezeichnung: '', menge: 1 }]); }; const removeEditItem = (idx: number) => { setEditItems(prev => prev.filter((_, i) => i !== idx)); }; const updateEditItem = (idx: number, field: string, value: unknown) => { setEditItems(prev => prev.map((item, i) => i === idx ? { ...item, [field]: value } : item)); }; const anfrage = detail?.anfrage; const canEdit = anfrage && ( canEditAny || (anfrage.anfrager_id === user?.id && anfrage.status === 'offen') ); return ( {/* Header */} navigate('/ausruestungsanfrage')}> Anfrage {anfrage ? formatOrderId(anfrage) : '...'} {anfrage?.bezeichnung && ` — ${anfrage.bezeichnung}`} {anfrage && ( <> {detail?.im_haus && ( )} )} {isLoading ? ( ) : isError ? ( Fehler beim Laden der Anfrage. ) : !detail ? ( Anfrage nicht gefunden. ) : editing ? ( /* ── Edit Mode ── */ setEditBezeichnung(e.target.value)} fullWidth /> setEditNotizen(e.target.value)} multiline rows={2} fullWidth /> Positionen {editItems.map((item, idx) => ( typeof o === 'string' ? o : o.bezeichnung} value={item.artikel_id ? catalogItems.find(c => c.id === item.artikel_id) || item.bezeichnung : item.bezeichnung} onChange={(_, v) => { if (typeof v === 'string') { updateEditItem(idx, 'bezeichnung', v); updateEditItem(idx, 'artikel_id', undefined); } else if (v) { setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it)); loadEigenschaftenForItem(v.id); setEditItemEigenschaftValues(prev => { const n = { ...prev }; delete n[idx]; return n; }); } }} onInputChange={(_, val, reason) => { if (reason === 'input') { setEditItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: val, artikel_id: undefined } : it)); } }} renderInput={params => } sx={{ flexGrow: 1 }} /> updateEditItem(idx, 'menge', Math.max(1, Number(e.target.value)))} sx={{ width: 90 }} inputProps={{ min: 1 }} /> removeEditItem(idx)}> {item.artikel_id && editItemEigenschaften[item.artikel_id]?.length > 0 && ( {editItemEigenschaften[item.artikel_id].map(e => ( e.typ === 'options' && e.optionen?.length ? ( setEditItemEigenschaftValues(prev => ({ ...prev, [idx]: { ...(prev[idx] || {}), [e.id]: ev.target.value } }))} sx={{ minWidth: 140 }} > {e.optionen.map(opt => {opt})} ) : ( setEditItemEigenschaftValues(prev => ({ ...prev, [idx]: { ...(prev[idx] || {}), [e.id]: ev.target.value } }))} sx={{ minWidth: 160 }} /> ) ))} )} updateEditItem(idx, 'ist_ersatz', e.target.checked)} /> } label="Ersatzbeschaffung" /> {item.ist_ersatz && ( Altes Gerät muss zurückgegeben werden )} ))} ) : ( /* ── View Mode ── */ <> {/* Meta info */} {(anfrage!.anfrager_name || anfrage!.fuer_benutzer_name) && ( Anfrage für {anfrage!.fuer_benutzer_name ? `${anfrage!.fuer_benutzer_name} (erstellt von ${anfrage!.anfrager_name || 'Unbekannt'})` : anfrage!.anfrager_name} )} Erstellt am {new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')} {anfrage!.bearbeitet_von_name && ( Bearbeitet von {anfrage!.bearbeitet_von_name} )} {anfrage!.notizen && ( Notizen {anfrage!.notizen} )} {anfrage!.admin_notizen && ( Admin Notizen {anfrage!.admin_notizen} )} {/* Positionen */} Positionen ({detail.positionen.length}) {showAdminActions && Geliefert} Artikel Menge Details {detail.positionen.map(p => ( {showAdminActions && ( geliefertMut.mutate({ positionId: p.id, geliefert: checked })} /> )} {p.bezeichnung} {p.ist_ersatz && ( )} {p.geliefert && detail?.im_haus && ( )} {p.geliefert && p.zuweisung_typ === 'keine' && ( )} {p.geliefert && p.zuweisung_typ === 'persoenlich' && ( )} {p.geliefert && p.zuweisung_typ === 'ausruestung' && ( )} {p.eigenschaften && p.eigenschaften.length > 0 && p.eigenschaften.map(e => ( ))} {p.menge}x {p.notizen && {p.notizen}} {p.ist_ersatz && ( zurueckgegebenMut.mutate({ positionId: p.id, zurueckgegeben: checked })} /> } label={Altes Gerät zurückgegeben} /> )} ))}
{/* Linked Bestellungen */} {detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && ( Verknüpfte Bestellungen {detail.linked_bestellungen.map(b => ( ))} )} {/* Action buttons */} {showAdminActions && anfrage && anfrage.status === 'offen' && ( <> )} {showAdminActions && anfrage && ( Status ändern )} {showAdminActions && anfrage && anfrage.status === 'genehmigt' && canLink && ( )} {showAdminActions && anfrage && anfrage.status === 'erledigt' && detail && getUnassignedPositions(detail.positionen).length > 0 && ( )} {canEdit && !editing && ( )} )} {/* Approve/Reject sub-dialog */} setActionDialog(null)} maxWidth="sm" fullWidth> {actionDialog?.action === 'genehmigt' ? 'Anfrage genehmigen' : 'Anfrage ablehnen'} setAdminNotizen(e.target.value)} multiline rows={2} />
); }