import { useState, useMemo, useEffect } from 'react'; import { Box, Tab, Tabs, Typography, Card, CardContent, CardActions, Grid, Button, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, MenuItem, Select, FormControl, InputLabel, Autocomplete, Divider, } from '@mui/material'; import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, ShoppingCart, Check as CheckIcon, Close as CloseIcon, Link as LinkIcon, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useSearchParams } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { useNotification } from '../contexts/NotificationContext'; import { usePermissionContext } from '../contexts/PermissionContext'; import { ausruestungsanfrageApi } from '../services/ausruestungsanfrage'; import { bestellungApi } from '../services/bestellung'; import { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/ausruestungsanfrage.types'; import type { AusruestungArtikel, AusruestungArtikelFormData, AusruestungAnfrageFormItem, AusruestungAnfrageDetailResponse, AusruestungAnfrageStatus, AusruestungAnfrage, AusruestungOverview } from '../types/ausruestungsanfrage.types'; import type { Bestellung } from '../types/bestellung.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}`; } const ACTIVE_STATUSES: AusruestungAnfrageStatus[] = ['offen', 'genehmigt', 'bestellt']; // ─── Detail Modal ───────────────────────────────────────────────────────────── interface DetailModalProps { requestId: number | null; onClose: () => void; showAdminActions?: boolean; showEditButton?: boolean; canEditAny?: boolean; currentUserId?: string; } function DetailModal({ requestId, onClose, showAdminActions, showEditButton, canEditAny, currentUserId }: DetailModalProps) { const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); const { hasPermission } = usePermissionContext(); 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(''); const [linkDialog, setLinkDialog] = useState(false); const [selectedBestellung, setSelectedBestellung] = useState(null); const { data: detail, isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'request', requestId], queryFn: () => ausruestungsanfrageApi.getRequest(requestId!), enabled: requestId != null, }); const { data: catalogItems = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'items-for-edit'], queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }), enabled: editing, }); const { data: bestellungen = [] } = useQuery({ queryKey: ['bestellungen'], queryFn: () => bestellungApi.getOrders(), enabled: linkDialog, }); 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: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Status aktualisiert'); setActionDialog(null); setAdminNotizen(''); setStatusChangeValue(''); }, onError: () => showError('Fehler beim Aktualisieren'), }); const linkMut = useMutation({ mutationFn: (bestellungId: number) => ausruestungsanfrageApi.linkToOrder(requestId!, bestellungId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Verknüpfung erstellt'); setLinkDialog(false); setSelectedBestellung(null); }, onError: () => showError('Fehler beim Verknüpfen'), }); 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, }))); setEditing(true); }; const handleSaveEdit = () => { if (editItems.length === 0) return; updateMut.mutate({ bezeichnung: editBezeichnung || undefined, notizen: editNotizen || undefined, items: editItems, }); }; 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)); }; if (!requestId) return null; const anfrage = detail?.anfrage; const canEdit = anfrage && ( canEditAny || (anfrage.anfrager_id === currentUserId && anfrage.status === 'offen') ); return ( <> Anfrage {anfrage ? formatOrderId(anfrage) : '...'} {anfrage?.bezeichnung && ` — ${anfrage.bezeichnung}`} {anfrage && ( )} {isLoading ? ( Lade Details... ) : !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)); } }} 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)}> ))} ) : ( /* ── View Mode ── */ <> {anfrage!.anfrager_name && ( Anfrager: {anfrage!.anfrager_name} )} {anfrage!.notizen && ( Notizen: {anfrage!.notizen} )} {anfrage!.admin_notizen && ( Admin Notizen: {anfrage!.admin_notizen} )} Erstellt am: {new Date(anfrage!.erstellt_am).toLocaleDateString('de-AT')} Positionen {detail.positionen.map(p => ( - {p.menge}x {p.bezeichnung}{p.notizen ? ` (${p.notizen})` : ''} ))} {detail.linked_bestellungen && detail.linked_bestellungen.length > 0 && ( <> Verknüpfte Bestellungen {detail.linked_bestellungen.map(b => ( ))} )} )} {editing ? ( <> ) : ( <> {/* Admin actions */} {showAdminActions && anfrage && anfrage.status === 'offen' && ( <> )} {showAdminActions && anfrage && hasPermission('ausruestungsanfrage:approve') && ( Status ändern )} {showAdminActions && anfrage && anfrage.status === 'genehmigt' && hasPermission('ausruestungsanfrage:link_orders') && ( )} {(showEditButton || canEditAny) && 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} /> {/* Link to order sub-dialog */} { setLinkDialog(false); setSelectedBestellung(null); }} maxWidth="sm" fullWidth> Mit Bestellung verknüpfen `#${o.id} – ${o.bezeichnung}`} value={selectedBestellung} onChange={(_, v) => setSelectedBestellung(v)} renderInput={params => } /> ); } // ─── Catalog Tab ──────────────────────────────────────────────────────────── function KatalogTab() { const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const queryClient = useQueryClient(); const canManage = hasPermission('ausruestungsanfrage:manage_catalog'); const [filterKategorie, setFilterKategorie] = useState(''); const [artikelDialogOpen, setArtikelDialogOpen] = useState(false); const [editArtikel, setEditArtikel] = useState(null); const [artikelForm, setArtikelForm] = useState({ bezeichnung: '' }); const { data: items = [], isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'items', filterKategorie], queryFn: () => ausruestungsanfrageApi.getItems(filterKategorie ? { kategorie: filterKategorie } : undefined), }); const { data: categories = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'categories'], queryFn: () => ausruestungsanfrageApi.getCategories(), }); const createItemMut = useMutation({ mutationFn: (data: AusruestungArtikelFormData) => ausruestungsanfrageApi.createItem(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel erstellt'); setArtikelDialogOpen(false); }, onError: () => showError('Fehler beim Erstellen'), }); const updateItemMut = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => ausruestungsanfrageApi.updateItem(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel aktualisiert'); setArtikelDialogOpen(false); }, onError: () => showError('Fehler beim Aktualisieren'), }); const deleteItemMut = useMutation({ mutationFn: (id: number) => ausruestungsanfrageApi.deleteItem(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel gelöscht'); }, onError: () => showError('Fehler beim Löschen'), }); const openNewArtikel = () => { setEditArtikel(null); setArtikelForm({ bezeichnung: '' }); setArtikelDialogOpen(true); }; const openEditArtikel = (a: AusruestungArtikel) => { setEditArtikel(a); setArtikelForm({ bezeichnung: a.bezeichnung, beschreibung: a.beschreibung, kategorie: a.kategorie }); setArtikelDialogOpen(true); }; const saveArtikel = () => { if (!artikelForm.bezeichnung.trim()) return; if (editArtikel) updateItemMut.mutate({ id: editArtikel.id, data: artikelForm }); else createItemMut.mutate(artikelForm); }; return ( {/* Filter */} Kategorie {/* Catalog grid */} {isLoading ? ( Lade Katalog... ) : items.length === 0 ? ( Keine Artikel vorhanden. ) : ( {items.map(item => ( {item.bild_pfad ? ( ) : ( )} {item.bezeichnung} {item.beschreibung && {item.beschreibung}} {item.kategorie && ( )} {canManage && ( openEditArtikel(item)}> deleteItemMut.mutate(item.id)}> )} ))} )} {/* Artikel create/edit dialog */} setArtikelDialogOpen(false)} maxWidth="sm" fullWidth> {editArtikel ? 'Artikel bearbeiten' : 'Neuer Artikel'} setArtikelForm(f => ({ ...f, bezeichnung: e.target.value }))} fullWidth /> setArtikelForm(f => ({ ...f, beschreibung: e.target.value }))} /> setArtikelForm(f => ({ ...f, kategorie: val || undefined }))} renderInput={params => } /> {/* FAB for new catalog item */} {canManage && ( )} ); } // ─── My Requests Tab ──────────────────────────────────────────────────────── function MeineAnfragenTab() { const { hasPermission } = usePermissionContext(); const queryClient = useQueryClient(); const { showSuccess, showError } = useNotification(); const canCreate = hasPermission('ausruestungsanfrage:create_request'); const canOrderForUser = hasPermission('ausruestungsanfrage:order_for_user'); const canEditAny = hasPermission('ausruestungsanfrage:edit'); const [detailId, setDetailId] = useState(null); const [statusFilter, setStatusFilter] = useState(ACTIVE_STATUSES); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [newBezeichnung, setNewBezeichnung] = useState(''); const [newNotizen, setNewNotizen] = useState(''); const [newFuerBenutzer, setNewFuerBenutzer] = useState<{ id: string; name: string } | null>(null); const [newItems, setNewItems] = useState([{ bezeichnung: '', menge: 1 }]); const { data: requests = [], isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'myRequests'], queryFn: () => ausruestungsanfrageApi.getMyRequests(), }); const { data: catalogItems = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'items-for-create'], queryFn: () => ausruestungsanfrageApi.getItems({ aktiv: true }), enabled: createDialogOpen, }); const { data: orderUsers = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'orderUsers'], queryFn: () => ausruestungsanfrageApi.getOrderUsers(), enabled: createDialogOpen && canOrderForUser, }); const createMut = useMutation({ mutationFn: (args: { items: AusruestungAnfrageFormItem[]; notizen?: string; bezeichnung?: string; fuer_benutzer_id?: string }) => ausruestungsanfrageApi.createRequest(args.items, args.notizen, args.bezeichnung, args.fuer_benutzer_id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Anfrage erstellt'); setCreateDialogOpen(false); resetCreateForm(); }, onError: () => showError('Fehler beim Erstellen der Anfrage'), }); const resetCreateForm = () => { setNewBezeichnung(''); setNewNotizen(''); setNewFuerBenutzer(null); setNewItems([{ bezeichnung: '', menge: 1 }]); }; const handleCreateSubmit = () => { const validItems = newItems.filter(i => i.bezeichnung.trim()); if (validItems.length === 0) return; createMut.mutate({ items: validItems, notizen: newNotizen || undefined, bezeichnung: newBezeichnung || undefined, fuer_benutzer_id: newFuerBenutzer?.id, }); }; const filteredRequests = useMemo(() => { if (statusFilter.length === 0) return requests; return requests.filter(r => statusFilter.includes(r.status)); }, [requests, statusFilter]); const handleStatusFilterChange = (value: string) => { if (value === 'all') { setStatusFilter([]); } else if (value === 'active') { setStatusFilter(ACTIVE_STATUSES); } else { setStatusFilter([value as AusruestungAnfrageStatus]); } }; const currentFilterValue = useMemo(() => { if (statusFilter.length === 0) return 'all'; if (statusFilter.length === ACTIVE_STATUSES.length && ACTIVE_STATUSES.every(s => statusFilter.includes(s))) return 'active'; return statusFilter[0] || 'all'; }, [statusFilter]); if (isLoading) return Lade Anfragen...; return ( Status {filteredRequests.length === 0 ? ( Keine Anfragen vorhanden. ) : ( Anfrage Bezeichnung Status Positionen Erstellt am {filteredRequests.map(r => ( setDetailId(r.id)}> {formatOrderId(r)} {r.bezeichnung || '-'} {r.positionen_count ?? r.items_count ?? '-'} {new Date(r.erstellt_am).toLocaleDateString('de-AT')} ))}
)} {/* Detail Modal */} setDetailId(null)} showEditButton canEditAny={canEditAny} /> {/* Create Request Dialog */} { setCreateDialogOpen(false); resetCreateForm(); }} maxWidth="sm" fullWidth> Neue Bestellung setNewBezeichnung(e.target.value)} fullWidth /> {canOrderForUser && ( o.name} value={newFuerBenutzer} onChange={(_, v) => setNewFuerBenutzer(v)} renderInput={params => } /> )} setNewNotizen(e.target.value)} multiline rows={2} fullWidth /> Positionen {newItems.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') { setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: v, artikel_id: undefined } : it)); } else if (v) { setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, artikel_id: v.id, bezeichnung: v.bezeichnung } : it)); } }} onInputChange={(_, val, reason) => { if (reason === 'input') { setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, bezeichnung: val, artikel_id: undefined } : it)); } }} renderInput={params => } sx={{ flexGrow: 1 }} /> setNewItems(prev => prev.map((it, i) => i === idx ? { ...it, menge: Math.max(1, Number(e.target.value)) } : it))} sx={{ width: 90 }} inputProps={{ min: 1 }} /> setNewItems(prev => prev.filter((_, i) => i !== idx))} disabled={newItems.length <= 1}> ))} {/* FAB for creating new request */} {canCreate && ( setCreateDialogOpen(true)} aria-label="Neue Anfrage erstellen"> )}
); } // ─── Admin All Requests Tab ───────────────────────────────────────────────── function AlleAnfragenTab() { const { hasPermission } = usePermissionContext(); const [statusFilter, setStatusFilter] = useState(''); const [detailId, setDetailId] = useState(null); const canEditAny = hasPermission('ausruestungsanfrage:edit'); const { data: requests = [], isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'requests', statusFilter], queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter ? { status: statusFilter } : undefined), }); // Summary counts const openCount = useMemo(() => requests.filter(r => r.status === 'offen').length, [requests]); const approvedCount = useMemo(() => requests.filter(r => r.status === 'genehmigt').length, [requests]); if (isLoading) return Lade Anfragen...; return ( {/* Summary cards */} {openCount} Offene Anfragen {approvedCount} Genehmigte Anfragen {requests.length} Alle Anfragen Status Filter {requests.length === 0 ? ( Keine Anfragen vorhanden. ) : ( Anfrage Bezeichnung Anfrager Status Positionen Erstellt am {requests.map(r => ( setDetailId(r.id)}> {formatOrderId(r)} {r.bezeichnung || '-'} {r.anfrager_name || r.anfrager_id} {r.positionen_count ?? r.items_count ?? '-'} {new Date(r.erstellt_am).toLocaleDateString('de-AT')} ))}
)} {/* Detail Modal with admin actions */} setDetailId(null)} showAdminActions showEditButton canEditAny={canEditAny} />
); } // ─── Overview Tab ──────────────────────────────────────────────────────────── function UebersichtTab() { const { data: overview, isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'overview'], queryFn: () => ausruestungsanfrageApi.getOverview(), }); if (isLoading) return Lade Übersicht...; if (!overview) return Keine Daten verfügbar.; return ( {overview.pending_count} Offene Anfragen {overview.approved_count} Genehmigte Anfragen {overview.total_items} Artikel insgesamt {overview.items.length === 0 ? ( Keine offenen/genehmigten Anfragen vorhanden. ) : ( Artikel Gesamtmenge Anfragen {overview.items.map(item => ( {item.bezeichnung} {item.total_menge} {item.anfrage_count} ))}
)}
); } // ─── Main Page ────────────────────────────────────────────────────────────── export default function Ausruestungsanfrage() { const [searchParams, setSearchParams] = useSearchParams(); const { hasPermission } = usePermissionContext(); const canView = hasPermission('ausruestungsanfrage:view'); const canCreate = hasPermission('ausruestungsanfrage:create_request'); const canApprove = hasPermission('ausruestungsanfrage:approve'); const canViewAll = hasPermission('ausruestungsanfrage:view_all'); const tabCount = 1 + (canCreate ? 1 : 0) + (canApprove ? 1 : 0) + (canViewAll ? 1 : 0); const [activeTab, setActiveTab] = useState(() => { const t = Number(searchParams.get('tab')); return t >= 0 && t < tabCount ? t : 0; }); // Sync tab from URL changes (e.g. sidebar navigation) useEffect(() => { const t = Number(searchParams.get('tab')); if (t >= 0 && t < tabCount) setActiveTab(t); }, [searchParams, tabCount]); const handleTabChange = (_: React.SyntheticEvent, val: number) => { setActiveTab(val); setSearchParams({ tab: String(val) }, { replace: true }); }; const tabIndex = useMemo(() => { const map: Record = {}; let next = 0; if (canCreate) { map.meine = next; next++; } if (canApprove) { map.alle = next; next++; } if (canViewAll) { map.uebersicht = next; next++; } map.katalog = next; return map; }, [canCreate, canApprove, canViewAll]); if (!canView) { return ( Keine Berechtigung. ); } return ( Interne Bestellungen {canCreate && } {canApprove && } {canViewAll && } {canCreate && activeTab === tabIndex.meine && } {canApprove && activeTab === tabIndex.alle && } {canViewAll && activeTab === tabIndex.uebersicht && } {activeTab === tabIndex.katalog && } ); }