import { useState, useMemo, useEffect, useCallback } from 'react'; import { Box, Tab, Tabs, Typography, Grid, Button, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, MenuItem, Divider, Tooltip, } from '@mui/material'; import { Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, Check as CheckIcon, Close as CloseIcon, Settings as SettingsIcon, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useSearchParams, useNavigate } 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 { AUSRUESTUNG_STATUS_LABELS, AUSRUESTUNG_STATUS_COLORS } from '../types/ausruestungsanfrage.types'; import type { AusruestungAnfrageStatus, AusruestungAnfrage, AusruestungOverview, } 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}`; } const ACTIVE_STATUSES: AusruestungAnfrageStatus[] = ['offen', 'genehmigt', 'bestellt']; // ─── Category Management Dialog ────────────────────────────────────────────── interface KategorieDialogProps { open: boolean; onClose: () => void; } function KategorieDialog({ open, onClose }: KategorieDialogProps) { const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); const [newName, setNewName] = useState(''); const [newParentId, setNewParentId] = useState(null); const [editId, setEditId] = useState(null); const [editName, setEditName] = useState(''); const { data: kategorien = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'kategorien'], queryFn: () => ausruestungsanfrageApi.getKategorien(), enabled: open, }); const topLevel = useMemo(() => kategorien.filter(k => !k.parent_id), [kategorien]); const childrenOf = useCallback((parentId: number) => kategorien.filter(k => k.parent_id === parentId), [kategorien]); const createMut = useMutation({ mutationFn: ({ name, parentId }: { name: string; parentId?: number | null }) => ausruestungsanfrageApi.createKategorie(name, parentId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie erstellt'); setNewName(''); setNewParentId(null); }, onError: () => showError('Fehler beim Erstellen'), }); const updateMut = useMutation({ mutationFn: ({ id, name }: { id: number; name: string }) => ausruestungsanfrageApi.updateKategorie(id, { name }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie aktualisiert'); setEditId(null); }, onError: () => showError('Fehler beim Aktualisieren'), }); const deleteMut = useMutation({ mutationFn: (id: number) => ausruestungsanfrageApi.deleteKategorie(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Kategorie gelöscht'); }, onError: () => showError('Fehler beim Löschen'), }); const renderKategorie = (k: { id: number; name: string; parent_id?: number | null }, indent: number) => { const children = childrenOf(k.id); return ( {editId === k.id ? ( <> setEditName(e.target.value)} sx={{ flexGrow: 1 }} onKeyDown={e => { if (e.key === 'Enter' && editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }} /> { if (editName.trim()) updateMut.mutate({ id: k.id, name: editName.trim() }); }}> setEditId(null)}> ) : ( <> {indent > 0 && '└ '}{k.name} setNewParentId(k.id)}> { setEditId(k.id); setEditName(k.name); }}> deleteMut.mutate(k.id)}> )} {children.map(c => renderKategorie(c, indent + 1))} ); }; return ( Kategorien verwalten setNewName(e.target.value)} sx={{ flexGrow: 1 }} onKeyDown={e => { if (e.key === 'Enter' && newName.trim()) createMut.mutate({ name: newName.trim(), parentId: newParentId }); }} /> {newParentId && ( k.id === newParentId)?.name}`} size="small" onDelete={() => setNewParentId(null)} /> )} {topLevel.length === 0 ? ( Keine Kategorien vorhanden. ) : ( topLevel.map(k => renderKategorie(k, 0)) )} ); } // ─── Catalog Tab ──────────────────────────────────────────────────────────── function KatalogTab() { const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const queryClient = useQueryClient(); const navigate = useNavigate(); const canManage = hasPermission('ausruestungsanfrage:manage_catalog'); const canManageCategories = hasPermission('ausruestungsanfrage:manage_categories'); const [filterKategorie, setFilterKategorie] = useState(''); const [kategorieDialogOpen, setKategorieDialogOpen] = useState(false); const { data: items = [], isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'items', filterKategorie], queryFn: () => ausruestungsanfrageApi.getItems(filterKategorie ? { kategorie_id: filterKategorie as number } : undefined), }); const { data: kategorien = [] } = useQuery({ queryKey: ['ausruestungsanfrage', 'kategorien'], queryFn: () => ausruestungsanfrageApi.getKategorien(), }); const kategorieOptions = useMemo(() => { const map = new Map(kategorien.map(k => [k.id, k])); const getDisplayName = (k: { id: number; name: string; parent_id?: number | null }): string => { if (k.parent_id) { const parent = map.get(k.parent_id); if (parent) return `${parent.name} > ${k.name}`; } return k.name; }; return kategorien.map(k => ({ id: k.id, name: getDisplayName(k), isChild: !!k.parent_id })); }, [kategorien]); const deleteItemMut = useMutation({ mutationFn: (id: number) => ausruestungsanfrageApi.deleteItem(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['ausruestungsanfrage'] }); showSuccess('Artikel geloescht'); }, onError: () => showError('Fehler beim Loeschen'), }); return ( setFilterKategorie(e.target.value as number | '')} sx={{ minWidth: 200 }} > Alle {kategorieOptions.map(k => {k.name})} {canManageCategories && ( setKategorieDialogOpen(true)}> )} {isLoading ? ( Lade Katalog... ) : items.length === 0 ? ( Keine Artikel vorhanden. ) : ( Bezeichnung Kategorie Beschreibung {canManage && Aktionen} {items.map(item => ( navigate(`/ausruestungsanfrage/artikel/${item.id}`)} > {item.bezeichnung} {(item.eigenschaften_count ?? 0) > 0 && ( )} {kategorieOptions.find(k => k.id === item.kategorie_id)?.name || item.kategorie_name || item.kategorie || '-'} {item.beschreibung || '-'} {canManage && ( { e.stopPropagation(); deleteItemMut.mutate(item.id); }}> )} ))}
)} setKategorieDialogOpen(false)} /> {canManage && ( navigate('/ausruestungsanfrage/artikel/neu')} aria-label="Artikel hinzufuegen"> )}
); } // ─── My Requests Tab ──────────────────────────────────────────────────────── function MeineAnfragenTab() { const { hasPermission } = usePermissionContext(); const navigate = useNavigate(); const canCreate = hasPermission('ausruestungsanfrage:create_request'); const [statusFilter, setStatusFilter] = useState(ACTIVE_STATUSES); const { data: requests = [], isLoading } = useQuery({ queryKey: ['ausruestungsanfrage', 'myRequests'], queryFn: () => ausruestungsanfrageApi.getMyRequests(), }); 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 ( handleStatusFilterChange(e.target.value)} sx={{ minWidth: 200 }} > Aktive Anfragen Alle {(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => ( {AUSRUESTUNG_STATUS_LABELS[s]} ))} {filteredRequests.length === 0 ? ( Keine Anfragen vorhanden. ) : ( Anfrage ID Bezeichnung Status Positionen Geliefert Erstellt am {filteredRequests.map(r => ( navigate('/ausruestungsanfrage/' + r.id)}> {formatOrderId(r)} {r.bezeichnung || '-'} {r.positionen_count ?? r.items_count ?? '-'} {r.positionen_count != null && r.positionen_count > 0 ? `${r.geliefert_count ?? 0}/${r.positionen_count}` : '-'} {new Date(r.erstellt_am).toLocaleDateString('de-AT')} ))}
)} {canCreate && ( navigate('/ausruestungsanfrage/neu')} aria-label="Neue Anfrage erstellen"> )}
); } // ─── Admin All Requests Tab (merged with overview) ────────────────────────── function AlleAnfragenTab() { const navigate = useNavigate(); const [statusFilter, setStatusFilter] = useState('alle'); const { data: requests = [], isLoading: requestsLoading, isError: requestsError } = useQuery({ queryKey: ['ausruestungsanfrage', 'allRequests', statusFilter], queryFn: () => ausruestungsanfrageApi.getRequests(statusFilter !== 'alle' ? { status: statusFilter } : undefined), }); const { data: overview } = useQuery({ queryKey: ['ausruestungsanfrage', 'overview-cards'], queryFn: () => ausruestungsanfrageApi.getOverview(), }); return ( {overview?.pending_count ?? '-'} Offene {overview?.approved_count ?? '-'} Genehmigte {overview?.unhandled_count ?? '-'} Neue (unbearbeitet) {overview?.total_items ?? '-'} Gesamt Artikel setStatusFilter(e.target.value)} sx={{ minWidth: 200, mb: 2 }} > Alle {(Object.keys(AUSRUESTUNG_STATUS_LABELS) as AusruestungAnfrageStatus[]).map(s => ( {AUSRUESTUNG_STATUS_LABELS[s]} ))} {requestsLoading ? ( Lade Anfragen... ) : requestsError ? ( Fehler beim Laden der Anfragen. ) : requests.length === 0 ? ( Keine Anfragen vorhanden. ) : ( Anfrage ID Bezeichnung Anfrage für Status Positionen Geliefert Erstellt am {requests.map(r => ( navigate('/ausruestungsanfrage/' + r.id)}> {formatOrderId(r)} {r.bezeichnung || '-'} {r.fuer_benutzer_name || r.anfrager_name || r.anfrager_id} {r.positionen_count ?? r.items_count ?? '-'} {r.positionen_count != null && r.positionen_count > 0 ? `${r.geliefert_count ?? 0}/${r.positionen_count}` : '-'} {new Date(r.erstellt_am).toLocaleDateString('de-AT')} ))}
)}
); } // ─── 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 tabCount = (canCreate ? 1 : 0) + (canApprove ? 1 : 0) + (canView ? 1 : 0); const [activeTab, setActiveTab] = useState(() => { const t = Number(searchParams.get('tab')); return t >= 0 && t < tabCount ? t : 0; }); 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 (canView) { map.katalog = next; next++; } return map; }, [canCreate, canApprove, canView]); if (!canView && !canCreate && !canApprove) { return ( Keine Berechtigung. ); } return ( Interne Bestellungen {canCreate && } {canApprove && } {canView && } {canCreate && activeTab === tabIndex.meine && } {canApprove && activeTab === tabIndex.alle && } {canView && activeTab === tabIndex.katalog && } ); }