u
This commit is contained in:
@@ -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<object>} [stockData.warehouses] Warehouse array.
|
||||
* @param {Array<object>} [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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export {
|
||||
export {
|
||||
default as articleIndexAutomat,
|
||||
setArticleIndexName,
|
||||
setArticleIndexStock,
|
||||
buildArticleIndex,
|
||||
selectArticleIndexItems,
|
||||
} from './articleIndexAutomat';
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -229,7 +229,21 @@ class SearchResultRow extends PureComponent {
|
||||
</>
|
||||
)}
|
||||
{article.sku}
|
||||
{article.stock != null && ` · Bestand ${formatStock(article.stock)}`}
|
||||
{article.stock != null && (
|
||||
<>
|
||||
{' · '}
|
||||
<Box
|
||||
component="span"
|
||||
title={
|
||||
article.warehouses?.length > 0
|
||||
? article.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Bestand {formatStock(article.stock)}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{article.available != null && ` · Verfügbar ${formatStock(article.available)}`}
|
||||
{primaryEan ? ` · EAN ${primaryEan}` : ''}
|
||||
</Typography>
|
||||
|
||||
@@ -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 (
|
||||
<ButtonBase
|
||||
component="button"
|
||||
@@ -488,25 +502,52 @@ export class ArticleTile extends Tile {
|
||||
{sku || '\u00A0'}
|
||||
</Typography>
|
||||
|
||||
{/* Row 5: Price */}
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
{/* Row 5: Price & Stock */}
|
||||
<Box
|
||||
sx={{
|
||||
color: GREEN_BRIGHT,
|
||||
fontWeight: 700,
|
||||
fontSize: '0.8rem',
|
||||
lineHeight: 1.2,
|
||||
height: '1.2em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 0.5,
|
||||
mt: 0.25,
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
height: '1.2em',
|
||||
}}
|
||||
>
|
||||
{grossPrice != null ? formatPrice(grossPrice) : '\u00A0'}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
color: GREEN_BRIGHT,
|
||||
fontWeight: 700,
|
||||
fontSize: '0.8rem',
|
||||
lineHeight: 1.2,
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{grossPrice != null ? formatPrice(grossPrice) : '\u00A0'}
|
||||
</Typography>
|
||||
{stockVal != null && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
title={warehouseTooltip}
|
||||
sx={{
|
||||
color: ICE,
|
||||
fontWeight: 600,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: 1.2,
|
||||
textShadow: TEXT_GLOW_DARK,
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{formatStock(stockVal)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
);
|
||||
|
||||
@@ -218,7 +218,16 @@ class ResultRow extends Component {
|
||||
)}
|
||||
{row.sku}
|
||||
{' · '}
|
||||
Bestand {formatStock(row.stock)}
|
||||
<Box
|
||||
component="span"
|
||||
title={
|
||||
row.warehouses?.length > 0
|
||||
? row.warehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Bestand {formatStock(row.stock)}
|
||||
</Box>
|
||||
{' · '}
|
||||
Verfügbar {formatStock(row.available)}
|
||||
{row.categories
|
||||
|
||||
Reference in New Issue
Block a user