286 lines
12 KiB
JavaScript
286 lines
12 KiB
JavaScript
import { PureComponent } from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import Button from '@mui/material/Button';
|
|
import Stack from '@mui/material/Stack';
|
|
import Table from '@mui/material/Table';
|
|
import TableBody from '@mui/material/TableBody';
|
|
import TableCell from '@mui/material/TableCell';
|
|
import TableContainer from '@mui/material/TableContainer';
|
|
import TableHead from '@mui/material/TableHead';
|
|
import TableRow from '@mui/material/TableRow';
|
|
import Typography from '@mui/material/Typography';
|
|
import AddIcon from '@mui/icons-material/Add';
|
|
import SearchIcon from '@mui/icons-material/Search';
|
|
import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined';
|
|
import { PanelTile } from 'dashgui/panels';
|
|
import { LINE_COLUMNS, hasArticleLines, parseNum, getFirstVal } from './manifestConfig';
|
|
import { formatPrice } from '../../utils/price';
|
|
import ManifestFormLine from './ManifestFormLine';
|
|
|
|
class ManifestLines extends PureComponent {
|
|
render() {
|
|
const {
|
|
kind = 'delivery',
|
|
lines,
|
|
saving,
|
|
partyId,
|
|
docId,
|
|
linesColor,
|
|
dirtyLineFields = {},
|
|
shipments,
|
|
onLineChange,
|
|
onAddLine,
|
|
onRemoveLine,
|
|
onMoveLine,
|
|
onRemoveAllLines,
|
|
columns = LINE_COLUMNS,
|
|
supplierArticlesMap = {},
|
|
supplierArticlesByLineKey = {},
|
|
supplierArticlesLoading = false,
|
|
loadingLineKeys = {},
|
|
onSkuBlur,
|
|
onCreateArticle,
|
|
onSearchArticle,
|
|
onRemoveSupplierArticle,
|
|
busySupplierArticleLineKeys = {},
|
|
wide = 4,
|
|
ledgerLinesById = {},
|
|
onBookLine,
|
|
} = this.props;
|
|
|
|
const hasArticles = hasArticleLines(lines);
|
|
|
|
let totalNet = 0;
|
|
let totalGross = 0;
|
|
if (Array.isArray(lines)) {
|
|
for (const line of lines) {
|
|
const amount = parseNum(getFirstVal(line.amount, line.count)) ?? 0;
|
|
const net = parseNum(getFirstVal(line.net, line.netprice, line.netPrice)) ?? 0;
|
|
const tax = parseNum(getFirstVal(line.tax, line.taxrate, line.taxRate)) ?? 0;
|
|
const rebate = parseNum(getFirstVal(line.rebate)) ?? 0;
|
|
const lineNet = amount * net * (1 - rebate / 100);
|
|
const lineGross = lineNet * (1 + tax / 100);
|
|
totalNet += lineNet;
|
|
totalGross += lineGross;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<PanelTile
|
|
wide={wide}
|
|
minHeight={280}
|
|
color={linesColor}
|
|
sx={{
|
|
...(wide >= 4 ? { gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } } : {}),
|
|
...(!hasArticles
|
|
? {
|
|
border: '1.5px solid',
|
|
borderColor: 'error.main',
|
|
}
|
|
: {}),
|
|
}}
|
|
>
|
|
<Stack spacing={1.5}>
|
|
<TableContainer>
|
|
<Table size="small" stickyHeader>
|
|
<TableHead>
|
|
<TableRow sx={{ '& th': { borderBottom: 'none', fontWeight: 700 } }}>
|
|
{columns.map((col) => (
|
|
<TableCell
|
|
key={col.id}
|
|
align={col.align}
|
|
sx={{
|
|
width: col.width,
|
|
whiteSpace: 'nowrap',
|
|
bgcolor: linesColor,
|
|
fontWeight: 700,
|
|
px: 1,
|
|
py: 0.75,
|
|
}}
|
|
>
|
|
{col.label}
|
|
</TableCell>
|
|
))}
|
|
<TableCell padding="none" sx={{ bgcolor: linesColor, borderBottom: 'none', width: onMoveLine ? 92 : 40 }} />
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{(() => {
|
|
const shippedSkus = new Set();
|
|
const shippedOrderLineIds = new Set();
|
|
const shippedNames = new Set();
|
|
if (kind === 'order' && Array.isArray(shipments)) {
|
|
for (const sh of shipments) {
|
|
for (const sl of sh.lines || []) {
|
|
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
|
|
if (sl.orderLineId != null) shippedOrderLineIds.add(sl.orderLineId);
|
|
if (!sl.sku && sl.name) shippedNames.add(String(sl.name).trim().toLowerCase());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!lines || lines.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return lines.map((line, index) => {
|
|
const lineKey = line.key ?? index;
|
|
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
|
const lineName = line.name != null ? String(line.name).trim() : '';
|
|
const isOnShipment = kind === 'order' && Boolean(
|
|
(rawSku && shippedSkus.has(rawSku.toLowerCase())) ||
|
|
(line.id != null && shippedOrderLineIds.has(line.id)) ||
|
|
(!rawSku && lineName && shippedNames.has(lineName.toLowerCase()))
|
|
);
|
|
const isDelivery = kind === 'delivery';
|
|
const matched = isDelivery
|
|
? supplierArticlesByLineKey[lineKey] ||
|
|
(rawSku ? supplierArticlesMap[rawSku.toLowerCase()] || supplierArticlesMap[rawSku] : null)
|
|
: null;
|
|
const isLineLoading = isDelivery
|
|
? loadingLineKeys[lineKey] ||
|
|
(supplierArticlesLoading && !matched)
|
|
: false;
|
|
const ledgerLine = line.id != null ? ledgerLinesById[line.id] : null;
|
|
const lineDirtyFields =
|
|
dirtyLineFields[lineKey] ||
|
|
(line.key != null ? dirtyLineFields[line.key] : null) ||
|
|
(line.id != null ? dirtyLineFields[`id-${line.id}`] : null) ||
|
|
dirtyLineFields[`idx-${index}`] ||
|
|
null;
|
|
|
|
return (
|
|
<ManifestFormLine
|
|
key={lineKey}
|
|
index={index}
|
|
positionIndex={index + 1}
|
|
kind={kind}
|
|
docId={docId}
|
|
line={line.key != null ? line : { ...line, key: index }}
|
|
columns={columns}
|
|
saving={saving}
|
|
dirtyFields={lineDirtyFields}
|
|
supplierArticle={matched}
|
|
loading={Boolean(isLineLoading)}
|
|
supplierArticleBusy={Boolean(busySupplierArticleLineKeys[lineKey])}
|
|
isOnShipment={isOnShipment}
|
|
onChange={onLineChange}
|
|
onRemove={onRemoveLine}
|
|
onMove={onMoveLine}
|
|
isFirstLine={index === 0}
|
|
isLastLine={index === lines.length - 1}
|
|
onSkuBlur={onSkuBlur}
|
|
onCreateArticle={onCreateArticle}
|
|
onSearchArticle={onSearchArticle}
|
|
onRemoveSupplierArticle={onRemoveSupplierArticle}
|
|
ledgerLine={ledgerLine}
|
|
onBookLine={onBookLine}
|
|
/>
|
|
);
|
|
});
|
|
})()}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', flexWrap: 'wrap' }}>
|
|
<Button
|
|
size="small"
|
|
startIcon={<AddIcon />}
|
|
disabled={saving}
|
|
onClick={onAddLine}
|
|
>
|
|
Zeile
|
|
</Button>
|
|
{onSearchArticle && (() => {
|
|
const searchArticleDisabled = saving || (kind === 'delivery' && !partyId);
|
|
const searchArticleTitle =
|
|
kind === 'delivery' && !partyId
|
|
? 'Bitte zuerst einen Lieferanten auswählen'
|
|
: undefined;
|
|
return (
|
|
<Button
|
|
size="small"
|
|
startIcon={<SearchIcon />}
|
|
disabled={searchArticleDisabled}
|
|
onClick={() => onSearchArticle(null)}
|
|
title={searchArticleTitle}
|
|
>
|
|
Artikel suchen
|
|
</Button>
|
|
);
|
|
})()}
|
|
{(() => {
|
|
const shippedSkus = new Set();
|
|
if (kind === 'order' && Array.isArray(shipments)) {
|
|
for (const sh of shipments) {
|
|
for (const sl of sh.lines || []) {
|
|
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
|
|
}
|
|
}
|
|
}
|
|
const allLinesShipped =
|
|
kind === 'order' &&
|
|
shippedSkus.size > 0 &&
|
|
Array.isArray(lines) &&
|
|
lines.length > 0 &&
|
|
lines.every((l) => {
|
|
const raw = l?.sku != null ? String(l.sku).trim().toLowerCase() : '';
|
|
return raw && shippedSkus.has(raw);
|
|
});
|
|
const allLinesLedgered =
|
|
kind === 'delivery' &&
|
|
Array.isArray(lines) &&
|
|
lines.length > 0 &&
|
|
lines.every((line) => {
|
|
const ledgerLine = line?.id != null ? ledgerLinesById?.[line.id] : null;
|
|
return Boolean(
|
|
ledgerLine?.isBooked ||
|
|
(ledgerLine && ledgerLine.targetAmount > 0 && ledgerLine.remainingAmount <= 0) ||
|
|
(ledgerLine && ledgerLine.bookedAmount > 0) ||
|
|
line?.isBooked ||
|
|
line?.is_booked
|
|
);
|
|
});
|
|
const disabled = saving || !lines || lines.length === 0 || allLinesShipped || allLinesLedgered;
|
|
const title = allLinesLedgered
|
|
? 'Alle Positionen der Lieferung sind bereits im Ledger verbucht'
|
|
: allLinesShipped
|
|
? 'Alle Positionen des Auftrags sind bereits ausgeliefert'
|
|
: 'Alle Zeilen entfernen';
|
|
return (
|
|
<Button
|
|
size="small"
|
|
startIcon={<DeleteSweepOutlinedIcon />}
|
|
disabled={disabled}
|
|
onClick={onRemoveAllLines}
|
|
title={title}
|
|
>
|
|
Alle Zeilen entfernen
|
|
</Button>
|
|
);
|
|
})()}
|
|
{(totalNet > 0 || totalGross > 0) && (
|
|
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 2, pr: 0.5 }}>
|
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 600, fontSize: '0.8rem' }}>
|
|
Netto:{' '}
|
|
<Box component="span" sx={{ color: 'text.primary', fontWeight: 700 }}>
|
|
{formatPrice(totalNet)}
|
|
</Box>
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: 'primary.dark', fontWeight: 700, fontSize: '0.85rem' }}>
|
|
Brutto:{' '}
|
|
<Box component="span" sx={{ color: 'primary.dark', fontWeight: 800 }}>
|
|
{formatPrice(totalGross)}
|
|
</Box>
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Stack>
|
|
</PanelTile>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default ManifestLines;
|