import { PureComponent } 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 DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogTitle from '@mui/material/DialogTitle'; import IconButton from '@mui/material/IconButton'; import MenuItem from '@mui/material/MenuItem'; 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 LocalShippingIcon from '@mui/icons-material/LocalShipping'; import AddIcon from '@mui/icons-material/Add'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import { enqueueSnackbar } from 'notistack'; import AppDialog from '../AppDialog'; import { shipment as shipmentApi, shipping as shippingApi } from '../../api'; import { notifyError } from '../../utils/notify'; import { formatDate } from './manifestConfig'; import orderAutomat from '../../automats/orderAutomat'; import shipmentAutomat from '../../automats/shipmentAutomat'; import carrierAutomat from '../../automats/carrierAutomat'; import { SURFACE_RAISED, WHITE } from '../../colors'; // MARK: class AddPackageDialog extends PureComponent { constructor(props) { super(props); const carriers = carrierAutomat.state.carriers || []; this.state = { carriers, carrierId: carriers[0]?.id ?? '', trackingCode: '', type: 'parcel', state: 'draft', saving: false, }; this.handleSubmit = this.handleSubmit.bind(this); } componentDidMount() { this.unsub = carrierAutomat.subscribe(this, (state) => { const carriers = state.carriers || []; const currentId = this.state.carrierId; const valid = carriers.some((c) => c.id === currentId); return { carriers, carrierId: valid ? currentId : (carriers[0]?.id ?? ''), }; }); if (carrierAutomat.state.status !== 'ready') { void carrierAutomat.actions.load().catch(() => {}); } } componentWillUnmount() { this.unsub?.(); } async handleSubmit() { const { shipment, onClose, onSuccess } = this.props; const { carrierId, trackingCode, type, state } = this.state; if (!shipment?.id || !carrierId) return; this.setState({ saving: true }); try { await shippingApi.create(shipment.id, { carrierId: Number(carrierId), trackingCode: trackingCode.trim() || undefined, type, state, }); enqueueSnackbar('Paket angelegt.', { variant: 'success' }); await Promise.all([ orderAutomat.actions.reload(), shipmentAutomat.actions.reload(), ]); onSuccess?.(); onClose(); } catch (err) { notifyError(err); this.setState({ saving: false }); } } render() { const { open, onClose, shipment } = this.props; const { carriers, carrierId, trackingCode, type, state, saving } = this.state; const canSubmit = Boolean(carrierId) && !saving; return ( Paket zu Lieferschein hinzufügen Lieferschein: {shipment?.number} this.setState({ carrierId: e.target.value })} error={carriers.length === 0} helperText={ carriers.length === 0 ? 'Keine Lieferarten eingerichtet (unter Auftrag → Lieferarten anlegen)' : undefined } > {carriers.map((c) => ( {c.name} ))} this.setState({ trackingCode: e.target.value })} placeholder="z.B. 00340434234234234" /> this.setState({ type: e.target.value })} > Paket (Parcel) Palette (Pallet) Brief / Dokument this.setState({ state: e.target.value })} helperText={state === 'shipped' ? 'Bucht bei Auswahl direkt das Lager ab.' : undefined} > Entwurf (draft) Gepackt (packed) Versendet (shipped — Lagerabbuchung) ); } } /** * Modal to view and manage an existing Lieferschein (Shipment) directly from the Order page. */ export default class ShipmentDetailDialog extends PureComponent { constructor(props) { super(props); this.state = { shipment: props.shipment || null, loading: false, error: null, addPackageDialogOpen: false, busyAction: false, }; this.handleRefresh = this.handleRefresh.bind(this); this.handleDeletePackage = this.handleDeletePackage.bind(this); this.handleMarkPackageShipped = this.handleMarkPackageShipped.bind(this); this.handleStateChange = this.handleStateChange.bind(this); } componentDidMount() { if (this.props.open && this.props.shipment?.id) { void this.handleRefresh(); } } componentDidUpdate(prevProps) { if (!prevProps.open && this.props.open) { this.setState({ shipment: this.props.shipment, error: null }); if (this.props.shipment?.id) { void this.handleRefresh(); } } else if (prevProps.shipment?.id !== this.props.shipment?.id && this.props.shipment?.id) { this.setState({ shipment: this.props.shipment }); void this.handleRefresh(); } } // MARK: calls shipmentApi.get from ./api/index.js // #region GET /api/shipment/:id — refresh details async handleRefresh() { const shipmentId = this.props.shipment?.id; if (!shipmentId) return; this.setState({ loading: true, error: null }); try { const res = await shipmentApi.get(shipmentId); const enriched = res?.shipment || res; this.setState({ shipment: enriched, loading: false }); } catch (err) { notifyError(err); this.setState({ error: err?.message || String(err), loading: false }); } } // #endregion async handleDeletePackage(pkg) { if (!window.confirm(`Paket „#${pkg.number || pkg.id}“ wirklich löschen?`)) return; this.setState({ busyAction: true }); try { await shippingApi.delete(pkg.id); enqueueSnackbar('Paket gelöscht.', { variant: 'success' }); await Promise.all([ orderAutomat.actions.reload(), shipmentAutomat.actions.reload(), ]); this.props.onChanged?.(); await this.handleRefresh(); } catch (err) { notifyError(err); } finally { this.setState({ busyAction: false }); } } async handleMarkPackageShipped(pkg) { this.setState({ busyAction: true }); try { await shippingApi.update(pkg.id, { state: 'shipped' }); enqueueSnackbar('Paket als versendet markiert (Lagerbestand abgebucht).', { variant: 'success' }); await Promise.all([ orderAutomat.actions.reload(), shipmentAutomat.actions.reload(), ]); this.props.onChanged?.(); await this.handleRefresh(); } catch (err) { notifyError(err); } finally { this.setState({ busyAction: false }); } } async handleStateChange(newState) { const shipmentId = this.state.shipment?.id; if (!shipmentId) return; this.setState({ busyAction: true }); try { await shipmentApi.update(shipmentId, { state: newState }); this.setState((prev) => ({ shipment: { ...prev.shipment, state: newState }, })); const label = newState === 'delivered' ? 'Versendet' : newState === 'returned' ? 'Retoure' : 'Neu'; enqueueSnackbar(`Lieferstatus geändert: ${label}`, { variant: 'success' }); await Promise.all([ orderAutomat.actions.reload(), shipmentAutomat.actions.reload(), ]); this.props.onChanged?.(); } catch (err) { notifyError(err); } finally { this.setState({ busyAction: false }); } } render() { const { open, onClose } = this.props; const { shipment, loading, error, addPackageDialogOpen, busyAction } = this.state; if (!shipment) return null; const shLines = Array.isArray(shipment.lines) ? shipment.lines : []; const shPackages = Array.isArray(shipment.shippings) ? shipment.shippings : []; return ( {shipment.number || 'Lieferschein'} {shipment.date ? `Datum: ${formatDate(shipment.date)}` : ''} {shipment.externalNumber ? ` • Ref: ${shipment.externalNumber}` : ''} theme.palette.grey[500] }} > {error && ( this.setState({ error: null })}> {error} )} {/* Shipped Article Positions */} SKU Bezeichnung Menge {shLines.length === 0 ? ( {loading ? 'Lädt Positionen…' : 'Keine Positionen auf diesem Lieferschein.'} ) : ( shLines.map((line, idx) => ( {line.sku} {line.name || '–'} {line.amount} )) )}
{/* Shipping Packages / Tracking */} 0 ? 1 : 0 }}> {shPackages.length > 0 ? ( {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)} > )} ); })} ) : null}
{(!shipment.state || shipment.state === 'new') && ( )} {shipment.state === 'delivered' && ( )} {shipment.state === 'returned' && ( )} {addPackageDialogOpen && ( this.setState({ addPackageDialogOpen: false })} onSuccess={async () => { this.props.onChanged?.(); await this.handleRefresh(); }} /> )}
); } }