1539 lines
52 KiB
JavaScript
1539 lines
52 KiB
JavaScript
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 Checkbox from '@mui/material/Checkbox';
|
|
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 Typography from '@mui/material/Typography';
|
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
|
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|
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 { formatStock } from '../article/searchFormat';
|
|
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,
|
|
GREEN,
|
|
alpha,
|
|
} from '../../colors';
|
|
|
|
const LOCKED_ROW_BG = alpha(GREEN, 0.08);
|
|
const LOCKED_ROW_HOVER_BG = alpha(GREEN, 0.12);
|
|
const LOCKED_CELL_BORDER = alpha(GREEN, 0.32);
|
|
const LOCKED_CELL_BG = alpha(GREEN, 0.08);
|
|
|
|
class ManifestFormLine extends PureComponent {
|
|
constructor(props) {
|
|
super(props);
|
|
const sku = this.getArticleSku(props);
|
|
this.state = {
|
|
cachedArticle: sku ? getArticle(sku) : null,
|
|
loadingArticle: false,
|
|
fallbackSuppliers: [],
|
|
editingField: null,
|
|
};
|
|
this.handleRemove = this.handleRemove.bind(this);
|
|
this.handleMoveUp = this.handleMoveUp.bind(this);
|
|
this.handleMoveDown = this.handleMoveDown.bind(this);
|
|
this.handleBlur = this.handleBlur.bind(this);
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
this.handleStartEdit = this.handleStartEdit.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;
|
|
}
|
|
|
|
isFullyLedgered(props = this.props) {
|
|
const { ledgerLine, line } = props;
|
|
return Boolean(
|
|
ledgerLine?.isBooked ||
|
|
(ledgerLine && ledgerLine.targetAmount > 0 && ledgerLine.remainingAmount <= 0) ||
|
|
line?.isBooked ||
|
|
line?.is_booked
|
|
);
|
|
}
|
|
|
|
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() {
|
|
if (this.props.isOnShipment || this.isFullyLedgered()) return;
|
|
this.props.onRemove(this.props.line.key);
|
|
}
|
|
|
|
handleMoveUp() {
|
|
this.props.onMove?.(this.props.line.key, -1);
|
|
}
|
|
|
|
handleMoveDown() {
|
|
this.props.onMove?.(this.props.line.key, 1);
|
|
}
|
|
|
|
handleStartEdit(columnId) {
|
|
if (columnId === 'pos' || columnId === 'shippable') return;
|
|
const { saving } = this.props;
|
|
const isFullyLedgered = this.isFullyLedgered();
|
|
const isDisabled = saving || isFullyLedgered || (columnId === 'sku' && Boolean(this.props.isOnShipment));
|
|
if (isDisabled) return;
|
|
if (this.state.editingField !== columnId) {
|
|
this.setState({ editingField: columnId });
|
|
}
|
|
}
|
|
|
|
handleKeyDown(e) {
|
|
if (e.key === 'Enter' || e.key === 'Escape') {
|
|
e.target.blur();
|
|
}
|
|
}
|
|
|
|
handleBlur(e, columnId) {
|
|
if (this.state.editingField === columnId) {
|
|
this.setState({ editingField: null });
|
|
}
|
|
if (columnId === 'sku' && this.props.onSkuBlur) {
|
|
this.props.onSkuBlur(this.props.line.key, e.target.value);
|
|
}
|
|
}
|
|
|
|
handleCreateArticle() {
|
|
if (this.isFullyLedgered()) return;
|
|
const { line, onCreateArticle } = this.props;
|
|
if (onCreateArticle) {
|
|
onCreateArticle(line);
|
|
}
|
|
}
|
|
|
|
handleSearchArticle() {
|
|
if (this.isFullyLedgered()) return;
|
|
const { line, onSearchArticle } = this.props;
|
|
if (onSearchArticle) {
|
|
onSearchArticle(line);
|
|
}
|
|
}
|
|
|
|
handleRemoveSupplierArticle() {
|
|
if (this.isFullyLedgered()) return;
|
|
const { line, supplierArticle, onRemoveSupplierArticle } = this.props;
|
|
if (onRemoveSupplierArticle) {
|
|
onRemoveSupplierArticle(line, supplierArticle);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
renderField(column, colIdx, totalCols) {
|
|
const { id, align, type } = column;
|
|
const { line, saving, onChange, columns } = this.props;
|
|
const isFirstLine = Boolean(this.props.isFirstLine ?? (this.props.index === 0));
|
|
const isLastLine = Boolean(this.props.isLastLine);
|
|
|
|
if (id === 'pos') {
|
|
const posVal =
|
|
this.props.positionIndex ??
|
|
(this.props.index != null ? this.props.index + 1 : colIdx + 1);
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: isFirstLine ? 40 : 41,
|
|
minHeight: isFirstLine ? 40 : 41,
|
|
maxHeight: isFirstLine ? 40 : 41,
|
|
mt: isFirstLine ? 0 : '-1px',
|
|
fontWeight: 700,
|
|
fontSize: '0.82rem',
|
|
color: 'text.secondary',
|
|
userSelect: 'none',
|
|
}}
|
|
>
|
|
{posVal}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (id === 'shippable') {
|
|
const isShippable = line.shippable !== false;
|
|
const isFullyLedgered = this.isFullyLedgered();
|
|
const isDisabled = saving || isFullyLedgered;
|
|
|
|
const hasPosCol = columns?.[0]?.id === 'pos';
|
|
const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
|
|
const isLast = colIdx === totalCols - 1;
|
|
|
|
const topLeftRadius = isFirstInput && isFirstLine ? 4 : 0;
|
|
const topRightRadius = isLast && isFirstLine ? 4 : 0;
|
|
const bottomRightRadius = isLast && isLastLine ? 4 : 0;
|
|
const bottomLeftRadius = isFirstInput && isLastLine ? 4 : 0;
|
|
const borderRadius = `${topLeftRadius}px ${topRightRadius}px ${bottomRightRadius}px ${bottomLeftRadius}px`;
|
|
|
|
const { dirtyFields } = this.props;
|
|
const isFieldDirty = Boolean(dirtyFields && (dirtyFields.__all__ || dirtyFields[id]));
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: isFirstLine ? 40 : 41,
|
|
minHeight: isFirstLine ? 40 : 41,
|
|
maxHeight: isFirstLine ? 40 : 41,
|
|
boxSizing: 'border-box',
|
|
position: 'relative',
|
|
ml: isFirstInput ? 0 : '-1px',
|
|
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
|
|
mt: isFirstLine ? 0 : '-1px',
|
|
borderRadius,
|
|
border: '1px solid',
|
|
borderColor: isFieldDirty
|
|
? 'warning.main'
|
|
: isFullyLedgered
|
|
? LOCKED_CELL_BORDER
|
|
: 'rgba(0, 0, 0, 0.23)',
|
|
zIndex: 1,
|
|
cursor: isDisabled ? 'default' : 'pointer',
|
|
...(isFullyLedgered && !isFieldDirty && {
|
|
bgcolor: LOCKED_CELL_BG,
|
|
}),
|
|
...(isFieldDirty && {
|
|
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
|
zIndex: 2,
|
|
}),
|
|
'&:hover': isDisabled
|
|
? undefined
|
|
: {
|
|
borderColor: isFieldDirty ? 'warning.main' : 'rgba(0, 0, 0, 0.87)',
|
|
},
|
|
}}
|
|
onClick={isDisabled ? undefined : () => onChange(line.key, 'shippable', !isShippable)}
|
|
>
|
|
<Checkbox
|
|
size="small"
|
|
checked={isShippable}
|
|
disabled={isDisabled}
|
|
onChange={(e) => {
|
|
e.stopPropagation();
|
|
onChange(line.key, 'shippable', e.target.checked);
|
|
}}
|
|
slotProps={{
|
|
input: { 'aria-label': isShippable ? 'Lieferbar' : 'Nicht lieferbar' },
|
|
}}
|
|
sx={{ p: 0.5 }}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
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 topLeftRadius = isFirstInput && isFirstLine ? 4 : 0;
|
|
const topRightRadius = isLast && isFirstLine ? 4 : 0;
|
|
const bottomRightRadius = isLast && isLastLine ? 4 : 0;
|
|
const bottomLeftRadius = isFirstInput && isLastLine ? 4 : 0;
|
|
const borderRadius = `${topLeftRadius}px ${topRightRadius}px ${bottomRightRadius}px ${bottomLeftRadius}px`;
|
|
|
|
const { dirtyFields } = this.props;
|
|
const isFieldDirty = Boolean(
|
|
dirtyFields && (dirtyFields.__all__ || dirtyFields[id])
|
|
);
|
|
|
|
const isEditing = this.state.editingField === id;
|
|
const isFullyLedgered = this.isFullyLedgered();
|
|
const isDisabled = saving || isFullyLedgered || (id === 'sku' && Boolean(this.props.isOnShipment));
|
|
const titleText = isFullyLedgered
|
|
? 'Position ist bereits vollständig im Ledger verbucht und kann nicht geändert werden'
|
|
: id === 'sku' && this.props.isOnShipment
|
|
? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden'
|
|
: undefined;
|
|
|
|
if (isEditing) {
|
|
return (
|
|
<TextField
|
|
autoFocus
|
|
size="small"
|
|
type={type}
|
|
value={val ?? ''}
|
|
error={isError}
|
|
disabled={isDisabled}
|
|
title={titleText}
|
|
autoComplete="off"
|
|
onChange={(e) => onChange(line.key, id, e.target.value)}
|
|
onBlur={(e) => this.handleBlur(e, id)}
|
|
onKeyDown={this.handleKeyDown}
|
|
slotProps={{
|
|
htmlInput: {
|
|
inputMode: type === 'number' ? 'decimal' : 'text',
|
|
style: { textAlign: align, ...(id === 'sku' ? { fontFamily: 'monospace' } : {}) },
|
|
autoComplete: 'off',
|
|
step: type === 'number' ? 'any' : undefined,
|
|
},
|
|
}}
|
|
sx={{
|
|
position: 'relative',
|
|
ml: isFirstInput ? 0 : '-1px',
|
|
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
|
|
mt: isFirstLine ? 0 : '-1px',
|
|
height: isFirstLine ? 40 : 41,
|
|
zIndex: 3,
|
|
'& .MuiOutlinedInput-root': {
|
|
height: isFirstLine ? 40 : 41,
|
|
borderRadius,
|
|
width: '100%',
|
|
...(isFieldDirty && {
|
|
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
|
}),
|
|
'&:hover': {
|
|
zIndex: 4,
|
|
},
|
|
'&.Mui-focused': {
|
|
zIndex: 5,
|
|
},
|
|
},
|
|
'& .MuiOutlinedInput-notchedOutline': {
|
|
borderWidth: '1px !important',
|
|
borderColor: isFieldDirty
|
|
? 'warning.main'
|
|
: isError
|
|
? 'error.main'
|
|
: 'rgba(0, 0, 0, 0.23)',
|
|
},
|
|
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
|
borderWidth: '1px !important',
|
|
borderColor: isFieldDirty
|
|
? 'warning.main'
|
|
: isError
|
|
? 'error.main'
|
|
: 'primary.main',
|
|
},
|
|
'&:hover .MuiOutlinedInput-notchedOutline': {
|
|
borderWidth: '1px !important',
|
|
borderColor: isFieldDirty
|
|
? 'warning.main'
|
|
: isError
|
|
? 'error.main'
|
|
: 'rgba(0, 0, 0, 0.87)',
|
|
},
|
|
'& input[type=number]': {
|
|
MozAppearance: 'textfield',
|
|
},
|
|
'& input[type=number]::-webkit-outer-spin-button, & input[type=number]::-webkit-inner-spin-button':
|
|
{
|
|
WebkitAppearance: 'none',
|
|
margin: 0,
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const hasValue = val != null && String(val).trim() !== '';
|
|
|
|
return (
|
|
<Box
|
|
tabIndex={isDisabled ? -1 : 0}
|
|
role={isDisabled ? undefined : 'button'}
|
|
onClick={isDisabled ? undefined : () => this.handleStartEdit(id)}
|
|
onFocus={isDisabled ? undefined : () => this.handleStartEdit(id)}
|
|
onKeyDown={(e) => {
|
|
if (isDisabled) return;
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
this.handleStartEdit(id);
|
|
}
|
|
}}
|
|
title={titleText}
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: align === 'right' ? 'flex-end' : align === 'center' ? 'center' : 'flex-start',
|
|
height: isFirstLine ? 40 : 41,
|
|
minHeight: isFirstLine ? 40 : 41,
|
|
maxHeight: isFirstLine ? 40 : 41,
|
|
boxSizing: 'border-box',
|
|
position: 'relative',
|
|
ml: isFirstInput ? 0 : '-1px',
|
|
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
|
|
mt: isFirstLine ? 0 : '-1px',
|
|
px: '14px',
|
|
py: 0,
|
|
borderRadius,
|
|
border: '1px solid',
|
|
borderColor: isFieldDirty
|
|
? 'warning.main'
|
|
: isError
|
|
? 'error.main'
|
|
: isFullyLedgered
|
|
? LOCKED_CELL_BORDER
|
|
: 'rgba(0, 0, 0, 0.23)',
|
|
cursor: isDisabled ? 'default' : 'text',
|
|
touchAction: 'manipulation',
|
|
userSelect: 'none',
|
|
zIndex: 1,
|
|
...(isFullyLedgered && !isFieldDirty && !isError && {
|
|
bgcolor: LOCKED_CELL_BG,
|
|
}),
|
|
...(isFieldDirty && {
|
|
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
|
zIndex: 2,
|
|
}),
|
|
...(isError && !isFieldDirty && {
|
|
bgcolor: 'rgba(211, 47, 47, 0.04)',
|
|
borderColor: 'error.main',
|
|
zIndex: 2,
|
|
}),
|
|
'&:hover': isDisabled
|
|
? undefined
|
|
: {
|
|
bgcolor: isFieldDirty
|
|
? 'rgba(237, 108, 2, 0.14)'
|
|
: isError
|
|
? 'rgba(211, 47, 47, 0.08)'
|
|
: 'action.hover',
|
|
borderColor: isFieldDirty
|
|
? 'warning.main'
|
|
: isError
|
|
? 'error.main'
|
|
: 'rgba(0, 0, 0, 0.87)',
|
|
zIndex: 3,
|
|
},
|
|
'&:focus-visible': {
|
|
outline: '2px solid',
|
|
outlineColor: 'primary.main',
|
|
outlineOffset: -2,
|
|
zIndex: 4,
|
|
},
|
|
}}
|
|
>
|
|
<Typography
|
|
variant="body2"
|
|
sx={{
|
|
textAlign: align,
|
|
width: '100%',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
fontSize: '0.875rem',
|
|
lineHeight: '20px',
|
|
fontWeight: id === 'sku' ? 600 : 400,
|
|
fontFamily: id === 'sku' ? 'monospace' : 'inherit',
|
|
color: isError
|
|
? 'error.main'
|
|
: hasValue
|
|
? 'text.primary'
|
|
: 'text.disabled',
|
|
fontStyle: 'normal',
|
|
}}
|
|
>
|
|
{hasValue ? String(val) : '—'}
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
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 (
|
|
<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 != null
|
|
? Number(vkPrice.net)
|
|
: cachedArticle.net != null
|
|
? Number(cachedArticle.net)
|
|
: cachedArticle.price != null
|
|
? Number(cachedArticle.price)
|
|
: null;
|
|
const vkTaxRate =
|
|
vkPrice?.taxRate ??
|
|
vkPrice?.taxrate ??
|
|
cachedArticle.taxrate ??
|
|
cachedArticle.taxRate ??
|
|
selectCompanyTaxrate() ??
|
|
19;
|
|
const vkBrutto = vkPrice
|
|
? priceBrutto(vkPrice) ??
|
|
(vkNetto != null && vkTaxRate != null
|
|
? Math.round((vkNetto * (1 + Number(vkTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
|
: null)
|
|
: vkNetto != null
|
|
? Math.round((vkNetto * (1 + Number(vkTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
|
: 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 marge =
|
|
vkNetto > 0 && ekNetto != null
|
|
? ((vkNetto - ekNetto) / vkNetto) * 100
|
|
: vkBrutto > 0 && ekBrutto != null
|
|
? ((vkBrutto - ekBrutto) / vkBrutto) * 100
|
|
: null;
|
|
|
|
const rebate = parseNum(this.props.line?.rebate) ?? 0;
|
|
const effectiveNet =
|
|
vkNetto != null && Number.isFinite(vkNetto)
|
|
? vkNetto * (1 - rebate / 100)
|
|
: null;
|
|
const effectiveMarge =
|
|
effectiveNet != null && effectiveNet > 0 && ekNetto != null
|
|
? ((effectiveNet - ekNetto) / effectiveNet) * 100
|
|
: null;
|
|
|
|
const hasPriceInfo =
|
|
vkNetto != null || vkBrutto != null || ekNetto != null || ekBrutto != null || kind === 'quote' || kind === 'order';
|
|
|
|
const rawWarehouses = Array.isArray(cachedArticle.warehouses) ? cachedArticle.warehouses : [];
|
|
const isTracked = cachedArticle.track_stock ?? cachedArticle.trackStock ?? (rawWarehouses.length > 0);
|
|
const stockVal = isTracked === false
|
|
? null
|
|
: (cachedArticle.stock != null && !Number.isNaN(Number(cachedArticle.stock))
|
|
? Number(cachedArticle.stock)
|
|
: (rawWarehouses.length > 0 || isTracked
|
|
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
|
|
: null));
|
|
const lineAmount = parseNum(getFirstVal(this.props.line?.amount, this.props.line?.menge)) ?? 0;
|
|
|
|
const undeliveredOrders = Array.isArray(cachedArticle.undeliveredOrders)
|
|
? cachedArticle.undeliveredOrders
|
|
: [];
|
|
const currentDocId = this.props.docId != null
|
|
? Number(this.props.docId)
|
|
: (this.props.line?.manifestId != null ? Number(this.props.line.manifestId) : null);
|
|
const otherOrders = currentDocId != null && kind === 'order'
|
|
? undeliveredOrders.filter((ord) => Number(ord.orderId) !== currentDocId)
|
|
: undeliveredOrders;
|
|
const reservedInOtherOrders = otherOrders.reduce(
|
|
(sum, ord) => sum + (Number(ord.amount) || 0),
|
|
0,
|
|
);
|
|
const reservedTitle = isTracked !== false && reservedInOtherOrders > 0
|
|
? `Bereits in anderen Aufträgen reserviert:\n${otherOrders
|
|
.map((ord) => `• ${ord.orderNumber || (ord.orderId ? `Auftrag #${ord.orderId}` : 'Auftrag')}: ${formatStock(ord.amount)}${ord.customerName ? ` (${ord.customerName})` : ''}`)
|
|
.join('\n')}`
|
|
: undefined;
|
|
|
|
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,
|
|
fontFamily: 'monospace',
|
|
bgcolor: 'primary.50',
|
|
color: 'primary.dark',
|
|
border: '1px solid',
|
|
borderColor: 'primary.200',
|
|
cursor: 'pointer',
|
|
textDecoration: 'none',
|
|
'&:hover': {
|
|
bgcolor: 'primary.100',
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{this.props.onSearchArticle && (
|
|
<Button
|
|
size="small"
|
|
variant="text"
|
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
|
disabled={this.props.saving || this.props.supplierArticleBusy || this.isFullyLedgered()}
|
|
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 neu zuordnen
|
|
</Button>
|
|
)}
|
|
|
|
{/* Brand */}
|
|
{cachedArticle.brand?.name && (
|
|
<BrandLabel
|
|
brand={cachedArticle.brand}
|
|
size={16}
|
|
typographyProps={{
|
|
variant: 'caption',
|
|
color: 'text.secondary',
|
|
fontWeight: 500,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Article Name: omit if already editable in line 1 or if kind is order */}
|
|
{!hasNameColumn && !isOrder && 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: omit for order */}
|
|
{!isOrder && 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>
|
|
)}
|
|
|
|
{/* EK Netto, EK Brutto, VK Netto, VK Brutto */}
|
|
{hasPriceInfo && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1.5,
|
|
ml: 'auto',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{/* Bestand */}
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
title={reservedTitle}
|
|
sx={{
|
|
color:
|
|
stockVal == null
|
|
? 'text.disabled'
|
|
: stockVal <= 0
|
|
? 'error.main'
|
|
: lineAmount > (stockVal - reservedInOtherOrders)
|
|
? 'warning.main'
|
|
: 'text.secondary',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
Bestand: {isTracked === false ? '∞' : formatStock(stockVal)}
|
|
{isTracked !== false && reservedInOtherOrders > 0
|
|
? ` (${formatStock(reservedInOtherOrders)} reserviert)`
|
|
: ''}
|
|
</Typography>
|
|
|
|
{/* Marge */}
|
|
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
sx={{
|
|
color: marge != null ? (marge < 0 ? 'error.main' : 'text.secondary') : 'text.disabled',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
Marge: {marge != null ? `${Math.floor(marge)}%` : '—'}
|
|
</Typography>
|
|
|
|
|
|
{/* EK Netto */}
|
|
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
sx={{
|
|
color: ekNetto != null ? 'text.secondary' : 'text.disabled',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'}
|
|
</Typography>
|
|
|
|
|
|
{/* EK Brutto */}
|
|
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
sx={{
|
|
color: ekBrutto != null ? 'text.secondary' : 'text.disabled',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'}
|
|
</Typography>
|
|
|
|
|
|
{/* VK Netto */}
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
sx={{
|
|
color: vkNetto != null ? 'text.secondary' : 'text.disabled',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
VK Netto: {vkNetto != null ? formatPrice(vkNetto) : '—'}
|
|
</Typography>
|
|
|
|
{/* VK Brutto */}
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
sx={{
|
|
color: vkBrutto != null ? 'primary.dark' : 'text.disabled',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
VK Brutto: {vkBrutto != null ? 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 ArtNr „{rawSku}“ 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 { supplierArticle, loading } = this.props;
|
|
|
|
if (loading) {
|
|
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 (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;
|
|
|
|
const priceNetNum = Number(priceNet);
|
|
const vkNettoNum = Number(vkNetto);
|
|
const margeDelivery =
|
|
vkNettoNum > 0 && priceNet != null && Number.isFinite(priceNetNum)
|
|
? ((vkNettoNum - priceNetNum) / vkNettoNum) * 100
|
|
: null;
|
|
|
|
const rawWarehouses = Array.isArray(fullArticle.warehouses) ? fullArticle.warehouses : [];
|
|
const isDeliveryTracked = fullArticle.track_stock ?? fullArticle.trackStock ?? (rawWarehouses.length > 0);
|
|
const deliveryStockVal = isDeliveryTracked === false
|
|
? null
|
|
: (fullArticle.stock != null && !Number.isNaN(Number(fullArticle.stock))
|
|
? Number(fullArticle.stock)
|
|
: (rawWarehouses.length > 0 || isDeliveryTracked
|
|
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
|
|
: null));
|
|
|
|
const deliveryUndelivered = Array.isArray(fullArticle.undeliveredOrders)
|
|
? fullArticle.undeliveredOrders
|
|
: [];
|
|
const reservedInOtherOrdersDelivery = deliveryUndelivered.reduce(
|
|
(sum, ord) => sum + (Number(ord.amount) || 0),
|
|
0,
|
|
);
|
|
const reservedTitleDelivery = isDeliveryTracked !== false && reservedInOtherOrdersDelivery > 0
|
|
? `In Kundenaufträgen reserviert:\n${deliveryUndelivered
|
|
.map((ord) => `• ${ord.orderNumber || (ord.orderId ? `Auftrag #${ord.orderId}` : 'Auftrag')}: ${formatStock(ord.amount)}${ord.customerName ? ` (${ord.customerName})` : ''}`)
|
|
.join('\n')}`
|
|
: undefined;
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1.25,
|
|
py: 0.25,
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
{/* Unlink button at the front */}
|
|
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
|
|
<span>
|
|
<IconButton
|
|
size="small"
|
|
color="error"
|
|
aria-label="Lieferanten-Verknüpfung entfernen"
|
|
disabled={this.props.saving || this.props.supplierArticleBusy || this.isFullyLedgered()}
|
|
onClick={this.handleRemoveSupplierArticle}
|
|
sx={{ p: 0.25, mr: -0.25 }}
|
|
>
|
|
<LinkOffOutlinedIcon sx={{ fontSize: 16 }} />
|
|
</IconButton>
|
|
</span>
|
|
)}
|
|
|
|
{/* 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 */}
|
|
{article.sku && (
|
|
<Chip
|
|
component={RouterLink}
|
|
to={artikelDetailPath(article)}
|
|
clickable
|
|
size="small"
|
|
label={`Art.-Nr.: ${article.sku}`}
|
|
sx={{
|
|
height: 20,
|
|
fontSize: '0.7rem',
|
|
fontWeight: 600,
|
|
fontFamily: 'monospace',
|
|
bgcolor: 'primary.50',
|
|
color: 'primary.dark',
|
|
border: '1px solid',
|
|
borderColor: 'primary.200',
|
|
cursor: 'pointer',
|
|
textDecoration: 'none',
|
|
'&:hover': {
|
|
bgcolor: 'primary.100',
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{this.props.onSearchArticle && (
|
|
<Button
|
|
size="small"
|
|
variant="text"
|
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
|
disabled={this.props.saving || this.props.supplierArticleBusy || this.isFullyLedgered()}
|
|
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 neu zuordnen
|
|
</Button>
|
|
)}
|
|
|
|
{/* Brand */}
|
|
{(fullArticle.brand?.name || article.brand?.name) && (
|
|
<BrandLabel
|
|
brand={fullArticle.brand || article.brand}
|
|
size={16}
|
|
typographyProps={{
|
|
variant: 'caption',
|
|
color: 'text.secondary',
|
|
fontWeight: 500,
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Article Name */}
|
|
{(fullArticle.name || article.name) && (
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
noWrap
|
|
sx={{ color: 'text.primary', maxWidth: { xs: 200, sm: 300, md: 400 } }}
|
|
title={fullArticle.name || article.name}
|
|
>
|
|
{fullArticle.name || article.name}
|
|
</Typography>
|
|
)}
|
|
|
|
{/* Supplier SKU if distinct */}
|
|
{supplierArticle.sku && supplierArticle.sku !== rawSku && supplierArticle.sku !== article.sku && (
|
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
|
(Lief.-ArtNr: <strong style={{ fontFamily: 'monospace' }}>{supplierArticle.sku}</strong>)
|
|
</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>
|
|
)}
|
|
|
|
{/* Stored Purchase Price (Netto), VK Netto, VK Brutto, Marge & Bestand */}
|
|
{(priceNet != null || vkNetto != null || vkBrutto != null || deliveryStockVal != null) && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 1.5,
|
|
ml: 'auto',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{/* Bestand */}
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
title={reservedTitleDelivery}
|
|
sx={{
|
|
color:
|
|
deliveryStockVal == null
|
|
? 'text.disabled'
|
|
: deliveryStockVal <= 0
|
|
? 'error.main'
|
|
: 'text.secondary',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
Bestand: {isDeliveryTracked === false ? '∞' : formatStock(deliveryStockVal)}
|
|
{isDeliveryTracked !== false && reservedInOtherOrdersDelivery > 0
|
|
? ` (${formatStock(reservedInOtherOrdersDelivery)} reserviert)`
|
|
: ''}
|
|
</Typography>
|
|
|
|
{/* Marge */}
|
|
{margeDelivery != null && (
|
|
<Typography
|
|
variant="caption"
|
|
fontWeight={600}
|
|
sx={{
|
|
color: margeDelivery < 0 ? 'error.main' : 'text.secondary',
|
|
fontSize: '0.72rem',
|
|
}}
|
|
>
|
|
Marge: {Math.floor(margeDelivery)}%
|
|
</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>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
if (rawSku) {
|
|
return (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
|
|
{this.props.onCreateArticle && (
|
|
<Button
|
|
size="small"
|
|
variant="text"
|
|
startIcon={<AddIcon sx={{ fontSize: 14 }} />}
|
|
disabled={this.isFullyLedgered()}
|
|
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
|
|
</Button>
|
|
)}
|
|
{this.props.onSearchArticle && (
|
|
<Button
|
|
size="small"
|
|
variant="text"
|
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
|
disabled={this.isFullyLedgered()}
|
|
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' }}>
|
|
{this.props.kind === 'delivery'
|
|
? `Kein Lieferanten-Artikel für ArtNr „${rawSku}“ gefunden.`
|
|
: `Kein Artikel für ArtNr „${rawSku}“ 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 }} />}
|
|
disabled={this.isFullyLedgered()}
|
|
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;
|
|
}
|
|
|
|
render() {
|
|
const { columns = LINE_COLUMNS, saving, ledgerLine } = this.props;
|
|
const isFullyLedgered = this.isFullyLedgered();
|
|
const secondLine = this.renderSecondLine();
|
|
|
|
return (
|
|
<Fragment>
|
|
{/* Row 1: Editable Line Inputs */}
|
|
<TableRow
|
|
hover
|
|
sx={{
|
|
'& > td': { borderBottom: 'none', p: 0 },
|
|
...(isFullyLedgered && {
|
|
bgcolor: LOCKED_ROW_BG,
|
|
'&:hover': {
|
|
bgcolor: LOCKED_ROW_HOVER_BG,
|
|
},
|
|
}),
|
|
}}
|
|
>
|
|
{columns.map((col, colIdx) => (
|
|
<TableCell
|
|
key={col.id}
|
|
align={col.align}
|
|
sx={{
|
|
p: 0,
|
|
width: col.width,
|
|
minWidth: col.width,
|
|
height: 40,
|
|
verticalAlign: 'top',
|
|
position: 'relative',
|
|
overflow: 'visible',
|
|
}}
|
|
>
|
|
{this.renderField(col, colIdx, columns.length)}
|
|
</TableCell>
|
|
))}
|
|
<TableCell
|
|
padding="none"
|
|
sx={{
|
|
py: 0.25,
|
|
pl: 0.5,
|
|
pr: 0.5,
|
|
verticalAlign: 'middle',
|
|
width: (this.props.onMove ? 80 : 36) + (this.props.kind === 'delivery' && Boolean(this.props.onBookLine) ? 36 : 0),
|
|
whiteSpace: 'nowrap',
|
|
...(isFullyLedgered && {
|
|
bgcolor: LOCKED_ROW_BG,
|
|
}),
|
|
}}
|
|
>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
|
{this.props.kind === 'delivery' && Boolean(this.props.onBookLine) && (
|
|
<span>
|
|
<IconButton
|
|
size="small"
|
|
aria-label="Position verbuchen"
|
|
disabled={saving || !ledgerLine}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (ledgerLine) {
|
|
this.props.onBookLine(ledgerLine);
|
|
}
|
|
}}
|
|
color={
|
|
!ledgerLine
|
|
? 'default'
|
|
: ledgerLine.isBooked
|
|
? 'success'
|
|
: ledgerLine.bookedAmount > 0
|
|
? 'warning'
|
|
: 'primary'
|
|
}
|
|
sx={{ p: 0.35 }}
|
|
>
|
|
{ledgerLine?.isBooked ? (
|
|
<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 18 }} />
|
|
) : (
|
|
<Inventory2OutlinedIcon sx={{ fontSize: 18 }} />
|
|
)}
|
|
</IconButton>
|
|
</span>
|
|
)}
|
|
{this.props.onMove && (
|
|
<>
|
|
<span>
|
|
<IconButton
|
|
size="small"
|
|
aria-label="Nach oben verschieben"
|
|
disabled={saving || this.props.isFirstLine}
|
|
onClick={this.handleMoveUp}
|
|
sx={{ p: 0.35 }}
|
|
>
|
|
<ArrowUpwardIcon sx={{ fontSize: 16 }} />
|
|
</IconButton>
|
|
</span>
|
|
<span>
|
|
<IconButton
|
|
size="small"
|
|
aria-label="Nach unten verschieben"
|
|
disabled={saving || this.props.isLastLine}
|
|
onClick={this.handleMoveDown}
|
|
sx={{ p: 0.35 }}
|
|
>
|
|
<ArrowDownwardIcon sx={{ fontSize: 16 }} />
|
|
</IconButton>
|
|
</span>
|
|
</>
|
|
)}
|
|
<IconButton
|
|
size="small"
|
|
aria-label="Zeile entfernen"
|
|
disabled={saving || Boolean(this.props.isOnShipment) || isFullyLedgered}
|
|
onClick={this.handleRemove}
|
|
sx={{ p: 0.35 }}
|
|
>
|
|
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
|
|
</IconButton>
|
|
</Box>
|
|
</TableCell>
|
|
</TableRow>
|
|
|
|
{/* Row 2: Matched Supplier Article Details */}
|
|
{secondLine && (
|
|
<TableRow
|
|
sx={{
|
|
'& > td': { pt: 0.25, pb: 0.75, borderBottom: 'none' },
|
|
...(isFullyLedgered && {
|
|
bgcolor: LOCKED_ROW_BG,
|
|
}),
|
|
}}
|
|
>
|
|
<TableCell
|
|
colSpan={columns.length + 1}
|
|
sx={{
|
|
py: 0.4,
|
|
px: 1,
|
|
bgcolor: 'transparent',
|
|
}}
|
|
>
|
|
{secondLine}
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default ManifestFormLine;
|