import { useState, useEffect, useMemo } from 'react'; import { Accordion, AccordionSummary, AccordionDetails, Box, IconButton, Tab, Tabs, Tooltip, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Chip, Button, Checkbox, FormControlLabel, FormGroup, LinearProgress, Divider, TextField, MenuItem, } from '@mui/material'; import { Add as AddIcon, ExpandMore as ExpandMoreIcon, FilterList as FilterListIcon, PictureAsPdf as PdfIcon } from '@mui/icons-material'; import { useQuery } from '@tanstack/react-query'; import { useNavigate, useSearchParams } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { KatalogTab } from '../components/shared/KatalogTab'; import { usePermissionContext } from '../contexts/PermissionContext'; import { bestellungApi } from '../services/bestellung'; import { configApi } from '../services/config'; import { addPdfHeader, addPdfFooter } from '../utils/pdfExport'; import { BESTELLUNG_STATUS_LABELS, BESTELLUNG_STATUS_COLORS } from '../types/bestellung.types'; import type { BestellungStatus, Bestellung } from '../types/bestellung.types'; // ── Helpers ── const formatCurrency = (value?: number) => value != null ? new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' }).format(value) : '–'; const formatDate = (iso?: string) => iso ? new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '–'; // ── Tab Panel ── interface TabPanelProps { children: React.ReactNode; index: number; value: number } function TabPanel({ children, value, index }: TabPanelProps) { if (value !== index) return null; return {children}; } const TAB_COUNT = 3; // ── Status options ── const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen']; const DEFAULT_EXCLUDED_STATUSES: BestellungStatus[] = ['abgeschlossen']; // ── Kennung formatter ── function formatKennung(o: Bestellung): string { if (o.laufende_nummer == null) return '–'; const year = new Date(o.erstellt_am).getFullYear(); return `${year}/${o.laufende_nummer}`; } // ── Brutto calculator ── function calcBrutto(o: Bestellung): number | undefined { if (o.total_cost == null) return undefined; const rate = (parseFloat(String(o.steuersatz)) || 20) / 100; return o.total_cost * (1 + rate); } // ══════════════════════════════════════════════════════════════════════════════ // Component // ══════════════════════════════════════════════════════════════════════════════ export default function Bestellungen() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); const { hasPermission } = usePermissionContext(); const canManageVendors = hasPermission('bestellungen:manage_vendors'); const canExport = hasPermission('bestellungen:export'); // Tab from URL const [tab, setTab] = useState(() => { const t = Number(searchParams.get('tab')); return t >= 0 && t < TAB_COUNT ? t : 0; }); useEffect(() => { const t = Number(searchParams.get('tab')); if (t >= 0 && t < TAB_COUNT) setTab(t); }, [searchParams]); // ── Filter state ── const [selectedVendors, setSelectedVendors] = useState | null>(null); // null = all const [selectedStatuses, setSelectedStatuses] = useState>( () => new Set(ALL_STATUSES.filter((s) => !DEFAULT_EXCLUDED_STATUSES.includes(s))) ); // ── Queries ── const { data: orders = [], isLoading: ordersLoading } = useQuery({ queryKey: ['bestellungen'], queryFn: () => bestellungApi.getOrders(), }); const { data: vendors = [], isLoading: vendorsLoading } = useQuery({ queryKey: ['lieferanten'], queryFn: bestellungApi.getVendors, }); // ── Derive unique filter values from data ── const uniqueVendors = useMemo(() => { const map = new Map(); orders.forEach((o) => { if (o.lieferant_name) map.set(String(o.lieferant_id ?? o.lieferant_name), o.lieferant_name); }); return Array.from(map.entries()).sort((a, b) => a[1].localeCompare(b[1])); }, [orders]); // ── Filtered orders ── const filteredOrders = useMemo(() => { return orders.filter((o) => { // Status filter if (!selectedStatuses.has(o.status)) return false; // Vendor filter (null = all selected) if (selectedVendors !== null) { const key = String(o.lieferant_id ?? o.lieferant_name ?? ''); if (!selectedVendors.has(key)) return false; } return true; }); }, [orders, selectedStatuses, selectedVendors]); // ── Active filter count ── const activeFilterCount = useMemo(() => { let count = 0; if (selectedStatuses.size !== ALL_STATUSES.length - DEFAULT_EXCLUDED_STATUSES.length) count++; if (selectedVendors !== null) count++; return count; }, [selectedStatuses, selectedVendors]); // ── Filter handlers ── function resetFilters() { setSelectedVendors(null); setSelectedStatuses(new Set(ALL_STATUSES.filter((s) => !DEFAULT_EXCLUDED_STATUSES.includes(s)))); } function toggleStatus(s: BestellungStatus) { setSelectedStatuses((prev) => { const next = new Set(prev); if (next.has(s)) next.delete(s); else next.add(s); return next; }); } function toggleVendor(key: string) { setSelectedVendors((prev) => { if (prev === null) { // was "all selected" → deselect this one const allKeys = new Set(uniqueVendors.map(([k]) => k)); allKeys.delete(key); return allKeys; } const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); // if all are selected again, go back to null if (next.size === uniqueVendors.length) return null; return next; }); } function isVendorSelected(key: string) { return selectedVendors === null || selectedVendors.has(key); } // ── PDF Export ── async function generateBestellungenPdf() { const { jsPDF } = await import('jspdf'); const autoTable = (await import('jspdf-autotable')).default; const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' }); let settings; try { settings = await configApi.getPdfSettings(); } catch { settings = { pdf_header: '', pdf_footer: '', pdf_logo: '', pdf_org_name: '' }; } let startY = await addPdfHeader(doc, settings, 210); // Document title below header doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text('Bestellungen — Übersicht', 10, startY); startY += 10; const rows = filteredOrders.map((o) => { const brutto = calcBrutto(o); return [ formatKennung(o), o.lieferant_name || '–', BESTELLUNG_STATUS_LABELS[o.status], String(o.items_count ?? 0), formatCurrency(brutto), formatDate(o.bestellt_am || o.erstellt_am), ]; }); autoTable(doc, { head: [['Kennung', 'Lieferant', 'Status', 'Pos.', 'Betrag (brutto)', 'Datum']], body: rows, startY, headStyles: { fillColor: [66, 66, 66], textColor: 255, fontStyle: 'bold' }, alternateRowStyles: { fillColor: [245, 245, 245] }, margin: { left: 10, right: 10 }, styles: { fontSize: 9, cellPadding: 2 }, columnStyles: { 0: { cellWidth: 22 }, 1: { cellWidth: 40 }, 2: { cellWidth: 30 }, 3: { cellWidth: 15, halign: 'right' }, 4: { cellWidth: 35, halign: 'right' }, 5: { cellWidth: 25 }, }, didDrawPage: addPdfFooter(doc, settings), }); const today = new Date().toISOString().slice(0, 10); doc.save(`bestellungen_uebersicht_${today}.pdf`); } // ── Render ── return ( Bestellungen {canExport && ( )} { setTab(v); navigate(`/bestellungen?tab=${v}`, { replace: true }); }} variant="scrollable" scrollButtons="auto"> {canManageVendors && } {/* ── Tab 0: Orders ── */} {/* ── Summary Cards ── */} {[ { label: 'Wartet auf Genehmigung', count: orders.filter(o => o.status === 'wartet_auf_genehmigung').length, color: 'warning.main' }, { label: 'Bereit zur Bestellung', count: orders.filter(o => o.status === 'bereit_zur_bestellung').length, color: 'info.main' }, { label: 'Bestellt', count: orders.filter(o => o.status === 'bestellt').length, color: 'primary.main' }, { label: 'Lieferung prüfen', count: orders.filter(o => o.status === 'lieferung_pruefen').length, color: 'secondary.main' }, ].map(({ label, count, color }) => ( {count} {label} ))} {/* ── Filter ── */} }> Filter {activeFilterCount > 0 && ( )} {/* Status */} Status {ALL_STATUSES.map((s) => ( toggleStatus(s)} />} label={BESTELLUNG_STATUS_LABELS[s]} /> ))} {/* Vendor */} {uniqueVendors.length > 0 && ( Lieferant {uniqueVendors.map(([key, label]) => ( toggleVendor(key)} />} label={label} /> ))} )} {/* Active filter info */} {activeFilterCount > 0 && ( )} {filteredOrders.length} von {orders.length} Bestellungen Kennung Bezeichnung Lieferant Besteller Status Positionen Gesamtpreis (brutto) Lieferung Erstellt am {ordersLoading ? ( Laden... ) : filteredOrders.length === 0 ? ( Keine Bestellungen vorhanden ) : ( filteredOrders.map((o) => { const brutto = calcBrutto(o); const totalOrdered = o.total_ordered ?? 0; const totalReceived = o.total_received ?? 0; const deliveryPct = totalOrdered > 0 ? (totalReceived / totalOrdered) * 100 : 0; return ( navigate(`/bestellungen/${o.id}`)} > {formatKennung(o)} {o.bezeichnung} {o.lieferant_name || '–'} {o.besteller_name || '–'} {o.items_count ?? 0} {formatCurrency(brutto)} {totalOrdered > 0 ? ( = 100 ? 'success' : 'primary'} sx={{ flexGrow: 1, height: 6, borderRadius: 3 }} /> {totalReceived}/{totalOrdered} ) : '–'} {formatDate(o.erstellt_am)} ); }) )}
{hasPermission('bestellungen:create') && ( navigate('/bestellungen/neu')} aria-label="Neue Bestellung"> )}
{/* ── Tab 1: Vendors ── */} {canManageVendors && ( Name Kontakt E-Mail Telefon Website {vendorsLoading ? ( Laden... ) : vendors.length === 0 ? ( Keine Lieferanten vorhanden ) : ( vendors.map((v) => ( navigate(`/bestellungen/lieferanten/${v.id}`)} > {v.name} {v.kontakt_name || '–'} {v.email ? e.stopPropagation()}>{v.email} : '–'} {v.telefon || '–'} {v.website ? ( e.stopPropagation()}>{v.website} ) : '–'} )) )}
navigate('/bestellungen/lieferanten/neu')} aria-label="Lieferant hinzufügen">
)} {/* ── Tab 2: Katalog ── */}
); }