Files
dashfront/src/components/manifest/ManifestLines.jsx
2026-10-27 00:28:06 +01:00

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;