This commit is contained in:
seb
2026-10-15 20:15:19 +02:00
parent 8e783a571f
commit beed294cb0
26 changed files with 3165 additions and 906 deletions

View File

@@ -1,4 +1,4 @@
import { PureComponent } from 'react';
import { PureComponent, createRef } from 'react';
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { enqueueSnackbar } from 'notistack';
@@ -7,16 +7,18 @@ import { PanelTileGrid } from 'dashgui/panels';
import { delivery as deliveryApi, quote as quoteApi, order as orderApi, shipment as shipmentApi, payment as paymentApi } from '../../api';
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import { notifyError, notifyErrorMessage } from '../../utils/notify';
import withNavigate from '../withNavigate';
import ManifestHeader from './ManifestHeader';
import ManifestLines from './ManifestLines';
import ManifestPdfDialog from './ManifestPdfDialog';
import ArticleSearchDialog from './ArticleSearchDialog';
import OrderDocumentsTiles from './OrderDocumentsTiles';
import OrderShipmentDialog from './OrderShipmentDialog';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import entityAutomat from '../../automats/entityAutomat';
import supplierAutomat from '../../automats/supplierAutomat';
import orderAutomat from '../../automats/orderAutomat';
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
import {
@@ -95,14 +97,18 @@ class ManifestForm extends PureComponent {
dirtyLineFields: initialEdit?.dirtyLineFields || {},
dirtySummary: initialEdit?.dirtySummary || [],
saving: false,
orderCreating: false,
partyError: false,
pdfUploading: false,
pdfCreating: false,
pdfDialogOpen: false,
shipmentDialogOpen: false,
pdfDialogData: null,
pdfFileName: '',
articleSearchDialogOpen: false,
articleSearchLine: null,
};
this.headerRef = createRef();
this._cancelled = false;
this.handleRetry = this.handleRetry.bind(this);
this.handleRestore = this.handleRestore.bind(this);
@@ -122,11 +128,16 @@ class ManifestForm extends PureComponent {
this.handleMoveLine = this.handleMoveLine.bind(this);
this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleCreateOrder = this.handleCreateOrder.bind(this);
this.handleOpenCreateShipment = this.handleOpenCreateShipment.bind(this);
this.handleCloseShipmentDialog = this.handleCloseShipmentDialog.bind(this);
this.handleShipmentCreated = this.handleShipmentCreated.bind(this);
this.handlePdfUpload = this.handlePdfUpload.bind(this);
this.handleCreatePdf = this.handleCreatePdf.bind(this);
this.handleAttachmentDelete = this.handleAttachmentDelete.bind(this);
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
this.handleAbortParse = this.handleAbortParse.bind(this);
this.isFullyDelivered = this.isFullyDelivered.bind(this);
}
componentDidMount() {
@@ -457,10 +468,10 @@ class ManifestForm extends PureComponent {
const { mode } = this.props;
const { requirePartyBeforeLines } = this.cfg;
if (mode === 'create' && requirePartyBeforeLines && partyId == null) {
this.setState({ partyId: null, lines: [emptyLine()] });
this.setState({ partyId: null, lines: [emptyLine()], partyError: false });
return;
}
this.setState({ partyId });
this.setState({ partyId, partyError: false });
}
handleDateChange(date) {
@@ -755,10 +766,46 @@ class ManifestForm extends PureComponent {
}));
}
isFullyDelivered() {
if (this.state.delivered) return true;
const lines = this.state.lines || [];
const validLines = lines.filter((l) => l.sku && Number(l.amount) > 0);
if (validLines.length === 0) return false;
const shippedBySku = {};
for (const sh of this.state.shipments || []) {
for (const sl of sh.lines || []) {
if (sl.sku) {
const k = String(sl.sku).trim().toLowerCase();
shippedBySku[k] = (shippedBySku[k] || 0) + (Number(sl.amount) || 0);
}
}
}
return validLines.every((l) => {
const k = String(l.sku).trim().toLowerCase();
return (shippedBySku[k] || 0) >= Number(l.amount);
});
}
handleRemoveLine(key) {
this.setState((prev) => {
const lineToRemove = prev.lines.find((line) => line.key === key);
if (lineToRemove && this.props.kind === 'order') {
const rawSku = lineToRemove.sku != null ? String(lineToRemove.sku).trim().toLowerCase() : '';
const isShipped = Boolean(
rawSku &&
(prev.shipments || []).some((sh) =>
(sh.lines || []).some((sl) => sl.sku && String(sl.sku).trim().toLowerCase() === rawSku)
)
);
if (isShipped) {
enqueueSnackbar(`Artikel „${lineToRemove.sku || lineToRemove.name}“ ist bereits auf einem Lieferschein und kann nicht entfernt werden.`, { variant: 'warning' });
return null;
}
}
const next = prev.lines.filter((line) => line.key !== key);
return { lines: next.length > 0 ? next : [emptyLine()] };
return { lines: next.length > 0 ? next : [emptyLine()], isDirty: true };
});
}
@@ -776,7 +823,29 @@ class ManifestForm extends PureComponent {
}
handleRemoveAllLines() {
this.setState({ lines: [] });
this.setState((prev) => {
if (this.props.kind === 'order') {
const shippedSkus = new Set();
for (const sh of prev.shipments || []) {
for (const sl of sh.lines || []) {
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
}
}
if (shippedSkus.size > 0) {
const remaining = prev.lines.filter((l) => {
const raw = l.sku != null ? String(l.sku).trim().toLowerCase() : '';
return shippedSkus.has(raw);
});
if (remaining.length === prev.lines.length) {
enqueueSnackbar('Alle Positionen sind bereits auf Lieferscheinen und können nicht gelöscht werden.', { variant: 'warning' });
return null;
}
enqueueSnackbar('Nicht ausgelieferte Positionen wurden entfernt.', { variant: 'info' });
return { lines: remaining.length > 0 ? remaining : [emptyLine()], isDirty: true };
}
}
return { lines: [emptyLine()], isDirty: true };
});
}
async handlePdfUpload(file) {
@@ -945,6 +1014,74 @@ class ManifestForm extends PureComponent {
}
}
// MARK: calls quoteApi.createOrder from ./api/index.js
// #region POST /api/quote/:id/order — createOrder
async handleCreateOrder(e) {
e?.preventDefault?.();
e?.stopPropagation?.();
const { docId, kind } = this.props;
if (this.state.orderCreating || this.state.saving) return;
if (docId == null || kind !== 'quote') return;
if (!this.state.partyId) {
this.setState({ partyError: true });
notifyErrorMessage(
'Für eine Bestellung muss ein Kunde ausgewählt sein. Bitte wählen Sie zuerst einen Kunden aus.',
);
this.headerRef.current?.focusPartyInput?.();
return;
}
if (this.state.isDirty) {
const saved = await this.handleSave();
if (!saved) return;
}
this.setState({ orderCreating: true });
try {
const res = await quoteApi.createOrder(docId, {
entityId: this.state.partyId,
});
requireObject(res, 'POST /api/quote/:id/order');
const order = requireObject(res.order, 'order');
requireDefined(order.id, 'order.id');
void orderAutomat.actions.reload();
enqueueSnackbar(
order.number
? `Bestellung ${order.number} erfolgreich erstellt.`
: 'Bestellung erfolgreich erstellt.',
{ variant: 'success' },
);
this.props.navigate(`/auftrag/bestellung/${order.id}`);
} catch (err) {
if (!this._cancelled) {
notifyError(err);
this.setState({ orderCreating: false });
}
}
}
// #endregion
async handleOpenCreateShipment() {
if (this.state.isDirty) {
const saved = await this.handleSave();
if (!saved) return;
}
this.setState({ shipmentDialogOpen: true });
}
handleCloseShipmentDialog() {
this.setState({ shipmentDialogOpen: false });
}
async handleShipmentCreated() {
await this.loadEdit();
void orderAutomat.actions.reload();
}
async handleAttachmentDelete(attachment) {
if (!attachment) return;
const fileId = attachment.fileId ?? attachment.id;
@@ -975,8 +1112,8 @@ class ManifestForm extends PureComponent {
async handleSave() {
const { mode, docId } = this.props;
const cfg = this.cfg;
if (this.state.saving) return;
if (mode === 'edit' && docId == null) return;
if (this.state.saving) return false;
if (mode === 'edit' && docId == null) return false;
const requireAtLeastOne = mode === 'create';
let body;
@@ -999,7 +1136,7 @@ class ManifestForm extends PureComponent {
);
} catch (err) {
notifyError(err);
return;
return false;
}
this.setState({ saving: true });
@@ -1012,10 +1149,10 @@ class ManifestForm extends PureComponent {
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
this.draftAutomat?.actions?.reset();
await cfg.listAutomat.actions.reload();
if (this._cancelled) return;
if (this._cancelled) return false;
enqueueSnackbar(cfg.createdMsg, { variant: 'success' });
this.props.navigate(cfg.editPath(data[cfg.docField].id));
return;
return true;
}
const data = await api.update(docId, body);
@@ -1023,7 +1160,7 @@ class ManifestForm extends PureComponent {
requireObject(data[cfg.docField], cfg.docField);
requireDefined(data[cfg.docField].id, `${cfg.docField}.id`);
await cfg.listAutomat.actions.reload();
if (this._cancelled) return;
if (this._cancelled) return false;
const doc = data[cfg.docField];
const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`);
const savedDoc = {
@@ -1051,16 +1188,18 @@ class ManifestForm extends PureComponent {
saving: false,
});
enqueueSnackbar(cfg.savedMsg, { variant: 'success' });
return true;
} catch (err) {
if (this._cancelled) return;
if (this._cancelled) return false;
notifyError(err);
this.setState({ saving: false });
return false;
}
}
renderArticles() {
const cfg = this.cfg;
const { lines, saving, dirtyLineFields } = this.state;
const { lines, saving, dirtyLineFields, shipments } = this.state;
return (
<ManifestLines
@@ -1071,6 +1210,7 @@ class ManifestForm extends PureComponent {
wide={4}
linesColor={cfg.linesColor}
dirtyLineFields={dirtyLineFields || {}}
shipments={shipments}
onLineChange={this.handleLineChange}
onSkuBlur={this.handleSkuBlur}
onAddLine={this.handleAddLine}
@@ -1120,6 +1260,7 @@ class ManifestForm extends PureComponent {
<>
<PanelTileGrid>
<ManifestHeader
ref={this.headerRef}
kind={kind}
headerColor={cfg.headerColor}
dateLabel={cfg.dateLabel}
@@ -1127,10 +1268,12 @@ class ManifestForm extends PureComponent {
parties={parties}
partiesLoading={partiesLoading || status === 'loadingParties'}
partyId={partyId}
partyError={this.state.partyError}
date={date}
number={number}
externalNumber={externalNumber}
saving={saving}
orderCreating={this.state.orderCreating}
pdfUploading={pdfUploading}
pdfCreating={this.state.pdfCreating}
lines={lines}
@@ -1143,6 +1286,9 @@ class ManifestForm extends PureComponent {
onPdfUpload={kind === 'delivery' ? this.handlePdfUpload : undefined}
onPdfDelete={this.handleAttachmentDelete}
onPdfCreate={kind === 'quote' || kind === 'order' ? this.handleCreatePdf : undefined}
onCreateOrder={kind === 'quote' && this.props.mode === 'edit' ? this.handleCreateOrder : undefined}
onOpenCreateShipment={kind === 'order' && this.props.mode === 'edit' ? this.handleOpenCreateShipment : undefined}
deliverDisabled={kind === 'order' && this.isFullyDelivered()}
onSave={this.handleSave}
onPartyChange={this.handlePartyChange}
onDateChange={this.handleDateChange}
@@ -1163,9 +1309,12 @@ class ManifestForm extends PureComponent {
shipments={this.state.shipments}
payments={this.state.payments}
attachments={this.state.attachments}
deliverDisabled={this.isFullyDelivered()}
onPdfCreate={this.handleCreatePdf}
pdfCreating={this.state.pdfCreating}
onAttachmentDelete={this.handleAttachmentDelete}
onOpenCreateShipment={this.handleOpenCreateShipment}
onShipmentChanged={this.handleShipmentCreated}
navigate={this.props.navigate}
/>
) : null}
@@ -1184,6 +1333,16 @@ class ManifestForm extends PureComponent {
onClose={this.handleCloseArticleSearch}
onAssign={this.handleAssignArticleForLine}
/>
<OrderShipmentDialog
open={this.state.shipmentDialogOpen}
onClose={this.handleCloseShipmentDialog}
orderId={this.props.docId}
orderNumber={number}
externalNumber={externalNumber}
lines={lines}
shipments={this.state.shipments}
onSuccess={this.handleShipmentCreated}
/>
</>
);
}