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 (
+
+ );
+ })}
+
+
+ this.setState({ accountId: e.target.value })}
@@ -288,13 +293,13 @@ class AddPaymentDialog extends PureComponent {
))
) : (
-
+
)}
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)}
-
-
-
- }
- onClick={() => this.setState({ addPaymentOpen: true })}
- disabled={busyAction}
- sx={{ alignSelf: 'center', height: 40 }}
- >
- Zahlung buchen
-
-
-
- {/* Payments list */}
-
- Gebuchte Zahlungseingänge ({payments.length})
-
-
- {payments.length === 0 ? (
-
-
- Bisher keine Zahlungen für diesen Auftrag erfasst.
-
- }
- onClick={() => this.setState({ addPaymentOpen: true })}
- >
- Erste Zahlung buchen
-
-
- ) : (
-
- {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}
+
+
+
+ }
+ onClick={() => this.setState({ recordDialogOpen: true })}
+ sx={{ height: 40 }}
+ >
+ Zahlungseingang erfassen
+
+
+
+ {/* 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 }}
- />
-
-
- );
- })}
-
-
-
-
-
-
- }>
- {saving ? 'Wird gespeichert…' : 'Lieferschein erstellen'}
-
-
-
-
- );
- }
-}
-
// 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 {
>
-
+
-
+
@@ -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 (
+
+ );
+ })}
+
+
+
+ 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 }}
+ />
+
+
+ );
+ })}
+
+
+
+ >
+ )}
+
+
+
+ }>
+ {saving ? 'Wird erstellt…' : 'Lieferschein anlegen'}
+
+
+
+
+ );
+ }
+}
+
+// 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
-
-
-
- }
- onClick={() => this.setState({ createShipmentOpen: true })}
- disabled={remaining <= 0 || busyAction}
- sx={{ alignSelf: 'center', height: 40 }}
- >
- Neue Teillieferung erfassen
-
-
-
- {/* Shipment Manifests List */}
-
- Zugeordnete Lieferscheine (Shipment Manifests)
-
-
- {shipments.length === 0 ? (
-
-
- Bisher keine Lieferscheine für diesen Auftrag erstellt.
-
- }
- onClick={() => this.setState({ createShipmentOpen: true })}
- disabled={remaining <= 0}
- >
- Erste Lieferung erstellen
-
-
- ) : (
-
- {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}` : ''}
-
-
-
-
-
- }
- onClick={() => this.setState({ addPackageShipment: sh })}
- disabled={busyAction}
- >
- Paket erfassen
-
- {!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 })}
+ />
+ }
+ onClick={() => this.setState({ newShipmentModalOpen: true })}
+ >
+ Neuer Lieferschein
+
+ {/* 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}` : ''}
+
+
+
+
+ }
+ onClick={() => this.setState({ addPackageDialogOpen: true })}
+ disabled={busyAction}
+ >
+ Paket hinzufügen
+
+ {!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.
+
+ }
+ onClick={() => this.setState({ addPackageDialogOpen: true })}
+ >
+ Paket anlegen
+
+
+ ) : (
+
+ {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 });
+ }}
/>
)}
>