u
This commit is contained in:
@@ -13,18 +13,24 @@ import Typography from '@mui/material/Typography';
|
||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutlineOutlined';
|
||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined';
|
||||
import { LINE_COLUMNS } from './manifestConfig';
|
||||
import { getFirstVal, parseNum } from './manifestConfig';
|
||||
import ArticlePhoto from '../article/ArticlePhoto';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
import { primaryPrice, priceBrutto } from '../../utils/articleDetail';
|
||||
import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat';
|
||||
import { artikelDetailPath } from '../../pages/artikel/articleRoute';
|
||||
|
||||
class ManifestFormLine extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const sku = props.supplierArticle?.article?.sku;
|
||||
this.state = {
|
||||
cachedArticle: sku ? getArticle(sku) : null,
|
||||
};
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
this.handleBlur = this.handleBlur.bind(this);
|
||||
this.handleCreateArticle = this.handleCreateArticle.bind(this);
|
||||
@@ -32,6 +38,41 @@ class ManifestFormLine extends PureComponent {
|
||||
this.handleRemoveSupplierArticle = this.handleRemoveSupplierArticle.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribeArticle(this.props.supplierArticle?.article?.sku);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const prevSku = prevProps.supplierArticle?.article?.sku;
|
||||
const nextSku = this.props.supplierArticle?.article?.sku;
|
||||
if (prevSku !== nextSku) {
|
||||
this.subscribeArticle(nextSku);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unsubArticle?.();
|
||||
}
|
||||
|
||||
subscribeArticle(sku) {
|
||||
this.unsubArticle?.();
|
||||
this.unsubArticle = null;
|
||||
if (!sku) {
|
||||
if (this.state.cachedArticle) {
|
||||
this.setState({ cachedArticle: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const current = getArticle(sku);
|
||||
this.setState({ cachedArticle: current });
|
||||
if (!current) {
|
||||
artikelAutomat.actions.loadArticle(sku).catch(() => {});
|
||||
}
|
||||
this.unsubArticle = artikelAutomat.subscribe(this, (state) => ({
|
||||
cachedArticle: createArticleSelector(sku)(state),
|
||||
}));
|
||||
}
|
||||
|
||||
handleRemove() {
|
||||
this.props.onRemove(this.props.line.key);
|
||||
}
|
||||
@@ -63,15 +104,112 @@ class ManifestFormLine extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
renderField(column) {
|
||||
renderLedgerChip() {
|
||||
const { ledgerLine, onBookLine } = this.props;
|
||||
if (!ledgerLine) return null;
|
||||
const isBooked = ledgerLine.isBooked;
|
||||
const isPartial = !isBooked && ledgerLine.bookedAmount > 0;
|
||||
const isClickable = Boolean(onBookLine);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
isBooked
|
||||
? `Vollständig im Lager gebucht: ${ledgerLine.bookedAmount} Stück (Klicken für Details)`
|
||||
: isPartial
|
||||
? `Teilweise im Lager gebucht: ${ledgerLine.bookedAmount} von ${ledgerLine.targetAmount} Stück (Klicken zum Einbuchen dieser Position)`
|
||||
: `Noch nicht im Lager gebucht (${ledgerLine.targetAmount} Stück – Klicken zum Einbuchen dieser Position)`
|
||||
}
|
||||
>
|
||||
<Chip
|
||||
size="small"
|
||||
clickable={isClickable}
|
||||
onClick={
|
||||
isClickable
|
||||
? (e) => {
|
||||
e.stopPropagation();
|
||||
onBookLine(ledgerLine);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
icon={
|
||||
isBooked ? (
|
||||
<CheckCircleOutlineOutlinedIcon sx={{ '&&': { fontSize: 13 } }} />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 13 } }} />
|
||||
)
|
||||
}
|
||||
label={
|
||||
isBooked
|
||||
? `Lager: ${ledgerLine.bookedAmount} Stk. gebucht`
|
||||
: isPartial
|
||||
? `Lager: ${ledgerLine.bookedAmount}/${ledgerLine.targetAmount} Stk.`
|
||||
: 'Lager: offen'
|
||||
}
|
||||
color={isBooked ? 'success' : isPartial ? 'warning' : 'default'}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
height: 20,
|
||||
fontSize: '0.68rem',
|
||||
fontWeight: 600,
|
||||
cursor: isClickable ? 'pointer' : 'default',
|
||||
bgcolor: isBooked ? 'success.50' : undefined,
|
||||
'&:hover': isClickable
|
||||
? {
|
||||
bgcolor: isBooked
|
||||
? 'success.100'
|
||||
: isPartial
|
||||
? 'warning.100'
|
||||
: 'action.hover',
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
renderField(column, colIdx, totalCols) {
|
||||
const { id, align, type } = column;
|
||||
const { line, saving, onChange } = this.props;
|
||||
let val = line[id];
|
||||
if (val == null || val === '') {
|
||||
if (id === 'tax' || id === 'taxrate') {
|
||||
val = line.taxrate ?? line.tax ?? '';
|
||||
} else if (id === 'net' || id === 'netprice') {
|
||||
val = line.netprice ?? line.net ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
let isError = false;
|
||||
if (id === 'name') {
|
||||
isError = !String(line.name ?? '').trim();
|
||||
} else if (id === 'amount') {
|
||||
const raw = getFirstVal(line.amount, line.count);
|
||||
isError = raw == null || parseNum(raw) == null;
|
||||
} else if (id === 'pu') {
|
||||
const raw = getFirstVal(line.pu, line.vpe);
|
||||
isError = raw == null || parseNum(raw) == null;
|
||||
} else if (id === 'tax' || id === 'taxrate') {
|
||||
const raw = getFirstVal(line.tax, line.taxrate, line.taxRate, line.mwst);
|
||||
isError = raw == null || parseNum(raw) == null;
|
||||
} else if (id === 'net' || id === 'netprice') {
|
||||
const raw = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto);
|
||||
isError = raw == null || parseNum(raw) == null;
|
||||
} else if (id === 'sku') {
|
||||
isError = !String(line.sku ?? '').trim() && !String(line.name ?? '').trim();
|
||||
}
|
||||
|
||||
const isFirst = colIdx === 0;
|
||||
const isLast = colIdx === totalCols - 1;
|
||||
|
||||
return (
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
type={type}
|
||||
value={line[id] ?? ''}
|
||||
value={val ?? ''}
|
||||
error={isError}
|
||||
disabled={saving}
|
||||
autoComplete="off"
|
||||
onChange={(e) => onChange(line.key, id, e.target.value)}
|
||||
@@ -86,6 +224,16 @@ class ManifestFormLine extends PureComponent {
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiOutlinedInput-root': {
|
||||
borderRadius: isFirst ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0,
|
||||
ml: isFirst ? 0 : '-1px',
|
||||
'&:hover': {
|
||||
zIndex: 1,
|
||||
},
|
||||
'&.Mui-focused': {
|
||||
zIndex: 2,
|
||||
},
|
||||
},
|
||||
'& input[type=number]': {
|
||||
MozAppearance: 'textfield',
|
||||
},
|
||||
@@ -116,18 +264,28 @@ class ManifestFormLine extends PureComponent {
|
||||
|
||||
if (supplierArticle) {
|
||||
const article = supplierArticle.article || {};
|
||||
const photo = article.photos?.[0] || article.articlepic?.[0];
|
||||
const fullArticle = this.state.cachedArticle || article;
|
||||
const photo = fullArticle.photos?.[0] || fullArticle.articlepic?.[0] || article.photos?.[0] || article.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 = article.eans ?? article.articleean ?? [];
|
||||
const eans = fullArticle.eans ?? fullArticle.articleean ?? article.eans ?? article.articleean ?? [];
|
||||
const primaryEan = Array.isArray(eans) ? eans[0] : eans;
|
||||
const priceNet = supplierArticle.price?.net;
|
||||
const taxRate = supplierArticle.price?.taxRate;
|
||||
|
||||
const vkPrice = primaryPrice(fullArticle);
|
||||
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={{
|
||||
@@ -138,6 +296,24 @@ class ManifestFormLine extends PureComponent {
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{/* Unlink button at the front */}
|
||||
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
|
||||
<Tooltip title="Lieferanten-Verknüpfung aufheben">
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
aria-label="Lieferanten-Verknüpfung entfernen"
|
||||
disabled={this.props.saving || this.props.supplierArticleBusy}
|
||||
onClick={this.handleRemoveSupplierArticle}
|
||||
sx={{ p: 0.25, mr: -0.25 }}
|
||||
>
|
||||
<LinkOffOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Article Photo Thumbnail */}
|
||||
<Box
|
||||
sx={{
|
||||
@@ -166,9 +342,6 @@ class ManifestFormLine extends PureComponent {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Match Indicator */}
|
||||
<CheckCircleOutlinedIcon sx={{ fontSize: 16, color: 'success.main', flexShrink: 0 }} />
|
||||
|
||||
{/* Internal Article SKU */}
|
||||
{article.sku && (
|
||||
<Chip
|
||||
@@ -195,15 +368,15 @@ class ManifestFormLine extends PureComponent {
|
||||
)}
|
||||
|
||||
{/* Article Name */}
|
||||
{article.name && (
|
||||
{(fullArticle.name || article.name) && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
noWrap
|
||||
sx={{ color: 'text.primary', maxWidth: { xs: 200, sm: 300, md: 400 } }}
|
||||
title={article.name}
|
||||
title={fullArticle.name || article.name}
|
||||
>
|
||||
{article.name}
|
||||
{fullArticle.name || article.name}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -225,32 +398,49 @@ class ManifestFormLine extends PureComponent {
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Stored Purchase Price (Netto) */}
|
||||
{priceNet != null && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
sx={{ color: 'success.dark', ml: 'auto', fontSize: '0.72rem', whiteSpace: 'nowrap' }}
|
||||
{/* Stored Purchase Price (Netto), VK Netto & VK Brutto */}
|
||||
{(priceNet != null || vkNetto != null || vkBrutto != null) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
ml: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
EK Netto: {formatPrice(priceNet)}
|
||||
{taxRate != null ? ` (${taxRate}% MwSt)` : ''}
|
||||
</Typography>
|
||||
{priceNet != null && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
fontWeight={600}
|
||||
sx={{ color: 'success.dark', fontSize: '0.72rem' }}
|
||||
>
|
||||
EK Netto: {formatPrice(priceNet)}
|
||||
{taxRate != null ? ` (${taxRate}% MwSt)` : ''}
|
||||
</Typography>
|
||||
)}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Remove supplier-article connection */}
|
||||
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
|
||||
<span style={{ marginLeft: priceNet != null ? 0 : 'auto' }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
aria-label="Lieferanten-Verknüpfung entfernen"
|
||||
disabled={this.props.saving || this.props.supplierArticleBusy}
|
||||
onClick={this.handleRemoveSupplierArticle}
|
||||
>
|
||||
<LinkOffOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</span>
|
||||
)}
|
||||
{this.renderLedgerChip()}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -258,10 +448,6 @@ class ManifestFormLine extends PureComponent {
|
||||
if (rawSku) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
|
||||
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
||||
Kein Lieferanten-Artikel für SKU „{rawSku}“ gefunden.
|
||||
</Typography>
|
||||
{this.props.onCreateArticle && (
|
||||
<Button
|
||||
size="small"
|
||||
@@ -306,34 +492,44 @@ class ManifestFormLine extends PureComponent {
|
||||
Artikel suchen
|
||||
</Button>
|
||||
)}
|
||||
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
||||
Kein Lieferanten-Artikel für SKU „{rawSku}“ gefunden.
|
||||
</Typography>
|
||||
{this.renderLedgerChip()}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography variant="caption" color="text.disabled" sx={{ fontStyle: 'italic', fontSize: '0.7rem' }}>
|
||||
SKU eingeben oder Dokument scannen für automatische Artikelzuordnung
|
||||
</Typography>
|
||||
);
|
||||
if (this.props.ledgerLine) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
|
||||
{this.renderLedgerChip()}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { columns = LINE_COLUMNS, saving } = this.props;
|
||||
const secondLine = this.renderSecondLine();
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{/* Row 1: Editable Line Inputs */}
|
||||
<TableRow hover sx={{ '& > td': { borderBottom: 'none', py: 0.75 } }}>
|
||||
{columns.map((col) => (
|
||||
<TableRow hover sx={{ '& > td': { borderBottom: 'none', p: 0 } }}>
|
||||
{columns.map((col, colIdx) => (
|
||||
<TableCell
|
||||
key={col.id}
|
||||
align={col.align}
|
||||
sx={{ py: 0.75, verticalAlign: 'middle' }}
|
||||
sx={{ p: 0, width: col.width, verticalAlign: 'middle' }}
|
||||
>
|
||||
{this.renderField(col)}
|
||||
{this.renderField(col, colIdx, columns.length)}
|
||||
</TableCell>
|
||||
))}
|
||||
<TableCell padding="checkbox" sx={{ py: 0.75 }}>
|
||||
<TableCell padding="none" sx={{ py: 0.25, pl: 0.75, pr: 0, verticalAlign: 'middle', width: 40 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Zeile entfernen"
|
||||
@@ -346,18 +542,20 @@ class ManifestFormLine extends PureComponent {
|
||||
</TableRow>
|
||||
|
||||
{/* Row 2: Matched Supplier Article Details */}
|
||||
<TableRow sx={{ '& > td': { pt: 0, pb: 0.75, borderBottom: 'none' } }}>
|
||||
<TableCell
|
||||
colSpan={columns.length + 1}
|
||||
sx={{
|
||||
py: 0.4,
|
||||
px: 2,
|
||||
bgcolor: 'transparent',
|
||||
}}
|
||||
>
|
||||
{this.renderSecondLine()}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{secondLine && (
|
||||
<TableRow sx={{ '& > td': { pt: 0.25, pb: 0.75, borderBottom: 'none' } }}>
|
||||
<TableCell
|
||||
colSpan={columns.length + 1}
|
||||
sx={{
|
||||
py: 0.4,
|
||||
px: 1,
|
||||
bgcolor: 'transparent',
|
||||
}}
|
||||
>
|
||||
{secondLine}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user