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 { todayInputValue } from './manifestConfig'; // MARK: helper to sum already shipped quantities per SKU across order shipments function computeShippedBySku(shipments) { const map = {}; for (const sh of shipments || []) { for (const sl of sh.lines || []) { if (sl.sku) { map[sl.sku] = (map[sl.sku] || 0) + (Number(sl.amount) || 0); } } } return map; } // 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.handleToggleSku = this.handleToggleSku.bind(this); this.handleToggleAll = this.handleToggleAll.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleShipAll = this.handleShipAll.bind(this); } 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, }); } } calculateInitialState(props) { const shippedBySku = computeShippedBySku(props.shipments); const initialAmounts = {}; const initialSelected = new Set(); for (const l of props.lines || []) { if (!l.sku) continue; const ordered = Number(l.amount) || 0; const shipped = shippedBySku[l.sku] || 0; const rem = Math.max(0, ordered - shipped); if (rem > 0) { initialAmounts[l.sku] = String(rem); initialSelected.add(l.sku); } else { initialAmounts[l.sku] = '0'; } } return { initialAmounts, initialSelected }; } handleToggleSku(sku, remaining) { this.setState((prev) => { const nextSelected = new Set(prev.selectedSkus); const nextAmounts = { ...prev.lineAmounts }; if (nextSelected.has(sku)) { nextSelected.delete(sku); } else { nextSelected.add(sku); if (!Number(nextAmounts[sku]) || Number(nextAmounts[sku]) <= 0) { nextAmounts[sku] = String(remaining); } } return { selectedSkus: nextSelected, lineAmounts: nextAmounts }; }); } handleToggleAll(openItems) { this.setState((prev) => { const allSelected = openItems.length > 0 && openItems.every((it) => prev.selectedSkus.has(it.sku)); const nextSelected = new Set(); const nextAmounts = { ...prev.lineAmounts }; if (!allSelected) { for (const it of openItems) { nextSelected.add(it.sku); if (!Number(nextAmounts[it.sku]) || Number(nextAmounts[it.sku]) <= 0) { nextAmounts[it.sku] = String(it.remaining); } } } return { selectedSkus: nextSelected, lineAmounts: nextAmounts }; }); } handleAmountChange(sku, val) { this.setState((prev) => { const nextAmounts = { ...prev.lineAmounts, [sku]: val }; const nextSelected = new Set(prev.selectedSkus); const num = Number(val); if (num > 0) { nextSelected.add(sku); } else { nextSelected.delete(sku); } 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 linesToShip = []; for (const sku of selectedSkus) { const amount = Number(lineAmounts[sku]); if (amount > 0) { linesToShip.push({ sku, 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) }); } } // #endregion // 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, lines = [], shipments = [] } = this.props; const { date, externalNumber, lineAmounts, selectedSkus, saving, error } = this.state; const nextShipmentNumber = getNextShipmentNumber(orderNumber, shipments); const shippedBySku = computeShippedBySku(shipments); const activeLines = lines.filter((l) => Boolean(l.sku)); const items = activeLines.map((line) => { const ordered = Number(line.amount) || 0; const shipped = shippedBySku[line.sku] || 0; const remaining = Math.max(0, ordered - shipped); const isSelected = selectedSkus.has(line.sku); const shipAmount = lineAmounts[line.sku] ?? (remaining > 0 ? String(remaining) : '0'); return { ...line, ordered, shipped, remaining, isSelected, shipAmount, }; }); const openItems = items.filter((it) => it.remaining > 0); const allShipped = activeLines.length > 0 && openItems.length === 0; const isAllSelected = openItems.length > 0 && openItems.every((it) => it.isSelected); const selectedCount = openItems.filter((it) => it.isSelected).length; return ( Lieferschein erstellen {orderNumber ? ( Zu: {orderNumber} ) : null} theme.palette.grey[500] }} > {error && ( this.setState({ error: null })}> {error} )} {allShipped && ( } sx={{ mb: 2 }}> Alle Artikel dieses Auftrags wurden bereits vollständig geliefert. )} {/* Header controls: date, preview shipment number, and external reference */} this.setState({ date: e.target.value })} slotProps={{ inputLabel: { shrink: true } }} sx={{ minWidth: 160 }} /> this.setState({ externalNumber: e.target.value })} fullWidth /> 0 && !isAllSelected} disabled={saving || openItems.length === 0} onChange={() => this.handleToggleAll(openItems)} /> SKU Artikel Bestellt Geliefert Offen Liefermenge {items.length === 0 ? ( Keine Artikel im Auftrag vorhanden. ) : ( items.map((item) => { const isClosed = item.remaining <= 0; return ( this.handleToggleSku(item.sku, item.remaining)} /> {item.sku} {item.name || '–'} {item.ordered} {item.shipped} 0 ? 'primary.main' : 'text.disabled', }} > {item.remaining} {isClosed ? ( ) : ( this.handleAmountChange(item.sku, e.target.value)} slotProps={{ htmlInput: { min: 0, max: item.remaining, step: 'any', style: { textAlign: 'right', fontWeight: 600 }, }, }} sx={{ width: 100 }} /> )} ); }) )}
); } }