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 ── */}
);
}