diff --git a/src/components/manifest/OrderPaymentList.jsx b/src/components/manifest/OrderPaymentList.jsx index df7d5fa..160b976 100644 --- a/src/components/manifest/OrderPaymentList.jsx +++ b/src/components/manifest/OrderPaymentList.jsx @@ -4,27 +4,26 @@ import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; import Divider from '@mui/material/Divider'; -import FormControlLabel from '@mui/material/FormControlLabel'; import IconButton from '@mui/material/IconButton'; import ListItemButton from '@mui/material/ListItemButton'; -import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import MenuItem from '@mui/material/MenuItem'; import PaymentsIcon from '@mui/icons-material/Payments'; +import AccountBalanceIcon from '@mui/icons-material/AccountBalance'; +import PointOfSaleIcon from '@mui/icons-material/PointOfSale'; +import CreditCardIcon from '@mui/icons-material/CreditCard'; import AddIcon from '@mui/icons-material/Add'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import CloseIcon from '@mui/icons-material/Close'; -import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import { enqueueSnackbar } from 'notistack'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import FillSpace from 'dashgui/FillSpace'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import BackendError from '../BackendError'; import AppDialog from '../AppDialog'; -import orderAutomat, { - isOrderPaid, -} from '../../automats/orderAutomat'; +import orderAutomat from '../../automats/orderAutomat'; import accountAutomat from '../../automats/accountAutomat'; import { payment as paymentApi } from '../../api'; import { backendErrorFrom } from '../../utils/backendError'; @@ -35,12 +34,10 @@ import { formatDate, loadParties, manifestConfig, - numberDisplay, partyDisplayName, } from './manifestConfig'; import { YELLOW_PANEL, - SURFACE_BASE, SURFACE_RAISED, WHITE, } from '../../colors'; @@ -48,46 +45,31 @@ import { const cfg = manifestConfig('order'); const PAYMENT_TYPES = [ - { value: 'transfer', label: 'Banküberweisung' }, - { value: 'cash', label: 'Barzahlung' }, - { value: 'card', label: 'EC- / Kreditkarte' }, - { value: 'paypal', label: 'PayPal' }, - { value: 'sepa', label: 'SEPA-Lastschrift' }, - { value: 'invoice', label: 'Auf Rechnung' }, - { value: 'other', label: 'Sonstige' }, + { value: 'transfer', label: 'Banküberweisung', icon: AccountBalanceIcon }, + { value: 'cash', label: 'Barzahlung', icon: PointOfSaleIcon }, + { value: 'card', label: 'EC- / Kreditkarte', icon: CreditCardIcon }, + { value: 'paypal', label: 'PayPal', icon: PaymentsIcon }, + { value: 'sepa', label: 'SEPA-Lastschrift', icon: AccountBalanceIcon }, + { value: 'invoice', label: 'Auf Rechnung', icon: PaymentsIcon }, + { value: 'other', label: 'Sonstige', icon: PaymentsIcon }, ]; -function getOrderFinancials(order) { - const lines = Array.isArray(order?.lines) ? order.lines : []; - let total = Number(order?.orderTotal) || 0; - if (!total && lines.length > 0) { - total = lines.reduce((acc, line) => { - const net = (Number(line.amount) || 0) * (Number(line.net) || 0) * (1 - (Number(line.rebate) || 0) / 100); - return acc + net * (1 + (Number(line.taxrate) || 0) / 100); - }, 0); - } - total = Math.round(total * 100) / 100; +function typeLabel(val) { + const match = PAYMENT_TYPES.find((t) => t.value === val); + return match?.label || val || 'Zahlung'; +} - const payments = Array.isArray(order?.payments) ? order.payments : []; - let paid = Number(order?.paidTotal) || 0; - if (!paid && payments.length > 0) { - paid = payments.reduce((acc, p) => acc + (Number(p.amount) || 0), 0); - } - paid = Math.round(paid * 100) / 100; - - const remaining = Math.max(0, Math.round((total - paid) * 100) / 100); - return { - total, - paid, - remaining, - }; +function typeIcon(val) { + const match = PAYMENT_TYPES.find((t) => t.value === val); + const IconComp = match?.icon || PaymentsIcon; + return ; } const rowSx = { alignItems: 'center', - height: 64, - minHeight: 64, - maxHeight: 64, + height: 56, + minHeight: 56, + maxHeight: 56, boxSizing: 'border-box', py: 0, gap: 1.5, @@ -95,23 +77,20 @@ const rowSx = { const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; -const rowContext = { +const paymentRowContext = { partiesById: {}, - onOpen: null, + accountsById: {}, + busyId: null, + onDelete: null, + onNavigateOrder: null, }; -// MARK: -class OrderPaymentRow extends PureComponent { - partyName(partyId) { - if (partyId == null) return '–'; - const party = rowContext.partiesById[partyId]; - return partyDisplayName('order', party) || `Kunde #${partyId}`; - } - +// MARK: +class PaymentTableRow extends PureComponent { render() { - const { item: order } = this.props; + const { item: payment } = this.props; - if (!order) { + if (!payment) { return (   @@ -119,131 +98,135 @@ class OrderPaymentRow extends PureComponent { ); } - const { onOpen } = rowContext; - const { total, paid, remaining } = getOrderFinancials(order); - const isPaid = isOrderPaid(order) || (total > 0 && Math.abs(paid - total) < 0.01); - const isPartial = paid > 0 && !isPaid; - const paymentsCount = Array.isArray(order.payments) ? order.payments.length : 0; + const { partiesById, accountsById, busyId, onDelete, onNavigateOrder } = paymentRowContext; + const party = partiesById[payment.entityId]; + const customerName = partyDisplayName('order', party) || (payment.entityId ? `Kunde #${payment.entityId}` : '–'); + const accountName = accountsById[payment.accountId] || `Konto #${payment.accountId}`; + const isBusy = busyId === payment.id; return ( - onOpen?.(order)}> - - - {order.number} - - - {numberDisplay(order.externalNumber)} + onNavigateOrder?.(payment.orderId)}> + {/* Payment ID from payment table */} + + + + + {/* Amount */} + + + +{formatPrice(payment.amount)} - - - {order.date ? formatDate(order.date) : '–'} + {/* Payment Type */} + + {typeIcon(payment.type)} + + {typeLabel(payment.type)} + {/* Account from account table */} + + + {accountName} + + + + {/* Linked Manifest (Auftrag) */} + + + {payment.orderNumber} + + + + {/* Customer Name */} - - {this.partyName(order.entityId)} + + {customerName} - {/* Amounts */} - - - {total > 0 ? formatPrice(total) : '—'} - - - - - 0 ? 'success.main' : 'text.disabled' }}> - {paid > 0 ? formatPrice(paid) : '0,00 €'} - - - - - 0 ? 'error.main' : 'text.secondary' }}> - {remaining > 0 ? formatPrice(remaining) : '0,00 €'} - - - - {/* Status chip */} - - {isPaid ? ( - - ) : isPartial ? ( - - ) : ( - - )} - - - {/* Payments count */} - - 0 ? 'action.selected' : 'action.hover', - }} - /> - - + {/* Action: Delete payment */} { e.stopPropagation(); - onOpen?.(order); + onDelete?.(payment); }} + aria-label={`Zahlung #${payment.id} löschen`} sx={{ width: 36, height: 36, flexShrink: 0 }} > - + ); } } -// MARK: -class AddPaymentDialog extends PureComponent { +// MARK: +class RecordPaymentDialog extends PureComponent { constructor(props) { super(props); - const { order } = props; - const { remaining } = getOrderFinancials(order); + const orders = props.orders || []; + // Prefer unpaid orders + const unpaidOrders = orders.filter((o) => { + const tot = Number(o.orderTotal) || 0; + const pd = Number(o.paidTotal) || 0; + return tot === 0 || pd < tot; + }); + + const selectedOrder = unpaidOrders[0] || orders[0] || null; + const remaining = selectedOrder ? Math.max(0, Math.round(((Number(selectedOrder.orderTotal) || 0) - (Number(selectedOrder.paidTotal) || 0)) * 100) / 100) : 0; const accounts = accountAutomat.state.items || []; - const defaultAccount = accounts[0]?.id || 1; + const defaultAccountId = accounts[0]?.id || 1; this.state = { - accountId: defaultAccount, + orderId: selectedOrder?.id || '', + accountId: defaultAccountId, type: 'transfer', amount: remaining > 0 ? String(remaining) : '', saving: false, }; + this.handleOrderChange = this.handleOrderChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } + handleOrderChange(newOrderId) { + const order = this.props.orders.find((o) => o.id === Number(newOrderId)); + const remaining = order ? Math.max(0, Math.round(((Number(order.orderTotal) || 0) - (Number(order.paidTotal) || 0)) * 100) / 100) : 0; + this.setState({ + orderId: newOrderId, + amount: remaining > 0 ? String(remaining) : '', + }); + } + async handleSubmit() { - const { order, onClose, onSuccess } = this.props; - const { accountId, type, amount } = this.state; + const { onClose, onSuccess } = this.props; + const { orderId, accountId, type, amount } = this.state; const numAmount = Number(String(amount).replace(',', '.')); + if (!orderId) { + enqueueSnackbar('Bitte einen Auftrag auswählen.', { variant: 'warning' }); + return; + } + if (!Number.isFinite(numAmount) || numAmount <= 0) { - enqueueSnackbar('Bitte einen gültigen Zahlungsbetrag größer 0 eingeben.', { variant: 'warning' }); + enqueueSnackbar('Bitte einen gültigen Betrag größer 0 eingeben.', { variant: 'warning' }); return; } this.setState({ saving: true }); try { - await paymentApi.create(order.id, { + await paymentApi.create(orderId, { accountId: Number(accountId), type, amount: numAmount, }); - enqueueSnackbar('Zahlung erfolgreich verbucht!', { variant: 'success' }); + enqueueSnackbar('Zahlungseingang erfolgreich verbucht!', { variant: 'success' }); await orderAutomat.actions.reload(); onSuccess?.(); onClose(); @@ -254,29 +237,51 @@ class AddPaymentDialog extends PureComponent { } render() { - const { open, onClose, order } = this.props; - const { accountId, type, amount, saving } = this.state; + const { open, onClose, orders, partiesById } = this.props; + const { orderId, accountId, type, amount, saving } = this.state; const accounts = accountAutomat.state.items || []; return ( - - Zahlung erfassen - + + + + Zahlungseingang erfassen + + - - Auftrag: {order?.number} - this.handleOrderChange(e.target.value)} + fullWidth + > + {orders.map((o) => { + const party = partiesById[o.entityId]; + const partyName = partyDisplayName('order', party) || `Kunde #${o.entityId}`; + const tot = Number(o.orderTotal) || 0; + const pd = Number(o.paidTotal) || 0; + const rem = Math.max(0, tot - pd); + return ( + + {o.number} — {partyName} (Offen: {formatPrice(rem)}) + + ); + })} + + + this.setState({ accountId: e.target.value })} @@ -288,13 +293,13 @@ class AddPaymentDialog extends PureComponent { )) ) : ( - Standard-Bankkonto + Standardkonto )} this.setState({ type: e.target.value })} @@ -307,7 +312,7 @@ class AddPaymentDialog extends PureComponent { -class OrderPaymentDetailDialog extends PureComponent { - constructor(props) { - super(props); - this.state = { - addPaymentOpen: false, - busyAction: false, - }; - this.handleDeletePayment = this.handleDeletePayment.bind(this); - } - - async handleDeletePayment(pay) { - if (!window.confirm(`Zahlung über ${formatPrice(pay.amount)} wirklich löschen?`)) return; - this.setState({ busyAction: true }); - try { - await paymentApi.delete(pay.id); - enqueueSnackbar('Zahlung gelöscht.', { variant: 'success' }); - await orderAutomat.actions.reload(); - } catch (err) { - notifyError(err); - } finally { - this.setState({ busyAction: false }); - } - } - - render() { - const { open, onClose, orderId, partiesById } = this.props; - const { addPaymentOpen, busyAction } = this.state; - - const order = orderAutomat.state.items?.find((o) => o.id === orderId); - if (!order) return null; - - const party = partiesById[order.entityId]; - const customerName = partyDisplayName('order', party) || `Kunde #${order.entityId}`; - const { total, paid, remaining } = getOrderFinancials(order); - const payments = Array.isArray(order.payments) ? order.payments : []; - const accounts = accountAutomat.state.items || []; - const accountsById = Object.fromEntries(accounts.map((a) => [a.id, a.name])); - - return ( - - - {/* Header */} - - - - - Zahlungen: {order.number} - - {remaining === 0 ? ( - - ) : paid > 0 ? ( - - ) : ( - - )} - - - Kunde: {customerName} • Datum: {order.date ? formatDate(order.date) : '–'} - - - - - - - - {/* Quick Metrics Bar */} - - - Auftrags-Gesamtbetrag (Brutto) - {formatPrice(total)} - - - - Bereits bezahlt - 0 ? 'success.main' : 'text.primary' }}> - {formatPrice(paid)} - - - - - Offener Saldo - 0 ? 'error.main' : 'text.secondary' }}> - {formatPrice(remaining)} - - - - - - - {/* Payments list */} - - Gebuchte Zahlungseingänge ({payments.length}) - - - {payments.length === 0 ? ( - - - Bisher keine Zahlungen für diesen Auftrag erfasst. - - - - ) : ( - - {payments.map((p) => { - const typeObj = PAYMENT_TYPES.find((pt) => pt.value === p.type); - const typeLabel = typeObj?.label || p.type; - const accountName = accountsById[p.accountId] || `Konto #${p.accountId}`; - - return ( - - - - - - {typeLabel} - - - {accountName} - - - - - - - +{formatPrice(p.amount)} - - this.handleDeletePayment(p)} - aria-label="Zahlung löschen" - > - - - - - ); - })} - - )} - - - - - - - {addPaymentOpen && ( - this.setState({ addPaymentOpen: false })} - /> - )} - - ); - } -} - // MARK: export class OrderPaymentList extends PureComponent { constructor(props) { @@ -528,25 +344,29 @@ export class OrderPaymentList extends PureComponent { status: s.status === 'ready' ? 'ready' : 'loading', error: s.error ? backendErrorFrom(s.error) : null, items: s.items ?? [], - total: s.total ?? 0, filter: '', - onlyUnpaid: true, // Default to unpaid partiesById: {}, - selectedOrderId: null, + accountsById: {}, + busyId: null, + recordDialogOpen: false, }; this._cancelled = false; this.handleRetry = this.handleRetry.bind(this); - this.handleOpenOrder = this.handleOpenOrder.bind(this); + this.handleDeletePayment = this.handleDeletePayment.bind(this); + this.handleNavigateOrder = this.handleNavigateOrder.bind(this); } componentDidMount() { this._cancelled = false; this.unsub = orderAutomat.subscribe(this, (state) => ({ items: state.items ?? [], - total: state.total ?? 0, status: state.status, error: state.error ? backendErrorFrom(state.error) : null, })); + this.unsubAccount = accountAutomat.subscribe(this, (state) => ({ + accountsById: Object.fromEntries((state.items || []).map((a) => [a.id, a.name])), + })); + if (orderAutomat.state.status === 'idle') { void orderAutomat.actions.load().catch(() => {}); } @@ -557,6 +377,7 @@ export class OrderPaymentList extends PureComponent { componentWillUnmount() { this._cancelled = true; this.unsub?.(); + this.unsubAccount?.(); } async loadPartiesData() { @@ -574,32 +395,73 @@ export class OrderPaymentList extends PureComponent { void orderAutomat.actions.load().catch(() => {}); } - handleOpenOrder(order) { - this.setState({ selectedOrderId: order.id }); + extractAllPayments(orders = this.state.items) { + const list = []; + for (const order of orders) { + if (!Array.isArray(order.payments)) continue; + for (const pay of order.payments) { + list.push({ + ...pay, + orderId: order.id, + orderNumber: order.number, + orderDate: order.date, + entityId: order.entityId, + orderTotal: order.orderTotal, + paidTotal: order.paidTotal, + }); + } + } + // Sort descending by payment id + return list.sort((a, b) => Number(b.id || 0) - Number(a.id || 0)); } - getFilteredOrders() { - const { items, filter, onlyUnpaid, partiesById } = this.state; - const q = filter.trim().toLowerCase(); - - return items.filter((order) => { - const { total, paid } = getOrderFinancials(order); - const isPaid = isOrderPaid(order) || (total > 0 && Math.abs(paid - total) < 0.01); - if (onlyUnpaid && isPaid) return false; - - if (q) { - const party = partiesById[order.entityId]; - const partyName = partyDisplayName('order', party); - const matches = [order.number, order.externalNumber, partyName] - .some((val) => String(val || '').toLowerCase().includes(q)); - if (!matches) return false; + async handleDeletePayment(payment) { + if (!window.confirm(`Zahlung #${payment.id} über ${formatPrice(payment.amount)} wirklich löschen?`)) return; + this.setState({ busyId: payment.id }); + try { + await paymentApi.delete(payment.id); + enqueueSnackbar(`Zahlung #${payment.id} gelöscht.`, { variant: 'success' }); + await orderAutomat.actions.reload(); + } catch (err) { + notifyError(err); + } finally { + if (!this._cancelled) { + this.setState({ busyId: null }); } - return true; + } + } + + handleNavigateOrder(orderId) { + if (orderId) { + this.props.navigate(`/auftrag/bestellung/${orderId}`); + } + } + + getFilteredPayments() { + const all = this.extractAllPayments(); + const q = this.state.filter.trim().toLowerCase(); + if (!q) return all; + + const { partiesById, accountsById } = this.state; + return all.filter((pay) => { + const party = partiesById[pay.entityId]; + const customerName = partyDisplayName('order', party); + const accountName = accountsById[pay.accountId]; + const typeStr = typeLabel(pay.type); + + return [ + String(pay.id), + pay.orderNumber, + customerName, + accountName, + typeStr, + pay.type, + ].some((val) => String(val || '').toLowerCase().includes(q)); }); } render() { - const { status, error, filter, onlyUnpaid, partiesById, selectedOrderId } = this.state; + const { status, error, filter, partiesById, accountsById, busyId, recordDialogOpen, items } = this.state; if (status === 'loading' || status === 'idle') { return ( @@ -613,72 +475,84 @@ export class OrderPaymentList extends PureComponent { return ; } - const filtered = this.getFilteredOrders(); - rowContext.partiesById = partiesById; - rowContext.onOpen = this.handleOpenOrder; + const filtered = this.getFilteredPayments(); + const allPayments = this.extractAllPayments(); + const totalAmount = allPayments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0); + + paymentRowContext.partiesById = partiesById; + paymentRowContext.accountsById = accountsById; + paymentRowContext.busyId = busyId; + paymentRowContext.onDelete = this.handleDeletePayment; + paymentRowContext.onNavigateOrder = this.handleNavigateOrder; return ( <> + {/* Metrics Banner */} + + + + Gesamtsumme aller Zahlungen + + + {formatPrice(totalAmount)} + + + + + + Anzahl Zahlungseinträge (Tabelle payment) + + + {allPayments.length} + + + + + + + {/* Search Bar */} this.setState({ filter: next })} /> - - - - this.setState({ onlyUnpaid: e.target.checked })} - /> - } - label={ - - Nur unbezahlte Aufträge - - } - sx={{ m: 0 }} - /> - - - - {filtered.length} Auftrag / Aufträge zur Zahlungsüberwachung - - + {/* Payments Table (connected directly to payment records) */} - Auftragsnr. - Datum + ID + Betrag + Zahlungsart + Konto + Auftrag Kunde - Gesamt - Bezahlt - Offen - Status - Zahlungen } @@ -723,12 +595,12 @@ export class OrderPaymentList extends PureComponent { - {selectedOrderId != null && ( - this.setState({ selectedOrderId: null })} + onClose={() => this.setState({ recordDialogOpen: false })} /> )} diff --git a/src/components/manifest/ShipmentList.jsx b/src/components/manifest/ShipmentList.jsx index d413c96..6e4fab9 100644 --- a/src/components/manifest/ShipmentList.jsx +++ b/src/components/manifest/ShipmentList.jsx @@ -4,10 +4,8 @@ import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; import Divider from '@mui/material/Divider'; -import FormControlLabel from '@mui/material/FormControlLabel'; import IconButton from '@mui/material/IconButton'; import ListItemButton from '@mui/material/ListItemButton'; -import Switch from '@mui/material/Switch'; import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import Table from '@mui/material/Table'; @@ -24,16 +22,13 @@ import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import CloseIcon from '@mui/icons-material/Close'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import Inventory2Icon from '@mui/icons-material/Inventory2'; import { enqueueSnackbar } from 'notistack'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; -import FillSpace from 'dashgui/FillSpace'; import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import BackendError from '../BackendError'; import AppDialog from '../AppDialog'; -import orderAutomat, { - filterOrders, - isOrderDelivered, -} from '../../automats/orderAutomat'; +import orderAutomat from '../../automats/orderAutomat'; import { shipment as shipmentApi, shipping as shippingApi } from '../../api'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; @@ -46,13 +41,9 @@ import { partyDisplayName, } from './manifestConfig'; import { - GREEN, GREEN_PANEL, - ORANGE_PANEL, - PURPLE_PANEL, SURFACE_BASE, SURFACE_RAISED, - BORDER, WHITE, } from '../../colors'; @@ -60,344 +51,8 @@ const cfg = manifestConfig('order'); const CARRIERS = ['DHL', 'UPS', 'DPD', 'GLS', 'Hermes', 'Spedition', 'Selbstabholung']; -// MARK: pure helpers -function getOrderQuantities(order) { - const lines = Array.isArray(order.lines) ? order.lines : []; - const ordered = Number(order.orderedQuantity) || lines.reduce((acc, l) => acc + (Number(l.amount) || 0), 0); - const shipments = Array.isArray(order.shipments) ? order.shipments : []; - const shipped = Number(order.shippedQuantity) || shipments.reduce((acc, s) => { - return acc + (Array.isArray(s.lines) ? s.lines.reduce((lAcc, l) => lAcc + (Number(l.amount) || 0), 0) : 0); - }, 0); - return { - ordered: Math.round(ordered * 1000) / 1000, - shipped: Math.round(shipped * 1000) / 1000, - remaining: Math.max(0, Math.round((ordered - shipped) * 1000) / 1000), - }; -} - -function getTotalPackages(order) { - const shipments = Array.isArray(order?.shipments) ? order.shipments : []; - return shipments.reduce((acc, s) => acc + (Array.isArray(s.shippings) ? s.shippings.length : 0), 0); -} - -const rowSx = { - alignItems: 'center', - height: 64, - minHeight: 64, - maxHeight: 64, - boxSizing: 'border-box', - py: 0, - gap: 1.5, -}; - const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; -const rowContext = { - partiesById: {}, - onOpen: null, -}; - -// MARK: -class ShipmentOrderRow extends PureComponent { - partyName(partyId) { - if (partyId == null) return '–'; - const party = rowContext.partiesById[partyId]; - return partyDisplayName('order', party) || `Kunde #${partyId}`; - } - - render() { - const { item: order } = this.props; - - if (!order) { - return ( - -   - - ); - } - - const { onOpen } = rowContext; - const { ordered, shipped } = getOrderQuantities(order); - const isShipped = isOrderDelivered(order) || (ordered > 0 && shipped >= ordered); - const isPartial = shipped > 0 && shipped < ordered; - const shipmentCount = Array.isArray(order.shipments) ? order.shipments.length : 0; - const packageCount = getTotalPackages(order); - - return ( - onOpen?.(order)}> - - - {order.number} - - - {numberDisplay(order.externalNumber)} - - - - - - {order.date ? formatDate(order.date) : '–'} - - - - - - {this.partyName(order.entityId)} - - - - {/* Quantities & Status */} - - - - {shipped} / {ordered} geliefert - - - {isShipped ? ( - - ) : isPartial ? ( - - ) : ( - - )} - - - {/* Shipment & Package counts */} - - 0 ? 'action.selected' : 'action.hover', - }} - /> - 0 ? 'action.selected' : 'action.hover', - }} - /> - - - { - e.stopPropagation(); - onOpen?.(order); - }} - sx={{ width: 36, height: 36, flexShrink: 0 }} - > - - - - ); - } -} - -// MARK: -class CreateShipmentDialog extends PureComponent { - constructor(props) { - super(props); - const { order } = props; - const initialLines = {}; - const lines = Array.isArray(order?.lines) ? order.lines : []; - - // Precalculate remaining amounts per sku - const shippedBySku = {}; - for (const sh of order?.shipments || []) { - for (const sl of sh.lines || []) { - if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0); - } - } - - for (const line of lines) { - if (!line.sku) continue; - const orderedAmt = Number(line.amount) || 0; - const shippedAmt = shippedBySku[line.sku] || 0; - const rem = Math.max(0, orderedAmt - shippedAmt); - initialLines[line.sku] = rem > 0 ? String(rem) : '0'; - } - - this.state = { - date: new Date().toISOString().slice(0, 10), - externalNumber: '', - lineAmounts: initialLines, - saving: false, - }; - - this.handleAmountChange = this.handleAmountChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - - handleAmountChange(sku, val) { - this.setState((prev) => ({ - lineAmounts: { ...prev.lineAmounts, [sku]: val }, - })); - } - - async handleSubmit() { - const { order, onClose, onSuccess } = this.props; - const { date, externalNumber, lineAmounts } = this.state; - const linesToShip = []; - - for (const [sku, amtStr] of Object.entries(lineAmounts)) { - const amt = Number(amtStr); - if (amt > 0) { - const orderLine = order.lines?.find((l) => l.sku === sku); - linesToShip.push({ - sku, - amount: amt, - name: orderLine?.name ?? '', - pu: orderLine?.pu ?? 1, - net: orderLine?.net ?? 0, - rebate: orderLine?.rebate ?? 0, - taxrate: orderLine?.taxrate ?? 0, - }); - } - } - - if (linesToShip.length === 0) { - enqueueSnackbar('Mindestens ein Artikel muss eine Liefermenge > 0 haben.', { variant: 'warning' }); - return; - } - - this.setState({ saving: true }); - try { - const res = await shipmentApi.create(order.id, { - date, - externalNumber: externalNumber.trim() || undefined, - lines: linesToShip, - }); - enqueueSnackbar(`Lieferschein ${res.shipment?.number || ''} erfolgreich angelegt!`, { variant: 'success' }); - await orderAutomat.actions.reload(); - onSuccess?.(); - onClose(); - } catch (err) { - notifyError(err); - this.setState({ saving: false }); - } - } - - render() { - const { open, onClose, order } = this.props; - const { date, externalNumber, lineAmounts, saving } = this.state; - const lines = Array.isArray(order?.lines) ? order.lines : []; - - const shippedBySku = {}; - for (const sh of order?.shipments || []) { - for (const sl of sh.lines || []) { - if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0); - } - } - - return ( - - - - - - Neue Teillieferung / Sendung erfassen - - - Auftrag {order?.number} - - - - - - - - - this.setState({ date: e.target.value })} - InputLabelProps={{ shrink: true }} - sx={{ width: 180 }} - /> - this.setState({ externalNumber: e.target.value })} - sx={{ flex: 1 }} - /> - - - - Positionen & Liefermengen (Maximal restante Bestellmenge) - - - - - - - SKU - Artikel - Bestellt - Geliefert - Offen - Jetzt liefern - - - - {lines.map((line) => { - const orderedAmt = Number(line.amount) || 0; - const shippedAmt = shippedBySku[line.sku] || 0; - const remaining = Math.max(0, orderedAmt - shippedAmt); - const currentVal = lineAmounts[line.sku] ?? '0'; - - return ( - - {line.sku} - {line.name} - {orderedAmt} - {shippedAmt} - 0 ? 'primary.main' : 'text.disabled' }}> - {remaining} - - - this.handleAmountChange(line.sku, e.target.value)} - disabled={remaining <= 0} - inputProps={{ min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } }} - sx={{ width: 100 }} - /> - - - ); - })} - -
-
- - - - - -
-
- ); - } -} - // MARK: class AddPackageDialog extends PureComponent { constructor(props) { @@ -450,7 +105,7 @@ class AddPackageDialog extends PureComponent {
- Lieferschein: {shipment?.number} + Lieferschein: {shipment?.number} @@ -483,7 +138,7 @@ class AddPackageDialog extends PureComponent { > Paket (Parcel) Palette (Pallet) - Brief / Großbrief + Brief / Dokument Entwurf (draft) Gepackt (packed) - Versendet (shipped — bucht Lager ab) + Versendet (shipped — Lagerabbuchung) @@ -514,20 +169,351 @@ class AddPackageDialog extends PureComponent { } } -// MARK: -class OrderShipmentDetailDialog extends PureComponent { +// MARK: +class NewShipmentModal extends PureComponent { constructor(props) { super(props); + const orders = props.orders.filter((o) => { + const ordQty = Number(o.orderedQuantity) || 0; + const shpQty = Number(o.shippedQuantity) || 0; + return ordQty === 0 || shpQty < ordQty; + }); + + const selectedOrder = orders[0] || null; + const initialAmounts = {}; + if (selectedOrder) { + this.populateAmounts(selectedOrder, initialAmounts); + } + this.state = { - createShipmentOpen: false, - addPackageShipment: null, + orderId: selectedOrder?.id || '', + date: new Date().toISOString().slice(0, 10), + externalNumber: '', + lineAmounts: initialAmounts, + saving: false, + }; + + this.handleOrderChange = this.handleOrderChange.bind(this); + this.handleAmountChange = this.handleAmountChange.bind(this); + this.handleSubmit = this.handleSubmit.bind(this); + } + + populateAmounts(order, targetMap) { + const shippedBySku = {}; + for (const sh of order.shipments || []) { + for (const sl of sh.lines || []) { + if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0); + } + } + for (const l of order.lines || []) { + if (!l.sku) continue; + const orderedAmt = Number(l.amount) || 0; + const shippedAmt = shippedBySku[l.sku] || 0; + const rem = Math.max(0, orderedAmt - shippedAmt); + targetMap[l.sku] = rem > 0 ? String(rem) : '0'; + } + } + + handleOrderChange(newOrderId) { + const order = this.props.orders.find((o) => o.id === newOrderId); + const newAmounts = {}; + if (order) { + this.populateAmounts(order, newAmounts); + } + this.setState({ orderId: newOrderId, lineAmounts: newAmounts }); + } + + handleAmountChange(sku, val) { + this.setState((prev) => ({ + lineAmounts: { ...prev.lineAmounts, [sku]: val }, + })); + } + + async handleSubmit() { + const { orders, onClose, onSuccess } = this.props; + const { orderId, date, externalNumber, lineAmounts } = this.state; + const order = orders.find((o) => o.id === Number(orderId)); + if (!order) return; + + const linesToShip = []; + for (const [sku, amtStr] of Object.entries(lineAmounts)) { + const amt = Number(amtStr); + if (amt > 0) { + const orderLine = order.lines?.find((l) => l.sku === sku); + linesToShip.push({ + sku, + amount: amt, + name: orderLine?.name ?? '', + pu: orderLine?.pu ?? 1, + net: orderLine?.net ?? 0, + rebate: orderLine?.rebate ?? 0, + taxrate: orderLine?.taxrate ?? 0, + }); + } + } + + if (linesToShip.length === 0) { + enqueueSnackbar('Mindestens ein Artikel muss eine Liefermenge > 0 haben.', { variant: 'warning' }); + return; + } + + this.setState({ saving: true }); + try { + const res = await shipmentApi.create(order.id, { + date, + externalNumber: externalNumber.trim() || undefined, + lines: linesToShip, + }); + enqueueSnackbar(`Lieferschein ${res.shipment?.number || ''} erfolgreich angelegt!`, { variant: 'success' }); + await orderAutomat.actions.reload(); + onSuccess?.(res.shipment?.id); + onClose(); + } catch (err) { + notifyError(err); + this.setState({ saving: false }); + } + } + + render() { + const { open, onClose, orders, partiesById } = this.props; + const { orderId, date, externalNumber, lineAmounts, saving } = this.state; + + const openOrders = orders.filter((o) => { + const ordQty = Number(o.orderedQuantity) || 0; + const shpQty = Number(o.shippedQuantity) || 0; + return ordQty === 0 || shpQty < ordQty; + }); + + const currentOrder = orders.find((o) => o.id === Number(orderId)); + const lines = Array.isArray(currentOrder?.lines) ? currentOrder.lines : []; + + const shippedBySku = {}; + for (const sh of currentOrder?.shipments || []) { + for (const sl of sh.lines || []) { + if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0); + } + } + + return ( + + + + + Neuen Lieferschein erstellen (Shipment Manifest) + + + + + + + + this.handleOrderChange(e.target.value)} + fullWidth + > + {openOrders.map((o) => { + const party = partiesById[o.entityId]; + const partyName = partyDisplayName('order', party) || `Kunde #${o.entityId}`; + return ( + + {o.number} — {partyName} ({o.shippedQuantity || 0}/{o.orderedQuantity || 0} geliefert) + + ); + })} + + + + this.setState({ date: e.target.value })} + InputLabelProps={{ shrink: true }} + sx={{ width: 180 }} + /> + this.setState({ externalNumber: e.target.value })} + sx={{ flex: 1 }} + /> + + + + {currentOrder && ( + <> + + Zu liefernde Positionen (Maximal restante Bestellmenge) + + + + + + + SKU + Artikel + Bestellt + Geliefert + Offen + Liefermenge + + + + {lines.map((line) => { + const orderedAmt = Number(line.amount) || 0; + const shippedAmt = shippedBySku[line.sku] || 0; + const remaining = Math.max(0, orderedAmt - shippedAmt); + const currentVal = lineAmounts[line.sku] ?? '0'; + + return ( + + {line.sku} + {line.name} + {orderedAmt} + {shippedAmt} + 0 ? 'primary.main' : 'text.disabled' }}> + {remaining} + + + this.handleAmountChange(line.sku, e.target.value)} + disabled={remaining <= 0} + inputProps={{ min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } }} + sx={{ width: 100 }} + /> + + + ); + })} + +
+
+ + )} + + + + + +
+
+ ); + } +} + +// MARK: +export class ShipmentList extends PureComponent { + constructor(props) { + super(props); + const s = orderAutomat.state; + this.state = { + status: s.status === 'ready' ? 'ready' : 'loading', + error: s.error ? backendErrorFrom(s.error) : null, + items: s.items ?? [], + filter: '', + partiesById: {}, + selectedShipmentId: null, + addPackageDialogOpen: false, + newShipmentModalOpen: false, busyAction: false, }; + this._cancelled = false; + this.handleRetry = this.handleRetry.bind(this); + this.handleSelectShipment = this.handleSelectShipment.bind(this); this.handleDeleteShipment = this.handleDeleteShipment.bind(this); this.handleDeletePackage = this.handleDeletePackage.bind(this); this.handleMarkPackageShipped = this.handleMarkPackageShipped.bind(this); } + componentDidMount() { + this._cancelled = false; + this.unsub = orderAutomat.subscribe(this, (state) => { + const items = state.items ?? []; + this.setState((prev) => { + // If current selectedShipmentId is no longer found, pick first + const allShipments = this.extractAllShipments(items); + let selectedShipmentId = prev.selectedShipmentId; + if (!selectedShipmentId && allShipments.length > 0) { + selectedShipmentId = allShipments[0].id; + } else if (selectedShipmentId && !allShipments.some((s) => s.id === selectedShipmentId)) { + selectedShipmentId = allShipments[0]?.id || null; + } + return { + items, + status: state.status, + error: state.error ? backendErrorFrom(state.error) : null, + selectedShipmentId, + }; + }); + }); + + if (orderAutomat.state.status === 'idle') { + void orderAutomat.actions.load().catch(() => {}); + } + void this.loadPartiesData(); + } + + componentWillUnmount() { + this._cancelled = true; + this.unsub?.(); + } + + async loadPartiesData() { + try { + const parties = await loadParties('order'); + if (!this._cancelled) { + this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); + } + } catch { + // Non-fatal + } + } + + extractAllShipments(orders = this.state.items) { + const list = []; + for (const order of orders) { + if (!Array.isArray(order.shipments)) continue; + for (const sh of order.shipments) { + list.push({ + ...sh, + parentOrderId: order.id, + parentOrderNumber: order.number, + parentOrderDate: order.date, + entityId: order.entityId, + parentOrder: order, + }); + } + } + // Sort descending by date / id + return list.sort((a, b) => { + const cmp = String(b.date || '').localeCompare(String(a.date || '')); + if (cmp !== 0) return cmp; + return Number(b.id || 0) - Number(a.id || 0); + }); + } + + handleRetry() { + void orderAutomat.actions.load().catch(() => {}); + } + + handleSelectShipment(shipment) { + this.setState({ selectedShipmentId: shipment.id }); + } + async handleDeleteShipment(sh) { if (!window.confirm(`Lieferschein „${sh.number}“ wirklich löschen?`)) return; this.setState({ busyAction: true }); @@ -560,7 +546,7 @@ class OrderShipmentDetailDialog extends PureComponent { this.setState({ busyAction: true }); try { await shippingApi.update(pkg.id, { state: 'shipped' }); - enqueueSnackbar('Paket als versendet markiert (Lager abgezogen).', { variant: 'success' }); + enqueueSnackbar('Paket als versendet markiert (Lagerbestand abgebucht).', { variant: 'success' }); await orderAutomat.actions.reload(); } catch (err) { notifyError(err); @@ -569,388 +555,25 @@ class OrderShipmentDetailDialog extends PureComponent { } } - render() { - const { open, onClose, orderId, partiesById } = this.props; - const { createShipmentOpen, addPackageShipment, busyAction } = this.state; + getFilteredShipments() { + const all = this.extractAllShipments(); + const q = this.state.filter.trim().toLowerCase(); + if (!q) return all; - // Read latest order from orderAutomat cache - const order = orderAutomat.state.items?.find((o) => o.id === orderId); - if (!order) return null; + const { partiesById } = this.state; + return all.filter((sh) => { + const party = partiesById[sh.entityId]; + const partyName = partyDisplayName('order', party); + const trackingCodes = (sh.shippings || []).map((p) => p.trackingCode).filter(Boolean); + const carriers = (sh.shippings || []).map((p) => p.carrier).filter(Boolean); - const party = partiesById[order.entityId]; - const customerName = partyDisplayName('order', party) || `Kunde #${order.entityId}`; - const { ordered, shipped, remaining } = getOrderQuantities(order); - const shipments = Array.isArray(order.shipments) ? order.shipments : []; - - return ( - - - {/* Header */} - - - - - Auslieferung: {order.number} - - {remaining === 0 ? ( - - ) : shipped > 0 ? ( - - ) : ( - - )} - - - Kunde: {customerName} • Datum: {order.date ? formatDate(order.date) : '–'} {order.externalNumber ? `• Ref: ${order.externalNumber}` : ''} - - - - - - - - {/* Quick Metrics Bar */} - - - Bestellte Menge - {ordered} Stück - - - - Bereits ausgeliefert - 0 ? 'success.main' : 'text.primary' }}> - {shipped} Stück - - - - - Restmenge (Offen) - 0 ? 'warning.main' : 'text.secondary' }}> - {remaining} Stück - - - - - - - {/* Shipment Manifests List */} - - Zugeordnete Lieferscheine (Shipment Manifests) - - - {shipments.length === 0 ? ( - - - Bisher keine Lieferscheine für diesen Auftrag erstellt. - - - - ) : ( - - {shipments.map((sh) => { - const shPackages = Array.isArray(sh.shippings) ? sh.shippings : []; - const shLines = Array.isArray(sh.lines) ? sh.lines : []; - const hasShippedPackages = shPackages.some((p) => p.state === 'shipped'); - - return ( - - {/* Shipment Manifest Header */} - - - - - - {sh.number} - - - Datum: {sh.date ? formatDate(sh.date) : '–'} {sh.externalNumber ? `• Ref: ${sh.externalNumber}` : ''} - - - - - - - {!hasShippedPackages && ( - this.handleDeleteShipment(sh)} - aria-label={`Lieferschein ${sh.number} löschen`} - > - - - )} - - - - {/* Shipped Items */} - - - Enthaltene Positionen ({shLines.length}) - - - - - SKU - Artikel - Gelieferte Menge - - - - {shLines.map((line, idx) => ( - - {line.sku} - {line.name} - {line.amount} - - ))} - -
-
- - {/* Packages / Logistics */} - - - Pakete & Sendungsverfolgung ({shPackages.length}) - - - {shPackages.length === 0 ? ( - - Noch kein Paket angelegt. Nutzen Sie „Paket erfassen“, um Tracking-Codes hinzuzufügen. - - ) : ( - - {shPackages.map((pkg) => { - const isShippedPkg = pkg.state === 'shipped'; - return ( - - - - - {pkg.carrier || 'Standard'} ({pkg.type === 'pallet' ? 'Palette' : 'Paket'}) - - {pkg.trackingCode ? ( - - - {pkg.trackingCode} - - { - navigator.clipboard.writeText(pkg.trackingCode); - enqueueSnackbar('Tracking-Code kopiert!', { variant: 'info' }); - }} - sx={{ p: 0.25 }} - > - - - - ) : ( - - Kein Tracking-Code - - )} - - - - {isShippedPkg ? ( - } - label="Versendet (Lager verbucht)" - color="success" - sx={{ fontWeight: 600 }} - /> - ) : ( - <> - - - this.handleDeletePackage(pkg)} - > - - - - )} - - - ); - })} - - )} - -
- ); - })} -
- )} - - - - -
- - {createShipmentOpen && ( - this.setState({ createShipmentOpen: false })} - /> - )} - - {addPackageShipment && ( - this.setState({ addPackageShipment: null })} - /> - )} -
- ); - } -} - -// MARK: -export class ShipmentList extends PureComponent { - constructor(props) { - super(props); - const s = orderAutomat.state; - this.state = { - status: s.status === 'ready' ? 'ready' : 'loading', - error: s.error ? backendErrorFrom(s.error) : null, - items: s.items ?? [], - total: s.total ?? 0, - filter: '', - onlyUndelivered: true, // Default to pending delivery orders - partiesById: {}, - selectedOrderId: null, - }; - this._cancelled = false; - this.handleRetry = this.handleRetry.bind(this); - this.handleOpenOrder = this.handleOpenOrder.bind(this); - } - - componentDidMount() { - this._cancelled = false; - this.unsub = orderAutomat.subscribe(this, (state) => ({ - items: state.items ?? [], - total: state.total ?? 0, - status: state.status, - error: state.error ? backendErrorFrom(state.error) : null, - })); - if (orderAutomat.state.status === 'idle') { - void orderAutomat.actions.load().catch(() => {}); - } - void this.loadPartiesData(); - } - - componentWillUnmount() { - this._cancelled = true; - this.unsub?.(); - } - - async loadPartiesData() { - try { - const parties = await loadParties('order'); - if (!this._cancelled) { - this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); - } - } catch { - // Non-fatal - } - } - - handleRetry() { - void orderAutomat.actions.load().catch(() => {}); - } - - handleOpenOrder(order) { - this.setState({ selectedOrderId: order.id }); - } - - getFilteredOrders() { - const { items, filter, onlyUndelivered, partiesById } = this.state; - const q = filter.trim().toLowerCase(); - - return items.filter((order) => { - const { ordered, shipped } = getOrderQuantities(order); - const isShipped = isOrderDelivered(order) || (ordered > 0 && shipped >= ordered); - if (onlyUndelivered && isShipped) return false; - - if (q) { - const party = partiesById[order.entityId]; - const partyName = partyDisplayName('order', party); - const trackingCodes = (order.shipments || []).flatMap((s) => (s.shippings || []).map((p) => p.trackingCode)).filter(Boolean); - const matches = [order.number, order.externalNumber, partyName, ...trackingCodes] - .some((val) => String(val || '').toLowerCase().includes(q)); - if (!matches) return false; - } - return true; + return [sh.number, sh.parentOrderNumber, sh.externalNumber, partyName, ...trackingCodes, ...carriers] + .some((val) => String(val || '').toLowerCase().includes(q)); }); } render() { - const { status, error, filter, onlyUndelivered, partiesById, selectedOrderId } = this.state; + const { status, error, filter, partiesById, selectedShipmentId, addPackageDialogOpen, newShipmentModalOpen, busyAction, items } = this.state; if (status === 'loading' || status === 'idle') { return ( @@ -964,119 +587,336 @@ export class ShipmentList extends PureComponent { return ; } - const filtered = this.getFilteredOrders(); - rowContext.partiesById = partiesById; - rowContext.onOpen = this.handleOpenOrder; + const filteredShipments = this.getFilteredShipments(); + const selectedShipment = filteredShipments.find((s) => s.id === selectedShipmentId) + || this.extractAllShipments().find((s) => s.id === selectedShipmentId) + || filteredShipments[0] + || null; + + const shPackages = Array.isArray(selectedShipment?.shippings) ? selectedShipment.shippings : []; + const shLines = Array.isArray(selectedShipment?.lines) ? selectedShipment.lines : []; + const hasShippedPackages = shPackages.some((p) => p.state === 'shipped'); return ( <> + {/* Top Filter and Action Bar */} - - this.setState({ filter: next })} - /> - - - - - this.setState({ onlyUndelivered: e.target.checked })} - /> - } - label={ - - Nur ungelieferte Aufträge - - } - sx={{ m: 0 }} - /> - - - - {filtered.length} Auftrag / Aufträge zur Auslieferung - - + this.setState({ filter: next })} + /> + + {/* Master-Detail Split Grid */} - - - Auftragsnr. - Datum - Kunde - Lieferstatus - Sendungen - + {/* Left: Shipment Manifests List */} + + + + Lieferscheine (Shipments) + + + + + + {filteredShipments.length === 0 ? ( + + + Keine Lieferscheine gefunden. + - } - /> + ) : ( + filteredShipments.map((sh) => { + const isSelected = selectedShipment?.id === sh.id; + const party = partiesById[sh.entityId]; + const partyName = partyDisplayName('order', party) || `Kunde #${sh.entityId}`; + const pkgCount = Array.isArray(sh.shippings) ? sh.shippings.length : 0; + const lineCount = Array.isArray(sh.lines) ? sh.lines.length : 0; + const allShipped = pkgCount > 0 && sh.shippings.every((p) => p.state === 'shipped'); + + return ( + this.handleSelectShipment(sh)} + sx={{ + py: 1.25, + px: 1.75, + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + gap: 0.5, + bgcolor: isSelected ? 'action.selected' : 'inherit', + }} + > + + + {sh.number} + + + {sh.date ? formatDate(sh.date) : '–'} + + + + + + Auftrag: {sh.parentOrderNumber} • {partyName} + + + + 0 ? 'primary' : 'default'} + label={`${pkgCount} Paket${pkgCount === 1 ? '' : 'e'}`} + sx={{ height: 18, fontSize: '0.65rem', fontWeight: 600 }} + /> + + + + ); + }) + )} + + + + {/* Right: Selected Shipment Detail & Shipping Packages */} + + {selectedShipment ? ( + + {/* Header */} + + + + + + {selectedShipment.number} + + + + + Zugehöriger Auftrag: {selectedShipment.parentOrderNumber} • Datum: {selectedShipment.date ? formatDate(selectedShipment.date) : '–'} + {selectedShipment.externalNumber ? ` • Ref: ${selectedShipment.externalNumber}` : ''} + + + + + + {!hasShippedPackages && ( + this.handleDeleteShipment(selectedShipment)} + aria-label={`Lieferschein ${selectedShipment.number} löschen`} + > + + + )} + + + + {/* Shipped Article Positions */} + + + Positionen dieses Lieferscheins ({shLines.length}) + + + + + + SKU + Bezeichnung + Menge + + + + {shLines.map((line, idx) => ( + + {line.sku} + {line.name} + {line.amount} + + ))} + +
+
+
+ + {/* Shipping Packages from 'shipping' Table */} + + + + Pakete aus der Versandtabelle (shipping) ({shPackages.length}) + + + + {shPackages.length === 0 ? ( + + + + Keine Pakete (Shippings) erfasst. + + + + ) : ( + + {shPackages.map((pkg) => { + const isShipped = pkg.state === 'shipped'; + return ( + + + + + + {pkg.carrier || 'DHL'} ({pkg.type === 'pallet' ? 'Palette' : 'Paket'}) + + {pkg.trackingCode ? ( + + + {pkg.trackingCode} + + { + navigator.clipboard.writeText(pkg.trackingCode); + enqueueSnackbar('Tracking-Code kopiert!', { variant: 'info' }); + }} + sx={{ p: 0.25 }} + > + + + + ) : ( + + Ohne Tracking + + )} + + + + + {isShipped ? ( + } + label="Versendet (Lager abgebucht)" + sx={{ fontWeight: 600 }} + /> + ) : ( + <> + + + this.handleDeletePackage(pkg)} + > + + + + )} + + + ); + })} + + )} + +
+ ) : ( + + + Wählen Sie einen Lieferschein aus der linken Liste aus. + + + )}
- {selectedOrderId != null && ( - this.setState({ addPackageDialogOpen: false })} + /> + )} + + {newShipmentModalOpen && ( + this.setState({ selectedOrderId: null })} + onClose={() => this.setState({ newShipmentModalOpen: false })} + onSuccess={(newId) => { + if (newId) this.setState({ selectedShipmentId: newId }); + }} /> )}