1174 lines
41 KiB
JavaScript
1174 lines
41 KiB
JavaScript
import { PureComponent, createRef } from 'react';
|
||
import Box from '@mui/material/Box';
|
||
import Button from '@mui/material/Button';
|
||
import Chip from '@mui/material/Chip';
|
||
import CircularProgress from '@mui/material/CircularProgress';
|
||
import IconButton from '@mui/material/IconButton';
|
||
import ListItemButton from '@mui/material/ListItemButton';
|
||
import TextField from '@mui/material/TextField';
|
||
import Typography from '@mui/material/Typography';
|
||
import MenuItem from '@mui/material/MenuItem';
|
||
import Stack from '@mui/material/Stack';
|
||
import Skeleton from '@mui/material/Skeleton';
|
||
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||
import CloseIcon from '@mui/icons-material/Close';
|
||
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
|
||
import Menu from '@mui/material/Menu';
|
||
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 shipmentAutomat, { CHUNK_SIZE } from '../../automats/shipmentAutomat';
|
||
import orderAutomat from '../../automats/orderAutomat';
|
||
import numberAutomat from '../../automats/numberAutomat';
|
||
import { shipment as shipmentApi } from '../../api';
|
||
import { backendErrorFrom } from '../../utils/backendError';
|
||
import { notifyError } from '../../utils/notify';
|
||
import { getMaxDocumentNumber } from '../../utils/documentNumber';
|
||
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||
import withNavigate from '../withNavigate';
|
||
import ShipmentDetailDialog from './ShipmentDetailDialog';
|
||
import { getNextShipmentNumber } from './OrderShipmentDialog';
|
||
import {
|
||
formatDate,
|
||
loadParties,
|
||
manifestConfig,
|
||
numberDisplay,
|
||
partyDisplayName,
|
||
} from './manifestConfig';
|
||
import {
|
||
GREEN_PANEL,
|
||
SURFACE_RAISED,
|
||
} from '../../colors';
|
||
|
||
const cfg = manifestConfig('order');
|
||
|
||
const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
|
||
|
||
const STATE_CONFIG = {
|
||
new: { label: 'Neu', color: 'info', icon: LocalShippingIcon },
|
||
delivered: { label: 'Geliefert', color: 'success', icon: CheckCircleOutlineOutlinedIcon },
|
||
returned: { label: 'Retoure', color: 'error', icon: CloseIcon },
|
||
};
|
||
|
||
// MARK: <NewShipmentModal/>
|
||
class NewShipmentModal extends PureComponent {
|
||
constructor(props) {
|
||
super(props);
|
||
this.state = {
|
||
orderId: '',
|
||
date: new Date().toISOString().slice(0, 10),
|
||
externalNumber: '',
|
||
lineAmounts: {},
|
||
saving: false,
|
||
};
|
||
this.handleOrderChange = this.handleOrderChange.bind(this);
|
||
this.handleSubmit = this.handleSubmit.bind(this);
|
||
}
|
||
|
||
componentDidUpdate(prevProps) {
|
||
if (!prevProps.open && this.props.open) {
|
||
const openOrders = (this.props.orders || []).filter((o) => {
|
||
const ordQty = Number(o.orderedQuantity) || 0;
|
||
const shpQty = Number(o.shippedQuantity) || 0;
|
||
return ordQty === 0 || shpQty < ordQty;
|
||
});
|
||
const first = openOrders[0];
|
||
if (first) {
|
||
this.handleOrderChange(first.id);
|
||
}
|
||
}
|
||
}
|
||
|
||
handleOrderChange(orderId) {
|
||
const { orders = [] } = this.props;
|
||
const order = orders.find((o) => o.id === Number(orderId));
|
||
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);
|
||
}
|
||
}
|
||
|
||
const lineAmounts = {};
|
||
for (const l of lines) {
|
||
if (!l.sku) continue;
|
||
const ordered = Number(l.amount) || 0;
|
||
const shipped = shippedBySku[l.sku] || 0;
|
||
const remaining = Math.max(0, ordered - shipped);
|
||
if (remaining > 0) {
|
||
lineAmounts[l.sku] = String(remaining);
|
||
}
|
||
}
|
||
|
||
this.setState({
|
||
orderId,
|
||
externalNumber: order?.externalNumber || '',
|
||
lineAmounts,
|
||
});
|
||
}
|
||
|
||
handleAmountChange(sku, val) {
|
||
this.setState((prev) => ({
|
||
lineAmounts: { ...prev.lineAmounts, [sku]: val },
|
||
}));
|
||
}
|
||
|
||
async handleSubmit() {
|
||
const { orderId, date, externalNumber, lineAmounts } = this.state;
|
||
if (!orderId) {
|
||
enqueueSnackbar('Bitte wählen Sie einen Auftrag aus.', { variant: 'warning' });
|
||
return;
|
||
}
|
||
|
||
const lines = Object.entries(lineAmounts)
|
||
.filter(([, amt]) => Number(amt) > 0)
|
||
.map(([sku, amt]) => ({ sku, amount: Number(amt) }));
|
||
|
||
if (lines.length === 0) {
|
||
enqueueSnackbar('Mindestens eine Position muss mit Menge > 0 geliefert werden.', { variant: 'warning' });
|
||
return;
|
||
}
|
||
|
||
this.setState({ saving: true });
|
||
try {
|
||
const res = await shipmentApi.create(orderId, {
|
||
date,
|
||
externalNumber: externalNumber.trim() || undefined,
|
||
lines,
|
||
});
|
||
|
||
enqueueSnackbar(res.message || 'Lieferschein erfolgreich erstellt.', { variant: 'success' });
|
||
void orderAutomat.actions.reload().catch(() => {});
|
||
void shipmentAutomat.actions.reload().catch(() => {});
|
||
|
||
this.props.onSuccess?.(res.shipment);
|
||
this.props.onClose?.();
|
||
} catch (err) {
|
||
notifyError(err);
|
||
} finally {
|
||
this.setState({ saving: false });
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const { open, onClose, orders = [], partiesById = {} } = this.props;
|
||
const { orderId, date, externalNumber, lineAmounts, saving } = this.state;
|
||
|
||
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 (
|
||
<AppDialog
|
||
open={open}
|
||
onClose={saving ? undefined : onClose}
|
||
title="Neuen Lieferschein erstellen"
|
||
maxWidth="md"
|
||
fullWidth
|
||
>
|
||
<Box sx={{ p: 2.5 }}>
|
||
<Box sx={{ mb: 2.5, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
<TextField
|
||
select
|
||
label="Bezugsauftrag auswählen"
|
||
size="small"
|
||
value={orderId}
|
||
onChange={(e) => this.handleOrderChange(e.target.value)}
|
||
fullWidth
|
||
>
|
||
{orders.map((o) => {
|
||
const customer = partiesById[o.entityId];
|
||
const custName = partyDisplayName('order', customer) || (o.entityId ? `Kunde #${o.entityId}` : '');
|
||
return (
|
||
<MenuItem key={o.id} value={o.id}>
|
||
{o.number} {custName ? `— ${custName}` : ''} {o.date ? `(${formatDate(o.date)})` : ''}
|
||
</MenuItem>
|
||
);
|
||
})}
|
||
</TextField>
|
||
|
||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||
<TextField
|
||
label="Lieferdatum"
|
||
type="date"
|
||
size="small"
|
||
value={date}
|
||
onChange={(e) => this.setState({ date: e.target.value })}
|
||
InputLabelProps={{ shrink: true }}
|
||
sx={{ width: 170 }}
|
||
/>
|
||
{currentOrder ? (
|
||
<TextField
|
||
label="Lieferschein-Nr."
|
||
size="small"
|
||
value={getNextShipmentNumber(currentOrder.number, currentOrder.shipments)}
|
||
disabled
|
||
slotProps={{ input: { readOnly: true } }}
|
||
sx={{ width: 190 }}
|
||
/>
|
||
) : null}
|
||
<TextField
|
||
label="Referenz"
|
||
size="small"
|
||
value={externalNumber}
|
||
onChange={(e) => this.setState({ externalNumber: e.target.value })}
|
||
sx={{ flex: 1 }}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
|
||
{currentOrder && (
|
||
<Box sx={{ mb: 2.5 }}>
|
||
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||
Lieferbare Positionen für Auftrag {currentOrder.number}:
|
||
</Typography>
|
||
<Stack spacing={1}>
|
||
{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] ?? '';
|
||
|
||
return (
|
||
<Box
|
||
key={line.sku}
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
p: 1.25,
|
||
borderRadius: 1,
|
||
bgcolor: SURFACE_RAISED,
|
||
}}
|
||
>
|
||
<Box sx={{ minWidth: 0, flex: 1, pr: 2 }}>
|
||
<Typography variant="body2" sx={{ fontWeight: 600, ...ellipsisSx }}>
|
||
{line.sku} — {line.name || '–'}
|
||
</Typography>
|
||
<Typography variant="caption" color="text.secondary">
|
||
Bestellt: {orderedAmt} • Bisher geliefert: {shippedAmt} • Noch offen: <strong>{remaining}</strong>
|
||
</Typography>
|
||
</Box>
|
||
<TextField
|
||
size="small"
|
||
type="number"
|
||
label="Liefern"
|
||
value={currentVal}
|
||
onChange={(e) => this.handleAmountChange(line.sku, e.target.value)}
|
||
disabled={remaining <= 0}
|
||
slotProps={{ htmlInput: { min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } } }}
|
||
sx={{ width: 110 }}
|
||
/>
|
||
</Box>
|
||
);
|
||
})}
|
||
</Stack>
|
||
</Box>
|
||
)}
|
||
|
||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1.5 }}>
|
||
<Button variant="outlined" onClick={onClose} disabled={saving}>
|
||
Abbrechen
|
||
</Button>
|
||
<Button variant="contained" onClick={this.handleSubmit} disabled={saving || !currentOrder} startIcon={<LocalShippingIcon />}>
|
||
{saving ? 'Wird erstellt…' : 'Lieferschein anlegen'}
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
</AppDialog>
|
||
);
|
||
}
|
||
}
|
||
|
||
// MARK: <DeliveryStateMenu/>
|
||
class DeliveryStateMenu extends PureComponent {
|
||
constructor(props) {
|
||
super(props);
|
||
this.state = {
|
||
anchorEl: null,
|
||
busy: false,
|
||
};
|
||
this.handleOpen = this.handleOpen.bind(this);
|
||
this.handleClose = this.handleClose.bind(this);
|
||
this.handleSelectState = this.handleSelectState.bind(this);
|
||
}
|
||
|
||
handleOpen(e) {
|
||
e.stopPropagation();
|
||
this.setState({ anchorEl: e.currentTarget });
|
||
}
|
||
|
||
handleClose(e) {
|
||
if (e) e.stopPropagation();
|
||
this.setState({ anchorEl: null });
|
||
}
|
||
|
||
async handleSelectState(newState) {
|
||
this.setState({ anchorEl: null, busy: true });
|
||
try {
|
||
await shipmentAutomat.actions.updateState(this.props.shipment.id, newState);
|
||
const label = STATE_CONFIG[newState]?.label || newState;
|
||
enqueueSnackbar(`Status geändert: ${label}`, { variant: 'success' });
|
||
} catch (err) {
|
||
notifyError(err);
|
||
} finally {
|
||
this.setState({ busy: false });
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const { shipment } = this.props;
|
||
const { anchorEl, busy } = this.state;
|
||
const currentState = shipment.state || 'new';
|
||
const conf = STATE_CONFIG[currentState] || STATE_CONFIG.new;
|
||
|
||
return (
|
||
<Box onClick={(e) => e.stopPropagation()}>
|
||
<Chip
|
||
size="small"
|
||
color={conf.color}
|
||
label={
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||
<span>{conf.label}</span>
|
||
<ArrowDropDownIcon sx={{ fontSize: 16 }} />
|
||
</Box>
|
||
}
|
||
disabled={busy}
|
||
onClick={this.handleOpen}
|
||
sx={{ fontWeight: 600, height: 24, cursor: 'pointer' }}
|
||
/>
|
||
<Menu
|
||
anchorEl={anchorEl}
|
||
open={Boolean(anchorEl)}
|
||
onClose={this.handleClose}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<MenuItem
|
||
selected={currentState === 'new'}
|
||
onClick={() => this.handleSelectState('new')}
|
||
>
|
||
Neu (In Vorbereitung)
|
||
</MenuItem>
|
||
<MenuItem
|
||
selected={currentState === 'delivered'}
|
||
onClick={() => this.handleSelectState('delivered')}
|
||
>
|
||
Geliefert (Zugestellt)
|
||
</MenuItem>
|
||
<MenuItem
|
||
selected={currentState === 'returned'}
|
||
onClick={() => this.handleSelectState('returned')}
|
||
>
|
||
Retoure (Retourniert)
|
||
</MenuItem>
|
||
</Menu>
|
||
</Box>
|
||
);
|
||
}
|
||
}
|
||
|
||
// Dummy shipment generator with max digits for layout testing and probe measurement
|
||
export function createDummyTestShipment() {
|
||
const maxOrderNumber = getMaxDocumentNumber('order');
|
||
return {
|
||
id: 999999,
|
||
number: `${maxOrderNumber}-L-99`,
|
||
externalNumber: 'REF-999999',
|
||
date: '2030-10-30T12:00:00Z',
|
||
orderNumber: maxOrderNumber,
|
||
customerName: 'Kunde',
|
||
positions: 999,
|
||
totalQuantity: 999,
|
||
packageCount: 9,
|
||
carrier: 'DHL',
|
||
trackingCode: '00340434161094042578',
|
||
state: 'delivered',
|
||
};
|
||
}
|
||
|
||
const rowGridSx = {
|
||
display: 'grid',
|
||
gridTemplateColumns: 'max-content max-content max-content 1fr max-content max-content max-content auto',
|
||
gridTemplateRows: '0px 1fr',
|
||
rowGap: 0,
|
||
columnGap: 1.5,
|
||
alignItems: 'center',
|
||
width: '100%',
|
||
boxSizing: 'border-box',
|
||
};
|
||
|
||
const measuringCellSx = {
|
||
visibility: 'hidden',
|
||
height: 0,
|
||
boxSizing: 'border-box',
|
||
pointerEvents: 'none',
|
||
textTransform: 'none',
|
||
letterSpacing: 'normal',
|
||
};
|
||
|
||
const headerCellSx = {
|
||
color: 'text.secondary',
|
||
typography: 'caption',
|
||
fontWeight: 600,
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.04em',
|
||
userSelect: 'none',
|
||
};
|
||
|
||
function renderInvisibleDummyRow(dummy) {
|
||
const d = dummy || createDummyTestShipment();
|
||
return (
|
||
<>
|
||
<Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
|
||
<Typography variant="body1" sx={{ fontWeight: 600 }}>{d.number}</Typography>
|
||
<Typography variant="body2">{d.externalNumber || 'Nr. / Ref.'}</Typography>
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
|
||
<Typography variant="body2">{d.date ? formatDate(d.date) : '–'}</Typography>
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
|
||
<Typography variant="body2" sx={{ fontWeight: 600 }}>{d.orderNumber}</Typography>
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
|
||
<Typography variant="body1">{d.customerName}</Typography>
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 5, gridRow: 1, display: 'flex', justifyContent: 'center', ...measuringCellSx }}>
|
||
<Chip size="small" label={d.positions} sx={{ height: 24, '& .MuiChip-label': { px: 0.5 } }} />
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 6, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||
<Chip size="small" variant="outlined" label="DHL" sx={{ height: 20, fontSize: '0.7rem' }} />
|
||
<Typography variant="caption" sx={{ fontWeight: 600, fontVariantNumeric: 'tabular-nums' }}>
|
||
{d.trackingCode}
|
||
</Typography>
|
||
</Box>
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 7, gridRow: 1, display: 'flex', justifyContent: 'center', ...measuringCellSx }}>
|
||
<Chip
|
||
size="small"
|
||
color="success"
|
||
label={
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||
<span>Geliefert</span>
|
||
<ArrowDropDownIcon sx={{ fontSize: 16 }} />
|
||
</Box>
|
||
}
|
||
sx={{ fontWeight: 600, height: 24 }}
|
||
/>
|
||
</Box>
|
||
<Box aria-hidden sx={{ gridColumn: 8, gridRow: 1, display: 'flex', justifyContent: 'center', ...measuringCellSx }}>
|
||
<IconButton sx={{ width: 40, height: 40 }} />
|
||
</Box>
|
||
</>
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Ambient context for `ShipmentRow`: FillSpace renders rows as bare elements
|
||
* (`<Row item index />`), so the list publishes callbacks and party lookup here.
|
||
*/
|
||
const rowContext = {
|
||
partiesById: {},
|
||
busyId: null,
|
||
onOpen: null,
|
||
onDelete: null,
|
||
headerItem: null,
|
||
};
|
||
|
||
// MARK: <ShipmentRow/>
|
||
class ShipmentRow extends PureComponent {
|
||
partyName(partyId) {
|
||
if (partyId == null) return '–';
|
||
const party = rowContext.partiesById[partyId];
|
||
return partyDisplayName('order', party) || `Kunde #${partyId}`;
|
||
}
|
||
|
||
render() {
|
||
const { item: shipment, disabled } = this.props;
|
||
const dummy = this.props.headerItem || rowContext.headerItem;
|
||
|
||
// Data-free probe (FillSpace measures the row height with `item` undefined).
|
||
if (!shipment) {
|
||
if (disabled) {
|
||
return (
|
||
<ListItemButton disabled divider sx={{ ...rowGridSx, height: 64, minHeight: 64, maxHeight: 64, px: 2, py: 0, opacity: 0, pointerEvents: 'none' }}>
|
||
<Typography variant="body2"> </Typography>
|
||
</ListItemButton>
|
||
);
|
||
}
|
||
return (
|
||
<ListItemButton disabled divider sx={{ ...rowGridSx, height: 64, minHeight: 64, maxHeight: 64, px: 2, py: 0 }}>
|
||
{renderInvisibleDummyRow(dummy)}
|
||
<Box sx={{ gridColumn: 1, gridRow: 2 }}><Skeleton variant="text" width="90%" height={20} /></Box>
|
||
<Box sx={{ gridColumn: 2, gridRow: 2 }}><Skeleton variant="text" width="90%" height={20} /></Box>
|
||
<Box sx={{ gridColumn: 3, gridRow: 2 }}><Skeleton variant="text" width="90%" height={20} /></Box>
|
||
<Box sx={{ gridColumn: 4, gridRow: 2 }}><Skeleton variant="text" width="60%" height={20} /></Box>
|
||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center' }}><Skeleton variant="rounded" width={28} height={20} /></Box>
|
||
<Box sx={{ gridColumn: 6, gridRow: 2 }}><Skeleton variant="text" width={100} height={20} /></Box>
|
||
<Box sx={{ gridColumn: 7, gridRow: 2, display: 'flex', justifyContent: 'center' }}><Skeleton variant="rounded" width={70} height={24} /></Box>
|
||
<Box sx={{ gridColumn: 8, gridRow: 2 }} />
|
||
</ListItemButton>
|
||
);
|
||
}
|
||
|
||
const { busyId, onOpen, onDelete } = rowContext;
|
||
const isBusy = busyId === shipment.id;
|
||
const canDelete = shipment.state === 'new';
|
||
|
||
return (
|
||
<ListItemButton
|
||
divider
|
||
sx={{ ...rowGridSx, height: 64, minHeight: 64, maxHeight: 64, px: 2, py: 0 }}
|
||
onClick={disabled ? undefined : () => onOpen?.(shipment)}
|
||
disabled={disabled}
|
||
>
|
||
{renderInvisibleDummyRow(dummy)}
|
||
|
||
{/* 1. Lieferschein-Nr & Ref */}
|
||
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
<Typography variant="body1" sx={{ fontWeight: 600, ...ellipsisSx }}>
|
||
{shipment.number}
|
||
</Typography>
|
||
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
|
||
{numberDisplay(shipment.externalNumber)}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* 2. Datum */}
|
||
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
|
||
{shipment.date ? formatDate(shipment.date) : '–'}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* 3. Bezugsauftrag */}
|
||
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{shipment.orderNumber ? (
|
||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'primary.main', ...ellipsisSx }}>
|
||
{shipment.orderNumber}
|
||
</Typography>
|
||
) : (
|
||
<Typography variant="body2" color="text.secondary">
|
||
–
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
|
||
{/* 4. Kunde */}
|
||
<Box sx={{ gridColumn: 4, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
<Typography variant="body1" sx={{ fontWeight: 500, ...ellipsisSx }}>
|
||
{shipment.customerName || this.partyName(shipment.entityId)}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{/* 5. Positionen */}
|
||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center' }}>
|
||
<Chip
|
||
size="small"
|
||
label={shipment.positions || 0}
|
||
title={`${shipment.positions || 0} Positionen (${shipment.totalQuantity || 0} Stk.)`}
|
||
sx={{ height: 24, '& .MuiChip-label': { px: 0.5 } }}
|
||
/>
|
||
</Box>
|
||
|
||
{/* 6. Paket / Tracking */}
|
||
<Box sx={{ gridColumn: 6, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{shipment.trackingCode ? (
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||
{shipment.carrier && (
|
||
<Chip size="small" variant="outlined" label={shipment.carrier} sx={{ height: 20, fontSize: '0.7rem' }} />
|
||
)}
|
||
<Typography variant="caption" sx={{ fontWeight: 600, fontVariantNumeric: 'tabular-nums', ...ellipsisSx }}>
|
||
{shipment.trackingCode}
|
||
</Typography>
|
||
</Box>
|
||
) : shipment.packageCount > 0 ? (
|
||
<Typography variant="caption" color="text.secondary">
|
||
{shipment.packageCount} {shipment.packageCount === 1 ? 'Paket' : 'Pakete'}
|
||
</Typography>
|
||
) : (
|
||
<Typography variant="caption" color="text.secondary">
|
||
–
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
|
||
{/* 7. Status */}
|
||
<Box sx={{ gridColumn: 7, gridRow: 2, display: 'flex', justifyContent: 'center' }}>
|
||
<DeliveryStateMenu shipment={shipment} />
|
||
</Box>
|
||
|
||
{/* 8. Aktionen (Löschen) */}
|
||
<Box sx={{ gridColumn: 8, gridRow: 2, display: 'flex', justifyContent: 'center' }}>
|
||
{canDelete ? (
|
||
<IconButton
|
||
aria-label={`„${shipment.number}“ löschen`}
|
||
disabled={isBusy || disabled}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onDelete?.(shipment);
|
||
}}
|
||
onMouseDown={(e) => e.stopPropagation()}
|
||
sx={{ width: 40, height: 40 }}
|
||
>
|
||
<DeleteOutlinedIcon sx={{ fontSize: 24 }} />
|
||
</IconButton>
|
||
) : (
|
||
<Box sx={{ width: 40, height: 40 }} />
|
||
)}
|
||
</Box>
|
||
</ListItemButton>
|
||
);
|
||
}
|
||
}
|
||
|
||
// MARK: <ShipmentSearchField/>
|
||
class ShipmentSearchField extends PureComponent {
|
||
constructor(props) {
|
||
super(props);
|
||
const s = shipmentAutomat.state;
|
||
this.state = {
|
||
input: s.filter ?? '',
|
||
stateFilter: s.stateFilter ?? '',
|
||
seekMonths: s.seekMonths ?? 0,
|
||
seekYears: s.seekYears ?? 0,
|
||
};
|
||
}
|
||
|
||
componentDidMount() {
|
||
this.unsub = shipmentAutomat.subscribe(this, (state) => ({
|
||
input: state.filter ?? '',
|
||
stateFilter: state.stateFilter ?? '',
|
||
seekMonths: state.seekMonths ?? 0,
|
||
seekYears: state.seekYears ?? 0,
|
||
}));
|
||
}
|
||
|
||
componentWillUnmount() {
|
||
this.unsub?.();
|
||
}
|
||
|
||
render() {
|
||
const {
|
||
inputRef,
|
||
onCreate,
|
||
customerFilterOpen,
|
||
onToggleCustomerFilter,
|
||
selectedCustomer,
|
||
onClearCustomerFilter,
|
||
} = this.props;
|
||
const { input, stateFilter, seekMonths, seekYears } = this.state;
|
||
const monthActive = seekMonths > 0;
|
||
const monthLabel = !monthActive ? '-1 Monat' : `-${seekMonths} Monat${seekMonths > 1 ? 'e' : ''}`;
|
||
const yearActive = seekYears > 0;
|
||
const yearLabel = !yearActive ? '-1 Jahr' : `-${seekYears} Jahr${seekYears > 1 ? 'e' : ''}`;
|
||
|
||
return (
|
||
<PanelTileGrid>
|
||
<Box
|
||
ref={inputRef}
|
||
sx={{
|
||
gridColumn: { xs: '1 / -1', sm: 'span 2', md: 'span 2' },
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 1.25,
|
||
pb: 2,
|
||
}}
|
||
>
|
||
{/* Top row: search field & create button */}
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 1,
|
||
}}
|
||
>
|
||
<SingleLineTextEdit
|
||
sx={{ flex: 1 }}
|
||
size="small"
|
||
search
|
||
clear
|
||
placeholder="Lieferschein-, Auftrags-, Tracking-Nr. oder Kunde suchen…"
|
||
value={input}
|
||
onChange={(next) => shipmentAutomat.actions.setFilter(next)}
|
||
/>
|
||
<PartyFilterButton
|
||
title="Kunden"
|
||
open={customerFilterOpen}
|
||
onClick={onToggleCustomerFilter}
|
||
/>
|
||
{selectedCustomer && (
|
||
<PartyFilterChip
|
||
label={partyDisplayName('order', selectedCustomer)}
|
||
onDelete={onClearCustomerFilter}
|
||
/>
|
||
)}
|
||
<Button variant="contained" onClick={onCreate}>
|
||
Lieferschein erstellen
|
||
</Button>
|
||
</Box>
|
||
|
||
{/* Filter Bar: Status chips & Negativeseek (-1 Month, -1 Year, Reset) */}
|
||
<Box
|
||
sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
flexWrap: 'wrap',
|
||
gap: 1.5,
|
||
}}
|
||
>
|
||
{/* Left: Status chips */}
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 0.75,
|
||
px: 1.25,
|
||
py: 0.25,
|
||
borderRadius: 2,
|
||
bgcolor: 'background.paper',
|
||
boxShadow: 1,
|
||
}}
|
||
>
|
||
<Chip
|
||
size="small"
|
||
label="Alle"
|
||
color={!stateFilter ? 'primary' : 'default'}
|
||
variant={!stateFilter ? 'filled' : 'outlined'}
|
||
onClick={() => shipmentAutomat.actions.setStateFilter('')}
|
||
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
||
/>
|
||
<Chip
|
||
size="small"
|
||
label="Neu"
|
||
color={stateFilter === 'new' ? 'info' : 'default'}
|
||
variant={stateFilter === 'new' ? 'filled' : 'outlined'}
|
||
onClick={() => shipmentAutomat.actions.setStateFilter('new')}
|
||
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
||
/>
|
||
<Chip
|
||
size="small"
|
||
label="Geliefert"
|
||
color={stateFilter === 'delivered' ? 'success' : 'default'}
|
||
variant={stateFilter === 'delivered' ? 'filled' : 'outlined'}
|
||
onClick={() => shipmentAutomat.actions.setStateFilter('delivered')}
|
||
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
||
/>
|
||
<Chip
|
||
size="small"
|
||
label="Retoure"
|
||
color={stateFilter === 'returned' ? 'error' : 'default'}
|
||
variant={stateFilter === 'returned' ? 'filled' : 'outlined'}
|
||
onClick={() => shipmentAutomat.actions.setStateFilter('returned')}
|
||
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
||
/>
|
||
</Box>
|
||
|
||
{/* Right: Negativeseek controls (-1 Month, -1 Year, Reset) */}
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||
<Box
|
||
sx={{
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 0.75,
|
||
px: 1.25,
|
||
py: 0.25,
|
||
borderRadius: 2,
|
||
bgcolor: 'background.paper',
|
||
boxShadow: 1,
|
||
}}
|
||
>
|
||
<Button
|
||
size="small"
|
||
variant={monthActive ? 'contained' : 'outlined'}
|
||
color={monthActive ? 'primary' : 'inherit'}
|
||
onClick={() => shipmentAutomat.actions.addSeekMonth()}
|
||
sx={{
|
||
minWidth: 88,
|
||
px: 1.25,
|
||
py: 0.25,
|
||
height: 28,
|
||
fontSize: '0.75rem',
|
||
fontWeight: 600,
|
||
textTransform: 'none',
|
||
boxSizing: 'border-box',
|
||
border: '1px solid',
|
||
borderColor: monthActive ? 'primary.main' : 'divider',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{monthLabel}
|
||
</Button>
|
||
|
||
<Button
|
||
size="small"
|
||
variant={yearActive ? 'contained' : 'outlined'}
|
||
color={yearActive ? 'primary' : 'inherit'}
|
||
onClick={() => shipmentAutomat.actions.addSeekYear()}
|
||
sx={{
|
||
minWidth: 78,
|
||
px: 1.25,
|
||
py: 0.25,
|
||
height: 28,
|
||
fontSize: '0.75rem',
|
||
fontWeight: 600,
|
||
textTransform: 'none',
|
||
boxSizing: 'border-box',
|
||
border: '1px solid',
|
||
borderColor: yearActive ? 'primary.main' : 'divider',
|
||
whiteSpace: 'nowrap',
|
||
}}
|
||
>
|
||
{yearLabel}
|
||
</Button>
|
||
|
||
<Button
|
||
size="small"
|
||
variant="text"
|
||
color="inherit"
|
||
disabled={!monthActive && !yearActive}
|
||
onClick={() => shipmentAutomat.actions.resetNegativeSeek()}
|
||
sx={{
|
||
minWidth: 'auto',
|
||
px: 1,
|
||
py: 0.25,
|
||
height: 28,
|
||
fontSize: '0.75rem',
|
||
fontWeight: 600,
|
||
textTransform: 'none',
|
||
}}
|
||
>
|
||
Reset
|
||
</Button>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</Box>
|
||
</PanelTileGrid>
|
||
);
|
||
}
|
||
}
|
||
|
||
// MARK: <ShipmentList/>
|
||
export class ShipmentList extends PureComponent {
|
||
constructor(props) {
|
||
super(props);
|
||
this.headerItem = createDummyTestShipment();
|
||
this.state = {
|
||
items: shipmentAutomat.getItems(),
|
||
total: shipmentAutomat.state.total ?? 0,
|
||
unfilteredTotal: shipmentAutomat.state.unfilteredTotal ?? 0,
|
||
filterKey: shipmentAutomat.getFilterKey?.() ?? '',
|
||
entityId: shipmentAutomat.state.entityId ?? null,
|
||
customerFilterOpen: false,
|
||
orders: orderAutomat.state?.items || [],
|
||
partiesById: {},
|
||
partiesList: [],
|
||
selectedShipment: null,
|
||
newShipmentModalOpen: false,
|
||
busyId: null,
|
||
};
|
||
this.searchRef = createRef();
|
||
this._cancelled = false;
|
||
this.handleRetry = this.handleRetry.bind(this);
|
||
this.handleOpenShipment = this.handleOpenShipment.bind(this);
|
||
this.handleCloseShipment = this.handleCloseShipment.bind(this);
|
||
this.handleDeleteShipment = this.handleDeleteShipment.bind(this);
|
||
this.handlePageChange = this.handlePageChange.bind(this);
|
||
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
|
||
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
|
||
}
|
||
|
||
componentDidMount() {
|
||
this._cancelled = false;
|
||
this.unsub = shipmentAutomat.subscribe(this, (state) => ({
|
||
items: state.items,
|
||
total: state.total ?? 0,
|
||
unfilteredTotal: state.unfilteredTotal ?? 0,
|
||
filterKey: shipmentAutomat.getFilterKey(state),
|
||
entityId: state.entityId ?? null,
|
||
}));
|
||
|
||
this.unsubOrder = orderAutomat.subscribe(this, (state) => ({
|
||
orders: state.items || [],
|
||
}));
|
||
|
||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||
this.headerItem = createDummyTestShipment();
|
||
this.forceUpdate();
|
||
});
|
||
void numberAutomat.actions.ensureLoaded().catch(() => {});
|
||
|
||
void shipmentAutomat.actions.load().catch(() => {});
|
||
void orderAutomat.actions.load().catch(() => {});
|
||
void this.loadParties();
|
||
|
||
// Check query params for targeted shipmentId
|
||
const searchParams = new URLSearchParams(this.props.location?.search || (typeof window !== 'undefined' ? window.location.search : ''));
|
||
const targetId = Number(searchParams.get('shipmentId') || searchParams.get('id')) || null;
|
||
if (targetId) {
|
||
this.setState({ selectedShipment: { id: targetId } });
|
||
}
|
||
}
|
||
|
||
componentDidUpdate(prevProps) {
|
||
const prevSearch = prevProps.location?.search;
|
||
const currentSearch = this.props.location?.search;
|
||
if (prevSearch !== currentSearch) {
|
||
const searchParams = new URLSearchParams(currentSearch || (typeof window !== 'undefined' ? window.location.search : ''));
|
||
const targetId = Number(searchParams.get('shipmentId') || searchParams.get('id')) || null;
|
||
if (targetId && targetId !== this.state.selectedShipment?.id) {
|
||
this.setState({ selectedShipment: { id: targetId } });
|
||
}
|
||
}
|
||
}
|
||
|
||
componentWillUnmount() {
|
||
this._cancelled = true;
|
||
this.unsub?.();
|
||
this.unsubOrder?.();
|
||
this.unsubNumber?.();
|
||
}
|
||
|
||
handleToggleCustomerFilter() {
|
||
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
|
||
}
|
||
|
||
handleSelectCustomer(entityId) {
|
||
shipmentAutomat.actions.setEntityId(entityId);
|
||
}
|
||
|
||
async loadParties() {
|
||
try {
|
||
const parties = await loadParties('order');
|
||
if (!this._cancelled) {
|
||
this.setState({
|
||
partiesById: cfg.partiesByIdFromList(parties),
|
||
partiesList: parties || [],
|
||
});
|
||
}
|
||
} catch {
|
||
// Non-fatal
|
||
}
|
||
}
|
||
|
||
handlePageChange(page, meta) {
|
||
const pageSize = meta?.pageSize ?? 10;
|
||
const startIndex = (page - 1) * pageSize;
|
||
const items = this.state.items;
|
||
const maxIndex = items ? items.length - 1 : startIndex;
|
||
const endIndex = Math.min(maxIndex, startIndex + pageSize - 1);
|
||
|
||
const startChunk = Math.floor(startIndex / CHUNK_SIZE);
|
||
const endChunk = Math.floor(endIndex / CHUNK_SIZE);
|
||
|
||
shipmentAutomat.requireChunk(startChunk, endChunk);
|
||
}
|
||
|
||
handleRetry() {
|
||
void shipmentAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => {});
|
||
}
|
||
|
||
handleOpenShipment(shipment) {
|
||
this.setState({ selectedShipment: shipment });
|
||
}
|
||
|
||
handleCloseShipment() {
|
||
this.setState({ selectedShipment: null });
|
||
}
|
||
|
||
async handleDeleteShipment(sh) {
|
||
if (this.state.busyId != null) return;
|
||
if (!window.confirm(`Lieferschein „${sh.number}“ wirklich löschen?`)) return;
|
||
this.setState({ busyId: sh.id });
|
||
try {
|
||
await shipmentAutomat.actions.deleteShipment(sh.id);
|
||
enqueueSnackbar(`Lieferschein ${sh.number} gelöscht.`, { variant: 'success' });
|
||
} catch (err) {
|
||
notifyError(err);
|
||
} finally {
|
||
if (!this._cancelled) this.setState({ busyId: null });
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const {
|
||
items,
|
||
total,
|
||
unfilteredTotal,
|
||
filterKey,
|
||
entityId,
|
||
customerFilterOpen,
|
||
orders,
|
||
partiesById,
|
||
partiesList,
|
||
selectedShipment,
|
||
newShipmentModalOpen,
|
||
busyId,
|
||
} = this.state;
|
||
|
||
rowContext.partiesById = partiesById;
|
||
rowContext.busyId = busyId;
|
||
rowContext.onOpen = this.handleOpenShipment;
|
||
rowContext.onDelete = this.handleDeleteShipment;
|
||
rowContext.headerItem = this.headerItem;
|
||
|
||
const customerCounts = {};
|
||
if (Array.isArray(items)) {
|
||
for (const s of items) {
|
||
if (s?.entityId) {
|
||
customerCounts[s.entityId] = (customerCounts[s.entityId] || 0) + 1;
|
||
}
|
||
}
|
||
}
|
||
const selectedCustomer = entityId ? (partiesById[entityId] || { id: entityId, name: `Kunde #${entityId}` }) : null;
|
||
|
||
const searchField = (
|
||
<ShipmentSearchField
|
||
inputRef={this.searchRef}
|
||
onCreate={() => this.setState({ newShipmentModalOpen: true })}
|
||
customerFilterOpen={customerFilterOpen}
|
||
onToggleCustomerFilter={this.handleToggleCustomerFilter}
|
||
selectedCustomer={selectedCustomer}
|
||
onClearCustomerFilter={() => this.handleSelectCustomer(null)}
|
||
/>
|
||
);
|
||
|
||
if (items === null && shipmentAutomat.state.status === 'failed') {
|
||
return (
|
||
<>
|
||
{searchField}
|
||
<PanelTileGrid>
|
||
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ p: 2 }}>
|
||
<BackendError error={backendErrorFrom(shipmentAutomat.state.error)} onRetry={this.handleRetry} />
|
||
</PanelTile>
|
||
</PanelTileGrid>
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{searchField}
|
||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||
{customerFilterOpen && (
|
||
<PartyFilterPanel
|
||
title="Kunden"
|
||
parties={partiesList}
|
||
selectedId={entityId}
|
||
onSelect={this.handleSelectCustomer}
|
||
onClose={this.handleToggleCustomerFilter}
|
||
counts={customerCounts}
|
||
searchPlaceholder="Kunde suchen…"
|
||
/>
|
||
)}
|
||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||
<PanelTileGrid>
|
||
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}>
|
||
<FillSpace
|
||
aboveRef={[this.searchRef]}
|
||
reserveBottom={32}
|
||
minPageSize={8}
|
||
minHeight={0}
|
||
resetKey={filterKey}
|
||
items={items || []}
|
||
totalCount={items ? items.length : 0}
|
||
unfilteredTotal={unfilteredTotal}
|
||
headerItem={this.headerItem}
|
||
renderItem={ShipmentRow}
|
||
virtualScrollbar
|
||
header={
|
||
<Box
|
||
sx={{
|
||
...rowGridSx,
|
||
height: 36,
|
||
minHeight: 36,
|
||
px: 2,
|
||
borderBottom: '1px solid',
|
||
borderColor: 'divider',
|
||
}}
|
||
>
|
||
{renderInvisibleDummyRow(this.headerItem)}
|
||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Lieferschein / Ref</Box>
|
||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Auftrag</Box>
|
||
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Paket / Tracking</Box>
|
||
<Box sx={{ gridColumn: 7, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Status</Box>
|
||
<Box sx={{ gridColumn: 8, gridRow: 2, ...headerCellSx }} />
|
||
</Box>
|
||
}
|
||
onPageChange={this.handlePageChange}
|
||
/>
|
||
{items === null && (
|
||
<Box
|
||
sx={{
|
||
position: 'absolute',
|
||
top: 52,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 32,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
pointerEvents: 'none',
|
||
zIndex: 2,
|
||
}}
|
||
>
|
||
<CircularProgress size={32} />
|
||
</Box>
|
||
)}
|
||
</PanelTile>
|
||
</PanelTileGrid>
|
||
</Box>
|
||
</Box>
|
||
|
||
{/* Modal: View / Manage Shipment Details */}
|
||
{selectedShipment && (
|
||
<ShipmentDetailDialog
|
||
open={Boolean(selectedShipment)}
|
||
shipment={selectedShipment}
|
||
onClose={this.handleCloseShipment}
|
||
onChanged={() => {
|
||
void shipmentAutomat.actions.reload();
|
||
void orderAutomat.actions.reload();
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{/* Modal: Create new shipment from open order */}
|
||
{newShipmentModalOpen && (
|
||
<NewShipmentModal
|
||
open={newShipmentModalOpen}
|
||
orders={orders}
|
||
partiesById={partiesById}
|
||
onClose={() => this.setState({ newShipmentModalOpen: false })}
|
||
onSuccess={(created) => {
|
||
if (created) {
|
||
this.setState({ selectedShipment: created });
|
||
}
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
}
|
||
|
||
export default withNavigate(ShipmentList);
|