diff --git a/src/automats/articleIndexAutomat.js b/src/automats/articleIndexAutomat.js index f0470f3..8c4ae14 100644 --- a/src/automats/articleIndexAutomat.js +++ b/src/automats/articleIndexAutomat.js @@ -99,6 +99,19 @@ function toIndexRecord(article, categoryId) { const eans = article.eans ?? article.eanCodes ?? article.ean ?? null; const childCount = Number(article.childCount) || 0; const price = article.price ?? article.net ?? article.vknetto ?? null; + const warehouses = Array.isArray(article.warehouses) ? article.warehouses : []; + const trackStock = article.track_stock ?? article.trackStock; + const stock = trackStock === false + ? null + : (article.stock != null && !Number.isNaN(Number(article.stock)) + ? Number(article.stock) + : (trackStock || warehouses.length > 0 + ? warehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0) + : null)); + const available = article.available != null && !Number.isNaN(Number(article.available)) + ? Number(article.available) + : stock; + return { ...article, id: article.id, @@ -106,6 +119,12 @@ function toIndexRecord(article, categoryId) { name: article.name, price, net: article.net ?? price, + stock, + available, + warehouses, + articlewarehousesummary: Array.isArray(article.articlewarehousesummary) + ? article.articlewarehousesummary + : [], eans: Array.isArray(eans) ? eans.filter(Boolean) : eans ? [eans] : [], categories: categoryId != null ? String(categoryId) : null, isParent: Boolean(article.isParent ?? article.is_parent ?? (childCount > 0)), @@ -154,6 +173,15 @@ export function buildArticleIndex(views) { existing.price = p; existing.net = article.net ?? p; } + if (existing.stock == null && (article.warehouses?.length > 0 || article.stock != null)) { + const rec = toIndexRecord(article, categoryId); + if (rec.stock != null) { + existing.stock = rec.stock; + existing.available = rec.available; + existing.warehouses = rec.warehouses; + existing.articlewarehousesummary = rec.articlewarehousesummary; + } + } } } @@ -280,6 +308,41 @@ export function setArticleIndexName(sku, name) { ); } +/** + * Patches a single record's stock in the derived index (e.g. after detail load or stock adjustment). + * + * @param {string|number} sku Article SKU. + * @param {object} stockData Stock updates. + * @param {number|null} [stockData.stock] Total stock. + * @param {number|null} [stockData.available] Available stock. + * @param {Array} [stockData.warehouses] Warehouse array. + * @param {Array} [stockData.articlewarehousesummary] Summary array. + * @returns {void} + */ +export function setArticleIndexStock(sku, { stock, available, warehouses, articlewarehousesummary } = {}) { + const items = articleIndexAutomat.state.items; + if (!items.length) return; + articleIndexAutomat.actions.replace( + items.map((row) => { + if (String(row.sku) !== String(sku)) return row; + const whs = warehouses ?? row.warehouses ?? []; + const computedStock = stock !== undefined + ? stock + : (whs.length > 0 ? whs.reduce((sum, w) => sum + (Number(w.stock) || 0), 0) : row.stock); + const computedAvailable = available !== undefined + ? available + : (computedStock !== undefined ? computedStock : row.available); + return { + ...row, + stock: computedStock, + available: computedAvailable, + ...(warehouses ? { warehouses } : {}), + ...(articlewarehousesummary ? { articlewarehousesummary } : {}), + }; + }), + ); +} + /** * Selects the derived index records. * diff --git a/src/automats/artikelAutomat.js b/src/automats/artikelAutomat.js index cea2a16..8d94ade 100644 --- a/src/automats/artikelAutomat.js +++ b/src/automats/artikelAutomat.js @@ -20,7 +20,7 @@ import { Automat } from 'react-automat'; import { article as articleApi } from '../api'; import { requireDefined, requireObject } from '../utils/apiShape'; import authAutomat from './authAutomat'; -import articleIndexAutomat, { setArticleIndexName } from './articleIndexAutomat'; +import articleIndexAutomat, { setArticleIndexName, setArticleIndexStock } from './articleIndexAutomat'; /** * @typedef {Object} ArtikelState @@ -113,6 +113,12 @@ const actions = { artikelAutomat.setState({ bySku: { ...artikelAutomat.state.bySku, [key]: article }, }); + if (article.warehouses || article.stock != null) { + setArticleIndexStock(article.sku, { + stock: article.stock, + warehouses: article.warehouses, + }); + } return article; }, diff --git a/src/automats/index.js b/src/automats/index.js index d3ebcee..948f8bf 100644 --- a/src/automats/index.js +++ b/src/automats/index.js @@ -62,6 +62,7 @@ export { export { default as articleIndexAutomat, setArticleIndexName, + setArticleIndexStock, buildArticleIndex, selectArticleIndexItems, } from './articleIndexAutomat'; diff --git a/src/automats/sseAutomat.js b/src/automats/sseAutomat.js index 90fb32b..f6a6149 100644 --- a/src/automats/sseAutomat.js +++ b/src/automats/sseAutomat.js @@ -222,6 +222,10 @@ function connect() { void warehouseAutomat.actions.reload().catch((err) => { console.error('[sse] warehouse refresh failed', err); }); + invalidateAllCategoryViews(); + void articleIndexAutomat.actions.reload().catch((err) => { + console.error('[sse] article index refresh on warehouse event failed', err); + }); }); addTrackedListener(es, 'carrier', () => { diff --git a/src/components/article/ArticleSearch.jsx b/src/components/article/ArticleSearch.jsx index 9b4f2cd..59fd116 100644 --- a/src/components/article/ArticleSearch.jsx +++ b/src/components/article/ArticleSearch.jsx @@ -229,7 +229,21 @@ class SearchResultRow extends PureComponent { )} {article.sku} - {article.stock != null && ` · Bestand ${formatStock(article.stock)}`} + {article.stock != null && ( + <> + {' · '} + 0 + ? article.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ') + : undefined + } + > + Bestand {formatStock(article.stock)} + + + )} {article.available != null && ` · Verfügbar ${formatStock(article.available)}`} {primaryEan ? ` · EAN ${primaryEan}` : ''} diff --git a/src/components/tiles/ArticleTile.jsx b/src/components/tiles/ArticleTile.jsx index da97601..9735534 100644 --- a/src/components/tiles/ArticleTile.jsx +++ b/src/components/tiles/ArticleTile.jsx @@ -5,6 +5,7 @@ import ButtonBase from '@mui/material/ButtonBase'; import Typography from '@mui/material/Typography'; import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles'; import { formatPrice, getArticleGross } from '../../utils/price'; +import { formatStock } from '../article/searchFormat'; import { getCachedImage, getThumbHashDataUrl, @@ -357,6 +358,19 @@ export class ArticleTile extends Tile { const sku = articleSku(article); const grossPrice = getArticleGross(article, companyAutomat.state.current?.taxrate); + const warehouses = Array.isArray(article?.warehouses) ? article.warehouses : []; + const isTracked = article?.track_stock ?? article?.trackStock ?? (warehouses.length > 0); + const stockVal = isTracked === false + ? null + : (article?.stock != null && !Number.isNaN(Number(article.stock)) + ? Number(article.stock) + : (warehouses.length > 0 || isTracked + ? warehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0) + : null)); + const warehouseTooltip = warehouses.length > 0 + ? warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ') + : undefined; + return ( - {/* Row 5: Price */} - - {grossPrice != null ? formatPrice(grossPrice) : '\u00A0'} - + + {grossPrice != null ? formatPrice(grossPrice) : '\u00A0'} + + {stockVal != null && ( + + {formatStock(stockVal)} + + )} + ); diff --git a/src/routes/artikel.jsx b/src/routes/artikel.jsx index d1dcf55..1588563 100644 --- a/src/routes/artikel.jsx +++ b/src/routes/artikel.jsx @@ -218,7 +218,16 @@ class ResultRow extends Component { )} {row.sku} {' · '} - Bestand {formatStock(row.stock)} + 0 + ? row.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ') + : undefined + } + > + Bestand {formatStock(row.stock)} + {' · '} Verfügbar {formatStock(row.available)} {row.categories