diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md index 63875fb..654b798 100644 --- a/.agents/skills/backend/SKILL.md +++ b/.agents/skills/backend/SKILL.md @@ -131,7 +131,7 @@ Strip supplierName?: str from T11 (resolve via entityId in cached supplier list) # Dash API Routes Overview (All) -Overview of all 263 API endpoints across all modules. +Overview of all 264 API endpoints across all modules. ### Notation @@ -784,7 +784,7 @@ _none_ - Params: `id: int` - Returns: `200 { shipments: T3[] }` #### Create shipment for an order — `POST /order/{id}/shipment` -- Params: `id: int, body?: { date?: str, externalNumber?: str, lines?: { sku: str, amount?: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[] }` +- Params: `id: int, body: { lines: { sku: str, amount?: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[], date?: str, warehouseId?: int, externalNumber?: str }` - Returns: `201 { shipment: T3 }` #### List shippings of an order — `GET /order/{id}/shipping` - Params: `id: int` @@ -936,6 +936,9 @@ _none_ ### Section: /shipment +#### List shipments — `GET /shipment` +- Params: `query: offset?: int, limit?: int, search?: str, state?: str, seekMonths?: int` +- Returns: `200 { shipments: { id: int, number: str, type: str, date: str, positions: int, totalQuantity: num, packageCount: int, state: str, entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, orderId?: int|null, orderNumber?: str|null, customerName?: str|null, carrier?: str|null, trackingCode?: str|null }[], total: int, unfilteredTotal: int, offset: int, limit: int }` #### Delete shipment — `DELETE /shipment/{id}` - Params: `id: int` - Returns: `204 void` diff --git a/src/components/manifest/OrderShipmentDialog.jsx b/src/components/manifest/OrderShipmentDialog.jsx index cd9889c..f908cb2 100644 --- a/src/components/manifest/OrderShipmentDialog.jsx +++ b/src/components/manifest/OrderShipmentDialog.jsx @@ -25,6 +25,8 @@ 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 across order shipments @@ -80,6 +82,23 @@ export default class OrderShipmentDialog extends PureComponent { 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) { @@ -93,6 +112,18 @@ export default class OrderShipmentDialog extends PureComponent { 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(() => {}); + } } } @@ -244,12 +275,13 @@ export default class OrderShipmentDialog extends PureComponent { // #endregion render() { - const { open, onClose, orderNumber, lines = [], shipments = [] } = this.props; + const { open, onClose, orderId, 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 currentOrderId = orderId != null ? Number(orderId) : null; const items = activeLines.map((line) => { const ordered = Number(line.amount) || 0; @@ -257,6 +289,40 @@ export default class OrderShipmentDialog extends PureComponent { const remaining = Math.max(0, ordered - shipped); const isSelected = selectedSkus.has(line.sku); const shipAmount = lineAmounts[line.sku] ?? (remaining > 0 ? String(remaining) : '0'); + + // Stock & reservation details from artikelAutomat + const cached = getArticle(line.sku); + const rawWarehouses = Array.isArray(cached?.warehouses) ? cached.warehouses : []; + const isTracked = cached ? (cached.track_stock ?? cached.trackStock ?? (rawWarehouses.length > 0)) : null; + const 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); + const availableVal = cached == null || isTracked === false + ? null + : (stockVal != null ? stockVal - reservedInOtherOrders : null); + + const 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; + + const stockTitle = isTracked !== false && rawWarehouses.length > 1 + ? `Lagerbestände:\n${rawWarehouses + .map((wh) => `• ${wh.name || 'Lager'}: ${formatStock(wh.stock)}`) + .join('\n')}` + : undefined; + return { ...line, ordered, @@ -264,6 +330,13 @@ export default class OrderShipmentDialog extends PureComponent { remaining, isSelected, shipAmount, + cached, + isTracked, + stockVal, + availableVal, + reservedInOtherOrders, + reservedTitle, + stockTitle, }; }); @@ -273,7 +346,7 @@ export default class OrderShipmentDialog extends PureComponent { const selectedCount = openItems.filter((it) => it.isSelected).length; return ( - + @@ -364,13 +437,15 @@ export default class OrderShipmentDialog extends PureComponent { Bestellt Geliefert Offen + Bestand + Verfügbar Liefermenge {items.length === 0 ? ( - + Keine Artikel im Auftrag vorhanden. @@ -411,6 +486,38 @@ export default class OrderShipmentDialog extends PureComponent { > {item.remaining} + + {!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)} + + item.availableVal + ? 'warning.main' + : 'text.secondary', + }} + > + {!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)} + {isClosed ? (