This commit is contained in:
seb
2026-10-19 13:24:39 +02:00
parent 3cec22e4e5
commit 883c856bdb
12 changed files with 943 additions and 275 deletions

View File

@@ -12,13 +12,14 @@ export const order = {
* → { orders: Order[], total: number, offset: number, limit: number } * → { orders: Order[], total: number, offset: number, limit: number }
*/ */
// #region GET /api/order — list // #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(); const params = new URLSearchParams();
if (offset != null) params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0)))); 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 (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 (search && String(search).trim()) params.set('search', String(search).trim());
if (onlyUnpaid === true || onlyUnpaid === 'true') params.set('onlyUnpaid', 'true'); if (onlyUnpaid === true || onlyUnpaid === 'true') params.set('onlyUnpaid', 'true');
if (onlyUndelivered === true || onlyUndelivered === 'true') params.set('onlyUndelivered', 'true'); if (onlyUndelivered === true || onlyUndelivered === 'true') params.set('onlyUndelivered', 'true');
if (entityId != null) params.set('entityId', String(entityId));
if (seekMonths != null && Number(seekMonths) < 0) { if (seekMonths != null && Number(seekMonths) < 0) {
params.set('seekMonths', String(Math.floor(Number(seekMonths)))); params.set('seekMonths', String(Math.floor(Number(seekMonths))));
} }

View File

@@ -13,11 +13,12 @@ export const quote = {
* → { quotes: QuoteSummary[], total: number, unfilteredTotal: number, offset: number, limit: number } * → { quotes: QuoteSummary[], total: number, unfilteredTotal: number, offset: number, limit: number }
*/ */
// #region GET /api/quote — list // #region GET /api/quote — list
list({ offset = 0, limit = 50, search, seekMonths } = {}) { list({ offset = 0, limit = 50, search, seekMonths, entityId } = {}) {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (offset != null) params.set('offset', String(Math.max(0, Math.floor(Number(offset) || 0)))); 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 (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 (search && String(search).trim()) params.set('search', String(search).trim());
if (entityId != null) params.set('entityId', String(entityId));
if (seekMonths != null && Number(seekMonths) < 0) { if (seekMonths != null && Number(seekMonths) < 0) {
params.set('seekMonths', String(Math.floor(Number(seekMonths)))); params.set('seekMonths', String(Math.floor(Number(seekMonths))));
} }

View File

@@ -13,6 +13,7 @@ export const shipment = {
if (params.search) q.set('search', String(params.search)); if (params.search) q.set('search', String(params.search));
if (params.state) q.set('state', String(params.state)); if (params.state) q.set('state', String(params.state));
if (params.orderId != null) q.set('orderId', String(params.orderId)); 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)); if (params.seekMonths != null) q.set('seekMonths', String(params.seekMonths));
const qs = q.toString(); const qs = q.toString();
return http(`/api/shipment${qs ? `?${qs}` : ''}`); return http(`/api/shipment${qs ? `?${qs}` : ''}`);

View File

@@ -69,6 +69,7 @@ const initialState = {
filter: '', filter: '',
onlyUnpaid: false, onlyUnpaid: false,
onlyUndelivered: false, onlyUndelivered: false,
entityId: null,
seekMonths: 0, seekMonths: 0,
seekYears: 0, seekYears: 0,
negativeSeekMonths: 0, negativeSeekMonths: 0,
@@ -117,25 +118,27 @@ export function getFilterParams(state = automat?.state || initialState) {
const filter = String(state.filter ?? '').trim(); const filter = String(state.filter ?? '').trim();
const onlyUnpaid = Boolean(state.onlyUnpaid); const onlyUnpaid = Boolean(state.onlyUnpaid);
const onlyUndelivered = Boolean(state.onlyUndelivered); 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) const negativeSeekMonths = (state.negativeSeekMonths != null && Number(state.negativeSeekMonths) < 0)
? Number(state.negativeSeekMonths) ? Number(state.negativeSeekMonths)
: 0; : 0;
return { filter, onlyUnpaid, onlyUndelivered, negativeSeekMonths }; return { filter, onlyUnpaid, onlyUndelivered, entityId, negativeSeekMonths };
} }
export function getFilterKey(state = automat?.state || initialState) { export function getFilterKey(state = automat?.state || initialState) {
const { filter, onlyUnpaid, onlyUndelivered, negativeSeekMonths } = getFilterParams(state); const { filter, onlyUnpaid, onlyUndelivered, entityId, negativeSeekMonths } = getFilterParams(state);
return JSON.stringify({ f: filter, u: onlyUnpaid, d: onlyUndelivered, s: negativeSeekMonths }); return JSON.stringify({ f: filter, u: onlyUnpaid, d: onlyUndelivered, e: entityId, s: negativeSeekMonths });
} }
export function formatFilterLabel(filterKeyOrState) { export function formatFilterLabel(filterKeyOrState) {
let f, u, d, s; let f, u, d, e, s;
if (typeof filterKeyOrState === 'string') { if (typeof filterKeyOrState === 'string') {
try { try {
const parsed = JSON.parse(filterKeyOrState); const parsed = JSON.parse(filterKeyOrState);
f = parsed.f; f = parsed.f;
u = parsed.u; u = parsed.u;
d = parsed.d; d = parsed.d;
e = parsed.e;
s = parsed.s; s = parsed.s;
} catch { } catch {
return filterKeyOrState; return filterKeyOrState;
@@ -145,18 +148,21 @@ export function formatFilterLabel(filterKeyOrState) {
f = filterKeyOrState.f; f = filterKeyOrState.f;
u = filterKeyOrState.u; u = filterKeyOrState.u;
d = filterKeyOrState.d; d = filterKeyOrState.d;
e = filterKeyOrState.e;
s = filterKeyOrState.s; s = filterKeyOrState.s;
} else { } else {
const params = getFilterParams(filterKeyOrState); const params = getFilterParams(filterKeyOrState);
f = params.filter; f = params.filter;
u = params.onlyUnpaid; u = params.onlyUnpaid;
d = params.onlyUndelivered; d = params.onlyUndelivered;
e = params.entityId;
s = params.negativeSeekMonths; s = params.negativeSeekMonths;
} }
} }
const parts = []; const parts = [];
if (f) parts.push(`Suche: „${f}“`); if (f) parts.push(`Suche: „${f}“`);
if (e != null) parts.push(`Kunde: #${e}`);
if (u) parts.push('Nur unbezahlt'); if (u) parts.push('Nur unbezahlt');
if (d) parts.push('Nur ungeliefert'); if (d) parts.push('Nur ungeliefert');
if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`); 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 (trimmed) params.search = trimmed;
if (filterParams.onlyUnpaid) params.onlyUnpaid = true; if (filterParams.onlyUnpaid) params.onlyUnpaid = true;
if (filterParams.onlyUndelivered) params.onlyUndelivered = true; if (filterParams.onlyUndelivered) params.onlyUndelivered = true;
if (filterParams.entityId != null) params.entityId = filterParams.entityId;
if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) { if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) {
params.seekMonths = Number(filterParams.negativeSeekMonths); params.seekMonths = Number(filterParams.negativeSeekMonths);
} }
@@ -859,6 +866,12 @@ const actions = {
onFilterChange(true); onFilterChange(true);
}, },
setEntityId(entityId) {
const parsed = entityId != null && entityId !== '' ? Number(entityId) : null;
automat.setState({ entityId: parsed });
onFilterChange(true);
},
addSeekMonth() { addSeekMonth() {
const seekMonths = (automat.state.seekMonths ?? 0) + 1; const seekMonths = (automat.state.seekMonths ?? 0) + 1;
const seekYears = automat.state.seekYears ?? 0; const seekYears = automat.state.seekYears ?? 0;

View File

@@ -70,6 +70,7 @@ const initialState = {
total: 0, total: 0,
unfilteredTotal: 0, unfilteredTotal: 0,
filter: '', filter: '',
entityId: null,
seekMonths: 0, seekMonths: 0,
seekYears: 0, seekYears: 0,
negativeSeekMonths: 0, negativeSeekMonths: 0,
@@ -112,23 +113,25 @@ export function parseLruKey(key) {
// MARK: pure filter helpers // MARK: pure filter helpers
export function getFilterParams(state = automat?.state || initialState) { export function getFilterParams(state = automat?.state || initialState) {
const filter = String(state.filter ?? '').trim(); 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) const negativeSeekMonths = (state.negativeSeekMonths != null && Number(state.negativeSeekMonths) < 0)
? Number(state.negativeSeekMonths) ? Number(state.negativeSeekMonths)
: 0; : 0;
return { filter, negativeSeekMonths }; return { filter, entityId, negativeSeekMonths };
} }
export function getFilterKey(state = automat?.state || initialState) { export function getFilterKey(state = automat?.state || initialState) {
const { filter, negativeSeekMonths } = getFilterParams(state); const { filter, entityId, negativeSeekMonths } = getFilterParams(state);
return JSON.stringify({ f: filter, s: negativeSeekMonths }); return JSON.stringify({ f: filter, e: entityId, s: negativeSeekMonths });
} }
export function formatFilterLabel(filterKeyOrState) { export function formatFilterLabel(filterKeyOrState) {
let f, s; let f, e, s;
if (typeof filterKeyOrState === 'string') { if (typeof filterKeyOrState === 'string') {
try { try {
const parsed = JSON.parse(filterKeyOrState); const parsed = JSON.parse(filterKeyOrState);
f = parsed.f; f = parsed.f;
e = parsed.e;
s = parsed.s; s = parsed.s;
} catch { } catch {
return filterKeyOrState; return filterKeyOrState;
@@ -136,16 +139,19 @@ export function formatFilterLabel(filterKeyOrState) {
} else if (filterKeyOrState && typeof filterKeyOrState === 'object') { } else if (filterKeyOrState && typeof filterKeyOrState === 'object') {
if ('f' in filterKeyOrState) { if ('f' in filterKeyOrState) {
f = filterKeyOrState.f; f = filterKeyOrState.f;
e = filterKeyOrState.e;
s = filterKeyOrState.s; s = filterKeyOrState.s;
} else { } else {
const params = getFilterParams(filterKeyOrState); const params = getFilterParams(filterKeyOrState);
f = params.filter; f = params.filter;
e = params.entityId;
s = params.negativeSeekMonths; s = params.negativeSeekMonths;
} }
} }
const parts = []; const parts = [];
if (f) parts.push(`Suche: „${f}“`); if (f) parts.push(`Suche: „${f}“`);
if (e != null) parts.push(`Kunde: #${e}`);
if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`); if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`);
if (parts.length === 0) return 'Standard (Alle Angebote)'; if (parts.length === 0) return 'Standard (Alle Angebote)';
return parts.join(' · '); return parts.join(' · ');
@@ -439,6 +445,7 @@ function buildQueryParamsFor(filterParams, offset = 0, limit = CHUNK_SIZE) {
const params = { offset, limit }; const params = { offset, limit };
const trimmed = String(filterParams.filter ?? '').trim(); const trimmed = String(filterParams.filter ?? '').trim();
if (trimmed) params.search = trimmed; if (trimmed) params.search = trimmed;
if (filterParams.entityId != null) params.entityId = filterParams.entityId;
if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) { if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) {
params.seekMonths = Number(filterParams.negativeSeekMonths); params.seekMonths = Number(filterParams.negativeSeekMonths);
} }
@@ -777,6 +784,12 @@ const actions = {
onFilterChange(false); onFilterChange(false);
}, },
setEntityId(entityId) {
const parsed = entityId != null && entityId !== '' ? Number(entityId) : null;
automat.setState({ entityId: parsed });
onFilterChange(true);
},
addSeekMonth() { addSeekMonth() {
const seekMonths = (automat.state.seekMonths ?? 0) + 1; const seekMonths = (automat.state.seekMonths ?? 0) + 1;
const seekYears = automat.state.seekYears ?? 0; const seekYears = automat.state.seekYears ?? 0;

View File

@@ -64,6 +64,7 @@ const initialState = {
unfilteredTotal: 0, unfilteredTotal: 0,
filter: '', filter: '',
stateFilter: '', stateFilter: '',
entityId: null,
seekMonths: 0, seekMonths: 0,
seekYears: 0, seekYears: 0,
negativeSeekMonths: 0, negativeSeekMonths: 0,
@@ -107,15 +108,16 @@ export function parseLruKey(key) {
export function getFilterParams(state = automat?.state || initialState) { export function getFilterParams(state = automat?.state || initialState) {
const filter = String(state.filter ?? '').trim(); const filter = String(state.filter ?? '').trim();
const stateFilter = String(state.stateFilter ?? '').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) const negativeSeekMonths = (state.negativeSeekMonths != null && Number(state.negativeSeekMonths) < 0)
? Number(state.negativeSeekMonths) ? Number(state.negativeSeekMonths)
: 0; : 0;
return { filter, stateFilter, negativeSeekMonths }; return { filter, stateFilter, entityId, negativeSeekMonths };
} }
export function getFilterKey(state = automat?.state || initialState) { export function getFilterKey(state = automat?.state || initialState) {
const { filter, stateFilter, negativeSeekMonths } = getFilterParams(state); const { filter, stateFilter, entityId, negativeSeekMonths } = getFilterParams(state);
return JSON.stringify({ f: filter, st: stateFilter, s: negativeSeekMonths }); return JSON.stringify({ f: filter, st: stateFilter, e: entityId, s: negativeSeekMonths });
} }
export function formatNegativeSeek(months) { export function formatNegativeSeek(months) {
@@ -134,12 +136,13 @@ export function formatNegativeSeek(months) {
} }
export function formatFilterLabel(filterKeyOrState) { export function formatFilterLabel(filterKeyOrState) {
let f, st, s; let f, st, e, s;
if (typeof filterKeyOrState === 'string') { if (typeof filterKeyOrState === 'string') {
try { try {
const parsed = JSON.parse(filterKeyOrState); const parsed = JSON.parse(filterKeyOrState);
f = parsed.f; f = parsed.f;
st = parsed.st; st = parsed.st;
e = parsed.e;
s = parsed.s; s = parsed.s;
} catch { } catch {
return filterKeyOrState; return filterKeyOrState;
@@ -148,17 +151,20 @@ export function formatFilterLabel(filterKeyOrState) {
if ('f' in filterKeyOrState) { if ('f' in filterKeyOrState) {
f = filterKeyOrState.f; f = filterKeyOrState.f;
st = filterKeyOrState.st; st = filterKeyOrState.st;
e = filterKeyOrState.e;
s = filterKeyOrState.s; s = filterKeyOrState.s;
} else { } else {
const params = getFilterParams(filterKeyOrState); const params = getFilterParams(filterKeyOrState);
f = params.filter; f = params.filter;
st = params.stateFilter; st = params.stateFilter;
e = params.entityId;
s = params.negativeSeekMonths; s = params.negativeSeekMonths;
} }
} }
const parts = []; const parts = [];
if (f) parts.push(`Suche: „${f}“`); if (f) parts.push(`Suche: „${f}“`);
if (e != null) parts.push(`Kunde: #${e}`);
if (st) parts.push(`Status: ${st}`); if (st) parts.push(`Status: ${st}`);
if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`); if (s < 0) parts.push(`Seek: ${formatNegativeSeek(s)}`);
if (parts.length === 0) return 'Standard (Alle Lieferscheine)'; 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(); const trimmed = String(filterParams.filter ?? '').trim();
if (trimmed) params.search = trimmed; if (trimmed) params.search = trimmed;
if (filterParams.stateFilter) params.state = filterParams.stateFilter; if (filterParams.stateFilter) params.state = filterParams.stateFilter;
if (filterParams.entityId != null) params.entityId = filterParams.entityId;
if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) { if (filterParams.negativeSeekMonths != null && Number(filterParams.negativeSeekMonths) < 0) {
params.seekMonths = Number(filterParams.negativeSeekMonths); params.seekMonths = Number(filterParams.negativeSeekMonths);
} }
@@ -784,6 +791,12 @@ const actions = {
onFilterChange(true); onFilterChange(true);
}, },
setEntityId(entityId) {
const parsed = entityId != null && entityId !== '' ? Number(entityId) : null;
automat.setState({ entityId: parsed });
onFilterChange(true);
},
setSeekMonths(months) { setSeekMonths(months) {
actions.addNegativeSeek(-months); actions.addNegativeSeek(-months);
}, },

View File

@@ -29,6 +29,7 @@ import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
import { getMaxDocumentNumber } from '../../utils/documentNumber'; import { getMaxDocumentNumber } from '../../utils/documentNumber';
import BookDeliveryDialog from './BookDeliveryDialog'; import BookDeliveryDialog from './BookDeliveryDialog';
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
import withNavigate from '../withNavigate'; import withNavigate from '../withNavigate';
import { import {
formatDate, formatDate,
@@ -442,7 +443,14 @@ class DeliverySearchField extends PureComponent {
} }
render() { render() {
const { inputRef, onCreate } = this.props; const {
inputRef,
onCreate,
supplierFilterOpen,
onToggleSupplierFilter,
selectedSupplier,
onClearSupplierFilter,
} = this.props;
const { input, onlyUnbooked, seekMonths, seekYears } = this.state; const { input, onlyUnbooked, seekMonths, seekYears } = this.state;
const monthActive = seekMonths > 0; const monthActive = seekMonths > 0;
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`; const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
@@ -461,22 +469,34 @@ class DeliverySearchField extends PureComponent {
pb: 2, pb: 2,
}} }}
> >
{/* Top row: search field & create button */} {/* Top row: search field, supplier filter button, active chip & create button */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 1, gap: 1,
flexWrap: 'wrap',
}} }}
> >
<SingleLineTextEdit <SingleLineTextEdit
sx={{ flex: 1 }} sx={{ flex: 1, minWidth: { xs: '100%', sm: 220 } }}
size="small" size="small"
search search
clear clear
value={input} value={input}
onChange={(next) => deliveryAutomat.actions.filter(next)} 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}> <Button variant="contained" onClick={onCreate}>
{cfg.createButton} {cfg.createButton}
</Button> </Button>
@@ -635,7 +655,10 @@ export class DeliveryList extends PureComponent {
items: deliveryAutomat.getItems(), items: deliveryAutomat.getItems(),
unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0, unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0,
filterKey: deliveryAutomat.getFilterKey?.() ?? '', filterKey: deliveryAutomat.getFilterKey?.() ?? '',
supplierId: deliveryAutomat.state.supplierId ?? null,
supplierFilterOpen: false,
suppliersById: {}, suppliersById: {},
suppliersList: [],
busyId: null, busyId: null,
bookingDoc: null, bookingDoc: null,
bookingOpen: false, bookingOpen: false,
@@ -650,6 +673,8 @@ export class DeliveryList extends PureComponent {
this.handleOpenBookDialog = this.handleOpenBookDialog.bind(this); this.handleOpenBookDialog = this.handleOpenBookDialog.bind(this);
this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this); this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this);
this.handleBookSuccess = this.handleBookSuccess.bind(this); this.handleBookSuccess = this.handleBookSuccess.bind(this);
this.handleToggleSupplierFilter = this.handleToggleSupplierFilter.bind(this);
this.handleSelectSupplier = this.handleSelectSupplier.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -658,12 +683,14 @@ export class DeliveryList extends PureComponent {
items: state.items, items: state.items,
unfilteredTotal: state.unfilteredTotal ?? 0, unfilteredTotal: state.unfilteredTotal ?? 0,
filterKey: deliveryAutomat.getFilterKey(state), filterKey: deliveryAutomat.getFilterKey(state),
supplierId: state.supplierId ?? null,
})); }));
if (this.state.items !== deliveryAutomat.state.items) { if (this.state.items !== deliveryAutomat.state.items) {
this.setState({ this.setState({
items: deliveryAutomat.state.items, items: deliveryAutomat.state.items,
unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0, unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0,
filterKey: deliveryAutomat.getFilterKey(deliveryAutomat.state), filterKey: deliveryAutomat.getFilterKey(deliveryAutomat.state),
supplierId: deliveryAutomat.state.supplierId ?? null,
}); });
} }
this.unsubNumber = numberAutomat.subscribe(this, () => { this.unsubNumber = numberAutomat.subscribe(this, () => {
@@ -680,11 +707,22 @@ export class DeliveryList extends PureComponent {
this.unsubNumber?.(); this.unsubNumber?.();
} }
handleToggleSupplierFilter() {
this.setState((prev) => ({ supplierFilterOpen: !prev.supplierFilterOpen }));
}
handleSelectSupplier(supplierId) {
deliveryAutomat.actions.setSupplierId(supplierId);
}
async loadSuppliers() { async loadSuppliers() {
try { try {
const parties = await loadParties('delivery'); const parties = await loadParties('delivery');
if (!this._cancelled) { if (!this._cancelled) {
this.setState({ suppliersById: cfg.partiesByIdFromList(parties) }); this.setState({
suppliersById: cfg.partiesByIdFromList(parties),
suppliersList: parties || [],
});
} }
} catch { } catch {
// Non-fatal: names fall back to ids. // Non-fatal: names fall back to ids.
@@ -750,7 +788,18 @@ export class DeliveryList extends PureComponent {
} }
render() { 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.suppliersById = suppliersById;
rowContext.busyId = busyId; rowContext.busyId = busyId;
@@ -759,10 +808,31 @@ export class DeliveryList extends PureComponent {
rowContext.onOpenBookDialog = this.handleOpenBookDialog; rowContext.onOpenBookDialog = this.handleOpenBookDialog;
rowContext.headerItem = this.headerItem; 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') { if (items === null && deliveryAutomat.state.status === 'failed') {
return ( return (
<> <>
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} /> {searchField}
<PanelTileGrid> <PanelTileGrid>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}> <PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(deliveryAutomat.state.error)} onRetry={this.handleRetry} /> <BackendError error={backendErrorFrom(deliveryAutomat.state.error)} onRetry={this.handleRetry} />
@@ -778,64 +848,79 @@ export class DeliveryList extends PureComponent {
return ( return (
<> <>
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} /> {searchField}
<PanelTileGrid> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}> {supplierFilterOpen && (
<FillSpace <PartyFilterPanel
aboveRef={[this.searchRef]} title="Lieferanten"
reserveBottom={32} parties={suppliersList}
minPageSize={8} selectedId={supplierId}
minHeight={0} onSelect={this.handleSelectSupplier}
resetKey={filterKey} onClose={this.handleToggleSupplierFilter}
items={items || []} counts={supplierCounts}
totalCount={items ? items.length : 0} searchPlaceholder="Lieferant suchen…"
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 <Box sx={{ flexGrow: 1, minWidth: 0 }}>
sx={{ <PanelTileGrid>
position: 'absolute', <PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
top: 52, <FillSpace
left: 0, aboveRef={[this.searchRef]}
right: 0, reserveBottom={32}
bottom: 32, minPageSize={8}
display: 'flex', minHeight={0}
alignItems: 'center', resetKey={filterKey}
justifyContent: 'center', items={items || []}
pointerEvents: 'none', totalCount={items ? items.length : 0}
zIndex: 2, unfilteredTotal={unfilteredTotal}
}} headerItem={this.headerItem}
> renderItem={DeliveryRow}
<CircularProgress size={32} /> virtualScrollbar
</Box> header={
)} <Box
</PanelTile> sx={{
</PanelTileGrid> ...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 <BookDeliveryDialog
open={bookingOpen} open={bookingOpen}

View File

@@ -32,6 +32,7 @@ import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
import { formatPrice } from '../../utils/price'; import { formatPrice } from '../../utils/price';
import { getMaxDocumentNumber } from '../../utils/documentNumber'; import { getMaxDocumentNumber } from '../../utils/documentNumber';
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
import withNavigate from '../withNavigate'; import withNavigate from '../withNavigate';
import { import {
formatDate, formatDate,
@@ -333,7 +334,14 @@ class OrderSearchField extends PureComponent {
} }
render() { render() {
const { inputRef, onCreate } = this.props; const {
inputRef,
onCreate,
customerFilterOpen,
onToggleCustomerFilter,
selectedCustomer,
onClearCustomerFilter,
} = this.props;
const { input, onlyUnpaid, onlyUndelivered, seekMonths, seekYears } = this.state; const { input, onlyUnpaid, onlyUndelivered, seekMonths, seekYears } = this.state;
const monthActive = seekMonths > 0; const monthActive = seekMonths > 0;
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`; const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
@@ -352,22 +360,34 @@ class OrderSearchField extends PureComponent {
pb: 2, pb: 2,
}} }}
> >
{/* Top row: search field & create button */} {/* Top row: search field, customer filter button, active chip & create button */}
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 1, gap: 1,
flexWrap: 'wrap',
}} }}
> >
<SingleLineTextEdit <SingleLineTextEdit
sx={{ flex: 1 }} sx={{ flex: 1, minWidth: { xs: '100%', sm: 220 } }}
size="small" size="small"
search search
clear clear
value={input} value={input}
onChange={(next) => orderAutomat.actions.filter(next)} 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}> <Button variant="contained" onClick={onCreate}>
{cfg.createButton} {cfg.createButton}
</Button> </Button>
@@ -548,7 +568,10 @@ export class OrderList extends PureComponent {
items: orderAutomat.getItems(), items: orderAutomat.getItems(),
unfilteredTotal: orderAutomat.state.unfilteredTotal ?? 0, unfilteredTotal: orderAutomat.state.unfilteredTotal ?? 0,
filterKey: orderAutomat.getFilterKey?.() ?? '', filterKey: orderAutomat.getFilterKey?.() ?? '',
entityId: orderAutomat.state.entityId ?? null,
customerFilterOpen: false,
partiesById: {}, partiesById: {},
partiesList: [],
busyId: null, busyId: null,
}; };
this.searchRef = createRef(); this.searchRef = createRef();
@@ -560,6 +583,8 @@ export class OrderList extends PureComponent {
this.handleTogglePaid = this.handleTogglePaid.bind(this); this.handleTogglePaid = this.handleTogglePaid.bind(this);
this.handleToggleDelivered = this.handleToggleDelivered.bind(this); this.handleToggleDelivered = this.handleToggleDelivered.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -568,6 +593,7 @@ export class OrderList extends PureComponent {
items: state.items, items: state.items,
unfilteredTotal: state.unfilteredTotal ?? 0, unfilteredTotal: state.unfilteredTotal ?? 0,
filterKey: orderAutomat.getFilterKey(state), filterKey: orderAutomat.getFilterKey(state),
entityId: state.entityId ?? null,
})); }));
this.unsubNumber = numberAutomat.subscribe(this, () => { this.unsubNumber = numberAutomat.subscribe(this, () => {
this.headerItem = createDummyTestOrder(); this.headerItem = createDummyTestOrder();
@@ -583,11 +609,22 @@ export class OrderList extends PureComponent {
this.unsubNumber?.(); this.unsubNumber?.();
} }
handleToggleCustomerFilter() {
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
}
handleSelectCustomer(entityId) {
orderAutomat.actions.setEntityId(entityId);
}
async loadParties() { async loadParties() {
try { try {
const parties = await loadParties('order'); const parties = await loadParties('order');
if (!this._cancelled) { if (!this._cancelled) {
this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); this.setState({
partiesById: cfg.partiesByIdFromList(parties),
partiesList: parties || [],
});
} }
} catch { } catch {
// Non-fatal: names fall back to ids. // Non-fatal: names fall back to ids.
@@ -663,7 +700,16 @@ export class OrderList extends PureComponent {
} }
render() { render() {
const { items, unfilteredTotal, filterKey, partiesById, busyId } = this.state; const {
items,
unfilteredTotal,
filterKey,
partiesById,
partiesList,
entityId,
customerFilterOpen,
busyId,
} = this.state;
rowContext.partiesById = partiesById; rowContext.partiesById = partiesById;
rowContext.busyId = busyId; rowContext.busyId = busyId;
@@ -673,10 +719,31 @@ export class OrderList extends PureComponent {
rowContext.onToggleDelivered = this.handleToggleDelivered; rowContext.onToggleDelivered = this.handleToggleDelivered;
rowContext.headerItem = this.headerItem; 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') { if (items === null && orderAutomat.state.status === 'failed') {
return ( return (
<> <>
<OrderSearchField inputRef={this.searchRef} onCreate={this.handleCreate} /> {searchField}
<PanelTileGrid> <PanelTileGrid>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}> <PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(orderAutomat.state.error)} onRetry={this.handleRetry} /> <BackendError error={backendErrorFrom(orderAutomat.state.error)} onRetry={this.handleRetry} />
@@ -688,73 +755,88 @@ export class OrderList extends PureComponent {
return ( return (
<> <>
<OrderSearchField inputRef={this.searchRef} onCreate={this.handleCreate} /> {searchField}
<PanelTileGrid> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}> {customerFilterOpen && (
<FillSpace <PartyFilterPanel
aboveRef={[this.searchRef]} title="Kunden"
reserveBottom={32} parties={partiesList}
minPageSize={8} selectedId={entityId}
minHeight={0} onSelect={this.handleSelectCustomer}
resetKey={filterKey} onClose={this.handleToggleCustomerFilter}
items={items || []} counts={customerCounts}
totalCount={items ? items.length : 0} searchPlaceholder="Kunde suchen…"
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 <Box sx={{ flexGrow: 1, minWidth: 0 }}>
sx={{ <PanelTileGrid>
position: 'absolute', <PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
top: 52, <FillSpace
left: 0, aboveRef={[this.searchRef]}
right: 0, reserveBottom={32}
bottom: 32, minPageSize={8}
display: 'flex', minHeight={0}
alignItems: 'center', resetKey={filterKey}
justifyContent: 'center', items={items || []}
pointerEvents: 'none', totalCount={items ? items.length : 0}
zIndex: 2, unfilteredTotal={unfilteredTotal}
}} headerItem={this.headerItem}
> renderItem={OrderRow}
<CircularProgress size={32} /> virtualScrollbar
</Box> header={
)} <Box
</PanelTile> sx={{
</PanelTileGrid> ...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>
</> </>
); );
} }

View 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>
);
}
}

View File

@@ -24,6 +24,7 @@ import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
import { formatPrice } from '../../utils/price'; import { formatPrice } from '../../utils/price';
import { getMaxDocumentNumber } from '../../utils/documentNumber'; import { getMaxDocumentNumber } from '../../utils/documentNumber';
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
import withNavigate from '../withNavigate'; import withNavigate from '../withNavigate';
import { import {
formatDate, formatDate,
@@ -385,7 +386,14 @@ class QuoteSearchField extends PureComponent {
} }
render() { render() {
const { inputRef, onCreate } = this.props; const {
inputRef,
onCreate,
customerFilterOpen,
onToggleCustomerFilter,
selectedCustomer,
onClearCustomerFilter,
} = this.props;
const { input, seekMonths, seekYears } = this.state; const { input, seekMonths, seekYears } = this.state;
const monthActive = seekMonths > 0; const monthActive = seekMonths > 0;
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`; const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
@@ -420,6 +428,17 @@ class QuoteSearchField extends PureComponent {
value={input} value={input}
onChange={(next) => quoteAutomat.actions.filter(next)} 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}> <Button variant="contained" onClick={onCreate}>
{cfg.createButton} {cfg.createButton}
</Button> </Button>
@@ -549,7 +568,10 @@ export class QuoteList extends PureComponent {
items: quoteAutomat.getItems(), items: quoteAutomat.getItems(),
unfilteredTotal: quoteAutomat.state.unfilteredTotal ?? 0, unfilteredTotal: quoteAutomat.state.unfilteredTotal ?? 0,
filterKey: quoteAutomat.getFilterKey?.() ?? '', filterKey: quoteAutomat.getFilterKey?.() ?? '',
entityId: quoteAutomat.state.entityId ?? null,
customerFilterOpen: false,
partiesById: {}, partiesById: {},
partiesList: [],
busyId: null, busyId: null,
}; };
this.searchRef = createRef(); this.searchRef = createRef();
@@ -559,6 +581,8 @@ export class QuoteList extends PureComponent {
this.handleOpen = this.handleOpen.bind(this); this.handleOpen = this.handleOpen.bind(this);
this.handleDelete = this.handleDelete.bind(this); this.handleDelete = this.handleDelete.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -567,6 +591,7 @@ export class QuoteList extends PureComponent {
items: state.items, items: state.items,
unfilteredTotal: state.unfilteredTotal ?? 0, unfilteredTotal: state.unfilteredTotal ?? 0,
filterKey: quoteAutomat.getFilterKey(state), filterKey: quoteAutomat.getFilterKey(state),
entityId: state.entityId ?? null,
})); }));
this.unsubNumber = numberAutomat.subscribe(this, () => { this.unsubNumber = numberAutomat.subscribe(this, () => {
this.headerItem = createDummyTestQuote(); this.headerItem = createDummyTestQuote();
@@ -582,11 +607,22 @@ export class QuoteList extends PureComponent {
this.unsubNumber?.(); this.unsubNumber?.();
} }
handleToggleCustomerFilter() {
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
}
handleSelectCustomer(entityId) {
quoteAutomat.actions.setEntityId(entityId);
}
async loadParties() { async loadParties() {
try { try {
const parties = await loadParties('quote'); const parties = await loadParties('quote');
if (!this._cancelled) { if (!this._cancelled) {
this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); this.setState({
partiesById: cfg.partiesByIdFromList(parties),
partiesList: parties || [],
});
} }
} catch { } catch {
// Non-fatal: names fall back to ids or customerName. // Non-fatal: names fall back to ids or customerName.
@@ -642,7 +678,16 @@ export class QuoteList extends PureComponent {
} }
render() { render() {
const { items, unfilteredTotal, filterKey, partiesById, busyId } = this.state; const {
items,
unfilteredTotal,
filterKey,
entityId,
customerFilterOpen,
partiesById,
partiesList,
busyId,
} = this.state;
rowContext.partiesById = partiesById; rowContext.partiesById = partiesById;
rowContext.busyId = busyId; rowContext.busyId = busyId;
@@ -650,10 +695,31 @@ export class QuoteList extends PureComponent {
rowContext.onDelete = this.handleDelete; rowContext.onDelete = this.handleDelete;
rowContext.headerItem = this.headerItem; 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') { if (items === null && quoteAutomat.state.status === 'failed') {
return ( return (
<> <>
<QuoteSearchField inputRef={this.searchRef} onCreate={this.handleCreate} /> {searchField}
<PanelTileGrid> <PanelTileGrid>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}> <PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(quoteAutomat.state.error)} onRetry={this.handleRetry} /> <BackendError error={backendErrorFrom(quoteAutomat.state.error)} onRetry={this.handleRetry} />
@@ -665,64 +731,79 @@ export class QuoteList extends PureComponent {
return ( return (
<> <>
<QuoteSearchField inputRef={this.searchRef} onCreate={this.handleCreate} /> {searchField}
<PanelTileGrid> <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}> {customerFilterOpen && (
<FillSpace <PartyFilterPanel
aboveRef={[this.searchRef]} title="Kunden"
reserveBottom={32} parties={partiesList}
minPageSize={8} selectedId={entityId}
minHeight={0} onSelect={this.handleSelectCustomer}
resetKey={filterKey} onClose={this.handleToggleCustomerFilter}
items={items || []} counts={customerCounts}
totalCount={items ? items.length : 0} searchPlaceholder="Kunde suchen…"
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 <Box sx={{ flexGrow: 1, minWidth: 0 }}>
sx={{ <PanelTileGrid>
position: 'absolute', <PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
top: 52, <FillSpace
left: 0, aboveRef={[this.searchRef]}
right: 0, reserveBottom={32}
bottom: 32, minPageSize={8}
display: 'flex', minHeight={0}
alignItems: 'center', resetKey={filterKey}
justifyContent: 'center', items={items || []}
pointerEvents: 'none', totalCount={items ? items.length : 0}
zIndex: 2, unfilteredTotal={unfilteredTotal}
}} headerItem={this.headerItem}
> renderItem={QuoteRow}
<CircularProgress size={32} /> virtualScrollbar
</Box> header={
)} <Box
</PanelTile> sx={{
</PanelTileGrid> ...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>
</> </>
); );
} }

View File

@@ -29,6 +29,7 @@ import { shipment as shipmentApi } from '../../api';
import { backendErrorFrom } from '../../utils/backendError'; import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify'; import { notifyError } from '../../utils/notify';
import { getMaxDocumentNumber } from '../../utils/documentNumber'; import { getMaxDocumentNumber } from '../../utils/documentNumber';
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
import withNavigate from '../withNavigate'; import withNavigate from '../withNavigate';
import ShipmentDetailDialog from './ShipmentDetailDialog'; import ShipmentDetailDialog from './ShipmentDetailDialog';
import { getNextShipmentNumber } from './OrderShipmentDialog'; import { getNextShipmentNumber } from './OrderShipmentDialog';
@@ -661,7 +662,14 @@ class ShipmentSearchField extends PureComponent {
} }
render() { render() {
const { inputRef, onCreate } = this.props; const {
inputRef,
onCreate,
customerFilterOpen,
onToggleCustomerFilter,
selectedCustomer,
onClearCustomerFilter,
} = this.props;
const { input, stateFilter, seekMonths, seekYears } = this.state; const { input, stateFilter, seekMonths, seekYears } = this.state;
const monthActive = seekMonths > 0; const monthActive = seekMonths > 0;
const monthLabel = !monthActive ? '-1 Monat' : `-${seekMonths} Monat${seekMonths > 1 ? 'e' : ''}`; const monthLabel = !monthActive ? '-1 Monat' : `-${seekMonths} Monat${seekMonths > 1 ? 'e' : ''}`;
@@ -697,6 +705,17 @@ class ShipmentSearchField extends PureComponent {
value={input} value={input}
onChange={(next) => shipmentAutomat.actions.setFilter(next)} 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}> <Button variant="contained" onClick={onCreate}>
Lieferschein erstellen Lieferschein erstellen
</Button> </Button>
@@ -854,8 +873,11 @@ export class ShipmentList extends PureComponent {
total: shipmentAutomat.state.total ?? 0, total: shipmentAutomat.state.total ?? 0,
unfilteredTotal: shipmentAutomat.state.unfilteredTotal ?? 0, unfilteredTotal: shipmentAutomat.state.unfilteredTotal ?? 0,
filterKey: shipmentAutomat.getFilterKey?.() ?? '', filterKey: shipmentAutomat.getFilterKey?.() ?? '',
entityId: shipmentAutomat.state.entityId ?? null,
customerFilterOpen: false,
orders: orderAutomat.state?.items || [], orders: orderAutomat.state?.items || [],
partiesById: {}, partiesById: {},
partiesList: [],
selectedShipment: null, selectedShipment: null,
newShipmentModalOpen: false, newShipmentModalOpen: false,
busyId: null, busyId: null,
@@ -867,6 +889,8 @@ export class ShipmentList extends PureComponent {
this.handleCloseShipment = this.handleCloseShipment.bind(this); this.handleCloseShipment = this.handleCloseShipment.bind(this);
this.handleDeleteShipment = this.handleDeleteShipment.bind(this); this.handleDeleteShipment = this.handleDeleteShipment.bind(this);
this.handlePageChange = this.handlePageChange.bind(this); this.handlePageChange = this.handlePageChange.bind(this);
this.handleToggleCustomerFilter = this.handleToggleCustomerFilter.bind(this);
this.handleSelectCustomer = this.handleSelectCustomer.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -876,6 +900,7 @@ export class ShipmentList extends PureComponent {
total: state.total ?? 0, total: state.total ?? 0,
unfilteredTotal: state.unfilteredTotal ?? 0, unfilteredTotal: state.unfilteredTotal ?? 0,
filterKey: shipmentAutomat.getFilterKey(state), filterKey: shipmentAutomat.getFilterKey(state),
entityId: state.entityId ?? null,
})); }));
this.unsubOrder = orderAutomat.subscribe(this, (state) => ({ this.unsubOrder = orderAutomat.subscribe(this, (state) => ({
@@ -919,11 +944,22 @@ export class ShipmentList extends PureComponent {
this.unsubNumber?.(); this.unsubNumber?.();
} }
handleToggleCustomerFilter() {
this.setState((prev) => ({ customerFilterOpen: !prev.customerFilterOpen }));
}
handleSelectCustomer(entityId) {
shipmentAutomat.actions.setEntityId(entityId);
}
async loadParties() { async loadParties() {
try { try {
const parties = await loadParties('order'); const parties = await loadParties('order');
if (!this._cancelled) { if (!this._cancelled) {
this.setState({ partiesById: cfg.partiesByIdFromList(parties) }); this.setState({
partiesById: cfg.partiesByIdFromList(parties),
partiesList: parties || [],
});
} }
} catch { } catch {
// Non-fatal // Non-fatal
@@ -975,8 +1011,11 @@ export class ShipmentList extends PureComponent {
total, total,
unfilteredTotal, unfilteredTotal,
filterKey, filterKey,
entityId,
customerFilterOpen,
orders, orders,
partiesById, partiesById,
partiesList,
selectedShipment, selectedShipment,
newShipmentModalOpen, newShipmentModalOpen,
busyId, busyId,
@@ -988,10 +1027,31 @@ export class ShipmentList extends PureComponent {
rowContext.onDelete = this.handleDeleteShipment; rowContext.onDelete = this.handleDeleteShipment;
rowContext.headerItem = this.headerItem; 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') { if (items === null && shipmentAutomat.state.status === 'failed') {
return ( return (
<> <>
<ShipmentSearchField inputRef={this.searchRef} onCreate={() => this.setState({ newShipmentModalOpen: true })} /> {searchField}
<PanelTileGrid> <PanelTileGrid>
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ p: 2 }}> <PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(shipmentAutomat.state.error)} onRetry={this.handleRetry} /> <BackendError error={backendErrorFrom(shipmentAutomat.state.error)} onRetry={this.handleRetry} />
@@ -1003,69 +1063,80 @@ export class ShipmentList extends PureComponent {
return ( return (
<> <>
<ShipmentSearchField {searchField}
inputRef={this.searchRef} <Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
onCreate={() => this.setState({ newShipmentModalOpen: true })} {customerFilterOpen && (
/> <PartyFilterPanel
title="Kunden"
<PanelTileGrid> parties={partiesList}
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}> selectedId={entityId}
<FillSpace onSelect={this.handleSelectCustomer}
aboveRef={[this.searchRef]} onClose={this.handleToggleCustomerFilter}
reserveBottom={32} counts={customerCounts}
minPageSize={8} searchPlaceholder="Kunde suchen…"
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 <Box sx={{ flexGrow: 1, minWidth: 0 }}>
sx={{ <PanelTileGrid>
position: 'absolute', <PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}>
top: 52, <FillSpace
left: 0, aboveRef={[this.searchRef]}
right: 0, reserveBottom={32}
bottom: 32, minPageSize={8}
display: 'flex', minHeight={0}
alignItems: 'center', resetKey={filterKey}
justifyContent: 'center', items={items || []}
pointerEvents: 'none', totalCount={items ? items.length : 0}
zIndex: 2, unfilteredTotal={unfilteredTotal}
}} headerItem={this.headerItem}
> renderItem={ShipmentRow}
<CircularProgress size={32} /> virtualScrollbar
</Box> header={
)} <Box
</PanelTile> sx={{
</PanelTileGrid> ...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 */} {/* Modal: View / Manage Shipment Details */}
{selectedShipment && ( {selectedShipment && (

View File

@@ -18,6 +18,7 @@ export { default as BookDeliveryDialog } from './BookDeliveryDialog';
export { default as OrderShipmentDialog } from './OrderShipmentDialog'; export { default as OrderShipmentDialog } from './OrderShipmentDialog';
export { default as ShipmentDetailDialog } from './ShipmentDetailDialog'; export { default as ShipmentDetailDialog } from './ShipmentDetailDialog';
export { default as OrderDocumentsTiles } from './OrderDocumentsTiles'; export { default as OrderDocumentsTiles } from './OrderDocumentsTiles';
export { default as PartyFilterPanel, PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
export { withManifestId } from './withManifestId'; export { withManifestId } from './withManifestId';
export { export {
manifestConfig, manifestConfig,