u
This commit is contained in:
@@ -12,13 +12,14 @@ export const order = {
|
||||
* → { orders: Order[], total: number, offset: number, limit: number }
|
||||
*/
|
||||
// #region GET /api/order — list
|
||||
list({ offset = 0, limit = 50, search, onlyUnpaid, onlyUndelivered, seekMonths } = {}) {
|
||||
list({ offset = 0, limit = 50, search, onlyUnpaid, onlyUndelivered, seekMonths, entityId } = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (offset != null) params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0))));
|
||||
if (limit != null) params.set('limit', String(Math.max(1, Math.floor(Number(limit) || 50))));
|
||||
if (search && String(search).trim()) params.set('search', String(search).trim());
|
||||
if (onlyUnpaid === true || onlyUnpaid === 'true') params.set('onlyUnpaid', 'true');
|
||||
if (onlyUndelivered === true || onlyUndelivered === 'true') params.set('onlyUndelivered', 'true');
|
||||
if (entityId != null) params.set('entityId', String(entityId));
|
||||
if (seekMonths != null && Number(seekMonths) < 0) {
|
||||
params.set('seekMonths', String(Math.floor(Number(seekMonths))));
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ export const quote = {
|
||||
* → { quotes: QuoteSummary[], total: number, unfilteredTotal: number, offset: number, limit: number }
|
||||
*/
|
||||
// #region GET /api/quote — list
|
||||
list({ offset = 0, limit = 50, search, seekMonths } = {}) {
|
||||
list({ offset = 0, limit = 50, search, seekMonths, entityId } = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (offset != null) params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0))));
|
||||
if (limit != null) params.set('limit', String(Math.max(1, Math.floor(Number(limit) || 50))));
|
||||
if (search && String(search).trim()) params.set('search', String(search).trim());
|
||||
if (entityId != null) params.set('entityId', String(entityId));
|
||||
if (seekMonths != null && Number(seekMonths) < 0) {
|
||||
params.set('seekMonths', String(Math.floor(Number(seekMonths))));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export const shipment = {
|
||||
if (params.search) q.set('search', String(params.search));
|
||||
if (params.state) q.set('state', String(params.state));
|
||||
if (params.orderId != null) q.set('orderId', String(params.orderId));
|
||||
if (params.entityId != null) q.set('entityId', String(params.entityId));
|
||||
if (params.seekMonths != null) q.set('seekMonths', String(params.seekMonths));
|
||||
const qs = q.toString();
|
||||
return http(`/api/shipment${qs ? `?${qs}` : ''}`);
|
||||
|
||||
@@ -69,6 +69,7 @@ const initialState = {
|
||||
filter: '',
|
||||
onlyUnpaid: false,
|
||||
onlyUndelivered: false,
|
||||
entityId: null,
|
||||
seekMonths: 0,
|
||||
seekYears: 0,
|
||||
negativeSeekMonths: 0,
|
||||
@@ -117,25 +118,27 @@ export function getFilterParams(state = automat?.state || initialState) {
|
||||
const filter = String(state.filter ?? '').trim();
|
||||
const onlyUnpaid = Boolean(state.onlyUnpaid);
|
||||
const onlyUndelivered = Boolean(state.onlyUndelivered);
|
||||
const entityId = state.entityId != null && state.entityId !== '' ? Number(state.entityId) : null;
|
||||
const negativeSeekMonths = (state.negativeSeekMonths != null && Number(state.negativeSeekMonths) < 0)
|
||||
? Number(state.negativeSeekMonths)
|
||||
: 0;
|
||||
return { filter, onlyUnpaid, onlyUndelivered, negativeSeekMonths };
|
||||
return { filter, onlyUnpaid, onlyUndelivered, entityId, negativeSeekMonths };
|
||||
}
|
||||
|
||||
export function getFilterKey(state = automat?.state || initialState) {
|
||||
const { filter, onlyUnpaid, onlyUndelivered, negativeSeekMonths } = getFilterParams(state);
|
||||
return JSON.stringify({ f: filter, u: onlyUnpaid, d: onlyUndelivered, s: negativeSeekMonths });
|
||||
const { filter, onlyUnpaid, onlyUndelivered, entityId, negativeSeekMonths } = getFilterParams(state);
|
||||
return JSON.stringify({ f: filter, u: onlyUnpaid, d: onlyUndelivered, e: entityId, s: negativeSeekMonths });
|
||||
}
|
||||
|
||||
export function formatFilterLabel(filterKeyOrState) {
|
||||
let f, u, d, s;
|
||||
let f, u, d, e, s;
|
||||
if (typeof filterKeyOrState === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(filterKeyOrState);
|
||||
f = parsed.f;
|
||||
u = parsed.u;
|
||||
d = parsed.d;
|
||||
e = parsed.e;
|
||||
s = parsed.s;
|
||||
} catch {
|
||||
return filterKeyOrState;
|
||||
@@ -145,18 +148,21 @@ export function formatFilterLabel(filterKeyOrState) {
|
||||
f = filterKeyOrState.f;
|
||||
u = filterKeyOrState.u;
|
||||
d = filterKeyOrState.d;
|
||||
e = filterKeyOrState.e;
|
||||
s = filterKeyOrState.s;
|
||||
} else {
|
||||
const params = getFilterParams(filterKeyOrState);
|
||||
f = params.filter;
|
||||
u = params.onlyUnpaid;
|
||||
d = params.onlyUndelivered;
|
||||
e = params.entityId;
|
||||
s = params.negativeSeekMonths;
|
||||
}
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (f) parts.push(`Suche: „${f}“`);
|
||||
if (e != null) parts.push(`Kunde: #${e}`);
|
||||
if (u) parts.push('Nur unbezahlt');
|
||||
if (d) parts.push('Nur ungeliefert');
|
||||
if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`);
|
||||
@@ -498,6 +504,7 @@ function buildQueryParamsFor(filterParams, offset = 0, limit = CHUNK_SIZE) {
|
||||
if (trimmed) params.search = trimmed;
|
||||
if (filterParams.onlyUnpaid) params.onlyUnpaid = true;
|
||||
if (filterParams.onlyUndelivered) params.onlyUndelivered = true;
|
||||
if (filterParams.entityId != null) params.entityId = filterParams.entityId;
|
||||
if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) {
|
||||
params.seekMonths = Number(filterParams.negativeSeekMonths);
|
||||
}
|
||||
@@ -859,6 +866,12 @@ const actions = {
|
||||
onFilterChange(true);
|
||||
},
|
||||
|
||||
setEntityId(entityId) {
|
||||
const parsed = entityId != null && entityId !== '' ? Number(entityId) : null;
|
||||
automat.setState({ entityId: parsed });
|
||||
onFilterChange(true);
|
||||
},
|
||||
|
||||
addSeekMonth() {
|
||||
const seekMonths = (automat.state.seekMonths ?? 0) + 1;
|
||||
const seekYears = automat.state.seekYears ?? 0;
|
||||
|
||||
@@ -70,6 +70,7 @@ const initialState = {
|
||||
total: 0,
|
||||
unfilteredTotal: 0,
|
||||
filter: '',
|
||||
entityId: null,
|
||||
seekMonths: 0,
|
||||
seekYears: 0,
|
||||
negativeSeekMonths: 0,
|
||||
@@ -112,23 +113,25 @@ export function parseLruKey(key) {
|
||||
// MARK: pure filter helpers
|
||||
export function getFilterParams(state = automat?.state || initialState) {
|
||||
const filter = String(state.filter ?? '').trim();
|
||||
const entityId = state.entityId != null && state.entityId !== '' ? Number(state.entityId) : null;
|
||||
const negativeSeekMonths = (state.negativeSeekMonths != null && Number(state.negativeSeekMonths) < 0)
|
||||
? Number(state.negativeSeekMonths)
|
||||
: 0;
|
||||
return { filter, negativeSeekMonths };
|
||||
return { filter, entityId, negativeSeekMonths };
|
||||
}
|
||||
|
||||
export function getFilterKey(state = automat?.state || initialState) {
|
||||
const { filter, negativeSeekMonths } = getFilterParams(state);
|
||||
return JSON.stringify({ f: filter, s: negativeSeekMonths });
|
||||
const { filter, entityId, negativeSeekMonths } = getFilterParams(state);
|
||||
return JSON.stringify({ f: filter, e: entityId, s: negativeSeekMonths });
|
||||
}
|
||||
|
||||
export function formatFilterLabel(filterKeyOrState) {
|
||||
let f, s;
|
||||
let f, e, s;
|
||||
if (typeof filterKeyOrState === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(filterKeyOrState);
|
||||
f = parsed.f;
|
||||
e = parsed.e;
|
||||
s = parsed.s;
|
||||
} catch {
|
||||
return filterKeyOrState;
|
||||
@@ -136,16 +139,19 @@ export function formatFilterLabel(filterKeyOrState) {
|
||||
} else if (filterKeyOrState && typeof filterKeyOrState === 'object') {
|
||||
if ('f' in filterKeyOrState) {
|
||||
f = filterKeyOrState.f;
|
||||
e = filterKeyOrState.e;
|
||||
s = filterKeyOrState.s;
|
||||
} else {
|
||||
const params = getFilterParams(filterKeyOrState);
|
||||
f = params.filter;
|
||||
e = params.entityId;
|
||||
s = params.negativeSeekMonths;
|
||||
}
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (f) parts.push(`Suche: „${f}“`);
|
||||
if (e != null) parts.push(`Kunde: #${e}`);
|
||||
if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`);
|
||||
if (parts.length === 0) return 'Standard (Alle Angebote)';
|
||||
return parts.join(' · ');
|
||||
@@ -439,6 +445,7 @@ function buildQueryParamsFor(filterParams, offset = 0, limit = CHUNK_SIZE) {
|
||||
const params = { offset, limit };
|
||||
const trimmed = String(filterParams.filter ?? '').trim();
|
||||
if (trimmed) params.search = trimmed;
|
||||
if (filterParams.entityId != null) params.entityId = filterParams.entityId;
|
||||
if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) {
|
||||
params.seekMonths = Number(filterParams.negativeSeekMonths);
|
||||
}
|
||||
@@ -777,6 +784,12 @@ const actions = {
|
||||
onFilterChange(false);
|
||||
},
|
||||
|
||||
setEntityId(entityId) {
|
||||
const parsed = entityId != null && entityId !== '' ? Number(entityId) : null;
|
||||
automat.setState({ entityId: parsed });
|
||||
onFilterChange(true);
|
||||
},
|
||||
|
||||
addSeekMonth() {
|
||||
const seekMonths = (automat.state.seekMonths ?? 0) + 1;
|
||||
const seekYears = automat.state.seekYears ?? 0;
|
||||
|
||||
@@ -64,6 +64,7 @@ const initialState = {
|
||||
unfilteredTotal: 0,
|
||||
filter: '',
|
||||
stateFilter: '',
|
||||
entityId: null,
|
||||
seekMonths: 0,
|
||||
seekYears: 0,
|
||||
negativeSeekMonths: 0,
|
||||
@@ -107,15 +108,16 @@ export function parseLruKey(key) {
|
||||
export function getFilterParams(state = automat?.state || initialState) {
|
||||
const filter = String(state.filter ?? '').trim();
|
||||
const stateFilter = String(state.stateFilter ?? '').trim();
|
||||
const entityId = state.entityId != null && state.entityId !== '' ? Number(state.entityId) : null;
|
||||
const negativeSeekMonths = (state.negativeSeekMonths != null && Number(state.negativeSeekMonths) < 0)
|
||||
? Number(state.negativeSeekMonths)
|
||||
: 0;
|
||||
return { filter, stateFilter, negativeSeekMonths };
|
||||
return { filter, stateFilter, entityId, negativeSeekMonths };
|
||||
}
|
||||
|
||||
export function getFilterKey(state = automat?.state || initialState) {
|
||||
const { filter, stateFilter, negativeSeekMonths } = getFilterParams(state);
|
||||
return JSON.stringify({ f: filter, st: stateFilter, s: negativeSeekMonths });
|
||||
const { filter, stateFilter, entityId, negativeSeekMonths } = getFilterParams(state);
|
||||
return JSON.stringify({ f: filter, st: stateFilter, e: entityId, s: negativeSeekMonths });
|
||||
}
|
||||
|
||||
export function formatNegativeSeek(months) {
|
||||
@@ -134,12 +136,13 @@ export function formatNegativeSeek(months) {
|
||||
}
|
||||
|
||||
export function formatFilterLabel(filterKeyOrState) {
|
||||
let f, st, s;
|
||||
let f, st, e, s;
|
||||
if (typeof filterKeyOrState === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(filterKeyOrState);
|
||||
f = parsed.f;
|
||||
st = parsed.st;
|
||||
e = parsed.e;
|
||||
s = parsed.s;
|
||||
} catch {
|
||||
return filterKeyOrState;
|
||||
@@ -148,17 +151,20 @@ export function formatFilterLabel(filterKeyOrState) {
|
||||
if ('f' in filterKeyOrState) {
|
||||
f = filterKeyOrState.f;
|
||||
st = filterKeyOrState.st;
|
||||
e = filterKeyOrState.e;
|
||||
s = filterKeyOrState.s;
|
||||
} else {
|
||||
const params = getFilterParams(filterKeyOrState);
|
||||
f = params.filter;
|
||||
st = params.stateFilter;
|
||||
e = params.entityId;
|
||||
s = params.negativeSeekMonths;
|
||||
}
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
if (f) parts.push(`Suche: „${f}“`);
|
||||
if (e != null) parts.push(`Kunde: #${e}`);
|
||||
if (st) parts.push(`Status: ${st}`);
|
||||
if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`);
|
||||
if (parts.length === 0) return 'Standard (Alle Lieferscheine)';
|
||||
@@ -436,6 +442,7 @@ function buildQueryParamsFor(filterParams, offset = 0, limit = CHUNK_SIZE) {
|
||||
const trimmed = String(filterParams.filter ?? '').trim();
|
||||
if (trimmed) params.search = trimmed;
|
||||
if (filterParams.stateFilter) params.state = filterParams.stateFilter;
|
||||
if (filterParams.entityId != null) params.entityId = filterParams.entityId;
|
||||
if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) {
|
||||
params.seekMonths = Number(filterParams.negativeSeekMonths);
|
||||
}
|
||||
@@ -784,6 +791,12 @@ const actions = {
|
||||
onFilterChange(true);
|
||||
},
|
||||
|
||||
setEntityId(entityId) {
|
||||
const parsed = entityId != null && entityId !== '' ? Number(entityId) : null;
|
||||
automat.setState({ entityId: parsed });
|
||||
onFilterChange(true);
|
||||
},
|
||||
|
||||
setSeekMonths(months) {
|
||||
actions.addNegativeSeek(-months);
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { getMaxDocumentNumber } from '../../utils/documentNumber';
|
||||
import BookDeliveryDialog from './BookDeliveryDialog';
|
||||
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||
import withNavigate from '../withNavigate';
|
||||
import {
|
||||
formatDate,
|
||||
@@ -442,7 +443,14 @@ class DeliverySearchField extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputRef, onCreate } = this.props;
|
||||
const {
|
||||
inputRef,
|
||||
onCreate,
|
||||
supplierFilterOpen,
|
||||
onToggleSupplierFilter,
|
||||
selectedSupplier,
|
||||
onClearSupplierFilter,
|
||||
} = this.props;
|
||||
const { input, onlyUnbooked, seekMonths, seekYears } = this.state;
|
||||
const monthActive = seekMonths > 0;
|
||||
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
|
||||
@@ -461,22 +469,34 @@ class DeliverySearchField extends PureComponent {
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
{/* Top row: search field & create button */}
|
||||
{/* Top row: search field, supplier filter button, active chip & create button */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<SingleLineTextEdit
|
||||
sx={{ flex: 1 }}
|
||||
sx={{ flex: 1, minWidth: { xs: '100%', sm: 220 } }}
|
||||
size="small"
|
||||
search
|
||||
clear
|
||||
value={input}
|
||||
onChange={(next) => deliveryAutomat.actions.filter(next)}
|
||||
/>
|
||||
<PartyFilterButton
|
||||
title="Lieferanten"
|
||||
open={supplierFilterOpen}
|
||||
onClick={onToggleSupplierFilter}
|
||||
/>
|
||||
{selectedSupplier && (
|
||||
<PartyFilterChip
|
||||
label={partyDisplayName('delivery', selectedSupplier)}
|
||||
onDelete={onClearSupplierFilter}
|
||||
/>
|
||||
)}
|
||||
<Button variant="contained" onClick={onCreate}>
|
||||
{cfg.createButton}
|
||||
</Button>
|
||||
@@ -635,7 +655,10 @@ export class DeliveryList extends PureComponent {
|
||||
items: deliveryAutomat.getItems(),
|
||||
unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0,
|
||||
filterKey: deliveryAutomat.getFilterKey?.() ?? '',
|
||||
supplierId: deliveryAutomat.state.supplierId ?? null,
|
||||
supplierFilterOpen: false,
|
||||
suppliersById: {},
|
||||
suppliersList: [],
|
||||
busyId: null,
|
||||
bookingDoc: null,
|
||||
bookingOpen: false,
|
||||
@@ -650,6 +673,8 @@ export class DeliveryList extends PureComponent {
|
||||
this.handleOpenBookDialog = this.handleOpenBookDialog.bind(this);
|
||||
this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this);
|
||||
this.handleBookSuccess = this.handleBookSuccess.bind(this);
|
||||
this.handleToggleSupplierFilter = this.handleToggleSupplierFilter.bind(this);
|
||||
this.handleSelectSupplier = this.handleSelectSupplier.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -658,12 +683,14 @@ export class DeliveryList extends PureComponent {
|
||||
items: state.items,
|
||||
unfilteredTotal: state.unfilteredTotal ?? 0,
|
||||
filterKey: deliveryAutomat.getFilterKey(state),
|
||||
supplierId: state.supplierId ?? null,
|
||||
}));
|
||||
if (this.state.items !== deliveryAutomat.state.items) {
|
||||
this.setState({
|
||||
items: deliveryAutomat.state.items,
|
||||
unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0,
|
||||
filterKey: deliveryAutomat.getFilterKey(deliveryAutomat.state),
|
||||
supplierId: deliveryAutomat.state.supplierId ?? null,
|
||||
});
|
||||
}
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
@@ -680,11 +707,22 @@ export class DeliveryList extends PureComponent {
|
||||
this.unsubNumber?.();
|
||||
}
|
||||
|
||||
handleToggleSupplierFilter() {
|
||||
this.setState((prev) => ({ supplierFilterOpen: !prev.supplierFilterOpen }));
|
||||
}
|
||||
|
||||
handleSelectSupplier(supplierId) {
|
||||
deliveryAutomat.actions.setSupplierId(supplierId);
|
||||
}
|
||||
|
||||
async loadSuppliers() {
|
||||
try {
|
||||
const parties = await loadParties('delivery');
|
||||
if (!this._cancelled) {
|
||||
this.setState({ suppliersById: cfg.partiesByIdFromList(parties) });
|
||||
this.setState({
|
||||
suppliersById: cfg.partiesByIdFromList(parties),
|
||||
suppliersList: parties || [],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: names fall back to ids.
|
||||
@@ -750,7 +788,18 @@ export class DeliveryList extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, unfilteredTotal, filterKey, suppliersById, busyId, bookingDoc, bookingOpen } = this.state;
|
||||
const {
|
||||
items,
|
||||
unfilteredTotal,
|
||||
filterKey,
|
||||
suppliersById,
|
||||
suppliersList,
|
||||
supplierId,
|
||||
supplierFilterOpen,
|
||||
busyId,
|
||||
bookingDoc,
|
||||
bookingOpen,
|
||||
} = this.state;
|
||||
|
||||
rowContext.suppliersById = suppliersById;
|
||||
rowContext.busyId = busyId;
|
||||
@@ -759,10 +808,31 @@ export class DeliveryList extends PureComponent {
|
||||
rowContext.onOpenBookDialog = this.handleOpenBookDialog;
|
||||
rowContext.headerItem = this.headerItem;
|
||||
|
||||
const supplierCounts = {};
|
||||
if (Array.isArray(items)) {
|
||||
for (const d of items) {
|
||||
if (d?.supplierId) {
|
||||
supplierCounts[d.supplierId] = (supplierCounts[d.supplierId] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const selectedSupplier = supplierId ? (suppliersById[supplierId] || { id: supplierId, name: `Lieferant #${supplierId}` }) : null;
|
||||
|
||||
const searchField = (
|
||||
<DeliverySearchField
|
||||
inputRef={this.searchRef}
|
||||
onCreate={this.handleCreate}
|
||||
supplierFilterOpen={supplierFilterOpen}
|
||||
onToggleSupplierFilter={this.handleToggleSupplierFilter}
|
||||
selectedSupplier={selectedSupplier}
|
||||
onClearSupplierFilter={() => this.handleSelectSupplier(null)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (items === null && deliveryAutomat.state.status === 'failed') {
|
||||
return (
|
||||
<>
|
||||
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
||||
{searchField}
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
|
||||
<BackendError error={backendErrorFrom(deliveryAutomat.state.error)} onRetry={this.handleRetry} />
|
||||
@@ -778,64 +848,79 @@ export class DeliveryList extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={DeliveryRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr. / Ref.</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Lieferung</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Lieferant</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Lagerbuchung</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Anhänge</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
{searchField}
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||
{supplierFilterOpen && (
|
||||
<PartyFilterPanel
|
||||
title="Lieferanten"
|
||||
parties={suppliersList}
|
||||
selectedId={supplierId}
|
||||
onSelect={this.handleSelectSupplier}
|
||||
onClose={this.handleToggleSupplierFilter}
|
||||
counts={supplierCounts}
|
||||
searchPlaceholder="Lieferant suchen…"
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
)}
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={DeliveryRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr. / Ref.</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Lieferung</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Lieferant</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Lagerbuchung</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Anhänge</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<BookDeliveryDialog
|
||||
open={bookingOpen}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
import { getMaxDocumentNumber } from '../../utils/documentNumber';
|
||||
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||
import withNavigate from '../withNavigate';
|
||||
import {
|
||||
formatDate,
|
||||
@@ -333,7 +334,14 @@ class OrderSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputRef, onCreate } = this.props;
|
||||
const {
|
||||
inputRef,
|
||||
onCreate,
|
||||
customerFilterOpen,
|
||||
onToggleCustomerFilter,
|
||||
selectedCustomer,
|
||||
onClearCustomerFilter,
|
||||
} = this.props;
|
||||
const { input, onlyUnpaid, onlyUndelivered, seekMonths, seekYears } = this.state;
|
||||
const monthActive = seekMonths > 0;
|
||||
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
|
||||
@@ -352,22 +360,34 @@ class OrderSearchField extends PureComponent {
|
||||
pb: 2,
|
||||
}}
|
||||
>
|
||||
{/* Top row: search field & create button */}
|
||||
{/* Top row: search field, customer filter button, active chip & create button */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<SingleLineTextEdit
|
||||
sx={{ flex: 1 }}
|
||||
sx={{ flex: 1, minWidth: { xs: '100%', sm: 220 } }}
|
||||
size="small"
|
||||
search
|
||||
clear
|
||||
value={input}
|
||||
onChange={(next) => orderAutomat.actions.filter(next)}
|
||||
/>
|
||||
<PartyFilterButton
|
||||
title="Kunden"
|
||||
open={customerFilterOpen}
|
||||
onClick={onToggleCustomerFilter}
|
||||
/>
|
||||
{selectedCustomer && (
|
||||
<PartyFilterChip
|
||||
label={partyDisplayName('order', selectedCustomer)}
|
||||
onDelete={onClearCustomerFilter}
|
||||
/>
|
||||
)}
|
||||
<Button variant="contained" onClick={onCreate}>
|
||||
{cfg.createButton}
|
||||
</Button>
|
||||
@@ -548,7 +568,10 @@ export class OrderList extends PureComponent {
|
||||
items: orderAutomat.getItems(),
|
||||
unfilteredTotal: orderAutomat.state.unfilteredTotal ?? 0,
|
||||
filterKey: orderAutomat.getFilterKey?.() ?? '',
|
||||
entityId: orderAutomat.state.entityId ?? null,
|
||||
customerFilterOpen: false,
|
||||
partiesById: {},
|
||||
partiesList: [],
|
||||
busyId: null,
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
@@ -560,6 +583,8 @@ export class OrderList extends PureComponent {
|
||||
this.handleTogglePaid = this.handleTogglePaid.bind(this);
|
||||
this.handleToggleDelivered = this.handleToggleDelivered.bind(this);
|
||||
this.handlePageChange = this.handlePageChange.bind(this);
|
||||
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
|
||||
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -568,6 +593,7 @@ export class OrderList extends PureComponent {
|
||||
items: state.items,
|
||||
unfilteredTotal: state.unfilteredTotal ?? 0,
|
||||
filterKey: orderAutomat.getFilterKey(state),
|
||||
entityId: state.entityId ?? null,
|
||||
}));
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestOrder();
|
||||
@@ -583,11 +609,22 @@ export class OrderList extends PureComponent {
|
||||
this.unsubNumber?.();
|
||||
}
|
||||
|
||||
handleToggleCustomerFilter() {
|
||||
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
|
||||
}
|
||||
|
||||
handleSelectCustomer(entityId) {
|
||||
orderAutomat.actions.setEntityId(entityId);
|
||||
}
|
||||
|
||||
async loadParties() {
|
||||
try {
|
||||
const parties = await loadParties('order');
|
||||
if (!this._cancelled) {
|
||||
this.setState({ partiesById: cfg.partiesByIdFromList(parties) });
|
||||
this.setState({
|
||||
partiesById: cfg.partiesByIdFromList(parties),
|
||||
partiesList: parties || [],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: names fall back to ids.
|
||||
@@ -663,7 +700,16 @@ export class OrderList extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, unfilteredTotal, filterKey, partiesById, busyId } = this.state;
|
||||
const {
|
||||
items,
|
||||
unfilteredTotal,
|
||||
filterKey,
|
||||
partiesById,
|
||||
partiesList,
|
||||
entityId,
|
||||
customerFilterOpen,
|
||||
busyId,
|
||||
} = this.state;
|
||||
|
||||
rowContext.partiesById = partiesById;
|
||||
rowContext.busyId = busyId;
|
||||
@@ -673,10 +719,31 @@ export class OrderList extends PureComponent {
|
||||
rowContext.onToggleDelivered = this.handleToggleDelivered;
|
||||
rowContext.headerItem = this.headerItem;
|
||||
|
||||
const customerCounts = {};
|
||||
if (Array.isArray(items)) {
|
||||
for (const ord of items) {
|
||||
if (ord?.entityId) {
|
||||
customerCounts[ord.entityId] = (customerCounts[ord.entityId] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const selectedCustomer = entityId ? (partiesById[entityId] || { id: entityId, name: `Kunde #${entityId}` }) : null;
|
||||
|
||||
const searchField = (
|
||||
<OrderSearchField
|
||||
inputRef={this.searchRef}
|
||||
onCreate={this.handleCreate}
|
||||
customerFilterOpen={customerFilterOpen}
|
||||
onToggleCustomerFilter={this.handleToggleCustomerFilter}
|
||||
selectedCustomer={selectedCustomer}
|
||||
onClearCustomerFilter={() => this.handleSelectCustomer(null)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (items === null && orderAutomat.state.status === 'failed') {
|
||||
return (
|
||||
<>
|
||||
<OrderSearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
||||
{searchField}
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
|
||||
<BackendError error={backendErrorFrom(orderAutomat.state.error)} onRetry={this.handleRetry} />
|
||||
@@ -688,73 +755,88 @@ export class OrderList extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<OrderSearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={OrderRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr. / Ref.</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', ...headerCellSx }}>
|
||||
<Tooltip title="Bezahlt">
|
||||
<PaymentsOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', ...headerCellSx }}>
|
||||
<Tooltip title="Geliefert">
|
||||
<LocalShippingOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, textAlign: 'right', ...headerCellSx }}>Gesamt</Box>
|
||||
<Box sx={{ gridColumn: 8, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
{searchField}
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||
{customerFilterOpen && (
|
||||
<PartyFilterPanel
|
||||
title="Kunden"
|
||||
parties={partiesList}
|
||||
selectedId={entityId}
|
||||
onSelect={this.handleSelectCustomer}
|
||||
onClose={this.handleToggleCustomerFilter}
|
||||
counts={customerCounts}
|
||||
searchPlaceholder="Kunde suchen…"
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
)}
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={OrderRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr. / Ref.</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', ...headerCellSx }}>
|
||||
<Tooltip title="Bezahlt">
|
||||
<PaymentsOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', ...headerCellSx }}>
|
||||
<Tooltip title="Geliefert">
|
||||
<LocalShippingOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, textAlign: 'right', ...headerCellSx }}>Gesamt</Box>
|
||||
<Box sx={{ gridColumn: 8, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
306
src/components/manifest/PartyFilterPanel.jsx
Normal file
306
src/components/manifest/PartyFilterPanel.jsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { entityDisplayName } from './manifestConfig';
|
||||
|
||||
/**
|
||||
* Filter toggle button matching the /artikel dropdown style.
|
||||
*/
|
||||
export class PartyFilterButton extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
open,
|
||||
onClick,
|
||||
disabled = false,
|
||||
sx,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={open ? 'contained' : 'outlined'}
|
||||
color="inherit"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
startIcon={<FilterListIcon />}
|
||||
endIcon={open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
sx={{
|
||||
height: 36,
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
textTransform: 'none',
|
||||
fontWeight: open ? 600 : 500,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Active filter chip displayed next to the search/filter controls.
|
||||
*/
|
||||
export class PartyFilterChip extends PureComponent {
|
||||
render() {
|
||||
const { label, onDelete, sx } = this.props;
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={label}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onDelete={onDelete}
|
||||
sx={{
|
||||
height: 36,
|
||||
borderRadius: 1.5,
|
||||
fontWeight: 500,
|
||||
flexShrink: 0,
|
||||
...sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Side-panel list of parties (suppliers / customers) with live search and count badges.
|
||||
*/
|
||||
export default class PartyFilterPanel extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
search: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleSearchChange = (e) => {
|
||||
this.setState({ search: e.target.value });
|
||||
};
|
||||
|
||||
handleClearSearch = () => {
|
||||
this.setState({ search: '' });
|
||||
};
|
||||
|
||||
getPartyName(party) {
|
||||
if (!party) return '';
|
||||
if (typeof party === 'string') return party;
|
||||
if (party.firstName || party.customergroupId) {
|
||||
return entityDisplayName(party);
|
||||
}
|
||||
return party.name || party.firstName || `ID #${party.id}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
parties = [],
|
||||
selectedId,
|
||||
onSelect,
|
||||
onClose,
|
||||
counts = {},
|
||||
searchPlaceholder,
|
||||
width = 250,
|
||||
} = this.props;
|
||||
const { search } = this.state;
|
||||
|
||||
const term = search.trim().toLowerCase();
|
||||
|
||||
// Filter by internal search
|
||||
const filtered = parties.filter((p) => {
|
||||
if (!term) return true;
|
||||
const name = this.getPartyName(p).toLowerCase();
|
||||
const number = String(p.number ?? '').toLowerCase();
|
||||
const city = String(p.city ?? '').toLowerCase();
|
||||
return name.includes(term) || number.includes(term) || city.includes(term);
|
||||
});
|
||||
|
||||
// Sort: selected first, then by count descending, then alphabetically
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
const aSelected = String(a.id) === String(selectedId);
|
||||
const bSelected = String(b.id) === String(selectedId);
|
||||
if (aSelected && !bSelected) return -1;
|
||||
if (!aSelected && bSelected) return 1;
|
||||
|
||||
const aCount = counts[a.id] || 0;
|
||||
const bCount = counts[b.id] || 0;
|
||||
if (bCount !== aCount) return bCount - aCount;
|
||||
|
||||
const aName = this.getPartyName(a);
|
||||
const bName = this.getPartyName(b);
|
||||
return aName.localeCompare(bName, 'de');
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width,
|
||||
flexShrink: 0,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 3,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 1,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1.25,
|
||||
bgcolor: 'action.hover',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onClose}
|
||||
title={`${title} einklappen`}
|
||||
aria-label={`${title} einklappen`}
|
||||
>
|
||||
<ChevronLeftIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{/* Quick filter input */}
|
||||
<Box sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider', flexShrink: 0 }}>
|
||||
<TextField
|
||||
size="small"
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
placeholder={searchPlaceholder || `${title} suchen…`}
|
||||
value={search}
|
||||
onChange={this.handleSearchChange}
|
||||
slotProps={{
|
||||
input: {
|
||||
sx: { height: 32, fontSize: '0.8125rem' },
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ fontSize: 18 }} color="action" />
|
||||
</InputAdornment>
|
||||
),
|
||||
endAdornment: search ? (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={this.handleClearSearch}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<ClearIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
) : null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Parties list */}
|
||||
<List dense disablePadding sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||
{sorted.length === 0 ? (
|
||||
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Treffer
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
sorted.map((party) => {
|
||||
const isSelected = String(party.id) === String(selectedId);
|
||||
const name = this.getPartyName(party);
|
||||
const count = counts[party.id] || 0;
|
||||
const label = count > 0 ? `${name} (${count})` : name;
|
||||
const details = party.number ? `Nr. ${party.number}` : null;
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
key={party.id}
|
||||
selected={isSelected}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={() => onSelect?.(isSelected ? null : party.id)}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'primary.light',
|
||||
color: 'primary.contrastText',
|
||||
'&:hover': {
|
||||
bgcolor: 'primary.main',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={label}
|
||||
secondary={details}
|
||||
slotProps={{
|
||||
primary: {
|
||||
variant: 'body2',
|
||||
noWrap: true,
|
||||
sx: { fontWeight: isSelected ? 600 : 400 },
|
||||
},
|
||||
secondary: {
|
||||
variant: 'caption',
|
||||
noWrap: true,
|
||||
sx: {
|
||||
color: isSelected ? 'inherit' : 'text.secondary',
|
||||
opacity: isSelected ? 0.8 : 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{isSelected && (
|
||||
<ClearIcon
|
||||
fontSize="small"
|
||||
sx={{
|
||||
ml: 1,
|
||||
flexShrink: 0,
|
||||
fontSize: 16,
|
||||
color: isSelected ? 'inherit' : 'action.active',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
import { getMaxDocumentNumber } from '../../utils/documentNumber';
|
||||
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||
import withNavigate from '../withNavigate';
|
||||
import {
|
||||
formatDate,
|
||||
@@ -385,7 +386,14 @@ class QuoteSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputRef, onCreate } = this.props;
|
||||
const {
|
||||
inputRef,
|
||||
onCreate,
|
||||
customerFilterOpen,
|
||||
onToggleCustomerFilter,
|
||||
selectedCustomer,
|
||||
onClearCustomerFilter,
|
||||
} = this.props;
|
||||
const { input, seekMonths, seekYears } = this.state;
|
||||
const monthActive = seekMonths > 0;
|
||||
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
|
||||
@@ -420,6 +428,17 @@ class QuoteSearchField extends PureComponent {
|
||||
value={input}
|
||||
onChange={(next) => quoteAutomat.actions.filter(next)}
|
||||
/>
|
||||
<PartyFilterButton
|
||||
title="Kunden"
|
||||
open={customerFilterOpen}
|
||||
onClick={onToggleCustomerFilter}
|
||||
/>
|
||||
{selectedCustomer && (
|
||||
<PartyFilterChip
|
||||
label={partyDisplayName('quote', selectedCustomer)}
|
||||
onDelete={onClearCustomerFilter}
|
||||
/>
|
||||
)}
|
||||
<Button variant="contained" onClick={onCreate}>
|
||||
{cfg.createButton}
|
||||
</Button>
|
||||
@@ -549,7 +568,10 @@ export class QuoteList extends PureComponent {
|
||||
items: quoteAutomat.getItems(),
|
||||
unfilteredTotal: quoteAutomat.state.unfilteredTotal ?? 0,
|
||||
filterKey: quoteAutomat.getFilterKey?.() ?? '',
|
||||
entityId: quoteAutomat.state.entityId ?? null,
|
||||
customerFilterOpen: false,
|
||||
partiesById: {},
|
||||
partiesList: [],
|
||||
busyId: null,
|
||||
};
|
||||
this.searchRef = createRef();
|
||||
@@ -559,6 +581,8 @@ export class QuoteList extends PureComponent {
|
||||
this.handleOpen = this.handleOpen.bind(this);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
this.handlePageChange = this.handlePageChange.bind(this);
|
||||
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
|
||||
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -567,6 +591,7 @@ export class QuoteList extends PureComponent {
|
||||
items: state.items,
|
||||
unfilteredTotal: state.unfilteredTotal ?? 0,
|
||||
filterKey: quoteAutomat.getFilterKey(state),
|
||||
entityId: state.entityId ?? null,
|
||||
}));
|
||||
this.unsubNumber = numberAutomat.subscribe(this, () => {
|
||||
this.headerItem = createDummyTestQuote();
|
||||
@@ -582,11 +607,22 @@ export class QuoteList extends PureComponent {
|
||||
this.unsubNumber?.();
|
||||
}
|
||||
|
||||
handleToggleCustomerFilter() {
|
||||
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
|
||||
}
|
||||
|
||||
handleSelectCustomer(entityId) {
|
||||
quoteAutomat.actions.setEntityId(entityId);
|
||||
}
|
||||
|
||||
async loadParties() {
|
||||
try {
|
||||
const parties = await loadParties('quote');
|
||||
if (!this._cancelled) {
|
||||
this.setState({ partiesById: cfg.partiesByIdFromList(parties) });
|
||||
this.setState({
|
||||
partiesById: cfg.partiesByIdFromList(parties),
|
||||
partiesList: parties || [],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: names fall back to ids or customerName.
|
||||
@@ -642,7 +678,16 @@ export class QuoteList extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { items, unfilteredTotal, filterKey, partiesById, busyId } = this.state;
|
||||
const {
|
||||
items,
|
||||
unfilteredTotal,
|
||||
filterKey,
|
||||
entityId,
|
||||
customerFilterOpen,
|
||||
partiesById,
|
||||
partiesList,
|
||||
busyId,
|
||||
} = this.state;
|
||||
|
||||
rowContext.partiesById = partiesById;
|
||||
rowContext.busyId = busyId;
|
||||
@@ -650,10 +695,31 @@ export class QuoteList extends PureComponent {
|
||||
rowContext.onDelete = this.handleDelete;
|
||||
rowContext.headerItem = this.headerItem;
|
||||
|
||||
const customerCounts = {};
|
||||
if (Array.isArray(items)) {
|
||||
for (const q of items) {
|
||||
if (q?.entityId) {
|
||||
customerCounts[q.entityId] = (customerCounts[q.entityId] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const selectedCustomer = entityId ? (partiesById[entityId] || { id: entityId, name: `Kunde #${entityId}` }) : null;
|
||||
|
||||
const searchField = (
|
||||
<QuoteSearchField
|
||||
inputRef={this.searchRef}
|
||||
onCreate={this.handleCreate}
|
||||
customerFilterOpen={customerFilterOpen}
|
||||
onToggleCustomerFilter={this.handleToggleCustomerFilter}
|
||||
selectedCustomer={selectedCustomer}
|
||||
onClearCustomerFilter={() => this.handleSelectCustomer(null)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (items === null && quoteAutomat.state.status === 'failed') {
|
||||
return (
|
||||
<>
|
||||
<QuoteSearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
||||
{searchField}
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
|
||||
<BackendError error={backendErrorFrom(quoteAutomat.state.error)} onRetry={this.handleRetry} />
|
||||
@@ -665,64 +731,79 @@ export class QuoteList extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<QuoteSearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={QuoteRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr.</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, textAlign: 'left', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', ...headerCellSx }}></Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, textAlign: 'right', ...headerCellSx }}>Gesamt</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
{searchField}
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||
{customerFilterOpen && (
|
||||
<PartyFilterPanel
|
||||
title="Kunden"
|
||||
parties={partiesList}
|
||||
selectedId={entityId}
|
||||
onSelect={this.handleSelectCustomer}
|
||||
onClose={this.handleToggleCustomerFilter}
|
||||
counts={customerCounts}
|
||||
searchPlaceholder="Kunde suchen…"
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
)}
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={QuoteRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr.</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, textAlign: 'left', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', ...headerCellSx }}></Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, textAlign: 'right', ...headerCellSx }}>Gesamt</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { shipment as shipmentApi } from '../../api';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { notifyError } from '../../utils/notify';
|
||||
import { getMaxDocumentNumber } from '../../utils/documentNumber';
|
||||
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||
import withNavigate from '../withNavigate';
|
||||
import ShipmentDetailDialog from './ShipmentDetailDialog';
|
||||
import { getNextShipmentNumber } from './OrderShipmentDialog';
|
||||
@@ -661,7 +662,14 @@ class ShipmentSearchField extends PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { inputRef, onCreate } = this.props;
|
||||
const {
|
||||
inputRef,
|
||||
onCreate,
|
||||
customerFilterOpen,
|
||||
onToggleCustomerFilter,
|
||||
selectedCustomer,
|
||||
onClearCustomerFilter,
|
||||
} = this.props;
|
||||
const { input, stateFilter, seekMonths, seekYears } = this.state;
|
||||
const monthActive = seekMonths > 0;
|
||||
const monthLabel = !monthActive ? '-1 Monat' : `-${seekMonths} Monat${seekMonths > 1 ? 'e' : ''}`;
|
||||
@@ -697,6 +705,17 @@ class ShipmentSearchField extends PureComponent {
|
||||
value={input}
|
||||
onChange={(next) => shipmentAutomat.actions.setFilter(next)}
|
||||
/>
|
||||
<PartyFilterButton
|
||||
title="Kunden"
|
||||
open={customerFilterOpen}
|
||||
onClick={onToggleCustomerFilter}
|
||||
/>
|
||||
{selectedCustomer && (
|
||||
<PartyFilterChip
|
||||
label={partyDisplayName('order', selectedCustomer)}
|
||||
onDelete={onClearCustomerFilter}
|
||||
/>
|
||||
)}
|
||||
<Button variant="contained" onClick={onCreate}>
|
||||
Lieferschein erstellen
|
||||
</Button>
|
||||
@@ -854,8 +873,11 @@ export class ShipmentList extends PureComponent {
|
||||
total: shipmentAutomat.state.total ?? 0,
|
||||
unfilteredTotal: shipmentAutomat.state.unfilteredTotal ?? 0,
|
||||
filterKey: shipmentAutomat.getFilterKey?.() ?? '',
|
||||
entityId: shipmentAutomat.state.entityId ?? null,
|
||||
customerFilterOpen: false,
|
||||
orders: orderAutomat.state?.items || [],
|
||||
partiesById: {},
|
||||
partiesList: [],
|
||||
selectedShipment: null,
|
||||
newShipmentModalOpen: false,
|
||||
busyId: null,
|
||||
@@ -867,6 +889,8 @@ export class ShipmentList extends PureComponent {
|
||||
this.handleCloseShipment = this.handleCloseShipment.bind(this);
|
||||
this.handleDeleteShipment = this.handleDeleteShipment.bind(this);
|
||||
this.handlePageChange = this.handlePageChange.bind(this);
|
||||
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
|
||||
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -876,6 +900,7 @@ export class ShipmentList extends PureComponent {
|
||||
total: state.total ?? 0,
|
||||
unfilteredTotal: state.unfilteredTotal ?? 0,
|
||||
filterKey: shipmentAutomat.getFilterKey(state),
|
||||
entityId: state.entityId ?? null,
|
||||
}));
|
||||
|
||||
this.unsubOrder = orderAutomat.subscribe(this, (state) => ({
|
||||
@@ -919,11 +944,22 @@ export class ShipmentList extends PureComponent {
|
||||
this.unsubNumber?.();
|
||||
}
|
||||
|
||||
handleToggleCustomerFilter() {
|
||||
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
|
||||
}
|
||||
|
||||
handleSelectCustomer(entityId) {
|
||||
shipmentAutomat.actions.setEntityId(entityId);
|
||||
}
|
||||
|
||||
async loadParties() {
|
||||
try {
|
||||
const parties = await loadParties('order');
|
||||
if (!this._cancelled) {
|
||||
this.setState({ partiesById: cfg.partiesByIdFromList(parties) });
|
||||
this.setState({
|
||||
partiesById: cfg.partiesByIdFromList(parties),
|
||||
partiesList: parties || [],
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal
|
||||
@@ -975,8 +1011,11 @@ export class ShipmentList extends PureComponent {
|
||||
total,
|
||||
unfilteredTotal,
|
||||
filterKey,
|
||||
entityId,
|
||||
customerFilterOpen,
|
||||
orders,
|
||||
partiesById,
|
||||
partiesList,
|
||||
selectedShipment,
|
||||
newShipmentModalOpen,
|
||||
busyId,
|
||||
@@ -988,10 +1027,31 @@ export class ShipmentList extends PureComponent {
|
||||
rowContext.onDelete = this.handleDeleteShipment;
|
||||
rowContext.headerItem = this.headerItem;
|
||||
|
||||
const customerCounts = {};
|
||||
if (Array.isArray(items)) {
|
||||
for (const s of items) {
|
||||
if (s?.entityId) {
|
||||
customerCounts[s.entityId] = (customerCounts[s.entityId] || 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const selectedCustomer = entityId ? (partiesById[entityId] || { id: entityId, name: `Kunde #${entityId}` }) : null;
|
||||
|
||||
const searchField = (
|
||||
<ShipmentSearchField
|
||||
inputRef={this.searchRef}
|
||||
onCreate={() => this.setState({ newShipmentModalOpen: true })}
|
||||
customerFilterOpen={customerFilterOpen}
|
||||
onToggleCustomerFilter={this.handleToggleCustomerFilter}
|
||||
selectedCustomer={selectedCustomer}
|
||||
onClearCustomerFilter={() => this.handleSelectCustomer(null)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (items === null && shipmentAutomat.state.status === 'failed') {
|
||||
return (
|
||||
<>
|
||||
<ShipmentSearchField inputRef={this.searchRef} onCreate={() => this.setState({ newShipmentModalOpen: true })} />
|
||||
{searchField}
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ p: 2 }}>
|
||||
<BackendError error={backendErrorFrom(shipmentAutomat.state.error)} onRetry={this.handleRetry} />
|
||||
@@ -1003,69 +1063,80 @@ export class ShipmentList extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShipmentSearchField
|
||||
inputRef={this.searchRef}
|
||||
onCreate={() => this.setState({ newShipmentModalOpen: true })}
|
||||
/>
|
||||
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={ShipmentRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Lieferschein / Ref</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Auftrag</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Paket / Tracking</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Status</Box>
|
||||
<Box sx={{ gridColumn: 8, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
{searchField}
|
||||
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||
{customerFilterOpen && (
|
||||
<PartyFilterPanel
|
||||
title="Kunden"
|
||||
parties={partiesList}
|
||||
selectedId={entityId}
|
||||
onSelect={this.handleSelectCustomer}
|
||||
onClose={this.handleToggleCustomerFilter}
|
||||
counts={customerCounts}
|
||||
searchPlaceholder="Kunde suchen…"
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
)}
|
||||
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||
<PanelTileGrid>
|
||||
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}>
|
||||
<FillSpace
|
||||
aboveRef={[this.searchRef]}
|
||||
reserveBottom={32}
|
||||
minPageSize={8}
|
||||
minHeight={0}
|
||||
resetKey={filterKey}
|
||||
items={items || []}
|
||||
totalCount={items ? items.length : 0}
|
||||
unfilteredTotal={unfilteredTotal}
|
||||
headerItem={this.headerItem}
|
||||
renderItem={ShipmentRow}
|
||||
virtualScrollbar
|
||||
header={
|
||||
<Box
|
||||
sx={{
|
||||
...rowGridSx,
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
px: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
{renderInvisibleDummyRow(this.headerItem)}
|
||||
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Lieferschein / Ref</Box>
|
||||
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Datum</Box>
|
||||
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Auftrag</Box>
|
||||
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Kunde</Box>
|
||||
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
|
||||
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Paket / Tracking</Box>
|
||||
<Box sx={{ gridColumn: 7, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Status</Box>
|
||||
<Box sx={{ gridColumn: 8, gridRow: 2, ...headerCellSx }} />
|
||||
</Box>
|
||||
}
|
||||
onPageChange={this.handlePageChange}
|
||||
/>
|
||||
{items === null && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 52,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 32,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<CircularProgress size={32} />
|
||||
</Box>
|
||||
)}
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Modal: View / Manage Shipment Details */}
|
||||
{selectedShipment && (
|
||||
|
||||
@@ -18,6 +18,7 @@ export { default as BookDeliveryDialog } from './BookDeliveryDialog';
|
||||
export { default as OrderShipmentDialog } from './OrderShipmentDialog';
|
||||
export { default as ShipmentDetailDialog } from './ShipmentDetailDialog';
|
||||
export { default as OrderDocumentsTiles } from './OrderDocumentsTiles';
|
||||
export { default as PartyFilterPanel, PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||
export { withManifestId } from './withManifestId';
|
||||
export {
|
||||
manifestConfig,
|
||||
|
||||
Reference in New Issue
Block a user