This commit is contained in:
seb
2026-10-19 02:37:16 +02:00
parent 7286cd8bf2
commit 06d0dd3bb2

View File

@@ -3,7 +3,9 @@ import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import WarehouseIcon from '@mui/icons-material/Warehouse';
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import Tooltip from '@mui/material/Tooltip';
import Chip from '@mui/material/Chip';
import { PanelTile } from 'dashgui/panels';
import { formatStock } from '../searchFormat';
import ArticleStockAdjustDialog from '../ArticleStockAdjustDialog';
@@ -18,6 +20,83 @@ import {
const SECOND_ROW_MIN_HEIGHT = 220;
function formatDate(iso) {
if (!iso) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return String(iso);
return d.toLocaleDateString('de-DE');
}
function extractIncoming(articleData, isParent) {
const items = [];
const collect = (article, variantInfo = null) => {
if (!article) return;
if (Array.isArray(article.incoming)) {
for (const inc of article.incoming) {
const amount = Number(inc?.amount) || 0;
if (amount > 0) {
items.push({
date: inc.date || null,
amount,
supplierName: inc.supplierName || null,
supplierSku: inc.sku || null,
pu: inc.pu || null,
variantSku: variantInfo?.sku || null,
variantName: variantInfo?.name || null,
});
}
}
}
if (Array.isArray(article.suppliers)) {
for (const sup of article.suppliers) {
if (Array.isArray(sup?.incoming)) {
for (const inc of sup.incoming) {
const amount = Number(inc?.amount) || 0;
if (amount > 0) {
items.push({
date: inc.date || null,
amount,
supplierName: sup.supplierName || null,
supplierSku: sup.sku || null,
pu: sup.pu || null,
variantSku: variantInfo?.sku || null,
variantName: variantInfo?.name || null,
});
}
}
}
}
}
};
collect(articleData);
if (isParent) {
const children = Array.isArray(articleData?.children) ? articleData.children : [];
for (const child of children) {
const fullChild = getArticle(child.sku) || child;
collect(fullChild, {
sku: fullChild.sku || child.sku,
name: fullChild.name || child.name,
});
}
}
items.sort((a, b) => {
if (a.date && b.date) {
return new Date(a.date).getTime() - new Date(b.date).getTime();
}
if (a.date) return -1;
if (b.date) return 1;
return 0;
});
return items;
}
export default class ArticleViewStock extends PureComponent {
constructor(props) {
super(props);
@@ -66,6 +145,40 @@ export default class ArticleViewStock extends PureComponent {
this.setState({ adjustOpen: false });
}
renderIncomingChip(totalIncoming, incomingItems, unit) {
const tooltipText = `${formatStock(totalIncoming)}${unit ? ` ${unit}` : ''} im Zulauf aus ${
incomingItems.length === 1
? 'einer offenen Lieferung'
: `${incomingItems.length} offenen Lieferungen`
}`;
return (
<Tooltip title={tooltipText} arrow placement="top-start">
<Chip
size="small"
icon={
<LocalShippingOutlinedIcon
sx={{ fontSize: '14px !important', transform: 'scaleX(-1)' }}
/>
}
label={`+${formatStock(totalIncoming)}${unit ? ` ${unit}` : ''} im Zulauf`}
onClick={(e) => e.stopPropagation()}
sx={{
fontWeight: 600,
fontSize: '0.75rem',
height: 22,
backgroundColor: VEIL_PANEL,
cursor: 'default',
'& .MuiChip-icon': {
color: 'text.secondary',
ml: '6px',
},
}}
/>
</Tooltip>
);
}
render() {
const { article: articleData } = this.props;
const { adjustOpen } = this.state;
@@ -123,6 +236,10 @@ export default class ArticleViewStock extends PureComponent {
? rawUnit
: '';
const incomingItems = extractIncoming(articleData, isParent);
const totalIncoming = incomingItems.reduce((sum, inc) => sum + inc.amount, 0);
const hasIncoming = totalIncoming > 0;
return (
<>
<PanelTile
@@ -163,8 +280,8 @@ export default class ArticleViewStock extends PureComponent {
</Typography>
{isParent ? (
anyVariantTracksStock || totalVariantsStock > 0 ? (
<Box sx={{ my: 0.75 }}>
<Box sx={{ my: 0.75 }}>
{anyVariantTracksStock || totalVariantsStock > 0 ? (
<Typography
component="div"
sx={{
@@ -176,12 +293,74 @@ export default class ArticleViewStock extends PureComponent {
>
Alle Varianten zusammen: {formatStock(totalVariantsStock)}
</Typography>
</Box>
) : null
) : null}
{hasIncoming ? (
<Box sx={{ mt: 0.75 }}>
{this.renderIncomingChip(totalIncoming, incomingItems, unit)}
</Box>
) : null}
</Box>
) : isStueckliste ? (
<Tooltip title="Virtueller Bestand – Anpassung erfolgt an den einzelnen Komponenten" arrow placement="top-start">
<Box sx={{ my: 0.5 }}>
<Box sx={{ display: 'inline-flex', alignItems: 'baseline', gap: 0.75 }}>
<Box sx={{ my: 0.5 }}>
<Tooltip title="Virtueller Bestand – Anpassung erfolgt an den einzelnen Komponenten" arrow placement="top-start">
<Box>
<Box sx={{ display: 'inline-flex', alignItems: 'baseline', gap: 0.75 }}>
<Typography
component="div"
sx={{
fontSize: '2.4rem',
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: '-0.02em',
color: totalStock > 0 ? 'text.primary' : 'text.secondary',
}}
>
{formatStock(totalStock)}
</Typography>
{unit ? (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontWeight: 500 }}
>
{unit}
</Typography>
) : null}
</Box>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 0.25 }}
>
Virtueller Bestand (aus Komponenten)
</Typography>
</Box>
</Tooltip>
{hasIncoming ? (
<Box sx={{ mt: 0.75 }}>
{this.renderIncomingChip(totalIncoming, incomingItems, unit)}
</Box>
) : null}
</Box>
) : isTrackStock ? (
<Box sx={{ my: 0.5 }}>
<Tooltip title="Klicken, um Bestand anzupassen" arrow placement="top-start">
<Box
onClick={this.handleOpenAdjust}
sx={{
display: 'inline-flex',
alignItems: 'baseline',
gap: 0.75,
cursor: 'pointer',
borderRadius: 1,
px: 0.5,
mx: -0.5,
transition: 'background-color 0.15s ease',
'&:hover': {
backgroundColor: TINT_ROW_HOVER,
},
}}
>
<Typography
component="div"
sx={{
@@ -204,24 +383,18 @@ export default class ArticleViewStock extends PureComponent {
</Typography>
) : null}
</Box>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 0.25 }}
>
Virtueller Bestand (aus Komponenten)
</Typography>
</Box>
</Tooltip>
) : isTrackStock ? (
<Tooltip title="Klicken, um Bestand anzupassen" arrow placement="top-start">
</Tooltip>
{hasIncoming ? (
<Box sx={{ mt: 0.75 }}>
{this.renderIncomingChip(totalIncoming, incomingItems, unit)}
</Box>
) : null}
</Box>
) : (
<Box sx={{ my: 0.75 }}>
<Box
onClick={this.handleOpenAdjust}
sx={{
display: 'inline-flex',
alignItems: 'baseline',
gap: 0.75,
my: 0.5,
cursor: 'pointer',
borderRadius: 1,
px: 0.5,
@@ -235,65 +408,33 @@ export default class ArticleViewStock extends PureComponent {
<Typography
component="div"
sx={{
fontSize: '2.4rem',
fontSize: '1.25rem',
fontWeight: 700,
lineHeight: 1.1,
letterSpacing: '-0.02em',
color: totalStock > 0 ? 'text.primary' : 'text.secondary',
lineHeight: 1.2,
color: 'text.secondary',
}}
>
{formatStock(totalStock)}
Keine Erfassung
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 0.25 }}
>
Bestand wird nicht überwacht
</Typography>
{unit ? (
<Typography
variant="body2"
color="text.secondary"
sx={{ fontWeight: 500 }}
>
{unit}
</Typography>
) : null}
</Box>
</Tooltip>
) : (
<Box
onClick={this.handleOpenAdjust}
sx={{
my: 0.75,
cursor: 'pointer',
borderRadius: 1,
px: 0.5,
mx: -0.5,
transition: 'background-color 0.15s ease',
'&:hover': {
backgroundColor: TINT_ROW_HOVER,
},
}}
>
<Typography
component="div"
sx={{
fontSize: '1.25rem',
fontWeight: 700,
lineHeight: 1.2,
color: 'text.secondary',
}}
>
Keine Erfassung
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 0.25 }}
>
Bestand wird nicht überwacht
</Typography>
{hasIncoming ? (
<Box sx={{ mt: 0.75 }}>
{this.renderIncomingChip(totalIncoming, incomingItems, unit)}
</Box>
) : null}
</Box>
)}
</Box>
{/* Bottom section: Per-warehouse breakdown */}
{isTrackStock && hasWarehouses ? (
{/* Bottom section: Per-warehouse breakdown & incoming shipments */}
{hasWarehouses || hasIncoming ? (
<Box
sx={{
mt: 1,
@@ -309,65 +450,176 @@ export default class ArticleViewStock extends PureComponent {
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{warehouses.map((wh) => (
<Box
key={wh.id}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
py: 0.35,
px: 0.75,
borderRadius: 1,
backgroundColor: VEIL_PANEL,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
minWidth: 0,
mr: 1,
}}
>
<WarehouseIcon
sx={{
fontSize: 15,
color: 'text.secondary',
mr: 0.5,
flexShrink: 0,
}}
/>
{/* Warehouse stocks */}
{hasWarehouses ? (
<>
{hasIncoming ? (
<Typography
variant="body2"
noWrap
title={wh.name}
variant="caption"
sx={{
fontWeight: 500,
fontSize: '0.82rem',
color: 'text.primary',
fontSize: '0.68rem',
fontWeight: 700,
color: 'text.secondary',
textTransform: 'uppercase',
letterSpacing: 0.5,
px: 0.5,
mb: 0.1,
display: 'block',
}}
>
{wh.name}
Lagerorte
</Typography>
</Box>
) : null}
{warehouses.map((wh) => (
<Box
key={wh.id}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
py: 0.35,
px: 0.75,
borderRadius: 1,
backgroundColor: VEIL_PANEL,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
minWidth: 0,
mr: 1,
}}
>
<WarehouseIcon
sx={{
fontSize: 15,
color: 'text.secondary',
mr: 0.5,
flexShrink: 0,
}}
/>
<Typography
variant="body2"
noWrap
title={wh.name}
sx={{
fontWeight: 500,
fontSize: '0.82rem',
color: 'text.primary',
}}
>
{wh.name}
</Typography>
</Box>
<Typography
variant="body2"
sx={{
fontWeight: 700,
fontSize: '0.82rem',
flexShrink: 0,
color:
Number(wh.stock) > 0
? 'text.primary'
: 'text.secondary',
}}
>
{formatStock(wh.stock)}
</Typography>
</Box>
))}
</>
) : null}
{/* Incoming shipments */}
{hasIncoming ? (
<>
<Typography
variant="body2"
variant="caption"
sx={{
fontSize: '0.68rem',
fontWeight: 700,
fontSize: '0.82rem',
flexShrink: 0,
color:
Number(wh.stock) > 0
? 'text.primary'
: 'text.secondary',
color: 'text.secondary',
textTransform: 'uppercase',
letterSpacing: 0.5,
px: 0.5,
mt: hasWarehouses ? 0.75 : 0,
mb: 0.1,
display: 'block',
}}
>
{formatStock(wh.stock)}
Im Zulauf
</Typography>
</Box>
))}
{incomingItems.map((inc, idx) => {
const dateLabel = inc.date ? formatDate(inc.date) : null;
const label = [
inc.supplierName || 'Lieferant',
dateLabel,
]
.filter(Boolean)
.join(' – ');
return (
<Box
key={idx}
title={label}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
py: 0.35,
px: 0.75,
borderRadius: 1,
backgroundColor: VEIL_PANEL,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
minWidth: 0,
mr: 1,
}}
>
<LocalShippingOutlinedIcon
sx={{
fontSize: 15,
color: 'text.secondary',
mr: 0.6,
flexShrink: 0,
transform: 'scaleX(-1)',
}}
/>
<Typography
variant="body2"
noWrap
sx={{
fontWeight: 500,
fontSize: '0.82rem',
color: 'text.primary',
}}
>
{label}
</Typography>
</Box>
<Typography
variant="body2"
sx={{
fontWeight: 700,
fontSize: '0.82rem',
flexShrink: 0,
color: 'text.primary',
}}
>
+{formatStock(inc.amount)}
</Typography>
</Box>
);
})}
</>
) : null}
</Box>
</Box>
) : null}