From eb15bcb83b3af5ae2f7414a7e0ca9810ae1e53b3 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 19 Oct 2026 01:02:08 +0200 Subject: [PATCH] u --- src/api/shipment.js | 12 + src/components/carrier/CarrierItem.jsx | 69 ++-- .../manifest/ShipmentDetailDialog.jsx | 340 ++++++++++++------ 3 files changed, 274 insertions(+), 147 deletions(-) diff --git a/src/api/shipment.js b/src/api/shipment.js index 66e4207..c02c50c 100644 --- a/src/api/shipment.js +++ b/src/api/shipment.js @@ -54,4 +54,16 @@ export const shipment = { return http(`/api/shipment/${id}/file/${fileId}`, { method: 'DELETE' }); }, // #endregion + + // #region POST /api/shipment/:id/pdf — createPdf + createPdf(id) { + return http(`/api/shipment/${id}/pdf`, { method: 'POST' }); + }, + // #endregion + + // #region GET /api/shipment/:id/file/:fileId — getFileUrl + getFileUrl(id, fileId) { + return `/api/shipment/${encodeURIComponent(id)}/file/${encodeURIComponent(fileId)}`; + }, + // #endregion }; diff --git a/src/components/carrier/CarrierItem.jsx b/src/components/carrier/CarrierItem.jsx index 8299959..10a1eba 100644 --- a/src/components/carrier/CarrierItem.jsx +++ b/src/components/carrier/CarrierItem.jsx @@ -4,7 +4,6 @@ import IconButton from '@mui/material/IconButton'; import ListItem from '@mui/material/ListItem'; import Switch from '@mui/material/Switch'; import FormControlLabel from '@mui/material/FormControlLabel'; -import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; @@ -158,43 +157,39 @@ class CarrierItem extends PureComponent { /> - - - } - label={ - - Pakete - - } - sx={{ m: 0, gap: 0.5 }} - /> - + + } + label={ + + Pakete + + } + sx={{ m: 0, gap: 0.5 }} + /> - - - } - label={ - - Retoure - - } - sx={{ m: 0, gap: 0.5 }} - /> - + + } + label={ + + Retoure + + } + sx={{ m: 0, gap: 0.5 }} + /> !c.isReverse); this.state = { carriers, carrierId: carriers[0]?.id ?? '', trackingCode: '', - type: 'parcel', - state: 'draft', saving: false, }; this.handleSubmit = this.handleSubmit.bind(this); @@ -52,7 +51,7 @@ class AddPackageDialog extends PureComponent { componentDidMount() { this.unsub = carrierAutomat.subscribe(this, (state) => { - const carriers = state.carriers || []; + const carriers = (state.carriers || []).filter((c) => !c.isReverse); const currentId = this.state.carrierId; const valid = carriers.some((c) => c.id === currentId); return { @@ -61,7 +60,7 @@ class AddPackageDialog extends PureComponent { }; }); if (carrierAutomat.state.status !== 'ready') { - void carrierAutomat.actions.load().catch(() => {}); + void carrierAutomat.actions.load().catch(() => { }); } } @@ -71,18 +70,32 @@ class AddPackageDialog extends PureComponent { async handleSubmit() { const { shipment, onClose, onSuccess } = this.props; - const { carrierId, trackingCode, type, state } = this.state; + const { carriers, carrierId, trackingCode } = this.state; if (!shipment?.id || !carrierId) return; + const selectedCarrier = carriers.find((c) => String(c.id) === String(carrierId)); + const hasPackages = Boolean(selectedCarrier?.hasPackages); + const trimmedTracking = hasPackages && trackingCode ? trackingCode.trim() : undefined; + this.setState({ saving: true }); try { await shippingApi.create(shipment.id, { carrierId: Number(carrierId), - trackingCode: trackingCode.trim() || undefined, - type, - state, + trackingCode: trimmedTracking || undefined, + type: 'parcel', + state: 'draft', }); - enqueueSnackbar('Paket angelegt.', { variant: 'success' }); + const oldPdf = (shipment.files || []).find( + (f) => f.mimetype === 'application/pdf' || f.name?.toLowerCase().endsWith('.pdf') + ); + if (oldPdf?.id) { + try { + await shipmentApi.deleteFile(shipment.id, oldPdf.id); + } catch { + // ignore + } + } + enqueueSnackbar('Versandart angelegt.', { variant: 'success' }); await Promise.all([ orderAutomat.actions.reload(), shipmentAutomat.actions.reload(), @@ -97,15 +110,17 @@ class AddPackageDialog extends PureComponent { render() { const { open, onClose, shipment } = this.props; - const { carriers, carrierId, trackingCode, type, state, saving } = this.state; + const { carriers, carrierId, trackingCode, saving } = this.state; const canSubmit = Boolean(carrierId) && !saving; + const selectedCarrier = carriers.find((c) => String(c.id) === String(carrierId)); + const hasPackages = Boolean(selectedCarrier?.hasPackages); return ( - Paket zu Lieferschein hinzufügen + Versandart hinzufügen @@ -118,14 +133,14 @@ class AddPackageDialog extends PureComponent { this.setState({ carrierId: e.target.value })} error={carriers.length === 0} helperText={ carriers.length === 0 - ? 'Keine Versandarten eingerichtet (unter Auftrag → Versandarten anlegen)' + ? 'Keine Versandarten eingerichtet' : undefined } > @@ -136,38 +151,15 @@ class AddPackageDialog extends PureComponent { ))} - 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) - + {hasPackages && ( + this.setState({ trackingCode: e.target.value })} + placeholder="z.B. Sendungsnummer, Paket-ID oder Referenz" + /> + )} @@ -175,7 +167,7 @@ class AddPackageDialog extends PureComponent { Abbrechen @@ -196,11 +188,14 @@ export default class ShipmentDetailDialog extends PureComponent { error: null, addPackageDialogOpen: false, busyAction: false, + pdfLoading: 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); + this.handleDeleteShipment = this.handleDeleteShipment.bind(this); + this.handleOpenPdf = this.handleOpenPdf.bind(this); } componentDidMount() { @@ -240,11 +235,21 @@ export default class ShipmentDetailDialog extends PureComponent { // #endregion async handleDeletePackage(pkg) { - if (!window.confirm(`Paket „#${pkg.number || pkg.id}“ wirklich löschen?`)) return; + if (!window.confirm(`Versandart „#${pkg.number || pkg.id}“ wirklich löschen?`)) return; this.setState({ busyAction: true }); try { await shippingApi.delete(pkg.id); - enqueueSnackbar('Paket gelöscht.', { variant: 'success' }); + const oldPdf = (this.state.shipment?.files || []).find( + (f) => f.mimetype === 'application/pdf' || f.name?.toLowerCase().endsWith('.pdf') + ); + if (oldPdf?.id) { + try { + await shipmentApi.deleteFile(this.state.shipment.id, oldPdf.id); + } catch { + // ignore + } + } + enqueueSnackbar('Versandart gelöscht.', { variant: 'success' }); await Promise.all([ orderAutomat.actions.reload(), shipmentAutomat.actions.reload(), @@ -282,6 +287,29 @@ export default class ShipmentDetailDialog extends PureComponent { this.setState({ busyAction: true }); try { + if (newState === 'delivered') { + // 1. Mark any unshipped packages as shipped (triggers backend stock deduction for linked shippings) + const unshippedPackages = (this.state.shipment?.shippings || []).filter( + (p) => p.state !== 'shipped' + ); + for (const pkg of unshippedPackages) { + try { + await shippingApi.update(pkg.id, { state: 'shipped' }); + } catch (err) { + console.warn('Could not mark shipping as shipped:', err); + } + } + + // 2. Ensure stock deduction is executed via stock.ship for the shipment lines + try { + await stockApi.ship({ shipmentId }); + } catch (err) { + if (!err?.message?.toLowerCase().includes('already shipped')) { + console.warn('Stock ship warning:', err); + } + } + } + await shipmentApi.update(shipmentId, { state: newState }); this.setState((prev) => ({ shipment: { ...prev.shipment, state: newState }, @@ -293,6 +321,7 @@ export default class ShipmentDetailDialog extends PureComponent { shipmentAutomat.actions.reload(), ]); this.props.onChanged?.(); + await this.handleRefresh(); } catch (err) { notifyError(err); } finally { @@ -300,14 +329,78 @@ export default class ShipmentDetailDialog extends PureComponent { } } + // MARK: calls shipmentApi.delete + // #region DELETE /api/shipment/:id — delete shipment + async handleDeleteShipment() { + const { shipment } = this.state; + if (!shipment?.id) return; + + const label = shipment.number || `Lieferschein #${shipment.id}`; + if (!window.confirm(`Möchten Sie den ${label} wirklich löschen?`)) { + return; + } + + this.setState({ busyAction: true }); + try { + await shipmentApi.delete(shipment.id); + enqueueSnackbar(`${label} gelöscht.`, { variant: 'success' }); + await Promise.all([ + orderAutomat.actions.reload(), + shipmentAutomat.actions.reload(), + ]); + this.props.onChanged?.(); + this.props.onClose(); + } catch (err) { + notifyError(err); + this.setState({ busyAction: false }); + } + } + // #endregion + + // MARK: calls shipmentApi.createPdf and shipmentApi.getFileUrl + // #region POST /api/shipment/:id/pdf & GET file — open PDF + async handleOpenPdf() { + const { shipment } = this.state; + if (!shipment?.id) return; + + this.setState({ pdfLoading: true }); + try { + const existingPdf = (shipment.files || []).find( + (f) => f.mimetype === 'application/pdf' || f.name?.toLowerCase().endsWith('.pdf') + ); + + let targetUrl = null; + if (existingPdf) { + targetUrl = shipmentApi.getFileUrl(shipment.id, existingPdf.fileId || existingPdf.id); + } else { + const res = await shipmentApi.createPdf(shipment.id); + const rawUrl = res?.fileUrl || (res?.attachment ? shipmentApi.getFileUrl(shipment.id, res.attachment.fileId || res.attachment.id) : null); + targetUrl = rawUrl ? (rawUrl.startsWith('/api') ? rawUrl : `/api${rawUrl}`) : null; + await this.handleRefresh(); + } + + if (targetUrl) { + window.open(targetUrl, '_blank'); + } else { + enqueueSnackbar('PDF-URL konnte nicht geladen werden.', { variant: 'error' }); + } + } catch (err) { + notifyError(err); + } finally { + this.setState({ pdfLoading: false }); + } + } + // #endregion + render() { const { open, onClose } = this.props; - const { shipment, loading, error, addPackageDialogOpen, busyAction } = this.state; + const { shipment, loading, error, addPackageDialogOpen, busyAction, pdfLoading } = this.state; if (!shipment) return null; const shLines = Array.isArray(shipment.lines) ? shipment.lines : []; const shPackages = Array.isArray(shipment.shippings) ? shipment.shippings : []; + const carriers = carrierAutomat.state.carriers || []; return ( @@ -386,7 +479,7 @@ export default class ShipmentDetailDialog extends PureComponent { disabled={busyAction} sx={{ textTransform: 'none', fontWeight: 600, height: 28, fontSize: '0.75rem' }} > - Paket hinzufügen + Versand hinzufügen @@ -394,6 +487,8 @@ export default class ShipmentDetailDialog extends PureComponent { {shPackages.map((pkg) => { const isShipped = pkg.state === 'shipped'; + const carrierObj = carriers.find((c) => Number(c.id) === Number(pkg.carrierId)); + const carrierHasPackages = carrierObj ? Boolean(carrierObj.hasPackages) : false; return ( - {pkg.carrier || 'DHL'} ({pkg.type === 'pallet' ? 'Palette' : 'Paket'}) + {pkg.carrier || 'Versand'} {pkg.trackingCode ? ( @@ -423,18 +518,14 @@ export default class ShipmentDetailDialog extends PureComponent { size="small" onClick={() => { navigator.clipboard.writeText(pkg.trackingCode); - enqueueSnackbar('Tracking-Code kopiert!', { variant: 'info' }); + enqueueSnackbar('Referenz kopiert!', { variant: 'info' }); }} sx={{ p: 0.25 }} > - ) : ( - - Ohne Tracking - - )} + ) : null} @@ -449,16 +540,18 @@ export default class ShipmentDetailDialog extends PureComponent { /> ) : ( <> - + {carrierHasPackages && ( + + )} - - {(!shipment.state || shipment.state === 'new') && ( + + + + - )} - {shipment.state === 'delivered' && ( - - )} - {shipment.state === 'returned' && ( - - )} + color="inherit" + startIcon={pdfLoading ? : } + disabled={busyAction || pdfLoading} + onClick={this.handleOpenPdf} + sx={{ textTransform: 'none', fontWeight: 600 }} + > + {pdfLoading ? 'PDF wird geladen…' : 'PDF'} + + + {(!shipment.state || shipment.state === 'new') && ( + + )} + {shipment.state === 'delivered' && ( + + )} + {shipment.state === 'returned' && ( + + )} + - {addPackageDialogOpen && ( - this.setState({ addPackageDialogOpen: false })} - onSuccess={async () => { - this.props.onChanged?.(); - await this.handleRefresh(); - }} - /> - )} - + { + addPackageDialogOpen && ( + this.setState({ addPackageDialogOpen: false })} + onSuccess={async () => { + this.props.onChanged?.(); + await this.handleRefresh(); + }} + /> + ) + } + ); } }