u
This commit is contained in:
@@ -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))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}` : ''}`);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,7 +848,20 @@ export class DeliveryList extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
{searchField}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||||
|
{supplierFilterOpen && (
|
||||||
|
<PartyFilterPanel
|
||||||
|
title="Lieferanten"
|
||||||
|
parties={suppliersList}
|
||||||
|
selectedId={supplierId}
|
||||||
|
onSelect={this.handleSelectSupplier}
|
||||||
|
onClose={this.handleToggleSupplierFilter}
|
||||||
|
counts={supplierCounts}
|
||||||
|
searchPlaceholder="Lieferant suchen…"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||||
<FillSpace
|
<FillSpace
|
||||||
@@ -836,6 +919,8 @@ export class DeliveryList extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<BookDeliveryDialog
|
<BookDeliveryDialog
|
||||||
open={bookingOpen}
|
open={bookingOpen}
|
||||||
|
|||||||
@@ -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,7 +755,20 @@ export class OrderList extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OrderSearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
{searchField}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||||
|
{customerFilterOpen && (
|
||||||
|
<PartyFilterPanel
|
||||||
|
title="Kunden"
|
||||||
|
parties={partiesList}
|
||||||
|
selectedId={entityId}
|
||||||
|
onSelect={this.handleSelectCustomer}
|
||||||
|
onClose={this.handleToggleCustomerFilter}
|
||||||
|
counts={customerCounts}
|
||||||
|
searchPlaceholder="Kunde suchen…"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||||
<FillSpace
|
<FillSpace
|
||||||
@@ -755,6 +835,8 @@ export class OrderList extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
306
src/components/manifest/PartyFilterPanel.jsx
Normal file
306
src/components/manifest/PartyFilterPanel.jsx
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
|
import List from '@mui/material/List';
|
||||||
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||||
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import { entityDisplayName } from './manifestConfig';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter toggle button matching the /artikel dropdown style.
|
||||||
|
*/
|
||||||
|
export class PartyFilterButton extends PureComponent {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
open,
|
||||||
|
onClick,
|
||||||
|
disabled = false,
|
||||||
|
sx,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant={open ? 'contained' : 'outlined'}
|
||||||
|
color="inherit"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={onClick}
|
||||||
|
startIcon={<FilterListIcon />}
|
||||||
|
endIcon={open ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||||
|
sx={{
|
||||||
|
height: 36,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
flexShrink: 0,
|
||||||
|
textTransform: 'none',
|
||||||
|
fontWeight: open ? 600 : 500,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active filter chip displayed next to the search/filter controls.
|
||||||
|
*/
|
||||||
|
export class PartyFilterChip extends PureComponent {
|
||||||
|
render() {
|
||||||
|
const { label, onDelete, sx } = this.props;
|
||||||
|
if (!label) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
label={label}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
onDelete={onDelete}
|
||||||
|
sx={{
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
fontWeight: 500,
|
||||||
|
flexShrink: 0,
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Side-panel list of parties (suppliers / customers) with live search and count badges.
|
||||||
|
*/
|
||||||
|
export default class PartyFilterPanel extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
search: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSearchChange = (e) => {
|
||||||
|
this.setState({ search: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClearSearch = () => {
|
||||||
|
this.setState({ search: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
getPartyName(party) {
|
||||||
|
if (!party) return '';
|
||||||
|
if (typeof party === 'string') return party;
|
||||||
|
if (party.firstName || party.customergroupId) {
|
||||||
|
return entityDisplayName(party);
|
||||||
|
}
|
||||||
|
return party.name || party.firstName || `ID #${party.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
parties = [],
|
||||||
|
selectedId,
|
||||||
|
onSelect,
|
||||||
|
onClose,
|
||||||
|
counts = {},
|
||||||
|
searchPlaceholder,
|
||||||
|
width = 250,
|
||||||
|
} = this.props;
|
||||||
|
const { search } = this.state;
|
||||||
|
|
||||||
|
const term = search.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Filter by internal search
|
||||||
|
const filtered = parties.filter((p) => {
|
||||||
|
if (!term) return true;
|
||||||
|
const name = this.getPartyName(p).toLowerCase();
|
||||||
|
const number = String(p.number ?? '').toLowerCase();
|
||||||
|
const city = String(p.city ?? '').toLowerCase();
|
||||||
|
return name.includes(term) || number.includes(term) || city.includes(term);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort: selected first, then by count descending, then alphabetically
|
||||||
|
const sorted = [...filtered].sort((a, b) => {
|
||||||
|
const aSelected = String(a.id) === String(selectedId);
|
||||||
|
const bSelected = String(b.id) === String(selectedId);
|
||||||
|
if (aSelected && !bSelected) return -1;
|
||||||
|
if (!aSelected && bSelected) return 1;
|
||||||
|
|
||||||
|
const aCount = counts[a.id] || 0;
|
||||||
|
const bCount = counts[b.id] || 0;
|
||||||
|
if (bCount !== aCount) return bCount - aCount;
|
||||||
|
|
||||||
|
const aName = this.getPartyName(a);
|
||||||
|
const bName = this.getPartyName(b);
|
||||||
|
return aName.localeCompare(bName, 'de');
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width,
|
||||||
|
flexShrink: 0,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
borderRadius: 3,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
boxShadow: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1.25,
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={onClose}
|
||||||
|
title={`${title} einklappen`}
|
||||||
|
aria-label={`${title} einklappen`}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Quick filter input */}
|
||||||
|
<Box sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider', flexShrink: 0 }}>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder={searchPlaceholder || `${title} suchen…`}
|
||||||
|
value={search}
|
||||||
|
onChange={this.handleSearchChange}
|
||||||
|
slotProps={{
|
||||||
|
input: {
|
||||||
|
sx: { height: 32, fontSize: '0.8125rem' },
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon sx={{ fontSize: 18 }} color="action" />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: search ? (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleClearSearch}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<ClearIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
) : null,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Parties list */}
|
||||||
|
<List dense disablePadding sx={{ flexGrow: 1, overflowY: 'auto' }}>
|
||||||
|
{sorted.length === 0 ? (
|
||||||
|
<Box sx={{ p: 2, textAlign: 'center' }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Keine Treffer
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
sorted.map((party) => {
|
||||||
|
const isSelected = String(party.id) === String(selectedId);
|
||||||
|
const name = this.getPartyName(party);
|
||||||
|
const count = counts[party.id] || 0;
|
||||||
|
const label = count > 0 ? `${name} (${count})` : name;
|
||||||
|
const details = party.number ? `Nr. ${party.number}` : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemButton
|
||||||
|
key={party.id}
|
||||||
|
selected={isSelected}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
onClick={() => onSelect?.(isSelected ? null : party.id)}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 0.75,
|
||||||
|
'&.Mui-selected': {
|
||||||
|
bgcolor: 'primary.light',
|
||||||
|
color: 'primary.contrastText',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={label}
|
||||||
|
secondary={details}
|
||||||
|
slotProps={{
|
||||||
|
primary: {
|
||||||
|
variant: 'body2',
|
||||||
|
noWrap: true,
|
||||||
|
sx: { fontWeight: isSelected ? 600 : 400 },
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
variant: 'caption',
|
||||||
|
noWrap: true,
|
||||||
|
sx: {
|
||||||
|
color: isSelected ? 'inherit' : 'text.secondary',
|
||||||
|
opacity: isSelected ? 0.8 : 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isSelected && (
|
||||||
|
<ClearIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{
|
||||||
|
ml: 1,
|
||||||
|
flexShrink: 0,
|
||||||
|
fontSize: 16,
|
||||||
|
color: isSelected ? 'inherit' : 'action.active',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ListItemButton>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import { backendErrorFrom } from '../../utils/backendError';
|
|||||||
import { notifyError } from '../../utils/notify';
|
import { 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,7 +731,20 @@ export class QuoteList extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<QuoteSearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
|
{searchField}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2.5, alignItems: 'stretch' }}>
|
||||||
|
{customerFilterOpen && (
|
||||||
|
<PartyFilterPanel
|
||||||
|
title="Kunden"
|
||||||
|
parties={partiesList}
|
||||||
|
selectedId={entityId}
|
||||||
|
onSelect={this.handleSelectCustomer}
|
||||||
|
onClose={this.handleToggleCustomerFilter}
|
||||||
|
counts={customerCounts}
|
||||||
|
searchPlaceholder="Kunde suchen…"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
|
||||||
<FillSpace
|
<FillSpace
|
||||||
@@ -723,6 +802,8 @@ export class QuoteList extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +1063,20 @@ 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"
|
||||||
|
parties={partiesList}
|
||||||
|
selectedId={entityId}
|
||||||
|
onSelect={this.handleSelectCustomer}
|
||||||
|
onClose={this.handleToggleCustomerFilter}
|
||||||
|
counts={customerCounts}
|
||||||
|
searchPlaceholder="Kunde suchen…"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}>
|
<PanelTile wide={3} minHeight={0} color={GREEN_PANEL} sx={{ pb: 0, position: 'relative' }}>
|
||||||
<FillSpace
|
<FillSpace
|
||||||
@@ -1066,6 +1135,8 @@ export class ShipmentList extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Modal: View / Manage Shipment Details */}
|
{/* Modal: View / Manage Shipment Details */}
|
||||||
{selectedShipment && (
|
{selectedShipment && (
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user