diff --git a/src/components/manifest/OrderList.jsx b/src/components/manifest/OrderList.jsx index e6a75e2..a312be5 100644 --- a/src/components/manifest/OrderList.jsx +++ b/src/components/manifest/OrderList.jsx @@ -14,6 +14,8 @@ import Skeleton from '@mui/material/Skeleton'; import Tooltip from '@mui/material/Tooltip'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import ViewModuleIcon from '@mui/icons-material/ViewModule'; +import PaymentsOutlinedIcon from '@mui/icons-material/PaymentsOutlined'; +import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined'; import { enqueueSnackbar } from 'notistack'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import FillSpace from 'dashgui/FillSpace'; @@ -24,10 +26,12 @@ import orderAutomat, { isOrderPaid, isOrderDelivered, } from '../../automats/orderAutomat'; +import numberAutomat from '../../automats/numberAutomat'; import { order as orderApi } 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, @@ -48,18 +52,86 @@ function orderGrossTotal(lines) { const cfg = manifestConfig('order'); -const rowSx = { +// Dummy order generator using the active sequence scheme with max digits for layout testing +export function createDummyTestOrder() { + const maxNumber = getMaxDocumentNumber('order'); + return { + id: 999999, + number: maxNumber, + externalNumber: 'REF-999999', + date: '2030-10-30T12:00:00Z', + customerName: 'Kunde', + positions: 999, + grossTotal: 30000.90, + }; +} + +const rowGridSx = { + display: 'grid', + gridTemplateColumns: 'max-content max-content 1fr max-content max-content max-content max-content auto', + gridTemplateRows: '0px 1fr', + rowGap: 0, + columnGap: 1.5, alignItems: 'center', - height: 64, - minHeight: 64, - maxHeight: 64, + width: '100%', boxSizing: 'border-box', - py: 0, - gap: 1.5, }; const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }; +const measuringCellSx = { + visibility: 'hidden', + height: 0, + boxSizing: 'border-box', + pointerEvents: 'none', + textTransform: 'none', + letterSpacing: 'normal', +}; + +const headerCellSx = { + color: 'text.secondary', + typography: 'caption', + fontWeight: 600, + textTransform: 'uppercase', + letterSpacing: '0.04em', + userSelect: 'none', +}; + +function renderInvisibleDummyRow(dummy) { + const d = dummy || createDummyTestOrder(); + return ( + <> + + {d.number} + {d.externalNumber || 'Nr. / Ref.'} + + + {d.date ? formatDate(d.date) : '–'} + + + {d.customerName} + + + + + + + + + + + + + {formatPrice(d.grossTotal)} + + + + + + + ); +} + /** * Ambient context for `OrderRow`: FillSpace renders rows as bare elements * (``), so the list publishes callbacks and the party map @@ -72,6 +144,7 @@ const rowContext = { onDelete: null, onTogglePaid: null, onToggleDelivered: null, + headerItem: null, }; // MARK: @@ -84,41 +157,28 @@ class OrderRow extends PureComponent { render() { const { item: order, disabled } = this.props; + const dummy = this.props.headerItem || rowContext.headerItem; // Data-free probe (FillSpace measures the row height with `item` undefined). if (!order) { if (disabled) { return ( - +   ); } return ( - - - - - - - - - - - - - - - - - - - - - - - - + + {renderInvisibleDummyRow(dummy)} + + + + + + + + ); } @@ -130,8 +190,15 @@ class OrderRow extends PureComponent { const delivered = isOrderDelivered(order); return ( - onOpen?.(order)}> - + onOpen?.(order)} + disabled={disabled} + > + {renderInvisibleDummyRow(dummy)} + + {order.number} @@ -140,19 +207,19 @@ class OrderRow extends PureComponent { - + {order.date ? formatDate(order.date) : '–'} - + {this.partyName(order.entityId)} - + - + - 0 ? 'text.primary' : 'text.disabled', - '& .MuiChip-label': { px: 0.5 }, - }} - /> + + 0 ? 'text.primary' : 'text.disabled', + '& .MuiChip-label': { px: 0.5 }, + }} + /> + - - {gross > 0 ? formatPrice(gross) : '—'} - + + + {gross > 0 ? formatPrice(gross) : '—'} + + - { - e.stopPropagation(); - onDelete?.(order); - }} - onMouseDown={(e) => e.stopPropagation()} - sx={{ width: 40, height: 40, flexShrink: 0 }} - > - - + + { + e.stopPropagation(); + onDelete?.(order); + }} + onMouseDown={(e) => e.stopPropagation()} + sx={{ width: 40, height: 40 }} + > + + + ); } @@ -471,6 +542,7 @@ class OrderSearchField extends PureComponent { export class OrderList extends PureComponent { constructor(props) { super(props); + this.headerItem = createDummyTestOrder(); // getItems() returns current items or null and triggers loading of the first chunk this.state = { items: orderAutomat.getItems(), @@ -497,12 +569,18 @@ export class OrderList extends PureComponent { unfilteredTotal: state.unfilteredTotal ?? 0, filterKey: orderAutomat.getFilterKey(state), })); + this.unsubNumber = numberAutomat.subscribe(this, () => { + this.headerItem = createDummyTestOrder(); + this.forceUpdate(); + }); + void numberAutomat.actions.ensureLoaded().catch(() => {}); void this.loadParties(); } componentWillUnmount() { this._cancelled = true; this.unsub?.(); + this.unsubNumber?.(); } async loadParties() { @@ -593,6 +671,7 @@ export class OrderList extends PureComponent { rowContext.onDelete = this.handleDelete; rowContext.onTogglePaid = this.handleTogglePaid; rowContext.onToggleDelivered = this.handleToggleDelivered; + rowContext.headerItem = this.headerItem; if (items === null && orderAutomat.state.status === 'failed') { return ( @@ -621,35 +700,37 @@ export class OrderList extends PureComponent { items={items || []} totalCount={items ? items.length : 0} unfilteredTotal={unfilteredTotal} + headerItem={this.headerItem} renderItem={OrderRow} virtualScrollbar header={ - Nr. / Ref. - Datum - Kunde - Bezahlt - Geliefert - Pos. - Gesamt - + {renderInvisibleDummyRow(this.headerItem)} + Nr. / Ref. + Datum + Kunde + + + + + + + + + + + Pos. + Gesamt + } onPageChange={this.handlePageChange}