import { useState, useEffect, useMemo } from 'react'; import { Accordion, AccordionSummary, AccordionDetails, Box, IconButton, Tab, Tabs, Tooltip, Typography, Chip, Button, Checkbox, FormControlLabel, FormGroup, LinearProgress, Divider, } 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'; import { StatusChip, DataTable, SummaryCards } from '../components/templates'; import type { SummaryStat } from '../components/templates'; // ── 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 ── */} o.status === 'wartet_auf_genehmigung').length, color: 'warning.main' }, { label: 'Bereit zur Bestellung', value: orders.filter(o => o.status === 'bereit_zur_bestellung').length, color: 'info.main' }, { label: 'Bestellt', value: orders.filter(o => o.status === 'bestellt').length, color: 'primary.main' }, { label: 'Lieferung prüfen', value: orders.filter(o => o.status === 'lieferung_pruefen').length, color: 'secondary.main' }, ] as SummaryStat[]} isLoading={ordersLoading} /> {/* ── 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 columns={[ { key: 'laufende_nummer', label: 'Kennung', width: 90, render: (o) => ( {formatKennung(o)} )}, { key: 'bezeichnung', label: 'Bezeichnung' }, { key: 'lieferant_name', label: 'Lieferant', render: (o) => o.lieferant_name || '–' }, { key: 'besteller_name', label: 'Besteller', render: (o) => o.besteller_name || '–' }, { key: 'status', label: 'Status', render: (o) => ( )}, { key: 'items_count', label: 'Positionen', align: 'right', render: (o) => o.items_count ?? 0 }, { key: 'total_cost', label: 'Gesamtpreis (brutto)', align: 'right', render: (o) => formatCurrency(calcBrutto(o)) }, { key: 'total_received', label: 'Lieferung', render: (o) => { const totalOrdered = o.total_ordered ?? 0; const totalReceived = o.total_received ?? 0; const deliveryPct = totalOrdered > 0 ? (totalReceived / totalOrdered) * 100 : 0; return totalOrdered > 0 ? ( = 100 ? 'success' : 'primary'} sx={{ flexGrow: 1, height: 6, borderRadius: 3 }} /> {totalReceived}/{totalOrdered} ) : '–'; }}, { key: 'erstellt_am', label: 'Erstellt am', render: (o) => formatDate(o.erstellt_am) }, ]} data={filteredOrders} rowKey={(o) => o.id} onRowClick={(o) => navigate(`/bestellungen/${o.id}`)} isLoading={ordersLoading} emptyMessage="Keine Bestellungen vorhanden" searchEnabled={false} /> {hasPermission('bestellungen:create') && ( navigate('/bestellungen/neu')} aria-label="Neue Bestellung"> )} {/* ── Tab 1: Vendors ── */} {canManageVendors && ( v.kontakt_name || '–' }, { key: 'email', label: 'E-Mail', render: (v) => v.email ? e.stopPropagation()}>{v.email} : '–' }, { key: 'telefon', label: 'Telefon', render: (v) => v.telefon || '–' }, { key: 'website', label: 'Website', render: (v) => v.website ? ( e.stopPropagation()}>{v.website} ) : '–' }, ]} data={vendors} rowKey={(v) => v.id} onRowClick={(v) => navigate(`/bestellungen/lieferanten/${v.id}`)} isLoading={vendorsLoading} emptyMessage="Keine Lieferanten vorhanden" /> navigate('/bestellungen/lieferanten/neu')} aria-label="Lieferant hinzufügen"> )} {/* ── Tab 2: Katalog ── */} ); }