From b727e0092853a32e70bee3cc834b8b5e8923ecdc Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 20 Oct 2026 02:56:31 +0200 Subject: [PATCH] u --- src/components/manifest/ManifestFormLine.jsx | 224 +++++++------------ src/components/manifest/ManifestHeader.jsx | 26 +++ src/utils/documentNumber.js | 139 ++++++++---- 3 files changed, 204 insertions(+), 185 deletions(-) diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index d501604..431157c 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -8,7 +8,6 @@ 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 Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; @@ -608,9 +607,6 @@ class ManifestFormLine extends PureComponent { : (rawWarehouses.length > 0 || isTracked ? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0) : null)); - const warehouseTooltip = rawWarehouses.length > 0 - ? rawWarehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ') - : (isTracked === false ? 'Keine Bestandsführung' : undefined); const lineAmount = parseNum(getFirstVal(this.props.line?.amount, this.props.line?.menge)) ?? 0; return ( @@ -730,93 +726,65 @@ class ManifestFormLine extends PureComponent { }} > {/* Bestand */} - stockVal + ? 'warning.main' + : 'text.secondary', + fontSize: '0.72rem', + }} > - stockVal - ? 'warning.main' - : 'text.secondary', - fontSize: '0.72rem', - }} - > - Bestand: {isTracked === false ? '∞' : formatStock(stockVal)} - - + Bestand: {isTracked === false ? '∞' : formatStock(stockVal)} + {/* Marge */} - 0 && effectiveMarge != null ? ` · Nach ${rebate}% Rabatt: ${effectiveMarge.toFixed(1).replace('.', ',')}%` : ''}` - : 'Kein EK- oder VK-Preis hinterlegt' - } + + - - Marge: {marge != null ? `${Math.floor(marge)}%` : '—'} - - + Marge: {marge != null ? `${Math.floor(marge)}%` : '—'} + + {/* EK Netto */} - - - EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'} - - + EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'} + + {/* EK Brutto */} - - - EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'} - - + EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'} + + {/* VK Netto */} 0 || isDeliveryTracked ? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0) : null)); - const deliveryWarehouseTooltip = rawWarehouses.length > 0 - ? rawWarehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ') - : (isDeliveryTracked === false ? 'Keine Bestandsführung' : undefined); return ( {/* Unlink button at the front */} {this.props.onRemoveSupplierArticle && supplierArticle.id != null && ( - - - - - - - + + + + + )} {/* Article Photo Thumbnail */} @@ -1109,48 +1066,34 @@ class ManifestFormLine extends PureComponent { }} > {/* Bestand */} - + Bestand: {isDeliveryTracked === false ? '∞' : formatStock(deliveryStockVal)} + + + {/* Marge */} + {margeDelivery != null && ( - Bestand: {isDeliveryTracked === false ? '∞' : formatStock(deliveryStockVal)} + Marge: {Math.floor(margeDelivery)}% - - - {/* Marge */} - {margeDelivery != null && ( - - - Marge: {Math.floor(margeDelivery)}% - - )} {priceNet != null && ( @@ -1363,13 +1306,6 @@ class ManifestFormLine extends PureComponent { aria-label="Zeile entfernen" disabled={saving || Boolean(this.props.isOnShipment) || isFullyLedgered} onClick={this.handleRemove} - title={ - isFullyLedgered - ? 'Position ist bereits vollständig im Ledger verbucht und kann nicht entfernt werden' - : this.props.isOnShipment - ? 'Artikel ist bereits auf einem Lieferschein und kann nicht entfernt werden' - : 'Zeile entfernen' - } sx={{ p: 0.35 }} > diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index ef28419..d55602c 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -27,6 +27,9 @@ import entityAutomat from '../../automats/entityAutomat'; import customergroupAutomat from '../../automats/customergroupAutomat'; import supplierAutomat from '../../automats/supplierAutomat'; import numberAutomat from '../../automats/numberAutomat'; +import quoteAutomat from '../../automats/quoteAutomat'; +import orderAutomat from '../../automats/orderAutomat'; +import deliveryAutomat from '../../automats/deliveryAutomat'; import { getNextDocumentNumber, ensureDocumentNumber } from '../../utils/documentNumber'; import { SURFACE_RAISED, @@ -233,11 +236,30 @@ class ManifestHeader extends PureComponent { this.props.onPdfUpload?.(file); } + subscribeListAutomat(kind) { + this.unsubList?.(); + this.unsubList = null; + const automat = + kind === 'quote' + ? quoteAutomat + : kind === 'order' + ? orderAutomat + : kind === 'delivery' + ? deliveryAutomat + : null; + if (automat) { + this.unsubList = automat.subscribe(this, () => { + if (!this._cancelled) this.forceUpdate(); + }); + } + } + componentDidMount() { this._cancelled = false; this.unsubNumber = numberAutomat.subscribe(this, () => { this.forceUpdate(); }); + this.subscribeListAutomat(this.props.kind); void numberAutomat.actions.ensureLoaded().catch(() => {}); if (!this.props.number && this.props.docId == null) { void ensureDocumentNumber(this.props.kind) @@ -249,6 +271,9 @@ class ManifestHeader extends PureComponent { } componentDidUpdate(prevProps) { + if (prevProps.kind !== this.props.kind) { + this.subscribeListAutomat(this.props.kind); + } if ( !this.props.number && this.props.docId == null && @@ -265,6 +290,7 @@ class ManifestHeader extends PureComponent { componentWillUnmount() { this._cancelled = true; this.unsubNumber?.(); + this.unsubList?.(); } render() { diff --git a/src/utils/documentNumber.js b/src/utils/documentNumber.js index 346b85d..876c5ca 100644 --- a/src/utils/documentNumber.js +++ b/src/utils/documentNumber.js @@ -2,6 +2,10 @@ import numberAutomat from '../automats/numberAutomat'; import quoteAutomat from '../automats/quoteAutomat'; import orderAutomat from '../automats/orderAutomat'; import deliveryAutomat from '../automats/deliveryAutomat'; +import { order as orderApi, quote as quoteApi, delivery as deliveryApi } from '../api'; + +/** Highest integer counter found per document kind. */ +const maxKnownNumberByKind = new Map(); /** * Formats a number pattern given prefix, padding width, and counter. @@ -20,6 +24,7 @@ export function formatNumberPattern(prefix, padding, next) { /** * Returns the next document number for quote, order, or delivery based on company sequences. + * Always verifies that the returned number does not collide with existing documents. * * @param {'quote'|'order'|'delivery'|string} kind * @returns {string} @@ -27,59 +32,63 @@ export function formatNumberPattern(prefix, padding, next) { export function getNextDocumentNumber(kind) { const numbers = numberAutomat.state.items ?? []; const seq = numbers.find((r) => r.type === kind); - if (seq) { - return formatNumberPattern(seq.prefix, seq.padding, seq.next); - } - - // Detect style from existing company sequences (sku, delivery, entity) - const skuSeq = numbers.find((r) => r.type === 'sku'); - const deliverySeq = numbers.find((r) => r.type === 'delivery'); - const entitySeq = numbers.find((r) => r.type === 'entity'); - const refSeq = skuSeq || deliverySeq || entitySeq; - - const pad = refSeq ? Math.max(0, Number(refSeq.padding) || 0) : 6; - const prefixStyle = refSeq?.prefix ?? ''; let prefix = ''; + let pad = 6; let startNum = 1; - if (kind === 'delivery') { - prefix = prefixStyle.startsWith('DEL-') ? 'DEL-' : (prefixStyle.startsWith('L-') ? 'L-' : 'LS-'); - } else if ( - prefixStyle.startsWith('ART-') || - prefixStyle.startsWith('LS-') || - prefixStyle.startsWith('KD-') || - prefixStyle.endsWith('-') - ) { - // Standard German ERP prefix style (ART-, KD-, LS-, KORR-) - prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); - } else if ( - prefixStyle.startsWith('SKU-') || - prefixStyle.startsWith('DEL-') || - prefixStyle.startsWith('C-') - ) { - prefix = kind === 'quote' ? 'Q-' : (kind === 'order' ? 'ORD-' : ''); - } else if ( - prefixStyle.startsWith('A-') || - prefixStyle.startsWith('L-') || - prefixStyle.startsWith('K-') - ) { - prefix = kind === 'quote' ? 'AG-' : (kind === 'order' ? 'AU-' : ''); - } else if (!prefixStyle && refSeq?.next >= 10000) { - prefix = ''; - startNum = kind === 'quote' ? 40001 : 50001; + if (seq) { + prefix = seq.prefix ?? ''; + pad = Math.max(0, Number(seq.padding) || 0); + startNum = Math.max(1, Number(seq.next) || 1); } else { - prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); + // Detect style from existing company sequences (sku, delivery, entity) + const skuSeq = numbers.find((r) => r.type === 'sku'); + const deliverySeq = numbers.find((r) => r.type === 'delivery'); + const entitySeq = numbers.find((r) => r.type === 'entity'); + const refSeq = skuSeq || deliverySeq || entitySeq; + + pad = refSeq ? Math.max(0, Number(refSeq.padding) || 0) : 6; + const prefixStyle = refSeq?.prefix ?? ''; + + if (kind === 'delivery') { + prefix = prefixStyle.startsWith('DEL-') ? 'DEL-' : (prefixStyle.startsWith('L-') ? 'L-' : 'LS-'); + } else if ( + prefixStyle.startsWith('ART-') || + prefixStyle.startsWith('LS-') || + prefixStyle.startsWith('KD-') || + prefixStyle.endsWith('-') + ) { + // Standard German ERP prefix style (ART-, KD-, LS-, KORR-) + prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); + } else if ( + prefixStyle.startsWith('SKU-') || + prefixStyle.startsWith('DEL-') || + prefixStyle.startsWith('C-') + ) { + prefix = kind === 'quote' ? 'Q-' : (kind === 'order' ? 'ORD-' : ''); + } else if ( + prefixStyle.startsWith('A-') || + prefixStyle.startsWith('L-') || + prefixStyle.startsWith('K-') + ) { + prefix = kind === 'quote' ? 'AG-' : (kind === 'order' ? 'AU-' : ''); + } else if (!prefixStyle && refSeq?.next >= 10000) { + prefix = ''; + startNum = kind === 'quote' ? 40001 : 50001; + } else { + prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); + } } // Check existing docs from quoteAutomat / orderAutomat / deliveryAutomat to avoid collisions const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : (kind === 'delivery' ? deliveryAutomat : null)); const existingDocs = Array.isArray(listAutomat?.state?.items) ? listAutomat.state.items : []; - let maxFound = startNum - 1; + let maxFound = Math.max(startNum - 1, maxKnownNumberByKind.get(kind) ?? 0); for (const doc of existingDocs) { if (!doc || typeof doc !== 'object') continue; - const docNum = String(doc.number ?? '').trim(); + const docNum = String(doc.number ?? doc.orderNumber ?? '').trim(); if (!docNum) continue; if (prefix && docNum.startsWith(prefix)) { const rest = docNum.slice(prefix.length); @@ -95,12 +104,15 @@ export function getNextDocumentNumber(kind) { } } + maxKnownNumberByKind.set(kind, maxFound); + const nextVal = maxFound >= startNum ? maxFound + 1 : startNum; return formatNumberPattern(prefix, pad, nextVal); } /** - * Asynchronously ensures required stores are loaded, then returns the next document number. + * Asynchronously ensures required stores are loaded, queries recent documents to detect + * the highest existing number, then returns the next document number. * * @param {'quote'|'order'|'delivery'|string} kind * @returns {Promise} @@ -111,6 +123,51 @@ export async function ensureDocumentNumber(kind) { numberAutomat.actions.ensureLoaded().catch(() => {}), listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(), ]); + + try { + let recentDocs = []; + if (kind === 'order') { + const res = await orderApi.list({ offset: 0, limit: 10 }); + recentDocs = Array.isArray(res?.orders) ? res.orders : []; + } else if (kind === 'quote') { + const res = await quoteApi.list({ offset: 0, limit: 10 }); + recentDocs = Array.isArray(res?.quotes) ? res.quotes : []; + } else if (kind === 'delivery') { + const res = await deliveryApi.list({ offset: 0, limit: 10 }); + recentDocs = Array.isArray(res?.deliveries) ? res.deliveries : []; + } + + if (recentDocs.length > 0) { + const numbers = numberAutomat.state.items ?? []; + const seq = numbers.find((r) => r.type === kind); + let prefix = seq?.prefix; + if (prefix == null) { + prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : 'LS-'); + } + + let maxFound = maxKnownNumberByKind.get(kind) ?? 0; + for (const doc of recentDocs) { + const docNum = String(doc.number ?? doc.orderNumber ?? '').trim(); + if (!docNum) continue; + if (prefix && docNum.startsWith(prefix)) { + const rest = docNum.slice(prefix.length); + const parsed = parseInt(rest, 10); + if (!Number.isNaN(parsed) && parsed > maxFound) { + maxFound = parsed; + } + } else if (!prefix) { + const parsed = parseInt(docNum, 10); + if (!Number.isNaN(parsed) && parsed > maxFound) { + maxFound = parsed; + } + } + } + maxKnownNumberByKind.set(kind, maxFound); + } + } catch (err) { + console.warn(`[documentNumber] ensureDocumentNumber error fetching latest ${kind}:`, err); + } + return getNextDocumentNumber(kind); }