651 lines
25 KiB
JavaScript
651 lines
25 KiB
JavaScript
import { PureComponent } from 'react';
|
||
import Box from '@mui/material/Box';
|
||
import Button from '@mui/material/Button';
|
||
import Checkbox from '@mui/material/Checkbox';
|
||
import Chip from '@mui/material/Chip';
|
||
import CircularProgress from '@mui/material/CircularProgress';
|
||
import DialogActions from '@mui/material/DialogActions';
|
||
import DialogContent from '@mui/material/DialogContent';
|
||
import DialogTitle from '@mui/material/DialogTitle';
|
||
import IconButton from '@mui/material/IconButton';
|
||
import Stack from '@mui/material/Stack';
|
||
import Table from '@mui/material/Table';
|
||
import TableBody from '@mui/material/TableBody';
|
||
import TableCell from '@mui/material/TableCell';
|
||
import TableContainer from '@mui/material/TableContainer';
|
||
import TableHead from '@mui/material/TableHead';
|
||
import TableRow from '@mui/material/TableRow';
|
||
import TextField from '@mui/material/TextField';
|
||
import Typography from '@mui/material/Typography';
|
||
import Alert from '@mui/material/Alert';
|
||
import CloseIcon from '@mui/icons-material/Close';
|
||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||
import { enqueueSnackbar } from 'notistack';
|
||
import AppDialog from '../AppDialog';
|
||
import { order as orderApi } from '../../api';
|
||
import { notifyError } from '../../utils/notify';
|
||
import { formatStock } from '../article/searchFormat';
|
||
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||
import { todayInputValue } from './manifestConfig';
|
||
|
||
// MARK: helper to sum already shipped quantities per SKU, orderLineId, and line name across shipments
|
||
export function computeShippedSummary(shipments) {
|
||
const bySku = {};
|
||
const byOrderLineId = {};
|
||
const byName = {};
|
||
|
||
for (const sh of shipments || []) {
|
||
for (const sl of sh.lines || []) {
|
||
const amt = Number(sl.amount) || 0;
|
||
if (sl.sku) {
|
||
const k = String(sl.sku).trim();
|
||
bySku[k] = (bySku[k] || 0) + amt;
|
||
}
|
||
if (sl.orderLineId != null) {
|
||
byOrderLineId[sl.orderLineId] = (byOrderLineId[sl.orderLineId] || 0) + amt;
|
||
}
|
||
if (sl.name) {
|
||
const n = String(sl.name).trim().toLowerCase();
|
||
byName[n] = (byName[n] || 0) + amt;
|
||
}
|
||
}
|
||
}
|
||
return { bySku, byOrderLineId, byName };
|
||
}
|
||
|
||
function getItemKey(line, index) {
|
||
if (line.sku && String(line.sku).trim()) {
|
||
return `sku:${String(line.sku).trim()}`;
|
||
}
|
||
if (line.id != null) {
|
||
return `id:${line.id}`;
|
||
}
|
||
return `idx:${index}`;
|
||
}
|
||
|
||
// MARK: helper to determine next shipment number preview
|
||
export function getNextShipmentNumber(orderNumber, shipments = []) {
|
||
if (!orderNumber) return '–';
|
||
let maxIdx = 0;
|
||
for (const s of shipments || []) {
|
||
const num = String(s.number ?? '').trim();
|
||
const match = num.match(/-L-(\d+)$/i) || num.match(/-(\d+)$/);
|
||
if (match) {
|
||
const parsed = parseInt(match[1], 10);
|
||
if (!Number.isNaN(parsed) && parsed > maxIdx) {
|
||
maxIdx = parsed;
|
||
}
|
||
}
|
||
}
|
||
const nextIdx = maxIdx > 0 ? maxIdx + 1 : ((shipments?.length || 0) + 1);
|
||
return `${orderNumber}-L-${nextIdx}`;
|
||
}
|
||
|
||
/**
|
||
* Dialog for selecting items and quantities to create a shipment from a sales order.
|
||
* Calls POST /api/order/:id/shipment.
|
||
*/
|
||
export default class OrderShipmentDialog extends PureComponent {
|
||
constructor(props) {
|
||
super(props);
|
||
const { initialAmounts, initialSelected } = this.calculateInitialState(props);
|
||
this.state = {
|
||
date: todayInputValue(),
|
||
externalNumber: '',
|
||
lineAmounts: initialAmounts,
|
||
selectedSkus: initialSelected,
|
||
saving: false,
|
||
error: null,
|
||
};
|
||
|
||
this.handleAmountChange = this.handleAmountChange.bind(this);
|
||
this.handleToggleItem = this.handleToggleItem.bind(this);
|
||
this.handleToggleSku = this.handleToggleItem; // Alias for backward compatibility
|
||
this.handleToggleAll = this.handleToggleAll.bind(this);
|
||
this.handleSubmit = this.handleSubmit.bind(this);
|
||
this.handleShipAll = this.handleShipAll.bind(this);
|
||
|
||
if (props.open) {
|
||
this.ensureArticlesLoaded(props.lines);
|
||
}
|
||
}
|
||
|
||
componentDidMount() {
|
||
this.unsubArticle = artikelAutomat.subscribe(() => {
|
||
this.forceUpdate();
|
||
});
|
||
if (this.props.open) {
|
||
this.ensureArticlesLoaded(this.props.lines);
|
||
}
|
||
}
|
||
|
||
componentWillUnmount() {
|
||
this.unsubArticle?.();
|
||
}
|
||
|
||
componentDidUpdate(prevProps) {
|
||
if (!prevProps.open && this.props.open) {
|
||
const { initialAmounts, initialSelected } = this.calculateInitialState(this.props);
|
||
this.setState({
|
||
date: todayInputValue(),
|
||
externalNumber: '',
|
||
lineAmounts: initialAmounts,
|
||
selectedSkus: initialSelected,
|
||
saving: false,
|
||
error: null,
|
||
});
|
||
this.ensureArticlesLoaded(this.props.lines);
|
||
} else if (this.props.open && prevProps.lines !== this.props.lines) {
|
||
this.ensureArticlesLoaded(this.props.lines);
|
||
}
|
||
}
|
||
|
||
ensureArticlesLoaded(lines) {
|
||
if (!Array.isArray(lines)) return;
|
||
for (const l of lines) {
|
||
if (l?.sku && !getArticle(l.sku)) {
|
||
artikelAutomat.actions.loadArticle(l.sku).catch(() => {});
|
||
}
|
||
}
|
||
}
|
||
|
||
calculateInitialState(props) {
|
||
const { bySku, byOrderLineId, byName } = computeShippedSummary(props.shipments);
|
||
const remainingShippedByName = { ...byName };
|
||
const initialAmounts = {};
|
||
const initialSelected = new Set();
|
||
|
||
(props.lines || []).forEach((l, idx) => {
|
||
const isShippable = l.shippable !== false;
|
||
const key = getItemKey(l, idx);
|
||
const ordered = Number(l.amount) || 0;
|
||
const sku = l.sku ? String(l.sku).trim() : null;
|
||
const orderLineId = l.id != null ? l.id : null;
|
||
|
||
let shipped = 0;
|
||
if (sku) {
|
||
shipped = bySku[sku] || 0;
|
||
} else if (orderLineId != null && byOrderLineId[orderLineId] != null) {
|
||
shipped = byOrderLineId[orderLineId];
|
||
} else if (l.name) {
|
||
const n = String(l.name).trim().toLowerCase();
|
||
const pool = remainingShippedByName[n] || 0;
|
||
const consumed = Math.min(ordered, pool);
|
||
shipped = consumed;
|
||
remainingShippedByName[n] = pool - consumed;
|
||
}
|
||
|
||
const rem = isShippable ? Math.max(0, ordered - shipped) : 0;
|
||
if (isShippable && rem > 0) {
|
||
initialAmounts[key] = String(rem);
|
||
initialSelected.add(key);
|
||
} else {
|
||
initialAmounts[key] = '0';
|
||
}
|
||
});
|
||
|
||
return { initialAmounts, initialSelected };
|
||
}
|
||
|
||
getItems() {
|
||
const { lines = [], shipments = [], orderId } = this.props;
|
||
const { lineAmounts, selectedSkus } = this.state;
|
||
const currentOrderId = orderId != null ? Number(orderId) : null;
|
||
const { bySku, byOrderLineId, byName } = computeShippedSummary(shipments);
|
||
const remainingShippedByName = { ...byName };
|
||
|
||
return (lines || []).map((line, idx) => {
|
||
const itemKey = getItemKey(line, idx);
|
||
const isShippable = line.shippable !== false;
|
||
const ordered = Number(line.amount) || 0;
|
||
const sku = line.sku ? String(line.sku).trim() : null;
|
||
const orderLineId = line.id != null ? line.id : null;
|
||
|
||
let shipped = 0;
|
||
if (sku) {
|
||
shipped = bySku[sku] || 0;
|
||
} else if (orderLineId != null && byOrderLineId[orderLineId] != null) {
|
||
shipped = byOrderLineId[orderLineId];
|
||
} else if (line.name) {
|
||
const n = String(line.name).trim().toLowerCase();
|
||
const pool = remainingShippedByName[n] || 0;
|
||
const consumed = Math.min(ordered, pool);
|
||
shipped = consumed;
|
||
remainingShippedByName[n] = pool - consumed;
|
||
}
|
||
|
||
const remaining = isShippable ? Math.max(0, ordered - shipped) : 0;
|
||
const isSelected = selectedSkus.has(itemKey);
|
||
const shipAmount = lineAmounts[itemKey] ?? (remaining > 0 ? String(remaining) : '0');
|
||
|
||
let cached = null;
|
||
let isTracked = null;
|
||
let stockVal = null;
|
||
let availableVal = null;
|
||
let reservedTitle = undefined;
|
||
let stockTitle = undefined;
|
||
|
||
if (sku) {
|
||
cached = getArticle(sku);
|
||
const rawWarehouses = Array.isArray(cached?.warehouses) ? cached.warehouses : [];
|
||
isTracked = cached ? (cached.track_stock ?? cached.trackStock ?? (rawWarehouses.length > 0)) : null;
|
||
stockVal = cached == null || isTracked === false
|
||
? null
|
||
: (cached.stock != null && !Number.isNaN(Number(cached.stock))
|
||
? Number(cached.stock)
|
||
: (rawWarehouses.length > 0 || isTracked
|
||
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
|
||
: null));
|
||
|
||
const undeliveredOrders = Array.isArray(cached?.undeliveredOrders) ? cached.undeliveredOrders : [];
|
||
const otherOrders = currentOrderId != null
|
||
? undeliveredOrders.filter((ord) => Number(ord.orderId) !== currentOrderId)
|
||
: undeliveredOrders;
|
||
const reservedInOtherOrders = otherOrders.reduce((sum, ord) => sum + (Number(ord.amount) || 0), 0);
|
||
availableVal = cached == null || isTracked === false
|
||
? null
|
||
: (stockVal != null ? stockVal - reservedInOtherOrders : null);
|
||
|
||
reservedTitle = isTracked !== false && reservedInOtherOrders > 0
|
||
? `Bereits in anderen Aufträgen reserviert:\n${otherOrders
|
||
.map((ord) => `• ${ord.orderNumber || (ord.orderId ? `Auftrag #${ord.orderId}` : 'Auftrag')}: ${formatStock(ord.amount)}${ord.customerName ? ` (${ord.customerName})` : ''}`)
|
||
.join('\n')}`
|
||
: undefined;
|
||
|
||
stockTitle = isTracked !== false && rawWarehouses.length > 1
|
||
? `Lagerbestände:\n${rawWarehouses
|
||
.map((wh) => `• ${wh.name || 'Lager'}: ${formatStock(wh.stock)}`)
|
||
.join('\n')}`
|
||
: undefined;
|
||
}
|
||
|
||
return {
|
||
...line,
|
||
itemKey,
|
||
sku,
|
||
orderLineId,
|
||
isShippable,
|
||
ordered,
|
||
shipped,
|
||
remaining,
|
||
isSelected,
|
||
shipAmount,
|
||
cached,
|
||
isTracked,
|
||
stockVal,
|
||
availableVal,
|
||
reservedTitle,
|
||
stockTitle,
|
||
};
|
||
});
|
||
}
|
||
|
||
handleToggleItem(itemKey, remaining) {
|
||
this.setState((prev) => {
|
||
const nextSelected = new Set(prev.selectedSkus);
|
||
const nextAmounts = { ...prev.lineAmounts };
|
||
if (nextSelected.has(itemKey)) {
|
||
nextSelected.delete(itemKey);
|
||
} else {
|
||
nextSelected.add(itemKey);
|
||
if (!Number(nextAmounts[itemKey]) || Number(nextAmounts[itemKey]) <= 0) {
|
||
nextAmounts[itemKey] = String(remaining);
|
||
}
|
||
}
|
||
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
|
||
});
|
||
}
|
||
|
||
handleToggleAll(openItems) {
|
||
this.setState((prev) => {
|
||
const allSelected = openItems.length > 0 && openItems.every((it) => prev.selectedSkus.has(it.itemKey));
|
||
const nextSelected = new Set();
|
||
const nextAmounts = { ...prev.lineAmounts };
|
||
|
||
if (!allSelected) {
|
||
for (const it of openItems) {
|
||
nextSelected.add(it.itemKey);
|
||
if (!Number(nextAmounts[it.itemKey]) || Number(nextAmounts[it.itemKey]) <= 0) {
|
||
nextAmounts[it.itemKey] = String(it.remaining);
|
||
}
|
||
}
|
||
}
|
||
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
|
||
});
|
||
}
|
||
|
||
handleAmountChange(itemKey, val) {
|
||
this.setState((prev) => {
|
||
const nextAmounts = { ...prev.lineAmounts, [itemKey]: val };
|
||
const nextSelected = new Set(prev.selectedSkus);
|
||
const num = Number(val);
|
||
if (num > 0) {
|
||
nextSelected.add(itemKey);
|
||
} else {
|
||
nextSelected.delete(itemKey);
|
||
}
|
||
return { lineAmounts: nextAmounts, selectedSkus: nextSelected };
|
||
});
|
||
}
|
||
|
||
// MARK: calls orderApi.createShipment from ./api/index.js
|
||
// #region POST /api/order/:id/shipment — submit selected lines
|
||
async handleSubmit(e) {
|
||
e?.preventDefault?.();
|
||
const { orderId, onClose, onSuccess } = this.props;
|
||
const { date, externalNumber, lineAmounts, selectedSkus, saving } = this.state;
|
||
if (saving || !orderId) return;
|
||
|
||
const items = this.getItems();
|
||
const linesToShip = [];
|
||
for (const it of items) {
|
||
if (!it.isShippable || !selectedSkus.has(it.itemKey)) continue;
|
||
const amount = Number(lineAmounts[it.itemKey]);
|
||
if (amount > 0) {
|
||
if (it.sku) {
|
||
linesToShip.push({ sku: it.sku, amount });
|
||
} else if (it.orderLineId != null) {
|
||
linesToShip.push({ orderLineId: it.orderLineId, amount });
|
||
}
|
||
}
|
||
}
|
||
|
||
if (linesToShip.length === 0) {
|
||
enqueueSnackbar('Bitte wählen Sie mindestens einen Artikel mit Liefermenge > 0 aus.', { variant: 'warning' });
|
||
return;
|
||
}
|
||
|
||
this.setState({ saving: true, error: null });
|
||
try {
|
||
const payload = {
|
||
date: date || undefined,
|
||
externalNumber: externalNumber.trim() || undefined,
|
||
lines: linesToShip,
|
||
};
|
||
|
||
const res = await orderApi.createShipment(orderId, payload);
|
||
const shipment = res?.shipment;
|
||
const shNum = shipment?.number || '';
|
||
|
||
enqueueSnackbar(
|
||
shNum ? `Lieferschein ${shNum} erfolgreich angelegt!` : 'Lieferschein erfolgreich angelegt!',
|
||
{ variant: 'success' },
|
||
);
|
||
|
||
onSuccess?.(shipment);
|
||
onClose();
|
||
} catch (err) {
|
||
notifyError(err);
|
||
this.setState({ saving: false, error: err?.message || String(err) });
|
||
}
|
||
}
|
||
// // MARK: calls orderApi.createShipment to ship all remaining lines
|
||
// #region POST /api/order/:id/shipment — ship all remaining
|
||
async handleShipAll() {
|
||
const { orderId, onClose, onSuccess } = this.props;
|
||
const { date, externalNumber, saving } = this.state;
|
||
if (saving || !orderId) return;
|
||
|
||
this.setState({ saving: true, error: null });
|
||
try {
|
||
const payload = {
|
||
date: date || undefined,
|
||
externalNumber: externalNumber.trim() || undefined,
|
||
};
|
||
|
||
const res = await orderApi.createShipment(orderId, payload);
|
||
const shipment = res?.shipment;
|
||
const shNum = shipment?.number || '';
|
||
|
||
enqueueSnackbar(
|
||
shNum ? `Lieferschein ${shNum} erfolgreich angelegt!` : 'Lieferschein erfolgreich angelegt!',
|
||
{ variant: 'success' },
|
||
);
|
||
|
||
onSuccess?.(shipment);
|
||
onClose();
|
||
} catch (err) {
|
||
notifyError(err);
|
||
this.setState({ saving: false, error: err?.message || String(err) });
|
||
}
|
||
}
|
||
// #endregion
|
||
|
||
render() {
|
||
const { open, onClose, orderNumber } = this.props;
|
||
const { date, externalNumber, saving, error } = this.state;
|
||
const nextShipmentNumber = getNextShipmentNumber(orderNumber, this.props.shipments);
|
||
|
||
const items = this.getItems();
|
||
const shippableItems = items.filter((it) => it.isShippable);
|
||
const openItems = shippableItems.filter((it) => it.remaining > 0);
|
||
const allShipped = shippableItems.length > 0 && openItems.length === 0;
|
||
const isAllSelected = openItems.length > 0 && openItems.every((it) => it.isSelected);
|
||
const selectedCount = openItems.filter((it) => it.isSelected).length;
|
||
|
||
return (
|
||
<AppDialog open={open} onClose={saving ? undefined : onClose} maxWidth="lg" fullWidth>
|
||
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||
<Stack direction="row" spacing={1.5} sx={{ alignItems: 'center' }}>
|
||
<LocalShippingOutlinedIcon color="primary" />
|
||
<Box>
|
||
<Typography variant="h6" component="div" sx={{ fontWeight: 700, lineHeight: 1.2 }}>
|
||
Lieferschein erstellen
|
||
</Typography>
|
||
{orderNumber ? (
|
||
<Typography variant="caption" color="text.secondary">
|
||
Zu: <strong style={{ fontFamily: 'monospace' }}>{orderNumber}</strong>
|
||
</Typography>
|
||
) : null}
|
||
</Box>
|
||
</Stack>
|
||
<IconButton
|
||
aria-label="close"
|
||
onClick={onClose}
|
||
disabled={saving}
|
||
size="small"
|
||
sx={{ color: (theme) => theme.palette.grey[500] }}
|
||
>
|
||
<CloseIcon />
|
||
</IconButton>
|
||
</DialogTitle>
|
||
|
||
<DialogContent dividers sx={{ p: 2.5 }}>
|
||
{error && (
|
||
<Alert severity="error" sx={{ mb: 2 }} onClose={() => this.setState({ error: null })}>
|
||
{error}
|
||
</Alert>
|
||
)}
|
||
|
||
{allShipped && (
|
||
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />} sx={{ mb: 2 }}>
|
||
Alle Artikel dieses Auftrags wurden bereits vollständig geliefert.
|
||
</Alert>
|
||
)}
|
||
|
||
{/* Header controls: date, preview shipment number, and external reference */}
|
||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ mb: 2.5 }}>
|
||
<TextField
|
||
size="small"
|
||
type="date"
|
||
label="Lieferdatum"
|
||
value={date}
|
||
disabled={saving}
|
||
onChange={(e) => this.setState({ date: e.target.value })}
|
||
slotProps={{ inputLabel: { shrink: true } }}
|
||
sx={{ minWidth: 160 }}
|
||
/>
|
||
<TextField
|
||
size="small"
|
||
label="Lieferschein-Nr."
|
||
value={nextShipmentNumber}
|
||
disabled
|
||
slotProps={{
|
||
input: {
|
||
readOnly: true,
|
||
},
|
||
}}
|
||
sx={{ minWidth: 190, '& .MuiInputBase-input': { fontFamily: 'monospace' } }}
|
||
/>
|
||
<TextField
|
||
size="small"
|
||
label="Referenz"
|
||
value={externalNumber}
|
||
disabled={saving}
|
||
onChange={(e) => this.setState({ externalNumber: e.target.value })}
|
||
fullWidth
|
||
slotProps={{ input: { sx: { fontFamily: 'monospace' } } }}
|
||
/>
|
||
</Stack>
|
||
|
||
<TableContainer sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
||
<Table size="small">
|
||
<TableHead sx={{ bgcolor: 'action.hover' }}>
|
||
<TableRow>
|
||
<TableCell padding="checkbox" sx={{ width: 44 }}>
|
||
<Checkbox
|
||
size="small"
|
||
checked={isAllSelected}
|
||
indeterminate={selectedCount > 0 && !isAllSelected}
|
||
disabled={saving || openItems.length === 0}
|
||
onChange={() => this.handleToggleAll(openItems)}
|
||
/>
|
||
</TableCell>
|
||
<TableCell sx={{ fontWeight: 600 }}>ArtNr</TableCell>
|
||
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestellt</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 600 }}>Geliefert</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 600 }}>Offen</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestand</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 600 }}>Verfügbar</TableCell>
|
||
<TableCell align="right" sx={{ fontWeight: 600, width: 130 }}>Liefermenge</TableCell>
|
||
</TableRow>
|
||
</TableHead>
|
||
<TableBody>
|
||
{items.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={9} align="center" sx={{ py: 3, color: 'text.secondary' }}>
|
||
Keine Artikel im Auftrag vorhanden.
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
items.map((item) => {
|
||
const isNonShippable = !item.isShippable;
|
||
const isClosed = isNonShippable || item.remaining <= 0;
|
||
return (
|
||
<TableRow
|
||
key={item.itemKey}
|
||
hover={!isClosed}
|
||
sx={{
|
||
opacity: isClosed ? 0.55 : 1,
|
||
bgcolor: isClosed ? 'action.hover' : undefined,
|
||
}}
|
||
>
|
||
<TableCell padding="checkbox">
|
||
<Checkbox
|
||
size="small"
|
||
checked={item.isSelected && !isClosed}
|
||
disabled={saving || isClosed}
|
||
onChange={() => this.handleToggleItem(item.itemKey, item.remaining)}
|
||
/>
|
||
</TableCell>
|
||
<TableCell sx={{ fontWeight: 600, fontFamily: 'monospace' }}>{item.sku || '–'}</TableCell>
|
||
<TableCell>
|
||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||
{item.name || '–'}
|
||
</Typography>
|
||
</TableCell>
|
||
<TableCell align="right">{item.ordered}</TableCell>
|
||
<TableCell align="right">{isNonShippable ? '–' : item.shipped}</TableCell>
|
||
<TableCell
|
||
align="right"
|
||
sx={{
|
||
fontWeight: 700,
|
||
color: item.remaining > 0 ? 'primary.main' : 'text.disabled',
|
||
}}
|
||
>
|
||
{isNonShippable ? '–' : item.remaining}
|
||
</TableCell>
|
||
<TableCell
|
||
align="right"
|
||
title={item.stockTitle}
|
||
sx={{
|
||
fontWeight: 500,
|
||
color:
|
||
!item.sku || !item.cached || item.stockVal == null
|
||
? 'text.disabled'
|
||
: item.stockVal <= 0
|
||
? 'error.main'
|
||
: 'text.secondary',
|
||
}}
|
||
>
|
||
{!item.sku ? '–' : !item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)}
|
||
</TableCell>
|
||
<TableCell
|
||
align="right"
|
||
title={item.reservedTitle}
|
||
sx={{
|
||
fontWeight: 600,
|
||
color:
|
||
!item.sku || !item.cached || item.availableVal == null
|
||
? 'text.disabled'
|
||
: item.availableVal <= 0
|
||
? 'error.main'
|
||
: item.remaining > item.availableVal
|
||
? 'warning.main'
|
||
: 'text.secondary',
|
||
}}
|
||
>
|
||
{!item.sku ? '–' : !item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)}
|
||
</TableCell>
|
||
<TableCell align="right">
|
||
{isNonShippable ? (
|
||
<Chip size="small" label="Nicht lieferbar" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
||
) : isClosed ? (
|
||
<Chip size="small" label="Geliefert" color="success" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
||
) : (
|
||
<TextField
|
||
size="small"
|
||
type="number"
|
||
disabled={saving || !item.isSelected}
|
||
value={item.shipAmount}
|
||
onChange={(e) => this.handleAmountChange(item.itemKey, e.target.value)}
|
||
slotProps={{
|
||
htmlInput: {
|
||
min: 0,
|
||
max: item.remaining,
|
||
step: 'any',
|
||
style: { textAlign: 'right', fontWeight: 600 },
|
||
},
|
||
}}
|
||
sx={{ width: 100 }}
|
||
/>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
);
|
||
})
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</TableContainer>
|
||
</DialogContent>
|
||
|
||
<DialogActions sx={{ p: 2, justifyContent: 'flex-end' }}>
|
||
<Button
|
||
variant="contained"
|
||
color="primary"
|
||
disabled={saving || allShipped || selectedCount === 0}
|
||
startIcon={saving ? <CircularProgress size={16} color="inherit" /> : <LocalShippingOutlinedIcon />}
|
||
onClick={this.handleSubmit}
|
||
sx={{ textTransform: 'none', fontWeight: 700 }}
|
||
>
|
||
{saving ? 'Wird erstellt…' : 'Lieferschein erstellen'}
|
||
</Button>
|
||
</DialogActions>
|
||
</AppDialog>
|
||
);
|
||
}
|
||
}
|