From 77f6a0815f894fec312a4f2b14eee8cf32b9b633 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 15 Oct 2026 02:33:24 +0200 Subject: [PATCH] u --- src/components/manifest/QuoteList.jsx | 281 ++++++++++++++++++-------- src/utils/documentNumber.js | 52 +++++ 2 files changed, 253 insertions(+), 80 deletions(-) diff --git a/src/components/manifest/QuoteList.jsx b/src/components/manifest/QuoteList.jsx index 525acde..bbc81ce 100644 --- a/src/components/manifest/QuoteList.jsx +++ b/src/components/manifest/QuoteList.jsx @@ -18,10 +18,12 @@ import { PanelTile, PanelTileGrid } from 'dashgui/panels'; import BackendError from '../BackendError'; import SkeleHashImage from '../settings/SkeleHashImage'; import quoteAutomat, { CHUNK_SIZE } from '../../automats/quoteAutomat'; +import numberAutomat from '../../automats/numberAutomat'; import { quote as quoteApi } from '../../api'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import { formatPrice } from '../../utils/price'; +import { getMaxDocumentNumber } from '../../utils/documentNumber'; import withNavigate from '../withNavigate'; import { formatDate, @@ -41,28 +43,130 @@ function quoteGrossTotal(lines) { const cfg = manifestConfig('quote'); -const rowSx = { +// Dummy quote generator using the active sequence scheme with max digits for layout testing +export function createDummyTestQuote() { + const maxNumber = getMaxDocumentNumber('quote'); + return { + id: 999999, + number: maxNumber, + date: '2030-10-30T12:00:00Z', + customerName: 'Testkunde (Mustermann GmbH)', + positions: 999, + grossTotal: 30000.90, + files: [ + { + id: 99991, + manifestId: 999999, + fileId: 99991, + name: `angebot-${maxNumber}.pdf`, + mimetype: 'application/pdf', + size: 2701, + thumbx: 141, + thumby: 200, + skeledash: '6w5VujjmxhSpoO8VSwebe2yXU3Tj4108PysbXPqoHmyYcs9oXH8z6npbrjwK90StUBOXY0LLPiDgZy9qjYjkFDVKSjUp3dcWQ', + }, + { + id: 99992, + manifestId: 999999, + fileId: 99992, + name: 'datenblatt-muster.pdf', + mimetype: 'application/pdf', + size: 15420, + thumbx: 141, + thumby: 200, + skeledash: '6w5VujjmxhSpoO8VSwebe2yXU3Tj4108PysbXPqoHmyYcs9oXH8z6npbrjwK90StUBOXY0LLPiDgZy9qjYjkFDVKSjUp3dcWQ', + }, + { + id: 99993, + manifestId: 999999, + fileId: 99993, + name: 'skizze-entwurf.pdf', + mimetype: 'application/pdf', + size: 8900, + thumbx: 141, + thumby: 200, + skeledash: '6w5VujjmxhSpoO8VSwebe2yXU3Tj4108PysbXPqoHmyYcs9oXH8z6npbrjwK90StUBOXY0LLPiDgZy9qjYjkFDVKSjUp3dcWQ', + }, + { + id: 99994, + manifestId: 999999, + fileId: 99994, + name: 'agb-anlage.pdf', + mimetype: 'application/pdf', + size: 4500, + thumbx: 141, + thumby: 200, + skeledash: '6w5VujjmxhSpoO8VSwebe2yXU3Tj4108PysbXPqoHmyYcs9oXH8z6npbrjwK90StUBOXY0LLPiDgZy9qjYjkFDVKSjUp3dcWQ', + }, + { + id: 99995, + manifestId: 999999, + fileId: 99995, + name: 'lieferschein-anhang.pdf', + mimetype: 'application/pdf', + size: 3200, + thumbx: 141, + thumby: 200, + skeledash: '6w5VujjmxhSpoO8VSwebe2yXU3Tj4108PysbXPqoHmyYcs9oXH8z6npbrjwK90StUBOXY0LLPiDgZy9qjYjkFDVKSjUp3dcWQ', + }, + ], + }; +} + +const rowGridSx = { + display: 'grid', + gridTemplateColumns: 'max-content max-content 1fr max-content max-content max-content auto', alignItems: 'center', - height: 64, - minHeight: 64, - maxHeight: 64, - boxSizing: 'border-box', - py: 0, gap: 1.5, + width: '100%', + boxSizing: 'border-box', }; const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; +function renderInvisibleDummyRow(dummy) { + if (!dummy) return null; + return ( + <> + + {dummy.number} + + + {dummy.date ? formatDate(dummy.date) : '–'} + + + {dummy.customerName} + + + + + + {[0, 1, 2, 3].map((i) => ( + + ))} + + + + {formatPrice(dummy.grossTotal)} + + + + + + + ); +} + /** * Ambient context for `QuoteRow`: FillSpace renders rows as bare elements - * (``), so the list publishes callbacks and the party map - * here on every render. + * (``), so the list publishes callbacks and the dummy quote here on every render. */ const rowContext = { partiesById: {}, busyId: null, onOpen: null, onDelete: null, + headerItem: null, }; // MARK: @@ -75,37 +179,27 @@ class QuoteRow extends PureComponent { render() { const { item: quote, disabled } = this.props; + const dummy = this.props.headerItem || rowContext.headerItem; // Data-free probe (FillSpace measures the row height with `item` undefined). if (!quote) { if (disabled) { return ( - +   ); } return ( - - - - - - - - - - - - - - - - - - - - + + {renderInvisibleDummyRow(dummy)} + + + + + + + ); } @@ -118,47 +212,54 @@ class QuoteRow extends PureComponent { const files = Array.isArray(quote.files) ? quote.files : []; return ( - onOpen?.(quote)}> - + onOpen?.(quote)} + disabled={disabled} + > + {renderInvisibleDummyRow(dummy)} + + {quote.number} - + {quote.date ? formatDate(quote.date) : '–'} - + {customer} - 0 ? 'text.primary' : 'text.disabled', - '& .MuiChip-label': { px: 0.5 }, - }} - /> + + 0 ? 'text.primary' : 'text.disabled', + '& .MuiChip-label': { px: 0.5 }, + }} + /> + {/* Files / Attachments column after Pos.: space for 4 per row, 2 lines max */} - - {gross > 0 ? formatPrice(gross) : '—'} - + + + {gross > 0 ? formatPrice(gross) : '—'} + + - { - e.stopPropagation(); - onDelete?.(quote); - }} - onMouseDown={(e) => e.stopPropagation()} - sx={{ width: 40, height: 40, flexShrink: 0 }} - > - - + + { + e.stopPropagation(); + onDelete?.(quote); + }} + onMouseDown={(e) => e.stopPropagation()} + sx={{ width: 40, height: 40 }} + > + + + ); } @@ -450,6 +555,7 @@ class QuoteSearchField extends PureComponent { export class QuoteList extends PureComponent { constructor(props) { super(props); + this.headerItem = createDummyTestQuote(); this.state = { items: quoteAutomat.getItems(), unfilteredTotal: quoteAutomat.state.unfilteredTotal ?? 0, @@ -473,12 +579,18 @@ export class QuoteList extends PureComponent { unfilteredTotal: state.unfilteredTotal ?? 0, filterKey: quoteAutomat.getFilterKey(state), })); + this.unsubNumber = numberAutomat.subscribe(this, () => { + this.headerItem = createDummyTestQuote(); + this.forceUpdate(); + }); + void numberAutomat.actions.ensureLoaded().catch(() => { }); void this.loadParties(); } componentWillUnmount() { this._cancelled = true; this.unsub?.(); + this.unsubNumber?.(); } async loadParties() { @@ -506,7 +618,7 @@ export class QuoteList extends PureComponent { } handleRetry() { - void quoteAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => {}); + void quoteAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => { }); } handleCreate() { @@ -514,10 +626,18 @@ export class QuoteList extends PureComponent { } handleOpen(quote) { + if (quote.id === 999999) { + enqueueSnackbar(`Test-Angebot „${quote.number}“ angeklickt.`, { variant: 'info' }); + return; + } this.props.navigate(cfg.editPath(quote.id)); } async handleDelete(quote) { + if (quote.id === 999999) { + enqueueSnackbar('Test-Angebot kann nicht gelöscht werden.', { variant: 'info' }); + return; + } if (this.state.busyId != null) return; if (!window.confirm(cfg.confirmDelete(quote.number))) return; this.setState({ busyId: quote.id }); @@ -539,6 +659,7 @@ export class QuoteList extends PureComponent { rowContext.busyId = busyId; rowContext.onOpen = this.handleOpen; rowContext.onDelete = this.handleDelete; + rowContext.headerItem = this.headerItem; if (items === null && quoteAutomat.state.status === 'failed') { return ( @@ -567,17 +688,16 @@ export class QuoteList extends PureComponent { items={items || []} totalCount={items ? items.length : 0} unfilteredTotal={unfilteredTotal} + headerItem={this.headerItem} renderItem={QuoteRow} virtualScrollbar header={ - Nr. - Datum - Kunde - Pos. - Dateien - Gesamt - + {renderInvisibleDummyRow(this.headerItem)} + Nr. + Datum + Kunde + Pos. + Dateien + Gesamt + } onPageChange={this.handlePageChange} diff --git a/src/utils/documentNumber.js b/src/utils/documentNumber.js index f5c3419..be6d062 100644 --- a/src/utils/documentNumber.js +++ b/src/utils/documentNumber.js @@ -110,3 +110,55 @@ export async function ensureDocumentNumber(kind) { ]); return getNextDocumentNumber(kind); } + +/** + * Returns the maximum document number pattern for quote or order based on company sequences. + * E.g. with prefix 'ANG-' and padding 6 -> 'ANG-999999'. + * + * @param {'quote'|'order'|string} [kind='quote'] + * @returns {string} + */ +export function getMaxDocumentNumber(kind = 'quote') { + const numbers = numberAutomat.state.items ?? []; + const seq = numbers.find((r) => r.type === kind); + if (seq) { + const pad = Math.max(1, Number(seq.padding) || 6); + const prefix = seq.prefix ?? ''; + return `${prefix}${'9'.repeat(pad)}`; + } + + 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(1, Number(refSeq.padding) || 6) : 6; + const prefixStyle = refSeq?.prefix ?? ''; + + let prefix = ''; + if ( + prefixStyle.startsWith('ART-') || + prefixStyle.startsWith('LS-') || + prefixStyle.startsWith('KD-') || + prefixStyle.endsWith('-') + ) { + 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 { + prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); + } + + return `${prefix}${'9'.repeat(pad)}`; +} +