import { Fragment, PureComponent } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
import Tooltip from '@mui/material/Tooltip';
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 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 { getFirstVal, parseNum, LINE_COLUMNS } 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 '../article/articleRoute';
import { BrandLabel } from '../brand';
import {
BORDER,
} from '../../colors';
class ManifestFormLine extends PureComponent {
constructor(props) {
super(props);
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);
this.handleCreateArticle = this.handleCreateArticle.bind(this);
this.handleSearchArticle = this.handleSearchArticle.bind(this);
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.getArticleSku());
}
componentDidUpdate(prevProps) {
const prevSku = this.getArticleSku(prevProps);
const nextSku = this.getArticleSku(this.props);
if (prevSku !== nextSku) {
this.subscribeArticle(nextSku);
}
}
componentWillUnmount() {
this.unsubArticle?.();
}
subscribeArticle(sku) {
this.unsubArticle?.();
this.unsubArticle = null;
if (!sku) {
if (this.state.cachedArticle || this.state.loadingArticle) {
this.setState({ cachedArticle: null, loadingArticle: false });
}
return;
}
const current = getArticle(sku);
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) => {
const art = createArticleSelector(sku)(state);
return {
cachedArticle: art ?? null,
...(art ? { loadingArticle: false } : {}),
};
});
}
handleRemove() {
this.props.onRemove(this.props.line.key);
}
handleBlur(e, columnId) {
if (columnId === 'sku' && this.props.onSkuBlur) {
this.props.onSkuBlur(this.props.line.key, e.target.value);
}
}
handleCreateArticle() {
const { line, onCreateArticle } = this.props;
if (onCreateArticle) {
onCreateArticle(line);
}
}
handleSearchArticle() {
const { line, onSearchArticle } = this.props;
if (onSearchArticle) {
onSearchArticle(line);
}
}
handleRemoveSupplierArticle() {
const { line, supplierArticle, onRemoveSupplierArticle } = this.props;
if (onRemoveSupplierArticle) {
onRemoveSupplierArticle(line, supplierArticle);
}
}
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 (
{
e.stopPropagation();
onBookLine(ledgerLine);
}
: undefined
}
icon={
isBooked ? (
) : (
)
}
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,
}}
/>
);
}
renderField(column, colIdx, totalCols) {
const { id, align, type } = column;
const { line, saving, onChange, columns } = this.props;
if (id === 'pos') {
const posVal =
this.props.positionIndex ??
(this.props.index != null ? this.props.index + 1 : colIdx + 1);
return (
{posVal}
);
}
let val = line[id];
if (val == null || val === '') {
if (id === 'name' && this.state.cachedArticle?.name) {
val = this.state.cachedArticle.name;
} else if (id === 'tax' || id === 'taxrate') {
val = line.taxrate ?? line.tax ?? '';
} else if (id === 'net' || id === 'netprice') {
val = line.netprice ?? line.net ?? '';
} else if (id === 'gross') {
const netNum = parseNum(line.netprice ?? line.net);
const taxNum = parseNum(line.taxrate ?? line.tax) ?? 0;
if (netNum != null) {
val = String(Math.round((netNum * (1 + taxNum / 100) + Number.EPSILON) * 100) / 100);
}
}
}
let isError = false;
if (id === 'name') {
isError = !String(line.name ?? '').trim() && !String(line.sku ?? '').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 === 'gross') {
const raw = getFirstVal(line.gross, val);
isError = raw == null || parseNum(raw) == null;
} else if (id === 'sku') {
isError = !String(line.sku ?? '').trim() && !String(line.name ?? '').trim();
}
const hasPosCol = columns?.[0]?.id === 'pos';
const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
const isLast = colIdx === totalCols - 1;
const { dirtyFields } = this.props;
const isFieldDirty = Boolean(
dirtyFields && (dirtyFields.__all__ || dirtyFields[id])
);
return (
onChange(line.key, id, e.target.value)}
onBlur={(e) => this.handleBlur(e, id)}
slotProps={{
htmlInput: {
inputMode: type === 'number' ? 'decimal' : 'text',
style: { textAlign: align },
autoComplete: 'off',
step: type === 'number' ? 'any' : undefined,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: isFirstInput ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0,
ml: isFirstInput ? 0 : '-1px',
...(isFieldDirty && {
bgcolor: 'rgba(237, 108, 2, 0.08)',
}),
'&:hover': {
zIndex: 1,
},
'&.Mui-focused': {
zIndex: 2,
},
},
...(isFieldDirty && {
zIndex: 1,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: 'warning.main',
borderWidth: 2,
},
'&:hover .MuiOutlinedInput-notchedOutline': {
borderColor: 'warning.main',
},
}),
'& input[type=number]': {
MozAppearance: 'textfield',
},
'& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button':
{
WebkitAppearance: 'none',
margin: 0,
},
}}
/>
);
}
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, kind, columns } = this.props;
const isOrder = kind === 'order';
const hasNameColumn = columns?.some((c) => c.id === 'name');
const { cachedArticle, loadingArticle } = this.state;
if (loading || loadingArticle) {
return (
Artikeldaten werden geladen…
);
}
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 (
{/* Article Photo Thumbnail */}
{hasPhoto ? (
) : (
)}
{/* Internal Article SKU */}
{cachedArticle.sku && (
)}
{/* Brand */}
{cachedArticle.brand?.name && (
)}
{/* Article Name: omit if already editable in line 1 or if kind is order */}
{!hasNameColumn && !isOrder && cachedArticle.name && (
{cachedArticle.name}
)}
{/* Primary EAN: omit for order */}
{!isOrder && primaryEan && (
EAN: {primaryEan}
)}
{/* VK Netto & VK Brutto: omit end on 2nd line for order */}
{!isOrder && (vkNetto != null || vkBrutto != null) && (
{vkNetto != null && (
VK Netto: {formatPrice(vkNetto)}
)}
{vkBrutto != null && (
VK Brutto: {formatPrice(vkBrutto)}
)}
)}
);
}
if (rawSku) {
return (
{this.props.onSearchArticle && (
}
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
)}
Kein Artikel für SKU „{rawSku}“ gefunden.
);
}
if (this.props.onSearchArticle) {
return (
}
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
);
}
return null;
}
renderDeliverySecondLine(rawSku) {
const { supplierArticle, loading } = this.props;
if (loading) {
return (
Artikeldaten werden geladen…
);
}
if (supplierArticle) {
const article = supplierArticle.article || {};
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 = 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 (
{/* Unlink button at the front */}
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
)}
{/* Article Photo Thumbnail */}
{hasPhoto ? (
) : (
)}
{/* Internal Article SKU */}
{article.sku && (
)}
{/* Brand */}
{(fullArticle.brand?.name || article.brand?.name) && (
)}
{/* Article Name */}
{(fullArticle.name || article.name) && (
{fullArticle.name || article.name}
)}
{/* Supplier SKU if distinct */}
{supplierArticle.sku && supplierArticle.sku !== rawSku && supplierArticle.sku !== article.sku && (
(Lief.-SKU: {supplierArticle.sku})
)}
{/* Primary EAN */}
{primaryEan && (
EAN: {primaryEan}
)}
{/* Stored Purchase Price (Netto), VK Netto & VK Brutto */}
{(priceNet != null || vkNetto != null || vkBrutto != null) && (
{priceNet != null && (
EK Netto: {formatPrice(priceNet)}
{taxRate != null ? ` (${taxRate}% MwSt)` : ''}
)}
{vkNetto != null && (
VK Netto: {formatPrice(vkNetto)}
)}
{vkBrutto != null && (
VK Brutto: {formatPrice(vkBrutto)}
)}
)}
{this.renderLedgerChip()}
);
}
if (rawSku) {
return (
{this.props.onCreateArticle && (
}
onClick={this.handleCreateArticle}
sx={{
fontSize: '0.7rem',
fontWeight: 600,
textTransform: 'none',
minWidth: 'auto',
py: 0.25,
px: 1,
color: 'primary.main',
'&:hover': {
bgcolor: 'primary.50',
},
}}
>
Artikel anlegen
)}
{this.props.onSearchArticle && (
}
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
)}
{this.props.kind === 'delivery'
? `Kein Lieferanten-Artikel für SKU „${rawSku}“ gefunden.`
: `Kein Artikel für SKU „${rawSku}“ gefunden.`}
{this.renderLedgerChip()}
);
}
if (this.props.ledgerLine) {
return (
{this.renderLedgerChip()}
);
}
return null;
}
render() {
const { columns = LINE_COLUMNS, saving } = this.props;
const secondLine = this.renderSecondLine();
return (
{/* Row 1: Editable Line Inputs */}
td': { borderBottom: 'none', p: 0 } }}>
{columns.map((col, colIdx) => (
{this.renderField(col, colIdx, columns.length)}
))}
{/* Row 2: Matched Supplier Article Details */}
{secondLine && (
td': { pt: 0.25, pb: 0.75, borderBottom: 'none' } }}>
{secondLine}
)}
);
}
}
export default ManifestFormLine;