This commit is contained in:
seb
2026-10-05 17:57:33 +02:00
parent f627ff5b5a
commit 0a1002227c
9 changed files with 548 additions and 69 deletions

View File

@@ -31,9 +31,10 @@ import {
class ManifestFormLine extends PureComponent {
constructor(props) {
super(props);
const sku = props.supplierArticle?.article?.sku;
const sku = this.getArticleSku(props);
this.state = {
cachedArticle: sku ? getArticle(sku) : null,
loadingArticle: false,
};
this.handleRemove = this.handleRemove.bind(this);
this.handleBlur = this.handleBlur.bind(this);
@@ -42,13 +43,20 @@ class ManifestFormLine extends PureComponent {
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
}
getArticleSku(props = this.props) {
if (props.kind === 'delivery') {
return props.supplierArticle?.article?.sku || null;
}
return props.line?.sku ? String(props.line.sku).trim() : null;
}
componentDidMount() {
this.subscribeArticle(this.props.supplierArticle?.article?.sku);
this.subscribeArticle(this.getArticleSku());
}
componentDidUpdate(prevProps) {
const prevSku = prevProps.supplierArticle?.article?.sku;
const nextSku = this.props.supplierArticle?.article?.sku;
const prevSku = this.getArticleSku(prevProps);
const nextSku = this.getArticleSku(this.props);
if (prevSku !== nextSku) {
this.subscribeArticle(nextSku);
}
@@ -62,19 +70,35 @@ class ManifestFormLine extends PureComponent {
this.unsubArticle?.();
this.unsubArticle = null;
if (!sku) {
if (this.state.cachedArticle) {
this.setState({ cachedArticle: null });
if (this.state.cachedArticle || this.state.loadingArticle) {
this.setState({ cachedArticle: null, loadingArticle: false });
}
return;
}
const current = getArticle(sku);
this.setState({ cachedArticle: current });
if (!current) {
artikelAutomat.actions.loadArticle(sku).catch(() => { });
if (current) {
this.setState({ cachedArticle: current, loadingArticle: false });
} else {
this.setState({ cachedArticle: null, loadingArticle: true });
artikelAutomat.actions.loadArticle(sku)
.then((art) => {
if (this.getArticleSku() === sku) {
this.setState({ cachedArticle: art, loadingArticle: false });
}
})
.catch(() => {
if (this.getArticleSku() === sku) {
this.setState({ cachedArticle: null, loadingArticle: false });
}
});
}
this.unsubArticle = artikelAutomat.subscribe(this, (state) => ({
cachedArticle: createArticleSelector(sku)(state),
}));
this.unsubArticle = artikelAutomat.subscribe(this, (state) => {
const art = createArticleSelector(sku)(state);
return {
cachedArticle: art ?? null,
...(art ? { loadingArticle: false } : {}),
};
});
}
handleRemove() {
@@ -251,8 +275,255 @@ class ManifestFormLine extends PureComponent {
}
renderSecondLine() {
const { kind = 'delivery' } = this.props;
const rawSku = this.props.line?.sku != null ? String(this.props.line.sku).trim() : '';
if (kind === 'delivery') {
return this.renderDeliverySecondLine(rawSku);
}
return this.renderArticleSecondLine(rawSku);
}
renderArticleSecondLine(rawSku) {
const { loading } = this.props;
const { cachedArticle, loadingArticle } = this.state;
if (loading || loadingArticle) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.25 }}>
<CircularProgress size={14} thickness={5} sx={{ color: 'primary.main' }} />
<Typography variant="caption" color="text.secondary" sx={{ fontStyle: 'italic', fontSize: '0.72rem' }}>
Artikeldaten werden geladen…
</Typography>
</Box>
);
}
if (cachedArticle) {
const photo = cachedArticle.photos?.[0] || cachedArticle.articlepic?.[0];
const fullHash = photo?.hash || photo?.fullHash;
const thumbHash = photo?.thumbhash || photo?.thumbHash;
const thumbW = photo?.thumbw || photo?.thumbW;
const thumbH = photo?.thumbh || photo?.thumbH;
const hasPhoto = fullHash != null || thumbHash != null;
const eans = cachedArticle.eans ?? cachedArticle.articleean ?? [];
const primaryEan = Array.isArray(eans) ? eans[0] : eans;
const vkPrice = primaryPrice(cachedArticle);
const vkNetto = vkPrice?.net;
const vkBrutto = vkPrice
? priceBrutto(vkPrice) ??
(vkPrice.net != null && vkPrice.taxrate != null
? vkPrice.net * (1 + vkPrice.taxrate / 100)
: null)
: null;
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.25,
py: 0.25,
flexWrap: 'wrap',
}}
>
{/* Article Photo Thumbnail */}
<Box
sx={{
width: 28,
height: 28,
minWidth: 28,
borderRadius: 0.75,
overflow: 'hidden',
bgcolor: BORDER,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: '1px solid',
borderColor: 'divider',
}}
>
{hasPhoto ? (
<ArticlePhoto
photo={{ fullHash, thumbHash, thumbW, thumbH }}
size={28}
/>
) : (
<Inventory2OutlinedIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
)}
</Box>
{/* Internal Article SKU */}
{cachedArticle.sku && (
<Chip
component={RouterLink}
to={artikelDetailPath(cachedArticle)}
clickable
size="small"
label={`Art.-Nr.: ${cachedArticle.sku}`}
sx={{
height: 20,
fontSize: '0.7rem',
fontWeight: 600,
bgcolor: 'primary.50',
color: 'primary.dark',
border: '1px solid',
borderColor: 'primary.200',
cursor: 'pointer',
textDecoration: 'none',
'&:hover': {
bgcolor: 'primary.100',
},
}}
/>
)}
{/* Brand */}
{cachedArticle.brand?.name && (
<BrandLabel
brand={cachedArticle.brand}
size={16}
typographyProps={{
variant: 'caption',
color: 'text.secondary',
fontWeight: 500,
}}
/>
)}
{/* Article Name */}
{cachedArticle.name && (
<Typography
variant="caption"
fontWeight={600}
noWrap
sx={{ color: 'text.primary', maxWidth: { xs: 200, sm: 300, md: 400 } }}
title={cachedArticle.name}
>
{cachedArticle.name}
</Typography>
)}
{/* Primary EAN */}
{primaryEan && (
<Typography
variant="caption"
color="text.secondary"
sx={{
fontFamily: 'monospace',
fontSize: '0.68rem',
bgcolor: 'action.hover',
px: 0.5,
py: 0.1,
borderRadius: 0.5,
}}
>
EAN: {primaryEan}
</Typography>
)}
{/* VK Netto & VK Brutto */}
{(vkNetto != null || vkBrutto != null) && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1.5,
ml: 'auto',
whiteSpace: 'nowrap',
}}
>
{vkNetto != null && (
<Typography
variant="caption"
fontWeight={600}
sx={{ color: 'text.secondary', fontSize: '0.72rem' }}
>
VK Netto: {formatPrice(vkNetto)}
</Typography>
)}
{vkBrutto != null && (
<Typography
variant="caption"
fontWeight={600}
sx={{ color: 'primary.dark', fontSize: '0.72rem' }}
>
VK Brutto: {formatPrice(vkBrutto)}
</Typography>
)}
</Box>
)}
</Box>
);
}
if (rawSku) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
{this.props.onSearchArticle && (
<Button
size="small"
variant="text"
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
onClick={this.handleSearchArticle}
sx={{
fontSize: '0.7rem',
fontWeight: 600,
textTransform: 'none',
minWidth: 'auto',
py: 0.25,
px: 1,
color: 'primary.main',
'&:hover': {
bgcolor: 'primary.50',
},
}}
>
Artikel suchen
</Button>
)}
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
Kein Artikel für SKU &bdquo;{rawSku}&ldquo; gefunden.
</Typography>
</Box>
);
}
if (this.props.onSearchArticle) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
<Button
size="small"
variant="text"
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
onClick={this.handleSearchArticle}
sx={{
fontSize: '0.7rem',
fontWeight: 600,
textTransform: 'none',
minWidth: 'auto',
py: 0.25,
px: 1,
color: 'primary.main',
'&:hover': {
bgcolor: 'primary.50',
},
}}
>
Artikel suchen
</Button>
</Box>
);
}
return null;
}
renderDeliverySecondLine(rawSku) {
const { line, supplierArticle, loading } = this.props;
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
if (loading) {
return (