diff --git a/src/api/order.js b/src/api/order.js
index 47f1853..299ed10 100644
--- a/src/api/order.js
+++ b/src/api/order.js
@@ -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))));
}
diff --git a/src/api/quote.js b/src/api/quote.js
index 27aae47..55af517 100644
--- a/src/api/quote.js
+++ b/src/api/quote.js
@@ -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))));
}
diff --git a/src/api/shipment.js b/src/api/shipment.js
index c02c50c..bdd3337 100644
--- a/src/api/shipment.js
+++ b/src/api/shipment.js
@@ -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}` : ''}`);
diff --git a/src/automats/orderAutomat.js b/src/automats/orderAutomat.js
index 068139f..0b9947b 100644
--- a/src/automats/orderAutomat.js
+++ b/src/automats/orderAutomat.js
@@ -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;
diff --git a/src/automats/quoteAutomat.js b/src/automats/quoteAutomat.js
index 9bff67b..dc372ff 100644
--- a/src/automats/quoteAutomat.js
+++ b/src/automats/quoteAutomat.js
@@ -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;
diff --git a/src/automats/shipmentAutomat.js b/src/automats/shipmentAutomat.js
index a56ddc8..a3927c3 100644
--- a/src/automats/shipmentAutomat.js
+++ b/src/automats/shipmentAutomat.js
@@ -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);
},
diff --git a/src/components/manifest/DeliveryList.jsx b/src/components/manifest/DeliveryList.jsx
index 3c1d272..da63c59 100644
--- a/src/components/manifest/DeliveryList.jsx
+++ b/src/components/manifest/DeliveryList.jsx
@@ -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 */}
deliveryAutomat.actions.filter(next)}
/>
+
+ {selectedSupplier && (
+
+ )}
@@ -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 = (
+ this.handleSelectSupplier(null)}
+ />
+ );
+
if (items === null && deliveryAutomat.state.status === 'failed') {
return (
<>
-
+ {searchField}
@@ -778,64 +848,79 @@ export class DeliveryList extends PureComponent {
return (
<>
-
-
-
-
- {renderInvisibleDummyRow(this.headerItem)}
- Nr. / Ref.
- Lieferung
- Lieferant
- Lagerbuchung
- Pos.
- Anhänge
-
-
- }
- onPageChange={this.handlePageChange}
+ {searchField}
+
+ {supplierFilterOpen && (
+
- {items === null && (
-
-
-
- )}
-
-
+ )}
+
+
+
+
+ {renderInvisibleDummyRow(this.headerItem)}
+ Nr. / Ref.
+ Lieferung
+ Lieferant
+ Lagerbuchung
+ Pos.
+ Anhänge
+
+
+ }
+ onPageChange={this.handlePageChange}
+ />
+ {items === null && (
+
+
+
+ )}
+
+
+
+
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 */}
orderAutomat.actions.filter(next)}
/>
+
+ {selectedCustomer && (
+
+ )}
@@ -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 = (
+ this.handleSelectCustomer(null)}
+ />
+ );
+
if (items === null && orderAutomat.state.status === 'failed') {
return (
<>
-
+ {searchField}
@@ -688,73 +755,88 @@ export class OrderList extends PureComponent {
return (
<>
-
-
-
-
- {renderInvisibleDummyRow(this.headerItem)}
- Nr. / Ref.
- Datum
- Kunde
-
-
-
-
-
-
-
-
-
-
- Pos.
- Gesamt
-
-
- }
- onPageChange={this.handlePageChange}
+ {searchField}
+
+ {customerFilterOpen && (
+
- {items === null && (
-
-
-
- )}
-
-
+ )}
+
+
+
+
+ {renderInvisibleDummyRow(this.headerItem)}
+ Nr. / Ref.
+ Datum
+ Kunde
+
+
+
+
+
+
+
+
+
+
+ Pos.
+ Gesamt
+
+
+ }
+ onPageChange={this.handlePageChange}
+ />
+ {items === null && (
+
+
+
+ )}
+
+
+
+
>
);
}
diff --git a/src/components/manifest/PartyFilterPanel.jsx b/src/components/manifest/PartyFilterPanel.jsx
new file mode 100644
index 0000000..aa85ba2
--- /dev/null
+++ b/src/components/manifest/PartyFilterPanel.jsx
@@ -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 (
+ }
+ endIcon={open ? : }
+ sx={{
+ height: 36,
+ whiteSpace: 'nowrap',
+ flexShrink: 0,
+ textTransform: 'none',
+ fontWeight: open ? 600 : 500,
+ ...sx,
+ }}
+ >
+ {title}
+
+ );
+ }
+}
+
+/**
+ * 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 (
+
+ );
+ }
+}
+
+/**
+ * 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 (
+
+
+ {/* Header */}
+
+
+ {title}
+
+
+
+
+
+
+ {/* Quick filter input */}
+
+
+
+
+ ),
+ endAdornment: search ? (
+
+ e.preventDefault()}
+ >
+
+
+
+ ) : null,
+ },
+ }}
+ />
+
+
+ {/* Parties list */}
+
+ {sorted.length === 0 ? (
+
+
+ Keine Treffer
+
+
+ ) : (
+ 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 (
+ 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',
+ },
+ },
+ }}
+ >
+
+ {isSelected && (
+
+ )}
+
+ );
+ })
+ )}
+
+
+
+ );
+ }
+}
diff --git a/src/components/manifest/QuoteList.jsx b/src/components/manifest/QuoteList.jsx
index 21ce0d1..8392a2b 100644
--- a/src/components/manifest/QuoteList.jsx
+++ b/src/components/manifest/QuoteList.jsx
@@ -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)}
/>
+
+ {selectedCustomer && (
+
+ )}
@@ -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 = (
+ this.handleSelectCustomer(null)}
+ />
+ );
+
if (items === null && quoteAutomat.state.status === 'failed') {
return (
<>
-
+ {searchField}
@@ -665,64 +731,79 @@ export class QuoteList extends PureComponent {
return (
<>
-
-
-
-
- {renderInvisibleDummyRow(this.headerItem)}
- Nr.
- Datum
- Kunde
- Pos.
-
- Gesamt
-
-
- }
- onPageChange={this.handlePageChange}
+ {searchField}
+
+ {customerFilterOpen && (
+
- {items === null && (
-
-
-
- )}
-
-
+ )}
+
+
+
+
+ {renderInvisibleDummyRow(this.headerItem)}
+ Nr.
+ Datum
+ Kunde
+ Pos.
+
+ Gesamt
+
+
+ }
+ onPageChange={this.handlePageChange}
+ />
+ {items === null && (
+
+
+
+ )}
+
+
+
+
>
);
}
diff --git a/src/components/manifest/ShipmentList.jsx b/src/components/manifest/ShipmentList.jsx
index 97f1033..38014b5 100644
--- a/src/components/manifest/ShipmentList.jsx
+++ b/src/components/manifest/ShipmentList.jsx
@@ -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)}
/>
+
+ {selectedCustomer && (
+
+ )}
@@ -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 = (
+ this.setState({ newShipmentModalOpen: true })}
+ customerFilterOpen={customerFilterOpen}
+ onToggleCustomerFilter={this.handleToggleCustomerFilter}
+ selectedCustomer={selectedCustomer}
+ onClearCustomerFilter={() => this.handleSelectCustomer(null)}
+ />
+ );
+
if (items === null && shipmentAutomat.state.status === 'failed') {
return (
<>
- this.setState({ newShipmentModalOpen: true })} />
+ {searchField}
@@ -1003,69 +1063,80 @@ export class ShipmentList extends PureComponent {
return (
<>
- this.setState({ newShipmentModalOpen: true })}
- />
-
-
-
-
- {renderInvisibleDummyRow(this.headerItem)}
- Lieferschein / Ref
- Datum
- Auftrag
- Kunde
- Pos.
- Paket / Tracking
- Status
-
-
- }
- onPageChange={this.handlePageChange}
+ {searchField}
+
+ {customerFilterOpen && (
+
- {items === null && (
-
-
-
- )}
-
-
+ )}
+
+
+
+
+ {renderInvisibleDummyRow(this.headerItem)}
+ Lieferschein / Ref
+ Datum
+ Auftrag
+ Kunde
+ Pos.
+ Paket / Tracking
+ Status
+
+
+ }
+ onPageChange={this.handlePageChange}
+ />
+ {items === null && (
+
+
+
+ )}
+
+
+
+
{/* Modal: View / Manage Shipment Details */}
{selectedShipment && (
diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js
index 12a760c..f3c8633 100644
--- a/src/components/manifest/index.js
+++ b/src/components/manifest/index.js
@@ -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,