feat(manifest): improve PDF extraction workflow and fix delivery lines rendering
- Fix SSE 'result' event handling in delivery creation and stop circular progress - Directly populate deliveryEditorSlice with parsed header, line items, and notes - Simplify ManifestPdfDialog to display LLM protocol log and status only - Render PDF analysis notes at the bottom of the delivery page - Keep article lines visible when imported items exist even if partyId is not yet selected - Bind handleEdit and handleDelete in ManifestList constructor to fix row click TypeError - Support delivery columns (name, pu, netprice, taxrate) in ManifestForm and lineFromApi - Remove obsolete Unit column from delivery lines
This commit is contained in:
@@ -176,7 +176,16 @@ class ManifestForm extends PureComponent {
|
|||||||
handleLineChange(key, field, value) {
|
handleLineChange(key, field, value) {
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
lines: prev.lines.map((line) =>
|
lines: prev.lines.map((line) =>
|
||||||
line.key === key ? { ...line, [field]: value } : line,
|
line.key === key
|
||||||
|
? {
|
||||||
|
...line,
|
||||||
|
[field]: value,
|
||||||
|
...(field === 'net' ? { netprice: value } : {}),
|
||||||
|
...(field === 'netprice' ? { net: value } : {}),
|
||||||
|
...(field === 'tax' ? { taxrate: value } : {}),
|
||||||
|
...(field === 'taxrate' ? { tax: value } : {}),
|
||||||
|
}
|
||||||
|
: line,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -260,7 +269,14 @@ class ManifestForm extends PureComponent {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
} finally {
|
} finally {
|
||||||
if (!this._cancelled) this.setState({ pdfUploading: false });
|
if (!this._cancelled) {
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfUploading: false,
|
||||||
|
pdfDialogData: prev.pdfDialogData
|
||||||
|
? { ...prev.pdfDialogData, streaming: false }
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,6 +374,7 @@ class ManifestForm extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ManifestLines
|
<ManifestLines
|
||||||
|
columns={cfg.columns}
|
||||||
lines={lines}
|
lines={lines}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
linesColor={cfg.linesColor}
|
linesColor={cfg.linesColor}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
fullWidth
|
fullWidth
|
||||||
type={type}
|
type={type}
|
||||||
value={line[id]}
|
value={line[id] ?? ''}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onChange={(e) => onChange(line.key, id, e.target.value)}
|
onChange={(e) => onChange(line.key, id, e.target.value)}
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ class ManifestLines extends PureComponent {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{lines.map((line) => (
|
{lines.map((line, index) => (
|
||||||
<ManifestFormLine
|
<ManifestFormLine
|
||||||
key={line.key}
|
key={line.key ?? index}
|
||||||
line={line}
|
line={line.key != null ? line : { ...line, key: index }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onChange={onLineChange}
|
onChange={onLineChange}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class ManifestList extends PureComponent {
|
|||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
this.handleCreate = this.handleCreate.bind(this);
|
this.handleCreate = this.handleCreate.bind(this);
|
||||||
|
this.handleEdit = this.handleEdit.bind(this);
|
||||||
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
|||||||
@@ -8,14 +8,7 @@ import Dialog from '@mui/material/Dialog';
|
|||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import Divider from '@mui/material/Divider';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
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 TableHead from '@mui/material/TableHead';
|
|
||||||
import TableRow from '@mui/material/TableRow';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
@@ -64,7 +57,7 @@ function EventLog({ messages, streaming }) {
|
|||||||
<Collapse in={expanded}>
|
<Collapse in={expanded}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
maxHeight: 180,
|
maxHeight: 280,
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
border: 1,
|
border: 1,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
@@ -98,73 +91,6 @@ function EventLog({ messages, streaming }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BaseDataSection({ basedata }) {
|
|
||||||
if (!basedata) return null;
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ display: 'block', mb: 0.5 }}>
|
|
||||||
Kopfdaten
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" flexWrap="wrap" gap={1}>
|
|
||||||
{basedata.date && <Chip size="small" label={`Datum: ${basedata.date}`} />}
|
|
||||||
{basedata.manifestNr && <Chip size="small" label={`Nr: ${basedata.manifestNr}`} />}
|
|
||||||
{basedata.manifestType && <Chip size="small" label={`Typ: ${basedata.manifestType}`} />}
|
|
||||||
{basedata.lieferantName && <Chip size="small" label={`Lieferant: ${basedata.lieferantName}`} />}
|
|
||||||
{basedata.kLieferant != null && <Chip size="small" label={`kLieferant: ${basedata.kLieferant}`} />}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemsSection({ items }) {
|
|
||||||
if (!items?.items?.length) return null;
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ display: 'block', mb: 0.5 }}>
|
|
||||||
Positionen ({items.items.length})
|
|
||||||
{items.pagesFrom != null && (
|
|
||||||
<span style={{ fontWeight: 400, marginLeft: 4 }}>
|
|
||||||
— Seiten {items.pagesFrom}–{items.pagesTo}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
<Box sx={{ overflowX: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
|
||||||
<Table size="small" padding="none" sx={{ minWidth: 520 }}>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow sx={{ bgcolor: 'action.hover' }}>
|
|
||||||
{['Pos.', 'Artikel', 'LiefArtNr', 'Menge', 'VPE', 'Einheit', 'EK-Netto', 'MwSt'].map((h) => (
|
|
||||||
<TableCell key={h} sx={{ px: 1, py: 0.5, fontSize: '0.72rem', fontWeight: 700, whiteSpace: 'nowrap' }}>
|
|
||||||
{h}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{items.items.map((item, i) => (
|
|
||||||
<TableRow key={i} hover>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem' }}>{item.position ?? i + 1}</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', maxWidth: 180, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{item.articleName ?? '–'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', fontFamily: 'monospace' }}>{item.liefArtNr ?? '–'}</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right' }}>{item.count}</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right' }}>{item.vpe ?? 1}</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem' }}>{item.unit ?? '–'}</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right', fontFamily: 'monospace' }}>
|
|
||||||
{item.eknetto != null ? item.eknetto.toFixed(2) : '–'}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right' }}>
|
|
||||||
{item.mwst != null ? `${item.mwst} %` : '–'}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
@@ -175,7 +101,7 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={open}
|
open={open}
|
||||||
onClose={streaming ? undefined : onClose}
|
onClose={streaming ? undefined : onClose}
|
||||||
maxWidth="md"
|
maxWidth="sm"
|
||||||
fullWidth
|
fullWidth
|
||||||
aria-labelledby="pdf-dialog-title"
|
aria-labelledby="pdf-dialog-title"
|
||||||
>
|
>
|
||||||
@@ -200,7 +126,7 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
|||||||
)}
|
)}
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Typography variant="subtitle1" component="div" fontWeight={600}>
|
<Typography variant="subtitle1" component="div" fontWeight={600}>
|
||||||
{streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyseergebnis'}
|
{streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyse abgeschlossen'}
|
||||||
</Typography>
|
</Typography>
|
||||||
{filename ? (
|
{filename ? (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
@@ -220,47 +146,22 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogContent dividers sx={{ p: 2 }}>
|
<DialogContent dividers sx={{ p: 2 }}>
|
||||||
<Stack spacing={2}>
|
{/* Error */}
|
||||||
{/* Error */}
|
{error && (
|
||||||
{error && (
|
<Alert severity="error" icon={<ErrorOutlinedIcon />} sx={{ mb: 2 }}>
|
||||||
<Alert severity="error" icon={<ErrorOutlinedIcon />}>
|
<Typography variant="body2" fontWeight={600}>{error}</Typography>
|
||||||
<Typography variant="body2" fontWeight={600}>{error}</Typography>
|
</Alert>
|
||||||
</Alert>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Live log */}
|
{/* Live log */}
|
||||||
<EventLog messages={messages} streaming={streaming} />
|
<EventLog messages={messages} streaming={streaming} />
|
||||||
|
|
||||||
{/* Notes */}
|
{/* Success notification */}
|
||||||
{result?.notes?.length > 0 && (
|
{hasResult && !streaming && !error && (
|
||||||
<Box>
|
<Alert severity="success" sx={{ mt: 2 }}>
|
||||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ display: 'block', mb: 0.5 }}>
|
Daten erfolgreich in das Formular übernommen.
|
||||||
Hinweise
|
</Alert>
|
||||||
</Typography>
|
)}
|
||||||
<Stack spacing={0.5}>
|
|
||||||
{result.notes.map((note, i) => (
|
|
||||||
<Alert key={i} severity="warning" sx={{ py: 0, '& .MuiAlert-message': { fontSize: '0.8rem' } }}>
|
|
||||||
{note}
|
|
||||||
</Alert>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Basedata + Items */}
|
|
||||||
{hasResult && (
|
|
||||||
<>
|
|
||||||
<Divider />
|
|
||||||
<BaseDataSection basedata={result.basedata} />
|
|
||||||
<ItemsSection items={result.items} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* No result yet and no error: show waiting state */}
|
|
||||||
{!hasResult && !error && !streaming && (
|
|
||||||
<Alert severity="info">Keine Daten extrahiert.</Alert>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions sx={{ p: 1.5, px: 2 }}>
|
<DialogActions sx={{ p: 1.5, px: 2 }}>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export { withManifestId } from './withManifestId';
|
|||||||
export {
|
export {
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
LINE_COLUMNS,
|
LINE_COLUMNS,
|
||||||
|
DELIVERY_LINE_COLUMNS,
|
||||||
emptyLine,
|
emptyLine,
|
||||||
lineFromApi,
|
lineFromApi,
|
||||||
buildManifestBody,
|
buildManifestBody,
|
||||||
|
|||||||
@@ -69,17 +69,27 @@ export function emptyLine() {
|
|||||||
tax: '',
|
tax: '',
|
||||||
rebate: '',
|
rebate: '',
|
||||||
net: '',
|
net: '',
|
||||||
|
name: '',
|
||||||
|
pu: '',
|
||||||
|
unit: '',
|
||||||
|
netprice: '',
|
||||||
|
taxrate: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lineFromApi(row) {
|
export function lineFromApi(row) {
|
||||||
return {
|
return {
|
||||||
key: `line-${row.id}`,
|
key: `line-${row.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`}`,
|
||||||
amount: row.amount == null ? '' : String(row.amount),
|
amount: row.amount == null ? '' : String(row.amount),
|
||||||
sku: row.sku ?? '',
|
sku: row.sku ?? '',
|
||||||
tax: row.tax == null ? '' : String(row.tax),
|
tax: row.tax == null ? '' : String(row.tax),
|
||||||
rebate: row.rebate == null ? '' : String(row.rebate),
|
rebate: row.rebate == null ? '' : String(row.rebate),
|
||||||
net: row.net == null ? '' : String(row.net),
|
net: row.net == null ? '' : String(row.net),
|
||||||
|
name: row.name ?? '',
|
||||||
|
pu: row.pu == null ? '' : String(row.pu),
|
||||||
|
unit: row.unit ?? '',
|
||||||
|
netprice: row.netprice != null ? String(row.netprice) : row.net != null ? String(row.net) : '',
|
||||||
|
taxrate: row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,8 +105,8 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const sku = String(line.sku ?? '').trim();
|
const sku = String(line.sku ?? '').trim();
|
||||||
const amount = parseNum(line.amount);
|
const amount = parseNum(line.amount);
|
||||||
const net = parseNum(line.net);
|
const net = parseNum(line.net ?? line.netprice);
|
||||||
const tax = parseNum(line.tax);
|
const tax = parseNum(line.tax ?? line.taxrate);
|
||||||
const rebate = parseNum(line.rebate);
|
const rebate = parseNum(line.rebate);
|
||||||
const blank =
|
const blank =
|
||||||
!sku && amount == null && net == null && tax == null && rebate == null;
|
!sku && amount == null && net == null && tax == null && rebate == null;
|
||||||
@@ -106,6 +116,9 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
|||||||
}
|
}
|
||||||
const row = { sku, amount, net, tax };
|
const row = { sku, amount, net, tax };
|
||||||
if (rebate != null) row.rebate = rebate;
|
if (rebate != null) row.rebate = rebate;
|
||||||
|
if (line.name) row.name = line.name;
|
||||||
|
if (line.pu != null) row.pu = line.pu;
|
||||||
|
if (line.unit) row.unit = line.unit;
|
||||||
outLines.push(row);
|
outLines.push(row);
|
||||||
}
|
}
|
||||||
if (requireAtLeastOne && outLines.length === 0) {
|
if (requireAtLeastOne && outLines.length === 0) {
|
||||||
@@ -157,6 +170,15 @@ export const LINE_COLUMNS = [
|
|||||||
{ id: 'net', label: 'Net', width: 104, align: 'right', type: 'number' },
|
{ id: 'net', label: 'Net', width: 104, align: 'right', type: 'number' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const DELIVERY_LINE_COLUMNS = [
|
||||||
|
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
|
||||||
|
{ id: 'name', label: 'Name', width: 220, align: 'left', type: 'text' },
|
||||||
|
{ id: 'amount', label: 'Amount', width: 88, align: 'right', type: 'number' },
|
||||||
|
{ id: 'pu', label: 'PU', width: 80, align: 'right', type: 'number' },
|
||||||
|
{ id: 'taxrate', label: 'Tax', width: 80, align: 'right', type: 'number' },
|
||||||
|
{ id: 'netprice', label: 'Net', width: 104, align: 'right', type: 'number' },
|
||||||
|
];
|
||||||
|
|
||||||
/** @typedef {'delivery'|'quote'|'order'} ManifestKind */
|
/** @typedef {'delivery'|'quote'|'order'} ManifestKind */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -220,6 +242,7 @@ export function manifestConfig(kind) {
|
|||||||
// Delivery uses a server-allocated read-only number plus an optional
|
// Delivery uses a server-allocated read-only number plus an optional
|
||||||
// caller-supplied external reference.
|
// caller-supplied external reference.
|
||||||
externalNumber: true,
|
externalNumber: true,
|
||||||
|
columns: DELIVERY_LINE_COLUMNS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (kind === 'quote') {
|
if (kind === 'quote') {
|
||||||
@@ -253,6 +276,7 @@ export function manifestConfig(kind) {
|
|||||||
listPromise: (ctx) => ctx.quotes ?? ctx.quotePromise,
|
listPromise: (ctx) => ctx.quotes ?? ctx.quotePromise,
|
||||||
partiesByIdFromList: (parties) =>
|
partiesByIdFromList: (parties) =>
|
||||||
Object.fromEntries(parties.map((row) => [row.id, row])),
|
Object.fromEntries(parties.map((row) => [row.id, row])),
|
||||||
|
columns: LINE_COLUMNS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (kind === 'order') {
|
if (kind === 'order') {
|
||||||
@@ -286,6 +310,7 @@ export function manifestConfig(kind) {
|
|||||||
listPromise: (ctx) => ctx.orders ?? ctx.orderPromise,
|
listPromise: (ctx) => ctx.orders ?? ctx.orderPromise,
|
||||||
partiesByIdFromList: (parties) =>
|
partiesByIdFromList: (parties) =>
|
||||||
Object.fromEntries(parties.map((row) => [row.id, row])),
|
Object.fromEntries(parties.map((row) => [row.id, row])),
|
||||||
|
columns: LINE_COLUMNS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(`Unknown manifest kind: ${kind}`);
|
throw new Error(`Unknown manifest kind: ${kind}`);
|
||||||
|
|||||||
@@ -1,10 +1,472 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { ManifestForm } from '../../components/manifest';
|
import { connect } from 'react-redux';
|
||||||
|
import Alert from '@mui/material/Alert';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
|
import BackendError from '../../components/BackendError';
|
||||||
|
import withNavigate from '../../components/withNavigate';
|
||||||
|
import {
|
||||||
|
ManifestHeader,
|
||||||
|
ManifestLines,
|
||||||
|
DELIVERY_LINE_COLUMNS,
|
||||||
|
buildManifestBody,
|
||||||
|
manifestConfig,
|
||||||
|
} from '../../components/manifest';
|
||||||
|
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
|
||||||
|
import { loadParties } from '../../components/manifest/manifestConfig';
|
||||||
|
import { StorageManagerContext } from '../../api/storageManagerContext';
|
||||||
|
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
import {
|
||||||
|
setDeliveryEntities,
|
||||||
|
setDate,
|
||||||
|
setDeliveryEntity,
|
||||||
|
setNumber,
|
||||||
|
setExternalNumber,
|
||||||
|
setType,
|
||||||
|
setLineitems,
|
||||||
|
setNotes,
|
||||||
|
addLineitem,
|
||||||
|
updateLineitem,
|
||||||
|
removeLineitem,
|
||||||
|
resetDelivery,
|
||||||
|
selectDeliveryEntities,
|
||||||
|
selectDeliveryDate,
|
||||||
|
selectDeliveryEntity,
|
||||||
|
selectDeliveryNumber,
|
||||||
|
selectDeliveryExternalNumber,
|
||||||
|
selectDeliveryLineitems,
|
||||||
|
selectDeliveryNotes,
|
||||||
|
} from '../../store/deliveryEditorSlice';
|
||||||
|
|
||||||
class LieferungNeuPage extends PureComponent {
|
class LieferungNeuPage extends PureComponent {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.cfg = manifestConfig('delivery');
|
||||||
|
this.state = {
|
||||||
|
status: 'loadingParties',
|
||||||
|
error: null,
|
||||||
|
partiesLoading: true,
|
||||||
|
saving: false,
|
||||||
|
pdfUploading: false,
|
||||||
|
pdfDialogOpen: false,
|
||||||
|
pdfDialogData: null,
|
||||||
|
pdfFileName: '',
|
||||||
|
};
|
||||||
|
this._cancelled = false;
|
||||||
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
|
this.handlePartyChange = this.handlePartyChange.bind(this);
|
||||||
|
this.handleDateChange = this.handleDateChange.bind(this);
|
||||||
|
this.handleNumberChange = this.handleNumberChange.bind(this);
|
||||||
|
this.handleExternalNumberChange = this.handleExternalNumberChange.bind(this);
|
||||||
|
this.handleLineChange = this.handleLineChange.bind(this);
|
||||||
|
this.handleAddLine = this.handleAddLine.bind(this);
|
||||||
|
this.handleRemoveLine = this.handleRemoveLine.bind(this);
|
||||||
|
this.handleSave = this.handleSave.bind(this);
|
||||||
|
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
||||||
|
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._cancelled = false;
|
||||||
|
void this.loadPartiesOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._cancelled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPartiesOnly() {
|
||||||
|
this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
|
||||||
|
try {
|
||||||
|
const parties = await loadParties('delivery', this.context);
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.props.setDeliveryEntities(parties);
|
||||||
|
this.setState({
|
||||||
|
status: 'ready',
|
||||||
|
partiesLoading: false,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({
|
||||||
|
status: 'failed',
|
||||||
|
error: backendErrorFrom(err),
|
||||||
|
partiesLoading: false,
|
||||||
|
});
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRetry() {
|
||||||
|
void this.loadPartiesOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePartyChange(partyId) {
|
||||||
|
this.props.setDeliveryEntity(partyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDateChange(date) {
|
||||||
|
this.props.setDate(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNumberChange(number) {
|
||||||
|
this.props.setNumber(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleExternalNumberChange(externalNumber) {
|
||||||
|
this.props.setExternalNumber(externalNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLineChange(key, field, value) {
|
||||||
|
const index = typeof key === 'number' ? key : Number(key);
|
||||||
|
this.props.updateLineitem({
|
||||||
|
index,
|
||||||
|
[field]: value,
|
||||||
|
...(field === 'net' ? { netprice: value } : {}),
|
||||||
|
...(field === 'tax' ? { taxrate: value } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddLine() {
|
||||||
|
this.props.addLineitem();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRemoveLine(key) {
|
||||||
|
const index = typeof key === 'number' ? key : Number(key);
|
||||||
|
this.props.removeLineitem(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
async handlePdfUpload(file) {
|
||||||
|
if (!file || this.state.pdfUploading) return;
|
||||||
|
this.setState({
|
||||||
|
pdfUploading: true,
|
||||||
|
pdfDialogOpen: true,
|
||||||
|
pdfFileName: file.name,
|
||||||
|
pdfDialogData: { streaming: true, messages: [], result: null, error: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendMessage = (msg) => {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfDialogData: {
|
||||||
|
...prev.pdfDialogData,
|
||||||
|
messages: [...(prev.pdfDialogData?.messages ?? []), msg],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gen = this.context?.delivery?.parsePdfSse;
|
||||||
|
if (!gen) throw new Error('parsePdfSse nicht verfügbar.');
|
||||||
|
|
||||||
|
for await (const { event, data } of this.context.delivery.parsePdfSse(file)) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
switch (event) {
|
||||||
|
case 'status':
|
||||||
|
appendMessage({ type: 'status', text: data.message ?? String(data) });
|
||||||
|
break;
|
||||||
|
case 'llm_call':
|
||||||
|
appendMessage({
|
||||||
|
type: 'llm_call',
|
||||||
|
text: `LLM-Aufruf: ${data.model} (${data.promptSize} tokens, ${data.messages} msg)`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'tool_call':
|
||||||
|
appendMessage({ type: 'tool_call', text: `Tool-Aufruf: ${data.name}` });
|
||||||
|
break;
|
||||||
|
case 'usage':
|
||||||
|
appendMessage({
|
||||||
|
type: 'usage',
|
||||||
|
text: `Token-Verbrauch: ${data.input} in / ${data.cached} cached / ${data.output} out — $${(data.cost ?? 0).toFixed(4)}`,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'progress':
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfDialogData: { ...prev.pdfDialogData, result: data },
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
case 'result':
|
||||||
|
case 'done': {
|
||||||
|
const result = data?.delivery ?? data;
|
||||||
|
const rawItems = Array.isArray(result?.lines)
|
||||||
|
? result.lines
|
||||||
|
: Array.isArray(result?.items?.items)
|
||||||
|
? result.items.items
|
||||||
|
: null;
|
||||||
|
const newLines = Array.isArray(rawItems)
|
||||||
|
? rawItems.map((l) => ({
|
||||||
|
name: l.name ?? l.articleName ?? null,
|
||||||
|
sku: l.sku ?? l.liefArtNr ?? '',
|
||||||
|
amount: (l.amount ?? l.count) != null ? String(l.amount ?? l.count) : '',
|
||||||
|
pu: (l.pu ?? l.vpe) != null ? String(l.pu ?? l.vpe) : '',
|
||||||
|
unit: l.unit ?? '',
|
||||||
|
taxrate: (l.taxrate ?? l.tax ?? l.mwst) != null ? String(l.taxrate ?? l.tax ?? l.mwst) : '',
|
||||||
|
netprice: (l.netprice ?? l.net ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : null)) != null
|
||||||
|
? String(l.netprice ?? l.net ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : ''))
|
||||||
|
: '',
|
||||||
|
}))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfDialogData: { ...prev.pdfDialogData, streaming: false, result },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const docDate = result?.date ?? result?.basedata?.date;
|
||||||
|
let supplierId = result?.supplierId ?? result?.basedata?.kLieferant;
|
||||||
|
const extNum = result?.externalNumber ?? result?.basedata?.manifestNr;
|
||||||
|
const docType = result?.type ?? result?.basedata?.manifestType;
|
||||||
|
const notes = Array.isArray(result?.notes) ? result.notes : [];
|
||||||
|
|
||||||
|
// If supplierId is null, try matching supplier by name from loaded delivery entities
|
||||||
|
if (supplierId == null && result?.basedata?.lieferantName && Array.isArray(this.props.deliveryEntities)) {
|
||||||
|
const nameToMatch = result.basedata.lieferantName.trim().toLowerCase();
|
||||||
|
const matched = this.props.deliveryEntities.find(
|
||||||
|
(e) =>
|
||||||
|
e.name &&
|
||||||
|
(e.name.trim().toLowerCase() === nameToMatch ||
|
||||||
|
e.name.toLowerCase().includes(nameToMatch) ||
|
||||||
|
nameToMatch.includes(e.name.toLowerCase())),
|
||||||
|
);
|
||||||
|
if (matched) supplierId = matched.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (docDate) this.props.setDate(docDate);
|
||||||
|
if (supplierId != null) this.props.setDeliveryEntity(supplierId);
|
||||||
|
if (extNum != null) {
|
||||||
|
this.props.setExternalNumber(String(extNum));
|
||||||
|
}
|
||||||
|
if (docType) this.props.setType(docType);
|
||||||
|
if (newLines && newLines.length > 0) {
|
||||||
|
this.props.setLineitems(newLines);
|
||||||
|
}
|
||||||
|
if (notes.length > 0) {
|
||||||
|
this.props.setNotes(notes);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'error':
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfDialogData: {
|
||||||
|
...prev.pdfDialogData,
|
||||||
|
streaming: false,
|
||||||
|
error: data.message ?? 'PDF-Analyse fehlgeschlagen.',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfDialogData: {
|
||||||
|
...prev.pdfDialogData,
|
||||||
|
streaming: false,
|
||||||
|
error: err.message ?? 'Netzwerkfehler bei PDF-Analyse.',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} finally {
|
||||||
|
if (!this._cancelled) {
|
||||||
|
this.setState((prev) => ({
|
||||||
|
pdfUploading: false,
|
||||||
|
pdfDialogData: prev.pdfDialogData
|
||||||
|
? { ...prev.pdfDialogData, streaming: false }
|
||||||
|
: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClosePdfDialog() {
|
||||||
|
this.setState({ pdfDialogOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSave() {
|
||||||
|
const cfg = this.cfg;
|
||||||
|
if (this.state.saving) return;
|
||||||
|
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = buildManifestBody(
|
||||||
|
{
|
||||||
|
number: this.props.number ?? '',
|
||||||
|
externalNumber: this.props.externalNumber ?? '',
|
||||||
|
date: this.props.date ?? '',
|
||||||
|
partyId: this.props.partyId,
|
||||||
|
lines: this.props.lines,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
partyField: cfg.partyField,
|
||||||
|
requireAtLeastOne: true,
|
||||||
|
externalNumber: cfg.externalNumber,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
notifyError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ saving: true });
|
||||||
|
try {
|
||||||
|
const data = await this.context[cfg.api].create(body);
|
||||||
|
requireObject(data, cfg.postPathLabel);
|
||||||
|
requireObject(data[cfg.docField], cfg.docField);
|
||||||
|
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
|
||||||
|
await this.context[cfg.updateCache]();
|
||||||
|
if (this._cancelled) return;
|
||||||
|
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
|
||||||
|
this.props.resetDelivery();
|
||||||
|
this.props.navigate(cfg.editPath(data[cfg.docField].id));
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ saving: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <ManifestForm kind="delivery" mode="create" />;
|
const cfg = this.cfg;
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
partiesLoading,
|
||||||
|
saving,
|
||||||
|
pdfUploading,
|
||||||
|
pdfDialogOpen,
|
||||||
|
pdfDialogData,
|
||||||
|
pdfFileName,
|
||||||
|
} = this.state;
|
||||||
|
const {
|
||||||
|
deliveryEntities,
|
||||||
|
date,
|
||||||
|
partyId,
|
||||||
|
number,
|
||||||
|
externalNumber,
|
||||||
|
lines,
|
||||||
|
notes,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (status === 'loadingParties') {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
return <BackendError error={error} onRetry={this.handleRetry} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PanelTileGrid>
|
||||||
|
<ManifestHeader
|
||||||
|
kind="delivery"
|
||||||
|
headerColor={cfg.headerColor}
|
||||||
|
dateLabel={cfg.dateLabel}
|
||||||
|
partyLabel={cfg.partyLabel}
|
||||||
|
parties={deliveryEntities}
|
||||||
|
partiesLoading={partiesLoading}
|
||||||
|
partyId={partyId}
|
||||||
|
date={date ?? ''}
|
||||||
|
number={number ?? ''}
|
||||||
|
externalNumber={externalNumber ?? ''}
|
||||||
|
saving={saving}
|
||||||
|
pdfUploading={pdfUploading}
|
||||||
|
onPdfUpload={this.handlePdfUpload}
|
||||||
|
onPartyChange={this.handlePartyChange}
|
||||||
|
onDateChange={this.handleDateChange}
|
||||||
|
onNumberChange={this.handleNumberChange}
|
||||||
|
onExternalNumberChange={this.handleExternalNumberChange}
|
||||||
|
/>
|
||||||
|
{partyId == null && (!lines || lines.length === 0) ? (
|
||||||
|
<PanelTile wide={3} minHeight={160} color="#e2e8f0">
|
||||||
|
<Typography color="text.secondary">
|
||||||
|
{cfg.partyGateMessage}
|
||||||
|
</Typography>
|
||||||
|
</PanelTile>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{partyId == null && lines && lines.length > 0 && (
|
||||||
|
<PanelTile wide={3} color="#fef3c7">
|
||||||
|
<Alert severity="warning" sx={{ py: 0.5 }}>
|
||||||
|
Bitte wählen Sie oben einen Lieferanten aus, um die erfassten Positionen speichern zu können.
|
||||||
|
</Alert>
|
||||||
|
</PanelTile>
|
||||||
|
)}
|
||||||
|
<ManifestLines
|
||||||
|
columns={DELIVERY_LINE_COLUMNS}
|
||||||
|
lines={lines}
|
||||||
|
saving={saving}
|
||||||
|
linesColor={cfg.linesColor}
|
||||||
|
onLineChange={this.handleLineChange}
|
||||||
|
onAddLine={this.handleAddLine}
|
||||||
|
onRemoveLine={this.handleRemoveLine}
|
||||||
|
onSave={() => void this.handleSave()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{notes && notes.length > 0 && (
|
||||||
|
<PanelTile wide={3} color="#fffbeb">
|
||||||
|
<Typography variant="subtitle2" fontWeight={700} color="warning.dark" sx={{ mb: 1.5 }}>
|
||||||
|
Hinweise zur PDF-Analyse ({notes.length})
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{notes.map((note, i) => (
|
||||||
|
<Alert key={i} severity="warning" variant="outlined" sx={{ bgcolor: 'background.paper', py: 0.5 }}>
|
||||||
|
{note}
|
||||||
|
</Alert>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</PanelTile>
|
||||||
|
)}
|
||||||
|
</PanelTileGrid>
|
||||||
|
<ManifestPdfDialog
|
||||||
|
open={pdfDialogOpen}
|
||||||
|
filename={pdfFileName}
|
||||||
|
data={pdfDialogData}
|
||||||
|
onClose={this.handleClosePdfDialog}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LieferungNeuPage;
|
const mapStateToProps = (state) => ({
|
||||||
|
deliveryEntities: selectDeliveryEntities(state),
|
||||||
|
date: selectDeliveryDate(state),
|
||||||
|
partyId: selectDeliveryEntity(state),
|
||||||
|
number: selectDeliveryNumber(state),
|
||||||
|
externalNumber: selectDeliveryExternalNumber(state),
|
||||||
|
lines: selectDeliveryLineitems(state),
|
||||||
|
notes: selectDeliveryNotes(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setDeliveryEntities,
|
||||||
|
setDate,
|
||||||
|
setDeliveryEntity,
|
||||||
|
setNumber,
|
||||||
|
setExternalNumber,
|
||||||
|
setType,
|
||||||
|
setLineitems,
|
||||||
|
setNotes,
|
||||||
|
addLineitem,
|
||||||
|
updateLineitem,
|
||||||
|
removeLineitem,
|
||||||
|
resetDelivery,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(withNavigate(LieferungNeuPage));
|
||||||
|
|||||||
127
src/store/deliveryEditorSlice.js
Normal file
127
src/store/deliveryEditorSlice.js
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
|
||||||
|
export function createEmptyLineitem(line = {}) {
|
||||||
|
return {
|
||||||
|
name: line.name ?? null,
|
||||||
|
sku: line.sku ?? null,
|
||||||
|
amount: line.amount ?? null,
|
||||||
|
pu: line.pu ?? null,
|
||||||
|
unit: line.unit ?? null,
|
||||||
|
netprice: line.netprice ?? null,
|
||||||
|
taxrate: line.taxrate ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialDelivery = {
|
||||||
|
date: null,
|
||||||
|
deliveryEntity: null,
|
||||||
|
external_number: null,
|
||||||
|
number: null,
|
||||||
|
type: null,
|
||||||
|
lineitems: [],
|
||||||
|
notes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
// system now delivery entity list
|
||||||
|
deliveryEntities: [],
|
||||||
|
// delivery being edited
|
||||||
|
delivery: initialDelivery,
|
||||||
|
};
|
||||||
|
|
||||||
|
const deliveryEditorSlice = createSlice({
|
||||||
|
name: 'deliveryEditor',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setDeliveryEntities(state, action) {
|
||||||
|
state.deliveryEntities = action.payload ?? [];
|
||||||
|
},
|
||||||
|
setDelivery(state, action) {
|
||||||
|
state.delivery = {
|
||||||
|
...initialDelivery,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setDate(state, action) {
|
||||||
|
state.delivery.date = action.payload;
|
||||||
|
},
|
||||||
|
setDeliveryEntity(state, action) {
|
||||||
|
state.delivery.deliveryEntity = action.payload;
|
||||||
|
},
|
||||||
|
setExternalNumber(state, action) {
|
||||||
|
state.delivery.external_number = action.payload;
|
||||||
|
},
|
||||||
|
setNumber(state, action) {
|
||||||
|
state.delivery.number = action.payload;
|
||||||
|
},
|
||||||
|
setType(state, action) {
|
||||||
|
state.delivery.type = action.payload;
|
||||||
|
},
|
||||||
|
setLineitems(state, action) {
|
||||||
|
state.delivery.lineitems = action.payload ?? [];
|
||||||
|
},
|
||||||
|
setNotes(state, action) {
|
||||||
|
state.delivery.notes = Array.isArray(action.payload)
|
||||||
|
? action.payload
|
||||||
|
: action.payload ? [action.payload] : [];
|
||||||
|
},
|
||||||
|
addLineitem(state, action) {
|
||||||
|
state.delivery.lineitems.push(createEmptyLineitem(action.payload));
|
||||||
|
},
|
||||||
|
updateLineitem(state, action) {
|
||||||
|
const { index, ...fields } = action.payload;
|
||||||
|
if (index >= 0 && index < state.delivery.lineitems.length) {
|
||||||
|
state.delivery.lineitems[index] = {
|
||||||
|
...state.delivery.lineitems[index],
|
||||||
|
...fields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeLineitem(state, action) {
|
||||||
|
const index = action.payload;
|
||||||
|
if (typeof index === 'number' && index >= 0 && index < state.delivery.lineitems.length) {
|
||||||
|
state.delivery.lineitems.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetDelivery(state) {
|
||||||
|
state.delivery = { ...initialDelivery };
|
||||||
|
},
|
||||||
|
resetDeliveryEditor() {
|
||||||
|
return {
|
||||||
|
...initialState,
|
||||||
|
delivery: { ...initialDelivery },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setDeliveryEntities,
|
||||||
|
setDelivery,
|
||||||
|
setDate,
|
||||||
|
setDeliveryEntity,
|
||||||
|
setExternalNumber,
|
||||||
|
setNumber,
|
||||||
|
setType,
|
||||||
|
setLineitems,
|
||||||
|
setNotes,
|
||||||
|
addLineitem,
|
||||||
|
updateLineitem,
|
||||||
|
removeLineitem,
|
||||||
|
resetDelivery,
|
||||||
|
resetDeliveryEditor,
|
||||||
|
} = deliveryEditorSlice.actions;
|
||||||
|
|
||||||
|
// Selectors
|
||||||
|
export const selectDeliveryEntities = (state) => state.deliveryEditor.deliveryEntities;
|
||||||
|
export const selectDelivery = (state) => state.deliveryEditor.delivery;
|
||||||
|
export const selectDeliveryDate = (state) => state.deliveryEditor.delivery.date;
|
||||||
|
export const selectDeliveryEntity = (state) => state.deliveryEditor.delivery.deliveryEntity;
|
||||||
|
export const selectDeliveryExternalNumber = (state) => state.deliveryEditor.delivery.external_number;
|
||||||
|
export const selectDeliveryNumber = (state) => state.deliveryEditor.delivery.number;
|
||||||
|
export const selectDeliveryType = (state) => state.deliveryEditor.delivery.type;
|
||||||
|
export const selectDeliveryLineitems = (state) => state.deliveryEditor.delivery.lineitems;
|
||||||
|
export const selectDeliveryNotes = (state) => state.deliveryEditor.delivery.notes;
|
||||||
|
|
||||||
|
export default deliveryEditorSlice.reducer;
|
||||||
@@ -5,6 +5,7 @@ import categoryReducer from './categorySlice';
|
|||||||
import companyReducer, { companyLoadListener } from './companySlice';
|
import companyReducer, { companyLoadListener } from './companySlice';
|
||||||
import posterminalReducer, { posterminalLoadListener } from './posterminalSlice';
|
import posterminalReducer, { posterminalLoadListener } from './posterminalSlice';
|
||||||
import unitReducer, { unitLoadListener } from './unitSlice';
|
import unitReducer, { unitLoadListener } from './unitSlice';
|
||||||
|
import deliveryEditorReducer from './deliveryEditorSlice';
|
||||||
|
|
||||||
const listenerMiddleware = createListenerMiddleware();
|
const listenerMiddleware = createListenerMiddleware();
|
||||||
companyLoadListener(listenerMiddleware);
|
companyLoadListener(listenerMiddleware);
|
||||||
@@ -52,6 +53,7 @@ export const store = configureStore({
|
|||||||
brand: brandReducer,
|
brand: brandReducer,
|
||||||
posterminal: posterminalReducer,
|
posterminal: posterminalReducer,
|
||||||
unit: unitReducer,
|
unit: unitReducer,
|
||||||
|
deliveryEditor: deliveryEditorReducer,
|
||||||
},
|
},
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware()
|
getDefaultMiddleware()
|
||||||
|
|||||||
Reference in New Issue
Block a user