import React, { useState, useRef } from 'react'; import { Box, Typography, Paper, Button, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Grid, Card, CardContent, LinearProgress, Checkbox, Menu, MenuItem, Accordion, AccordionSummary, AccordionDetails, Autocomplete, Tooltip, } from '@mui/material'; import { ArrowBack, Add as AddIcon, Delete as DeleteIcon, Edit as EditIcon, AttachFile, Alarm, History, Upload as UploadIcon, ArrowDropDown, ExpandMore as ExpandMoreIcon, Save as SaveIcon, PictureAsPdf as PdfIcon, } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useNavigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import GermanDateField from '../components/shared/GermanDateField'; import { useNotification } from '../contexts/NotificationContext'; 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, BestellpositionFormData, ErinnerungFormData } from '../types/bestellung.types'; // ── Helpers ── const DIENSTGRAD_KURZ: Record = { 'Feuerwehranwärter': 'FA', 'Jugendfeuerwehrmann': 'JFM', 'Probefeuerwehrmann': 'PFM', 'Feuerwehrmann': 'FM', 'Feuerwehrfrau': 'FF', 'Oberfeuerwehrmann': 'OFM', 'Oberfeuerwehrfrau': 'OFF', 'Hauptfeuerwehrmann': 'HFM', 'Hauptfeuerwehrfrau': 'HFF', 'Löschmeister': 'LM', 'Oberlöschmeister': 'OLM', 'Hauptlöschmeister': 'HLM', 'Brandmeister': 'BM', 'Oberbrandmeister': 'OBM', 'Hauptbrandmeister': 'HBM', 'Brandinspektor': 'BI', 'Oberbrandinspektor': 'OBI', 'Brandoberinspektor': 'BOI', 'Brandamtmann': 'BAM', 'Verwaltungsmeister': 'VM', 'Oberverwaltungsmeister': 'OVM', 'Hauptverwaltungsmeister': 'HVM', 'Verwalter': 'V', 'Ehren-Feuerwehrmann': 'E-FM', 'Ehren-Feuerwehrfrau': 'E-FF', 'Ehren-Oberfeuerwehrmann': 'E-OFM', 'Ehren-Oberfeuerwehrfrau': 'E-OFF', 'Ehren-Hauptfeuerwehrmann': 'E-HFM', 'Ehren-Hauptfeuerwehrfrau': 'E-HFF', 'Ehren-Löschmeister': 'E-LM', 'Ehren-Oberlöschmeister': 'E-OLM', 'Ehren-Hauptlöschmeister': 'E-HLM', 'Ehren-Brandmeister': 'E-BM', 'Ehren-Oberbrandmeister': 'E-OBM', 'Ehren-Hauptbrandmeister': 'E-HBM', 'Ehren-Brandinspektor': 'E-BI', 'Ehren-Oberbrandinspektor': 'E-OBI', 'Ehren-Brandoberinspektor': 'E-BOI', 'Ehren-Brandamtmann': 'E-BAM', 'Ehren-Verwaltungsmeister': 'E-VM', 'Ehren-Oberverwaltungsmeister': 'E-OVM', 'Ehren-Hauptverwaltungsmeister': 'E-HVM', 'Ehren-Verwalter': 'E-V', }; const kurzDienstgrad = (d?: string) => (d ? (DIENSTGRAD_KURZ[d] ?? d) : undefined); 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' }) : '–'; const formatDateTime = (iso?: string) => iso ? new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '–'; const formatFileSize = (bytes?: number) => { if (!bytes) return '–'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; // Valid status transitions (must match backend VALID_STATUS_TRANSITIONS) const STATUS_TRANSITIONS: Record = { entwurf: ['wartet_auf_genehmigung'], wartet_auf_genehmigung: ['bereit_zur_bestellung', 'entwurf'], bereit_zur_bestellung: ['bestellt'], bestellt: ['teillieferung', 'lieferung_pruefen'], teillieferung: ['lieferung_pruefen'], lieferung_pruefen: ['abgeschlossen'], abgeschlossen: [], }; // Empty line item form const emptyItem: BestellpositionFormData = { bezeichnung: '', artikelnummer: '', menge: 1, einheit: 'Stk', einzelpreis: undefined }; // ══════════════════════════════════════════════════════════════════════════════ // Component // ══════════════════════════════════════════════════════════════════════════════ export default function BestellungDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { showSuccess, showError } = useNotification(); const { hasPermission } = usePermissionContext(); const fileInputRef = useRef(null); const orderId = Number(id); // ── State ── const [newItem, setNewItem] = useState({ ...emptyItem }); const [statusConfirmTarget, setStatusConfirmTarget] = useState(null); const [statusForce, setStatusForce] = useState(false); const [overrideMenuAnchor, setOverrideMenuAnchor] = useState(null); const [deleteItemTarget, setDeleteItemTarget] = useState(null); const [deleteFileTarget, setDeleteFileTarget] = useState(null); const [editMode, setEditMode] = useState(false); const [editOrderData, setEditOrderData] = useState<{ bezeichnung: string; lieferant_id?: number; besteller_id?: string; notizen: string; steuersatz: number; }>({ bezeichnung: '', notizen: '', steuersatz: 20 }); const [editItemsData, setEditItemsData] = useState>({}); const [isSavingAll, setIsSavingAll] = useState(false); const [reminderForm, setReminderForm] = useState({ faellig_am: '', nachricht: '' }); const [reminderFormOpen, setReminderFormOpen] = useState(false); const [deleteReminderTarget, setDeleteReminderTarget] = useState(null); // ── Query ── const { data, isLoading, isError, error, refetch } = useQuery({ queryKey: ['bestellung', orderId], queryFn: () => bestellungApi.getOrder(orderId), enabled: !!orderId, }); const bestellung = data?.bestellung; const positionen = data?.positionen ?? []; const dateien = data?.dateien ?? []; const erinnerungen = data?.erinnerungen ?? []; const historie = data?.historie ?? []; const { data: vendors = [] } = useQuery({ queryKey: ['lieferanten'], queryFn: bestellungApi.getVendors, enabled: editMode, }); const { data: orderUsers = [] } = useQuery({ queryKey: ['bestellungen', 'order-users'], queryFn: bestellungApi.getOrderUsers, enabled: editMode, }); const canCreate = hasPermission('bestellungen:create'); const canDelete = hasPermission('bestellungen:delete'); const canManageReminders = hasPermission('bestellungen:manage_reminders'); const canManageOrders = hasPermission('bestellungen:manage_orders'); const canApprove = hasPermission('bestellungen:approve'); const canExport = hasPermission('bestellungen:export'); const validTransitions = bestellung ? STATUS_TRANSITIONS[bestellung.status] : []; // All statuses except current, for force override const ALL_STATUSES: BestellungStatus[] = ['entwurf', 'wartet_auf_genehmigung', 'bereit_zur_bestellung', 'bestellt', 'teillieferung', 'lieferung_pruefen', 'abgeschlossen']; const overrideStatuses = bestellung ? ALL_STATUSES.filter((s) => s !== bestellung.status) : []; // ── Mutations ── const updateStatus = useMutation({ mutationFn: ({ status, force }: { status: string; force?: boolean }) => bestellungApi.updateStatus(orderId, status, force), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); showSuccess('Status aktualisiert'); setStatusConfirmTarget(null); setStatusForce(false); }, onError: () => showError('Fehler beim Aktualisieren des Status'), }); const addItem = useMutation({ mutationFn: (data: BestellpositionFormData) => bestellungApi.addLineItem(orderId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); setNewItem({ ...emptyItem }); showSuccess('Position hinzugefügt'); }, onError: () => showError('Fehler beim Hinzufügen der Position'), }); const deleteItem = useMutation({ mutationFn: (itemId: number) => bestellungApi.deleteLineItem(itemId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); setDeleteItemTarget(null); showSuccess('Position gelöscht'); }, onError: () => showError('Fehler beim Löschen der Position'), }); const updateReceived = useMutation({ mutationFn: ({ itemId, menge }: { itemId: number; menge: number }) => bestellungApi.updateReceivedQty(itemId, menge), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); }, onError: () => showError('Fehler beim Aktualisieren'), }); const uploadFile = useMutation({ mutationFn: (file: File) => bestellungApi.uploadFile(orderId, file), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); showSuccess('Datei hochgeladen'); }, onError: () => showError('Fehler beim Hochladen der Datei'), }); const deleteFile = useMutation({ mutationFn: (fileId: number) => bestellungApi.deleteFile(fileId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); setDeleteFileTarget(null); showSuccess('Datei gelöscht'); }, onError: () => showError('Fehler beim Löschen der Datei'), }); const addReminder = useMutation({ mutationFn: (data: ErinnerungFormData) => bestellungApi.addReminder(orderId, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); setReminderForm({ faellig_am: '', nachricht: '' }); setReminderFormOpen(false); showSuccess('Erinnerung erstellt'); }, onError: () => showError('Fehler beim Erstellen der Erinnerung'), }); const markReminderDone = useMutation({ mutationFn: (reminderId: number) => bestellungApi.markReminderDone(reminderId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); }, onError: () => showError('Fehler beim Aktualisieren'), }); const deleteReminder = useMutation({ mutationFn: (reminderId: number) => bestellungApi.deleteReminder(reminderId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); setDeleteReminderTarget(null); showSuccess('Erinnerung gelöscht'); }, onError: () => showError('Fehler beim Löschen'), }); // ── Handlers ── function enterEditMode() { if (!bestellung) return; setEditOrderData({ bezeichnung: bestellung.bezeichnung, lieferant_id: bestellung.lieferant_id, besteller_id: bestellung.besteller_id || '', notizen: bestellung.notizen || '', steuersatz: parseFloat(String(bestellung.steuersatz ?? 20)), }); setEditItemsData( Object.fromEntries(positionen.map(p => [p.id, { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [], }])) ); setEditMode(true); } function cancelEditMode() { setEditMode(false); setEditItemsData({}); } async function handleSaveAll() { if (!bestellung) return; setIsSavingAll(true); try { await bestellungApi.updateOrder(orderId, { bezeichnung: editOrderData.bezeichnung, lieferant_id: editOrderData.lieferant_id, besteller_id: editOrderData.besteller_id || undefined, notizen: editOrderData.notizen, steuersatz: editOrderData.steuersatz, }); for (const item of positionen) { const itemEdit = editItemsData[item.id]; if (itemEdit) { await bestellungApi.updateLineItem(item.id, { ...itemEdit, spezifikationen: itemEdit.spezifikationen, }); } } await queryClient.invalidateQueries({ queryKey: ['bestellung', orderId] }); showSuccess('Änderungen gespeichert'); setEditMode(false); setEditItemsData({}); } catch { showError('Fehler beim Speichern'); } finally { setIsSavingAll(false); } } function handleAddItem() { if (!newItem.bezeichnung.trim()) return; addItem.mutate(newItem); } function handleFileSelect(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (file) uploadFile.mutate(file); e.target.value = ''; } // Compute totals (NUMERIC columns come as strings from PostgreSQL — parse to float) const steuersatz = editMode ? editOrderData.steuersatz : parseFloat(String(bestellung?.steuersatz ?? 20)); const totalCost = editMode ? positionen.reduce((sum, p) => { const d = editItemsData[p.id]; const einzelpreis = d?.einzelpreis != null ? d.einzelpreis : (parseFloat(String(p.einzelpreis)) || 0); const menge = d?.menge != null ? d.menge : (parseFloat(String(p.menge)) || 0); return sum + einzelpreis * menge; }, 0) : positionen.reduce((sum, p) => sum + (parseFloat(String(p.einzelpreis)) || 0) * (parseFloat(String(p.menge)) || 0), 0); const taxAmount = totalCost * (steuersatz / 100); const totalBrutto = totalCost + taxAmount; const totalReceived = positionen.length > 0 ? positionen.reduce((sum, p) => sum + (parseFloat(String(p.erhalten_menge)) || 0), 0) : 0; const totalOrdered = positionen.reduce((sum, p) => sum + (parseFloat(String(p.menge)) || 0), 0); const receivedPercent = totalOrdered > 0 ? Math.round((totalReceived / totalOrdered) * 100) : 0; // ── Loading / Error ── // ── PDF Export ── async function generateBestellungDetailPdf() { if (!bestellung) return; try { 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: '' }; } const kennung = bestellung.laufende_nummer ? `${new Date(bestellung.erstellt_am).getFullYear()}/${bestellung.laufende_nummer}` : String(bestellung.id); const title = `Bestellung #${kennung}`; let curY = await addPdfHeader(doc, settings, 210); // ── Document title ── doc.setFontSize(14); doc.setFont('helvetica', 'bold'); doc.text(title, 10, curY); curY += 10; // ── Helper: two-column label/value row ── const row = (label: string, value: string, col2X = 70) => { doc.setFontSize(9); doc.setFont('helvetica', 'bold'); doc.text(label + ':', 10, curY); doc.setFont('helvetica', 'normal'); doc.text(value, col2X, curY); curY += 5; }; // ── Vendor block ── if (bestellung.lieferant_name) { doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text('Lieferant', 10, curY); curY += 5; row('Name', bestellung.lieferant_name); if (bestellung.lieferant_kontakt_name) row('Ansprechpartner', bestellung.lieferant_kontakt_name); if (bestellung.lieferant_adresse) row('Adresse', bestellung.lieferant_adresse); if (bestellung.lieferant_email) row('E-Mail', bestellung.lieferant_email); if (bestellung.lieferant_telefon) row('Telefon', bestellung.lieferant_telefon); curY += 3; } // ── Kontaktperson (Besteller) block ── if (bestellung.besteller_name) { doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text('Kontaktperson', 10, curY); curY += 5; const nameWithRank = bestellung.besteller_dienstgrad ? `${kurzDienstgrad(bestellung.besteller_dienstgrad)} ${bestellung.besteller_name}` : bestellung.besteller_name; row('Name', nameWithRank); if (bestellung.besteller_email) row('E-Mail', bestellung.besteller_email); curY += 3; } // ── Order info block ── doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text('Bestellinformationen', 10, curY); curY += 5; row('Bezeichnung', bestellung.bezeichnung); row('Status', BESTELLUNG_STATUS_LABELS[bestellung.status]); row('Erstellt am', formatDate(bestellung.erstellt_am)); if (bestellung.bestellt_am) row('Bestelldatum', formatDate(bestellung.bestellt_am)); curY += 5; // ── Line items table ── const steuersatz = (parseFloat(String(bestellung.steuersatz)) || 20) / 100; const hasPrices = positionen.some((p) => p.einzelpreis != null && p.einzelpreis > 0); const totalNetto = positionen.reduce((sum, p) => { const ep = p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : 0; const m = parseFloat(String(p.menge)) || 0; return sum + ep * m; }, 0); const totalBrutto = totalNetto * (1 + steuersatz); if (hasPrices) { const rows: (string | number)[][] = []; for (const p of positionen) { const ep = p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined; const menge = parseFloat(String(p.menge)) || 0; const gesamt = ep != null ? ep * menge : undefined; rows.push([ p.bezeichnung, p.artikelnummer || '', `${menge} ${p.einheit}`, ep != null ? formatCurrency(ep) : '–', gesamt != null ? formatCurrency(gesamt) : '–', ]); for (const spec of p.spezifikationen || []) { rows.push([` • ${spec}`, '', '', '', '']); } } autoTable(doc, { head: [['Bezeichnung', 'Art.-Nr.', 'Menge', 'Einzelpreis', 'Gesamt']], body: rows, startY: curY, theme: 'plain', headStyles: { fillColor: [66, 66, 66], textColor: 255, fontStyle: 'bold' }, margin: { left: 10, right: 10 }, styles: { fontSize: 9, cellPadding: 2 }, columnStyles: { 2: { halign: 'right' }, 3: { halign: 'right' }, 4: { halign: 'right' }, }, didParseCell: (data: any) => { if (data.section === 'body') { const cell0 = String(data.row.raw[0] ?? ''); if (cell0.startsWith(' •')) { data.cell.styles.fontSize = 8; data.cell.styles.textColor = [100, 100, 100]; data.cell.styles.fillColor = [255, 255, 255]; } } }, didDrawCell: (data: any) => { if (data.section === 'body' && data.column.index === 0) { const cell0 = String(data.row.raw?.[0] ?? ''); if (!cell0.startsWith(' •') && data.row.index > 0) { data.doc.setDrawColor(200, 200, 200); data.doc.setLineWidth(0.2); data.doc.line( data.settings.margin.left, data.cell.y, data.doc.internal.pageSize.width - data.settings.margin.right, data.cell.y, ); } } }, foot: [ ['', '', '', 'Netto:', formatCurrency(totalNetto)], ['', '', '', `Brutto (${bestellung.steuersatz ?? 20}% USt.):`, formatCurrency(totalBrutto)], ], footStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: 'bold' }, didDrawPage: addPdfFooter(doc, settings), }); } else { const rows: string[][] = []; for (const p of positionen) { const menge = parseFloat(String(p.menge)) || 0; rows.push([p.bezeichnung, p.artikelnummer || '', `${menge} ${p.einheit}`]); for (const spec of p.spezifikationen || []) { rows.push([` • ${spec}`, '', '']); } } autoTable(doc, { head: [['Bezeichnung', 'Art.-Nr.', 'Menge']], body: rows, startY: curY, theme: 'plain', headStyles: { fillColor: [66, 66, 66], textColor: 255, fontStyle: 'bold' }, margin: { left: 10, right: 10 }, styles: { fontSize: 9, cellPadding: 2 }, columnStyles: { 2: { halign: 'right' }, }, didParseCell: (data: any) => { if (data.section === 'body') { const cell0 = String(data.row.raw[0] ?? ''); if (cell0.startsWith(' •')) { data.cell.styles.fontSize = 8; data.cell.styles.textColor = [100, 100, 100]; data.cell.styles.fillColor = [255, 255, 255]; } } }, didDrawCell: (data: any) => { if (data.section === 'body' && data.column.index === 0) { const cell0 = String(data.row.raw?.[0] ?? ''); if (!cell0.startsWith(' •') && data.row.index > 0) { data.doc.setDrawColor(200, 200, 200); data.doc.setLineWidth(0.2); data.doc.line( data.settings.margin.left, data.cell.y, data.doc.internal.pageSize.width - data.settings.margin.right, data.cell.y, ); } } }, didDrawPage: addPdfFooter(doc, settings), }); } // ── Signature section ── const signY = (doc as any).lastAutoTable?.finalY ?? curY; const sigStartY = signY + 15; // Ensure there's enough space; add a page if needed if (sigStartY + 25 > doc.internal.pageSize.height - 20) { doc.addPage(); curY = 20; } else { curY = sigStartY; } doc.setFontSize(9); doc.setFont('helvetica', 'normal'); doc.setTextColor(0, 0, 0); // Place and date (right-aligned, above signature line) const today = new Date(); const dd = String(today.getDate()).padStart(2, '0'); const mm = String(today.getMonth() + 1).padStart(2, '0'); const yyyy = today.getFullYear(); doc.text(`St. Valentin, am ${dd}.${mm}.${yyyy}`, 200, curY - 6, { align: 'right' }); // Signature line (right) doc.line(120, curY, 200, curY); curY += 4; const sigName = bestellung.besteller_dienstgrad ? `${kurzDienstgrad(bestellung.besteller_dienstgrad)} ${bestellung.besteller_name || ''}` : (bestellung.besteller_name || ''); doc.text(sigName, 120, curY); doc.save(`bestellung_${kennung.replace('/', '-')}.pdf`); } catch (err) { console.error('PDF generation failed:', err); showError('PDF konnte nicht erstellt werden'); } } if (isLoading) { return ( Laden... ); } if (isError || !bestellung) { const is404 = (error as any)?.response?.status === 404 || !bestellung; return ( {is404 ? 'Bestellung nicht gefunden.' : 'Fehler beim Laden der Bestellung.'} {!is404 && ( )} ); } return ( {/* ── Header ── */} navigate('/bestellungen')}> {bestellung.bezeichnung} {canExport && !editMode && ( )} {canCreate && !editMode && ( )} {editMode && ( <> )} {/* ── Info Cards ── */} {editMode ? ( setEditOrderData(d => ({ ...d, bezeichnung: e.target.value }))} /> o.name} value={vendors.find(v => v.id === editOrderData.lieferant_id) ?? null} onChange={(_, v) => setEditOrderData(d => ({ ...d, lieferant_id: v?.id }))} renderInput={(params) => } /> o.name} value={orderUsers.find(u => u.id === editOrderData.besteller_id) ?? null} onChange={(_, v) => setEditOrderData(d => ({ ...d, besteller_id: v?.id || '' }))} renderInput={(params) => } /> setEditOrderData(d => ({ ...d, notizen: e.target.value }))} /> ) : ( Lieferant {bestellung.lieferant_name || '–'} Besteller {bestellung.besteller_name || '–'} Erstellt am {formatDate(bestellung.erstellt_am)} )} {/* ── Status Action ── */} {(canManageOrders || canCreate || canApprove) && ( {validTransitions .filter((s) => { // Approve/reject transitions from wartet_auf_genehmigung require canApprove if (bestellung.status === 'wartet_auf_genehmigung') { return canApprove; } // Transition to bereit_zur_bestellung from other states also requires canApprove if (s === 'bereit_zur_bestellung') return canApprove; // All other transitions require canCreate or canManageOrders return canCreate || canManageOrders; }) .map((s) => { const isApprove = bestellung.status === 'wartet_auf_genehmigung' && s === 'bereit_zur_bestellung'; const isReject = bestellung.status === 'wartet_auf_genehmigung' && s === 'entwurf'; const label = isApprove ? 'Genehmigen' : isReject ? 'Ablehnen' : `Status: ${BESTELLUNG_STATUS_LABELS[s]}`; const color = isApprove ? 'success' : isReject ? 'error' : 'primary'; return ( ); })} {/* Manual override menu */} {overrideStatuses.length > 0 && canManageOrders && ( <> setOverrideMenuAnchor(null)} > {overrideStatuses.map((s) => ( { setOverrideMenuAnchor(null); setStatusForce(true); setStatusConfirmTarget(s); }} > {BESTELLUNG_STATUS_LABELS[s]} ))} )} )} {/* ── Delivery Progress ── */} {positionen.length > 0 && ( Lieferfortschritt: {totalReceived} / {totalOrdered} ({receivedPercent}%) = 100 ? 'success' : 'primary'} sx={{ height: 8, borderRadius: 4 }} /> )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Positionen */} {/* ══════════════════════════════════════════════════════════════════════ */} Positionen Bezeichnung Artikelnr. Menge Einheit Einzelpreis Gesamt Erhalten {(editMode && (canCreate || canDelete)) && Aktionen} {positionen.map((p) => editMode ? ( setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), bezeichnung: e.target.value } }))} /> setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), artikelnummer: e.target.value } }))} /> setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), menge: Number(e.target.value) } }))} /> setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), einheit: e.target.value } }))} /> setEditItemsData(d => ({ ...d, [p.id]: { ...(d[p.id] || { bezeichnung: p.bezeichnung, artikelnummer: p.artikelnummer || '', menge: parseFloat(String(p.menge)) || 1, einheit: p.einheit, einzelpreis: p.einzelpreis != null ? parseFloat(String(p.einzelpreis)) : undefined, spezifikationen: p.spezifikationen || [] }), einzelpreis: e.target.value ? Number(e.target.value) : undefined } }))} /> {formatCurrency((editItemsData[p.id]?.einzelpreis ?? parseFloat(String(p.einzelpreis ?? 0))) * (editItemsData[p.id]?.menge ?? parseFloat(String(p.menge))))} {canManageOrders ? ( updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })} /> ) : p.erhalten_menge} {(canCreate || canDelete) && ( {canDelete && ( setDeleteItemTarget(p.id)}> )} )} {/* Specifications editor row */} {(editItemsData[p.id]?.spezifikationen || []).map((spec, specIdx) => ( setEditItemsData(d => { const cur = d[p.id]?.spezifikationen ? [...d[p.id].spezifikationen] : []; cur[specIdx] = e.target.value; return { ...d, [p.id]: { ...d[p.id], spezifikationen: cur } }; })} /> setEditItemsData(d => { const cur = d[p.id]?.spezifikationen ? [...d[p.id].spezifikationen] : []; cur.splice(specIdx, 1); return { ...d, [p.id]: { ...d[p.id], spezifikationen: cur } }; })}> ))} ) : ( {p.bezeichnung} {p.artikelnummer || '–'} {p.menge} {p.einheit} {formatCurrency(p.einzelpreis)} {formatCurrency((p.einzelpreis ?? 0) * p.menge)} {canManageOrders ? ( updateReceived.mutate({ itemId: p.id, menge: Number(e.target.value) })} /> ) : ( p.erhalten_menge )} {p.spezifikationen && p.spezifikationen.length > 0 && ( {p.spezifikationen.map((spec, i) => ( • {spec} ))} )} ), )} {/* ── Add Item Row ── */} {editMode && canCreate && ( setNewItem((f) => ({ ...f, bezeichnung: e.target.value }))} /> setNewItem((f) => ({ ...f, artikelnummer: e.target.value }))} /> setNewItem((f) => ({ ...f, menge: Number(e.target.value) }))} /> setNewItem((f) => ({ ...f, einheit: e.target.value }))} /> setNewItem((f) => ({ ...f, einzelpreis: e.target.value ? Number(e.target.value) : undefined }))} /> {formatCurrency((newItem.einzelpreis ?? 0) * newItem.menge)} )} {/* ── Totals Rows (Netto / MwSt / Brutto) ── */} {positionen.length > 0 && ( <> Netto {formatCurrency(totalCost)} MwSt. {editMode && canCreate ? ( { const val = parseFloat(e.target.value); if (!isNaN(val) && val >= 0 && val <= 100) { setEditOrderData(d => ({ ...d, steuersatz: val })); } }} /> ) : ( {steuersatz} )} % {formatCurrency(taxAmount)} Brutto {formatCurrency(totalBrutto)} )}
{/* ══════════════════════════════════════════════════════════════════════ */} {/* Dateien */} {/* ══════════════════════════════════════════════════════════════════════ */} Dateien {canCreate && ( <> )} {dateien.length === 0 ? ( Keine Dateien vorhanden ) : ( {dateien.map((d) => ( {d.thumbnail_pfad && ( )} {d.dateiname} {formatFileSize(d.dateigroesse)} · {formatDate(d.hochgeladen_am)} {canDelete && ( setDeleteFileTarget(d.id)}> )} ))} )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Erinnerungen */} {/* ══════════════════════════════════════════════════════════════════════ */} Erinnerungen {canManageReminders && ( )} {erinnerungen.length === 0 && !reminderFormOpen ? ( Keine Erinnerungen ) : ( {erinnerungen.map((r) => ( markReminderDone.mutate(r.id)} size="small" /> {r.nachricht || 'Erinnerung'} Fällig: {formatDate(r.faellig_am)} {canManageReminders && ( setDeleteReminderTarget(r.id)}> )} ))} )} {/* Inline Add Reminder Form */} {reminderFormOpen && ( setReminderForm((f) => ({ ...f, faellig_am: iso }))} /> setReminderForm((f) => ({ ...f, nachricht: e.target.value }))} /> )} {/* ── Notizen ── */} {!editMode && bestellung.notizen && ( Notizen {bestellung.notizen} )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Historie */} {/* ══════════════════════════════════════════════════════════════════════ */} }> Historie ({historie.length} Einträge) {historie.length === 0 ? ( Keine Einträge ) : ( {historie.map((h) => ( {h.aktion} {h.erstellt_von_name || 'System'} · {formatDateTime(h.erstellt_am)} {h.details && ( {Object.entries(h.details).map(([k, v]) => `${k}: ${v}`).join(', ')} )} ))} )} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Dialogs */} {/* ══════════════════════════════════════════════════════════════════════ */} {/* Status Confirmation */} { setStatusConfirmTarget(null); setStatusForce(false); }}> Status ändern{statusForce ? ' (manuell)' : ''} Status von {BESTELLUNG_STATUS_LABELS[bestellung.status]} auf{' '} {statusConfirmTarget ? BESTELLUNG_STATUS_LABELS[statusConfirmTarget] : ''} ändern? {statusForce && ( Dies ist ein manueller Statusübergang außerhalb des normalen Ablaufs. )} {/* Delete Item Confirmation */} setDeleteItemTarget(null)}> Position löschen Soll diese Position wirklich gelöscht werden? {/* Delete File Confirmation */} setDeleteFileTarget(null)}> Datei löschen Soll diese Datei wirklich gelöscht werden? {/* Delete Reminder Confirmation */} setDeleteReminderTarget(null)}> Erinnerung löschen Soll diese Erinnerung wirklich gelöscht werden?
); }