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, grossFromNet } 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 { articleSupplierAutomat } from '../../automats/articleDetailAutomat'; import { selectCompanyTaxrate } from '../../automats/companyAutomat'; 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, fallbackSuppliers: [], }; 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?.(); this.unsubSupplier?.(); } subscribeArticle(sku) { this.unsubArticle?.(); this.unsubArticle = null; this.unsubSupplier?.(); this.unsubSupplier = null; if (!sku) { if (this.state.cachedArticle || this.state.loadingArticle || this.state.fallbackSuppliers?.length) { this.setState({ cachedArticle: null, loadingArticle: false, fallbackSuppliers: [] }); } 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 } : {}), }; }); // Fallback: subscribe to articleSupplierAutomat if cachedArticle doesn't have suppliers if (!current?.suppliers?.length) { const supKey = articleSupplierAutomat.key(sku); this.unsubSupplier = supKey.subscribe(this, (item) => ({ fallbackSuppliers: Array.isArray(item?.data) ? item.data : [], })); void articleSupplierAutomat.load(sku).catch(() => {}); } } 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; const rebateNum = parseNum(line.rebate) ?? 0; if (netNum != null) { val = String(grossFromNet(netNum, taxNum, rebateNum)); } } } 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; const suppliers = Array.isArray(cachedArticle.suppliers) && cachedArticle.suppliers.length > 0 ? cachedArticle.suppliers : (this.state.fallbackSuppliers ?? []); const topSupplier = suppliers.find((s) => s.sort === 0) ?? (suppliers.length > 0 ? [...suppliers].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0))[0] : null); const ekNetto = topSupplier?.price?.net != null ? Number(topSupplier.price.net) : topSupplier?.netPrice != null ? Number(topSupplier.netPrice) : topSupplier?.net != null ? Number(topSupplier.net) : null; const ekTaxRate = topSupplier?.price?.taxRate != null ? Number(topSupplier.price.taxRate) : topSupplier?.taxRate != null ? Number(topSupplier.taxRate) : parseNum(this.props.line?.taxrate ?? this.props.line?.tax) ?? selectCompanyTaxrate() ?? 19; const ekBrutto = ekNetto != null ? Math.round((ekNetto * (1 + Number(ekTaxRate) / 100) + Number.EPSILON) * 100) / 100 : null; const hasPriceInfo = vkNetto != null || vkBrutto != null || ekNetto != null || ekBrutto != null || kind === 'quote'; 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} )} {/* EK Netto, EK Brutto, VK Netto, VK Brutto */} {!isOrder && hasPriceInfo && ( {/* EK Netto */} EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'} {/* EK Brutto */} EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'} {/* VK Netto */} {vkNetto != null && ( VK Netto: {formatPrice(vkNetto)} )} {/* VK Brutto */} {vkBrutto != null && ( VK Brutto: {formatPrice(vkBrutto)} )} )} ); } if (rawSku) { return ( {this.props.onSearchArticle && ( )} Kein Artikel für SKU „{rawSku}“ gefunden. ); } if (this.props.onSearchArticle) { return ( ); } 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 && ( )} {this.props.onSearchArticle && ( )} {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;