u
This commit is contained in:
@@ -1475,9 +1475,17 @@ export const quote = {
|
|||||||
* Line shape same as quote (no transactedAmount).
|
* Line shape same as quote (no transactedAmount).
|
||||||
*/
|
*/
|
||||||
export const order = {
|
export const order = {
|
||||||
/** GET /api/order → { orders: OrderSummary[] } */
|
/**
|
||||||
list() {
|
* GET /api/order?offset=0&limit=20
|
||||||
return http('/api/order');
|
* @param {{ offset?: number, limit?: number }} [params]
|
||||||
|
* → { orders: Order[], total: number, offset: number, limit: number }
|
||||||
|
*/
|
||||||
|
list({ offset = 0, limit = 20 } = {}) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (offset != null) params.set('offset', String(offset));
|
||||||
|
if (limit != null) params.set('limit', String(limit));
|
||||||
|
const qs = params.toString();
|
||||||
|
return http(`/api/order${qs ? `?${qs}` : ''}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** GET /api/order/:id → { order: Order } */
|
/** GET /api/order/:id → { order: Order } */
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ export {
|
|||||||
} from './articleIndexAutomat';
|
} from './articleIndexAutomat';
|
||||||
export { default as deliveryAutomat } from './deliveryAutomat';
|
export { default as deliveryAutomat } from './deliveryAutomat';
|
||||||
export { default as quoteAutomat } from './quoteAutomat';
|
export { default as quoteAutomat } from './quoteAutomat';
|
||||||
export { default as orderAutomat } from './orderAutomat';
|
export {
|
||||||
|
default as orderAutomat,
|
||||||
|
selectOrders,
|
||||||
|
selectOrderTotal,
|
||||||
|
selectOrderPagination,
|
||||||
|
} from './orderAutomat';
|
||||||
export { default as entityAutomat } from './entityAutomat';
|
export { default as entityAutomat } from './entityAutomat';
|
||||||
export { default as customergroupAutomat } from './customergroupAutomat';
|
export { default as customergroupAutomat } from './customergroupAutomat';
|
||||||
export { default as numberAutomat } from './numberAutomat';
|
export { default as numberAutomat } from './numberAutomat';
|
||||||
|
|||||||
@@ -1,39 +1,236 @@
|
|||||||
/**
|
/**
|
||||||
* @file orderAutomat.js
|
* @file orderAutomat.js
|
||||||
* @description Manages cached company sales orders (`GET /api/order`).
|
* @description Manages cached and paginated company sales orders (`GET /api/order`).
|
||||||
* Built via `createListAutomat`.
|
|
||||||
*
|
*
|
||||||
* Upstream Triggers:
|
* Upstream Triggers:
|
||||||
* - `authAutomat`: Via `createListAutomat` — triggers `load()` on `'authed'` with
|
* - `authAutomat`: Triggers `actions.load()` on `'authed'` with a `selectedCompanyId`,
|
||||||
* a `selectedCompanyId`, and `reset()` on `'anon'`.
|
* and `actions.reset()` on `'anon'`.
|
||||||
* - UI / Routes: Manual calls to `load()` or `reload()`.
|
* - UI / Routes: Calls to `load({ offset, limit })`, `setPage(page)`, or `reload()`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Automat } from 'react-automat';
|
||||||
import { order as orderApi } from '../api';
|
import { order as orderApi } from '../api';
|
||||||
import { createListAutomat } from './createListAutomat';
|
import authAutomat from './authAutomat';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} OrderSummary
|
* @typedef {Object} OrderLine
|
||||||
* @property {string|number} id Order id.
|
* @property {number} id
|
||||||
|
* @property {string|null} sku
|
||||||
|
* @property {string|null} name
|
||||||
|
* @property {number} pu
|
||||||
|
* @property {number} amount
|
||||||
|
* @property {number} net
|
||||||
|
* @property {number} rebate
|
||||||
|
* @property {number} taxrate
|
||||||
|
* @property {string|null} comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Order
|
||||||
|
* @property {number} id Order id.
|
||||||
* @property {string} number Order number.
|
* @property {string} number Order number.
|
||||||
* @property {string} type Always `'order'`.
|
* @property {'order'} type Always `'order'`.
|
||||||
* @property {string|number|null} entityId Customer id.
|
* @property {number|null} entityId Customer id.
|
||||||
|
* @property {string|null} externalNumber External reference.
|
||||||
* @property {string} date Delivery date (ISO).
|
* @property {string} date Delivery date (ISO).
|
||||||
|
* @property {string} createdAt Creation timestamp (ISO).
|
||||||
|
* @property {OrderLine[]} lines Order line items.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Company sales orders (`GET /api/order`).
|
* @typedef {Object} OrderAutomatState
|
||||||
*
|
* @property {Order[]} items Loaded orders for the current page.
|
||||||
* @type {import('react-automat').Automat<import('./createListAutomat').ListAutomatState>}
|
* @property {number} total Total unpaged count for pagination calculation.
|
||||||
|
* @property {number} offset Current 0-based offset.
|
||||||
|
* @property {number} limit Current page limit.
|
||||||
|
* @property {'idle'|'loading'|'ready'|'failed'} status Load lifecycle state.
|
||||||
|
* @property {Error|string|null} error Last load error.
|
||||||
*/
|
*/
|
||||||
export const orderAutomat = createListAutomat({
|
|
||||||
name: 'order',
|
const initialState = {
|
||||||
fetch: async () => {
|
items: [],
|
||||||
const data = await orderApi.list();
|
total: 0,
|
||||||
return data.orders ?? [];
|
offset: 0,
|
||||||
|
limit: 20,
|
||||||
|
status: 'idle',
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let automat;
|
||||||
|
let inflightLoad = null;
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
/**
|
||||||
|
* Loads paginated orders, deduplicating concurrent callers.
|
||||||
|
*
|
||||||
|
* @param {{ offset?: number, limit?: number }} [params]
|
||||||
|
* @returns {Promise<Order[]>}
|
||||||
|
*/
|
||||||
|
load({ offset, limit } = {}) {
|
||||||
|
const currentOffset = offset ?? automat.state.offset ?? 0;
|
||||||
|
const currentLimit = limit ?? automat.state.limit ?? 20;
|
||||||
|
|
||||||
|
if (inflightLoad) return inflightLoad;
|
||||||
|
inflightLoad = (async () => {
|
||||||
|
automat.setState({ status: 'loading', error: null });
|
||||||
|
try {
|
||||||
|
const res = await orderApi.list({ offset: currentOffset, limit: currentLimit });
|
||||||
|
const orders = res?.orders ?? [];
|
||||||
|
automat.setState({
|
||||||
|
items: orders,
|
||||||
|
total: Number(res?.total ?? orders.length),
|
||||||
|
offset: Number(res?.offset ?? currentOffset),
|
||||||
|
limit: Number(res?.limit ?? currentLimit),
|
||||||
|
status: 'ready',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
return orders;
|
||||||
|
} catch (error) {
|
||||||
|
automat.setState({ status: 'failed', error });
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
inflightLoad = null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return inflightLoad;
|
||||||
},
|
},
|
||||||
sort: (items) =>
|
|
||||||
[...items].sort((a, b) => String(b.date).localeCompare(String(a.date))),
|
/**
|
||||||
|
* Silently re-fetches the current page of orders without flashing loading state.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Order[]>}
|
||||||
|
*/
|
||||||
|
async reload() {
|
||||||
|
const { offset, limit } = automat.state;
|
||||||
|
try {
|
||||||
|
const res = await orderApi.list({ offset, limit });
|
||||||
|
const orders = res?.orders ?? [];
|
||||||
|
automat.setState({
|
||||||
|
items: orders,
|
||||||
|
total: Number(res?.total ?? orders.length),
|
||||||
|
offset: Number(res?.offset ?? offset),
|
||||||
|
limit: Number(res?.limit ?? limit),
|
||||||
|
status: 'ready',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
return orders;
|
||||||
|
} catch (error) {
|
||||||
|
automat.setState({ error });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to a 1-based page using formula: offset = (page - 1) * limit
|
||||||
|
*
|
||||||
|
* @param {number} page 1-based page number.
|
||||||
|
* @returns {Promise<Order[]>}
|
||||||
|
*/
|
||||||
|
setPage(page) {
|
||||||
|
const limit = automat.state.limit || 20;
|
||||||
|
const offset = Math.max(0, (page - 1) * limit);
|
||||||
|
return actions.load({ offset, limit });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets pagination offset directly.
|
||||||
|
*
|
||||||
|
* @param {number} offset 0-based offset.
|
||||||
|
* @returns {Promise<Order[]>}
|
||||||
|
*/
|
||||||
|
setOffset(offset) {
|
||||||
|
return actions.load({ offset, limit: automat.state.limit });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets page limit (records to return) and resets offset to 0.
|
||||||
|
*
|
||||||
|
* @param {number} limit Records per page (1..100).
|
||||||
|
* @returns {Promise<Order[]>}
|
||||||
|
*/
|
||||||
|
setLimit(limit) {
|
||||||
|
return actions.load({ offset: 0, limit });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures orders are loaded once the selected company is known.
|
||||||
|
*
|
||||||
|
* @returns {Promise<Order[]>}
|
||||||
|
*/
|
||||||
|
ensureLoaded() {
|
||||||
|
if (automat.state.status === 'ready') {
|
||||||
|
return Promise.resolve(automat.state.items);
|
||||||
|
}
|
||||||
|
return actions.load();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces cached orders without network round-trip.
|
||||||
|
*
|
||||||
|
* @param {Order[]} items
|
||||||
|
* @returns {Order[]}
|
||||||
|
*/
|
||||||
|
replace(items) {
|
||||||
|
automat.setState({
|
||||||
|
items,
|
||||||
|
total: items.length,
|
||||||
|
status: 'ready',
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets to idle state (called on logout).
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
inflightLoad = null;
|
||||||
|
automat.setState(initialState);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const orderAutomat = new Automat(initialState, actions, { name: 'order' });
|
||||||
|
automat = orderAutomat;
|
||||||
|
|
||||||
|
// Load on session/company establishment, reset on logout.
|
||||||
|
authAutomat.subscribe((auth) => {
|
||||||
|
const companyId = auth.user?.selectedCompanyId;
|
||||||
|
if (auth.status === 'authed' && companyId) {
|
||||||
|
void actions.load().catch(() => {});
|
||||||
|
} else if (auth.status === 'anon') {
|
||||||
|
actions.reset();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Selects current orders */
|
||||||
|
export function selectOrders(state = automat.state) {
|
||||||
|
return state.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Selects total unpaged count */
|
||||||
|
export function selectOrderTotal(state = automat.state) {
|
||||||
|
return state.total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates client pagination metadata.
|
||||||
|
* Current Page: Math.floor(offset / limit) + 1
|
||||||
|
* Total Pages: Math.ceil(total / limit)
|
||||||
|
* Next Offset: (page - 1) * limit
|
||||||
|
*/
|
||||||
|
export function selectOrderPagination(state = automat.state) {
|
||||||
|
const total = state.total ?? 0;
|
||||||
|
const offset = state.offset ?? 0;
|
||||||
|
const limit = state.limit || 20;
|
||||||
|
const currentPage = Math.floor(offset / limit) + 1;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / limit));
|
||||||
|
return {
|
||||||
|
total,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
currentPage,
|
||||||
|
pageCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default orderAutomat;
|
export default orderAutomat;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ManifestLines from './ManifestLines';
|
|||||||
import ManifestPdfDialog from './ManifestPdfDialog';
|
import ManifestPdfDialog from './ManifestPdfDialog';
|
||||||
import ArticleSearchDialog from './ArticleSearchDialog';
|
import ArticleSearchDialog from './ArticleSearchDialog';
|
||||||
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||||||
|
import { netFromGross, getArticleGross } from '../../utils/price';
|
||||||
import {
|
import {
|
||||||
buildManifestBody,
|
buildManifestBody,
|
||||||
restoreAttachmentsFromApi,
|
restoreAttachmentsFromApi,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
loadParties,
|
loadParties,
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
apiFor,
|
apiFor,
|
||||||
|
parseNum,
|
||||||
} from './manifestConfig';
|
} from './manifestConfig';
|
||||||
|
|
||||||
class ManifestForm extends PureComponent {
|
class ManifestForm extends PureComponent {
|
||||||
@@ -195,18 +197,70 @@ class ManifestForm extends PureComponent {
|
|||||||
|
|
||||||
handleLineChange(key, field, value) {
|
handleLineChange(key, field, value) {
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
lines: prev.lines.map((line) =>
|
lines: prev.lines.map((line) => {
|
||||||
line.key === key
|
if (line.key !== key) return line;
|
||||||
? {
|
|
||||||
...line,
|
if (field === 'gross') {
|
||||||
[field]: value,
|
const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0;
|
||||||
...(field === 'net' ? { netprice: value } : {}),
|
const grossNum = parseNum(value);
|
||||||
...(field === 'netprice' ? { net: value } : {}),
|
let updatedNet = line.net;
|
||||||
...(field === 'tax' ? { taxrate: value } : {}),
|
if (grossNum != null) {
|
||||||
...(field === 'taxrate' ? { tax: value } : {}),
|
const netNum = netFromGross(grossNum, taxVal);
|
||||||
|
if (netNum != null) {
|
||||||
|
updatedNet = String(netNum);
|
||||||
}
|
}
|
||||||
: line,
|
} else if (value === '') {
|
||||||
),
|
updatedNet = '';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
gross: value,
|
||||||
|
net: updatedNet,
|
||||||
|
netprice: updatedNet,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'net' || field === 'netprice') {
|
||||||
|
const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0;
|
||||||
|
const netNum = parseNum(value);
|
||||||
|
let updatedGross = line.gross;
|
||||||
|
if (netNum != null) {
|
||||||
|
const grossNum = Math.round((netNum * (1 + taxVal / 100) + Number.EPSILON) * 100) / 100;
|
||||||
|
updatedGross = String(grossNum);
|
||||||
|
} else if (value === '') {
|
||||||
|
updatedGross = '';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
[field]: value,
|
||||||
|
net: value,
|
||||||
|
netprice: value,
|
||||||
|
gross: updatedGross,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field === 'tax' || field === 'taxrate') {
|
||||||
|
const taxNum = parseNum(value) ?? 0;
|
||||||
|
const netNum = parseNum(line.net ?? line.netprice);
|
||||||
|
let updatedGross = line.gross;
|
||||||
|
if (netNum != null) {
|
||||||
|
const grossNum = Math.round((netNum * (1 + taxNum / 100) + Number.EPSILON) * 100) / 100;
|
||||||
|
updatedGross = String(grossNum);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
[field]: value,
|
||||||
|
tax: value,
|
||||||
|
taxrate: value,
|
||||||
|
gross: updatedGross,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...line,
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +287,16 @@ class ManifestForm extends PureComponent {
|
|||||||
: art.tax != null
|
: art.tax != null
|
||||||
? String(art.tax)
|
? String(art.tax)
|
||||||
: '';
|
: '';
|
||||||
|
const netNum = parseNum(netVal);
|
||||||
|
const taxNum = parseNum(taxVal);
|
||||||
|
const artGross = getArticleGross(art, taxNum ?? 0);
|
||||||
|
let grossVal = '';
|
||||||
|
if (artGross != null) {
|
||||||
|
grossVal = String(artGross);
|
||||||
|
} else if (netNum != null) {
|
||||||
|
const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100;
|
||||||
|
grossVal = String(g);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...l,
|
...l,
|
||||||
sku: art.sku || l.sku,
|
sku: art.sku || l.sku,
|
||||||
@@ -241,6 +305,7 @@ class ManifestForm extends PureComponent {
|
|||||||
taxrate: l.taxrate || taxVal,
|
taxrate: l.taxrate || taxVal,
|
||||||
net: l.net || netVal,
|
net: l.net || netVal,
|
||||||
netprice: l.netprice || netVal,
|
netprice: l.netprice || netVal,
|
||||||
|
gross: l.gross || grossVal,
|
||||||
unit: art.unit || l.unit || 'Stück',
|
unit: art.unit || l.unit || 'Stück',
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -271,6 +336,16 @@ class ManifestForm extends PureComponent {
|
|||||||
: article.tax != null
|
: article.tax != null
|
||||||
? String(article.tax)
|
? String(article.tax)
|
||||||
: '';
|
: '';
|
||||||
|
const netNum = parseNum(netVal);
|
||||||
|
const taxNum = parseNum(taxVal);
|
||||||
|
const artGross = getArticleGross(article, taxNum ?? 0);
|
||||||
|
let grossVal = '';
|
||||||
|
if (artGross != null) {
|
||||||
|
grossVal = String(artGross);
|
||||||
|
} else if (netNum != null) {
|
||||||
|
const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100;
|
||||||
|
grossVal = String(g);
|
||||||
|
}
|
||||||
|
|
||||||
const newLine = {
|
const newLine = {
|
||||||
key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
@@ -281,6 +356,7 @@ class ManifestForm extends PureComponent {
|
|||||||
taxrate: taxVal,
|
taxrate: taxVal,
|
||||||
net: netVal,
|
net: netVal,
|
||||||
netprice: netVal,
|
netprice: netVal,
|
||||||
|
gross: grossVal,
|
||||||
rebate: '',
|
rebate: '',
|
||||||
pu: '1',
|
pu: '1',
|
||||||
unit: article.unit || 'Stück',
|
unit: article.unit || 'Stück',
|
||||||
@@ -332,6 +408,16 @@ class ManifestForm extends PureComponent {
|
|||||||
: article.tax != null
|
: article.tax != null
|
||||||
? String(article.tax)
|
? String(article.tax)
|
||||||
: '';
|
: '';
|
||||||
|
const netNum = parseNum(netVal);
|
||||||
|
const taxNum = parseNum(taxVal);
|
||||||
|
const artGross = getArticleGross(article, taxNum ?? 0);
|
||||||
|
let grossVal = '';
|
||||||
|
if (artGross != null) {
|
||||||
|
grossVal = String(artGross);
|
||||||
|
} else if (netNum != null) {
|
||||||
|
const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100;
|
||||||
|
grossVal = String(g);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
lines: prev.lines.map((l) => {
|
lines: prev.lines.map((l) => {
|
||||||
@@ -344,6 +430,7 @@ class ManifestForm extends PureComponent {
|
|||||||
taxrate: taxVal || l.taxrate,
|
taxrate: taxVal || l.taxrate,
|
||||||
net: netVal || l.net,
|
net: netVal || l.net,
|
||||||
netprice: netVal || l.netprice,
|
netprice: netVal || l.netprice,
|
||||||
|
gross: grossVal || l.gross,
|
||||||
unit: article.unit || l.unit || 'Stück',
|
unit: article.unit || l.unit || 'Stück',
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -199,19 +199,49 @@ class ManifestFormLine extends PureComponent {
|
|||||||
|
|
||||||
renderField(column, colIdx, totalCols) {
|
renderField(column, colIdx, totalCols) {
|
||||||
const { id, align, type } = column;
|
const { id, align, type } = column;
|
||||||
const { line, saving, onChange } = this.props;
|
const { line, saving, onChange, columns } = this.props;
|
||||||
|
|
||||||
|
if (id === 'pos') {
|
||||||
|
const posVal =
|
||||||
|
this.props.positionIndex ??
|
||||||
|
(this.props.index != null ? this.props.index + 1 : colIdx + 1);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: 38,
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
color: 'text.secondary',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{posVal}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let val = line[id];
|
let val = line[id];
|
||||||
if (val == null || val === '') {
|
if (val == null || val === '') {
|
||||||
if (id === 'tax' || id === 'taxrate') {
|
if (id === 'tax' || id === 'taxrate') {
|
||||||
val = line.taxrate ?? line.tax ?? '';
|
val = line.taxrate ?? line.tax ?? '';
|
||||||
} else if (id === 'net' || id === 'netprice') {
|
} else if (id === 'net' || id === 'netprice') {
|
||||||
val = line.netprice ?? line.net ?? '';
|
val = line.netprice ?? line.net ?? '';
|
||||||
|
} else if (id === 'gross') {
|
||||||
|
const netNum = parseNum(line.netprice ?? line.net);
|
||||||
|
const taxNum = parseNum(line.taxrate ?? line.tax) ?? 0;
|
||||||
|
if (netNum != null) {
|
||||||
|
val = String(Math.round((netNum * (1 + taxNum / 100) + Number.EPSILON) * 100) / 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let isError = false;
|
let isError = false;
|
||||||
if (id === 'name') {
|
if (id === 'name') {
|
||||||
isError = !String(line.name ?? '').trim();
|
isError = !String(line.name ?? '').trim() && !String(line.sku ?? '').trim();
|
||||||
} else if (id === 'amount') {
|
} else if (id === 'amount') {
|
||||||
const raw = getFirstVal(line.amount, line.count);
|
const raw = getFirstVal(line.amount, line.count);
|
||||||
isError = raw == null || parseNum(raw) == null;
|
isError = raw == null || parseNum(raw) == null;
|
||||||
@@ -224,11 +254,15 @@ class ManifestFormLine extends PureComponent {
|
|||||||
} else if (id === 'net' || id === 'netprice') {
|
} else if (id === 'net' || id === 'netprice') {
|
||||||
const raw = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto);
|
const raw = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto);
|
||||||
isError = raw == null || parseNum(raw) == null;
|
isError = raw == null || parseNum(raw) == null;
|
||||||
|
} else if (id === 'gross') {
|
||||||
|
const raw = getFirstVal(line.gross, val);
|
||||||
|
isError = raw == null || parseNum(raw) == null;
|
||||||
} else if (id === 'sku') {
|
} else if (id === 'sku') {
|
||||||
isError = !String(line.sku ?? '').trim() && !String(line.name ?? '').trim();
|
isError = !String(line.sku ?? '').trim() && !String(line.name ?? '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirst = colIdx === 0;
|
const hasPosCol = columns?.[0]?.id === 'pos';
|
||||||
|
const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
|
||||||
const isLast = colIdx === totalCols - 1;
|
const isLast = colIdx === totalCols - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -252,8 +286,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
'& .MuiOutlinedInput-root': {
|
'& .MuiOutlinedInput-root': {
|
||||||
borderRadius: isFirst ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0,
|
borderRadius: isFirstInput ? '4px 0 0 4px' : isLast ? '0 4px 4px 0' : 0,
|
||||||
ml: isFirst ? 0 : '-1px',
|
ml: isFirstInput ? 0 : '-1px',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
@@ -285,7 +319,9 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderArticleSecondLine(rawSku) {
|
renderArticleSecondLine(rawSku) {
|
||||||
const { loading } = this.props;
|
const { loading, kind, columns } = this.props;
|
||||||
|
const isOrder = kind === 'order';
|
||||||
|
const hasNameColumn = columns?.some((c) => c.id === 'name');
|
||||||
const { cachedArticle, loadingArticle } = this.state;
|
const { cachedArticle, loadingArticle } = this.state;
|
||||||
|
|
||||||
if (loading || loadingArticle) {
|
if (loading || loadingArticle) {
|
||||||
@@ -393,8 +429,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Article Name */}
|
{/* Article Name: omit if already editable in line 1 or if kind is order */}
|
||||||
{cachedArticle.name && (
|
{!hasNameColumn && !isOrder && cachedArticle.name && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
fontWeight={600}
|
fontWeight={600}
|
||||||
@@ -406,8 +442,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Primary EAN */}
|
{/* Primary EAN: omit for order */}
|
||||||
{primaryEan && (
|
{!isOrder && primaryEan && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
@@ -424,8 +460,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* VK Netto & VK Brutto */}
|
{/* VK Netto & VK Brutto: omit end on 2nd line for order */}
|
||||||
{(vkNetto != null || vkBrutto != null) && (
|
{!isOrder && (vkNetto != null || vkBrutto != null) && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -779,7 +815,9 @@ class ManifestFormLine extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
||||||
Kein Lieferanten-Artikel für SKU „{rawSku}“ gefunden.
|
{this.props.kind === 'delivery'
|
||||||
|
? `Kein Lieferanten-Artikel für SKU „${rawSku}“ gefunden.`
|
||||||
|
: `Kein Artikel für SKU „${rawSku}“ gefunden.`}
|
||||||
</Typography>
|
</Typography>
|
||||||
{this.renderLedgerChip()}
|
{this.renderLedgerChip()}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import TableCell from '@mui/material/TableCell';
|
|||||||
import TableContainer from '@mui/material/TableContainer';
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
import TableHead from '@mui/material/TableHead';
|
import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined';
|
import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined';
|
||||||
import { PanelTile } from 'dashgui/panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { LINE_COLUMNS, hasArticleLines } from './manifestConfig';
|
import { LINE_COLUMNS, hasArticleLines, parseNum, getFirstVal } from './manifestConfig';
|
||||||
|
import { formatPrice } from '../../utils/price';
|
||||||
import ManifestFormLine from './ManifestFormLine';
|
import ManifestFormLine from './ManifestFormLine';
|
||||||
|
|
||||||
class ManifestLines extends PureComponent {
|
class ManifestLines extends PureComponent {
|
||||||
@@ -43,6 +45,21 @@ class ManifestLines extends PureComponent {
|
|||||||
|
|
||||||
const hasArticles = hasArticleLines(lines);
|
const hasArticles = hasArticleLines(lines);
|
||||||
|
|
||||||
|
let totalNet = 0;
|
||||||
|
let totalGross = 0;
|
||||||
|
if (Array.isArray(lines)) {
|
||||||
|
for (const line of lines) {
|
||||||
|
const amount = parseNum(getFirstVal(line.amount, line.count)) ?? 0;
|
||||||
|
const net = parseNum(getFirstVal(line.net, line.netprice, line.netPrice)) ?? 0;
|
||||||
|
const tax = parseNum(getFirstVal(line.tax, line.taxrate, line.taxRate)) ?? 0;
|
||||||
|
const rebate = parseNum(getFirstVal(line.rebate)) ?? 0;
|
||||||
|
const lineNet = amount * net * (1 - rebate / 100);
|
||||||
|
const lineGross = lineNet * (1 + tax / 100);
|
||||||
|
totalNet += lineNet;
|
||||||
|
totalGross += lineGross;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTile
|
<PanelTile
|
||||||
wide={wide}
|
wide={wide}
|
||||||
@@ -102,6 +119,8 @@ class ManifestLines extends PureComponent {
|
|||||||
return (
|
return (
|
||||||
<ManifestFormLine
|
<ManifestFormLine
|
||||||
key={lineKey}
|
key={lineKey}
|
||||||
|
index={index}
|
||||||
|
positionIndex={index + 1}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
line={line.key != null ? line : { ...line, key: index }}
|
line={line.key != null ? line : { ...line, key: index }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -151,6 +170,22 @@ class ManifestLines extends PureComponent {
|
|||||||
>
|
>
|
||||||
Alle Zeilen entfernen
|
Alle Zeilen entfernen
|
||||||
</Button>
|
</Button>
|
||||||
|
{(totalNet > 0 || totalGross > 0) && (
|
||||||
|
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 2, pr: 0.5 }}>
|
||||||
|
<Typography variant="body2" sx={{ color: 'text.secondary', fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
Netto:{' '}
|
||||||
|
<Box component="span" sx={{ color: 'text.primary', fontWeight: 700 }}>
|
||||||
|
{formatPrice(totalNet)}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: 'primary.dark', fontWeight: 700, fontSize: '0.85rem' }}>
|
||||||
|
Brutto:{' '}
|
||||||
|
<Box component="span" sx={{ color: 'primary.dark', fontWeight: 800 }}>
|
||||||
|
{formatPrice(totalGross)}
|
||||||
|
</Box>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { backendErrorFrom } from '../../utils/backendError';
|
|||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import withNavigate from '../withNavigate';
|
import withNavigate from '../withNavigate';
|
||||||
import DeliveryList from './DeliveryList';
|
import DeliveryList from './DeliveryList';
|
||||||
|
import OrderList from './OrderList';
|
||||||
import {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
loadParties,
|
loadParties,
|
||||||
@@ -36,7 +37,7 @@ class ManifestList extends PureComponent {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
if (this.props.kind !== 'delivery') {
|
if (this.props.kind !== 'delivery' && this.props.kind !== 'order') {
|
||||||
void this.load();
|
void this.load();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +123,9 @@ class ManifestList extends PureComponent {
|
|||||||
if (this.props.kind === 'delivery') {
|
if (this.props.kind === 'delivery') {
|
||||||
return <DeliveryList {...this.props} />;
|
return <DeliveryList {...this.props} />;
|
||||||
}
|
}
|
||||||
|
if (this.props.kind === 'order') {
|
||||||
|
return <OrderList {...this.props} />;
|
||||||
|
}
|
||||||
|
|
||||||
const { status, error, docs, busyId } = this.state;
|
const { status, error, docs, busyId } = this.state;
|
||||||
const cfg = this.cfg;
|
const cfg = this.cfg;
|
||||||
|
|||||||
567
src/components/manifest/OrderList.jsx
Normal file
567
src/components/manifest/OrderList.jsx
Normal file
@@ -0,0 +1,567 @@
|
|||||||
|
import { Fragment, PureComponent } from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableFooter from '@mui/material/TableFooter';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
|
import BackendError from '../BackendError';
|
||||||
|
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
|
||||||
|
import orderAutomat from '../../automats/orderAutomat';
|
||||||
|
import { order as orderApi } from '../../api';
|
||||||
|
import { backendErrorFrom } from '../../utils/backendError';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
|
import { formatPrice } from '../../utils/price';
|
||||||
|
import withNavigate from '../withNavigate';
|
||||||
|
import ResultsPagination from '../article/ResultsPagination';
|
||||||
|
import {
|
||||||
|
formatDate,
|
||||||
|
loadParties,
|
||||||
|
manifestConfig,
|
||||||
|
numberDisplay,
|
||||||
|
partyDisplayName,
|
||||||
|
} from './manifestConfig';
|
||||||
|
|
||||||
|
function tintDivider(color, amount = 0.15) {
|
||||||
|
if (typeof color !== 'string') return 'divider';
|
||||||
|
const m = color.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
||||||
|
if (!m) return 'divider';
|
||||||
|
|
||||||
|
let hex = m[1];
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex
|
||||||
|
.split('')
|
||||||
|
.map((c) => c + c)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
const num = parseInt(hex, 16);
|
||||||
|
const channels = [16, 8, 0].map((shift) => {
|
||||||
|
const c = (num >> shift) & 0xff;
|
||||||
|
return Math.max(0, Math.round(c * (1 - amount)));
|
||||||
|
});
|
||||||
|
const toHex = (v) => v.toString(16).padStart(2, '0');
|
||||||
|
return `#${channels.map(toHex).join('')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOrderTotal(lines) {
|
||||||
|
if (!Array.isArray(lines) || lines.length === 0) return 0;
|
||||||
|
return lines.reduce((acc, line) => {
|
||||||
|
const amount = Number(line.amount) || 0;
|
||||||
|
const net = Number(line.net) || 0;
|
||||||
|
const rebate = Number(line.rebate) || 0;
|
||||||
|
const lineTotal = amount * net * (1 - rebate / 100);
|
||||||
|
return acc + lineTotal;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOrderGrossTotal(lines) {
|
||||||
|
if (!Array.isArray(lines) || lines.length === 0) return 0;
|
||||||
|
return lines.reduce((acc, line) => {
|
||||||
|
const amount = Number(line.amount) || 0;
|
||||||
|
const net = Number(line.net) || 0;
|
||||||
|
const rebate = Number(line.rebate) || 0;
|
||||||
|
const taxrate = Number(line.taxrate) || 0;
|
||||||
|
const lineNet = amount * net * (1 - rebate / 100);
|
||||||
|
const lineGross = lineNet * (1 + taxrate / 100);
|
||||||
|
return acc + lineGross;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OrderList extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.cfg = manifestConfig('order');
|
||||||
|
const orderState = orderAutomat.state;
|
||||||
|
this.state = {
|
||||||
|
status: orderState.status === 'ready' ? 'ready' : 'loading',
|
||||||
|
error: orderState.error,
|
||||||
|
orders: orderState.items ?? [],
|
||||||
|
total: orderState.total ?? 0,
|
||||||
|
offset: orderState.offset ?? 0,
|
||||||
|
limit: orderState.limit ?? 20,
|
||||||
|
partiesById: {},
|
||||||
|
busyId: null,
|
||||||
|
expandedIds: new Set(),
|
||||||
|
};
|
||||||
|
this._cancelled = false;
|
||||||
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
|
this.handleCreate = this.handleCreate.bind(this);
|
||||||
|
this.handleEdit = this.handleEdit.bind(this);
|
||||||
|
this.handleDelete = this.handleDelete.bind(this);
|
||||||
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
this.toggleExpand = this.toggleExpand.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._cancelled = false;
|
||||||
|
this.unsubOrder = orderAutomat.subscribe(this, (state) => ({
|
||||||
|
orders: state.items ?? [],
|
||||||
|
total: state.total ?? 0,
|
||||||
|
offset: state.offset ?? 0,
|
||||||
|
limit: state.limit ?? 20,
|
||||||
|
status: state.status,
|
||||||
|
error: state.error,
|
||||||
|
}));
|
||||||
|
|
||||||
|
void this.loadPartiesAndOrders();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._cancelled = true;
|
||||||
|
this.unsubOrder?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadPartiesAndOrders() {
|
||||||
|
try {
|
||||||
|
const parties = await loadParties('order');
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({
|
||||||
|
partiesById: this.cfg.partiesByIdFromList(parties),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Non-fatal, party names can fall back to IDs
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await orderAutomat.actions.ensureLoaded();
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleRetry() {
|
||||||
|
this.setState({ status: 'loading', error: null });
|
||||||
|
try {
|
||||||
|
await orderAutomat.actions.reload();
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ status: 'failed', error: backendErrorFrom(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go(to) {
|
||||||
|
const { navigate } = this.props;
|
||||||
|
if (typeof navigate === 'function') {
|
||||||
|
navigate(to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.location.assign(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCreate() {
|
||||||
|
this.go(this.cfg.createPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEdit(order) {
|
||||||
|
this.go(this.cfg.editPath(order.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleDelete(order) {
|
||||||
|
if (this.state.busyId != null) return;
|
||||||
|
if (!window.confirm(this.cfg.confirmDelete(order.number))) return;
|
||||||
|
|
||||||
|
this.setState({ busyId: order.id });
|
||||||
|
try {
|
||||||
|
await orderApi.delete(order.id);
|
||||||
|
await orderAutomat.actions.reload();
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ busyId: null });
|
||||||
|
enqueueSnackbar(this.cfg.deletedMsg, { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ busyId: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePageChange(page) {
|
||||||
|
// Formula: Next Offset: (page - 1) * limit
|
||||||
|
void orderAutomat.actions.setPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleExpand(orderId) {
|
||||||
|
this.setState((prev) => {
|
||||||
|
const next = new Set(prev.expandedIds);
|
||||||
|
if (next.has(orderId)) {
|
||||||
|
next.delete(orderId);
|
||||||
|
} else {
|
||||||
|
next.add(orderId);
|
||||||
|
}
|
||||||
|
return { expandedIds: next };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
partyName(partyId) {
|
||||||
|
if (partyId == null) return '–';
|
||||||
|
const party = this.state.partiesById[partyId];
|
||||||
|
return partyDisplayName('order', party) || `Kunde #${partyId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLoading() {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLineItems(order, borderColor) {
|
||||||
|
const lines = order.lines ?? [];
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ py: 1.5, px: 2, bgcolor: 'action.hover', borderRadius: 1 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
Keine Positionen vorhanden.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const netTotal = calculateOrderTotal(lines);
|
||||||
|
const grossTotal = calculateOrderGrossTotal(lines);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
py: 1,
|
||||||
|
px: 1.5,
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.45)',
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Table size="small" aria-label={`Positionen für Bestellung ${order.number}`}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell align="center" sx={{ fontWeight: 600, fontSize: '0.75rem', width: 44 }}>
|
||||||
|
Pos
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, fontSize: '0.75rem' }}>SKU</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, fontSize: '0.75rem' }}>Artikel</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
Menge
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
Netto
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
Brutto
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
Rabatt
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
MwSt.
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
Gesamt netto
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, fontSize: '0.75rem' }}>
|
||||||
|
Gesamt brutto
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{lines.map((line, idx) => {
|
||||||
|
const amount = Number(line.amount) || 0;
|
||||||
|
const net = Number(line.net) || 0;
|
||||||
|
const taxrate = Number(line.taxrate) || 0;
|
||||||
|
const gross = net * (1 + taxrate / 100);
|
||||||
|
const rebate = Number(line.rebate) || 0;
|
||||||
|
const lineNet = amount * net * (1 - rebate / 100);
|
||||||
|
const lineGross = lineNet * (1 + taxrate / 100);
|
||||||
|
const puText = Number(line.pu) > 1 ? ` (VPE ${line.pu})` : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow key={line.id ?? `line-${idx}`}>
|
||||||
|
<TableCell align="center" sx={{ fontSize: '0.75rem', color: 'text.secondary', fontWeight: 600 }}>
|
||||||
|
{idx + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.75rem' }}>
|
||||||
|
{line.sku || '–'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontSize: '0.75rem' }}>
|
||||||
|
<Typography variant="body2" sx={{ fontSize: '0.75rem', fontWeight: 500 }}>
|
||||||
|
{line.name || '–'}
|
||||||
|
</Typography>
|
||||||
|
{line.comment ? (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{line.comment}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem' }}>
|
||||||
|
{amount}
|
||||||
|
{puText}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem' }}>
|
||||||
|
{formatPrice(net)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem' }}>
|
||||||
|
{formatPrice(gross)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem' }}>
|
||||||
|
{rebate > 0 ? `${rebate}%` : '–'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem' }}>
|
||||||
|
{line.taxrate != null ? `${line.taxrate}%` : '–'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem', fontWeight: 600 }}>
|
||||||
|
{formatPrice(lineNet)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem', fontWeight: 600, color: 'primary.dark' }}>
|
||||||
|
{formatPrice(lineGross)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
<TableFooter>
|
||||||
|
<TableRow sx={{ '& > td': { borderTop: '2px solid', borderColor, fontWeight: 700 } }}>
|
||||||
|
<TableCell colSpan={8} align="right" sx={{ fontSize: '0.75rem', fontWeight: 700 }}>
|
||||||
|
Gesamtsumme:
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem', fontWeight: 700 }}>
|
||||||
|
{formatPrice(netTotal)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontSize: '0.75rem', fontWeight: 700, color: 'primary.dark' }}>
|
||||||
|
{formatPrice(grossTotal)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableFooter>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { status, error, orders, total, offset, limit, busyId, expandedIds } = this.state;
|
||||||
|
const cfg = this.cfg;
|
||||||
|
|
||||||
|
if (status === 'loading') return this.renderLoading();
|
||||||
|
if (status === 'failed') {
|
||||||
|
return (
|
||||||
|
<BackendError
|
||||||
|
error={error}
|
||||||
|
onRetry={this.handleRetry}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const borderColor = tintDivider(cfg.listColor);
|
||||||
|
|
||||||
|
// Client Pagination Formula:
|
||||||
|
// Current Page: Math.floor(offset / limit) + 1
|
||||||
|
// Total Pages: Math.ceil(total / limit)
|
||||||
|
const currentPage = Math.floor(offset / limit) + 1;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / limit));
|
||||||
|
const showPagination = total > limit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelTileGrid>
|
||||||
|
<PanelTile wide={3} minHeight={240} color={cfg.listColor}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
{/* Create Header Button */}
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={this.handleCreate}
|
||||||
|
>
|
||||||
|
{cfg.createButton}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Empty State */}
|
||||||
|
{orders.length === 0 ? (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{cfg.emptyList}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<TableContainer>
|
||||||
|
<Table
|
||||||
|
size="small"
|
||||||
|
sx={{
|
||||||
|
'& .MuiTableCell-root': {
|
||||||
|
borderColor,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell padding="checkbox" sx={{ width: 44 }} />
|
||||||
|
<TableCell sx={{ width: 130, fontWeight: 600 }}>Nummer</TableCell>
|
||||||
|
<TableCell sx={{ width: 130, fontWeight: 600 }}>Referenz</TableCell>
|
||||||
|
<TableCell sx={{ width: 130, fontWeight: 600 }}>{cfg.dateLabel}</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 160, fontWeight: 600 }}>{cfg.partyLabel}</TableCell>
|
||||||
|
<TableCell sx={{ minWidth: 180, fontWeight: 600 }}>Positionen</TableCell>
|
||||||
|
<TableCell padding="checkbox" align="right" sx={{ width: 84 }} />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{orders.map((order) => {
|
||||||
|
const isExpanded = expandedIds.has(order.id);
|
||||||
|
const lines = order.lines ?? [];
|
||||||
|
const netTotal = calculateOrderTotal(lines);
|
||||||
|
const grossTotal = calculateOrderGrossTotal(lines);
|
||||||
|
const hasLines = lines.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={order.id}>
|
||||||
|
<TableRow
|
||||||
|
hover
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => this.handleEdit(order)}
|
||||||
|
>
|
||||||
|
{/* Expand / Collapse toggle */}
|
||||||
|
<TableCell
|
||||||
|
padding="checkbox"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (hasLines) this.toggleExpand(order.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{hasLines ? (
|
||||||
|
<IconButton size="small" aria-label="Positionen ein-/ausblenden">
|
||||||
|
{isExpanded ? (
|
||||||
|
<KeyboardArrowUpIcon fontSize="small" />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDownIcon fontSize="small" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Number */}
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>{order.number}</TableCell>
|
||||||
|
|
||||||
|
{/* Reference / External Number */}
|
||||||
|
<TableCell>{numberDisplay(order.externalNumber)}</TableCell>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<TableCell>{formatDate(order.date)}</TableCell>
|
||||||
|
|
||||||
|
{/* Customer */}
|
||||||
|
<TableCell>{this.partyName(order.entityId)}</TableCell>
|
||||||
|
|
||||||
|
{/* Lines summary */}
|
||||||
|
<TableCell>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`${lines.length} Pos.`}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
height: 22,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{grossTotal > 0 ? (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 700, fontSize: '0.8rem', lineHeight: 1.2 }}>
|
||||||
|
{formatPrice(grossTotal)} brutto
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary', fontSize: '0.7rem' }}>
|
||||||
|
{formatPrice(netTotal)} netto
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : netTotal > 0 ? (
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.8rem' }}>
|
||||||
|
{formatPrice(netTotal)}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* Row Action buttons */}
|
||||||
|
<TableCell
|
||||||
|
padding="checkbox"
|
||||||
|
align="right"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 0.5 }}>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label={`„${order.number}“ bearbeiten`}
|
||||||
|
onClick={() => this.handleEdit(order)}
|
||||||
|
>
|
||||||
|
<EditOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
aria-label={`„${order.number}“ löschen`}
|
||||||
|
disabled={busyId != null}
|
||||||
|
onClick={() => this.handleDelete(order)}
|
||||||
|
>
|
||||||
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
{/* Collapsible Line Details Row */}
|
||||||
|
{hasLines && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
borderBottom: isExpanded ? undefined : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||||
|
<Box sx={{ py: 1.5, px: 2 }}>
|
||||||
|
{this.renderLineItems(order, borderColor)}
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{showPagination ? (
|
||||||
|
<Box sx={{ pt: 1 }}>
|
||||||
|
<ResultsPagination
|
||||||
|
offset={offset}
|
||||||
|
totalCount={total}
|
||||||
|
pageSize={limit}
|
||||||
|
pageCount={pageCount}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onPageChange={(e, page) => this.handlePageChange(page)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : null}
|
||||||
|
</Stack>
|
||||||
|
</PanelTile>
|
||||||
|
</PanelTileGrid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withNavigate(OrderList);
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export { default as ManifestList } from './ManifestList';
|
export { default as ManifestList } from './ManifestList';
|
||||||
export { default as DeliveryList } from './DeliveryList';
|
export { default as DeliveryList } from './DeliveryList';
|
||||||
|
export { default as OrderList } from './OrderList';
|
||||||
export { default as ManifestForm } from './ManifestForm';
|
export { default as ManifestForm } from './ManifestForm';
|
||||||
export { default as ManifestHeader } from './ManifestHeader';
|
export { default as ManifestHeader } from './ManifestHeader';
|
||||||
export { default as ManifestLines } from './ManifestLines';
|
export { default as ManifestLines } from './ManifestLines';
|
||||||
@@ -14,6 +15,7 @@ export { withManifestId } from './withManifestId';
|
|||||||
export {
|
export {
|
||||||
manifestConfig,
|
manifestConfig,
|
||||||
LINE_COLUMNS,
|
LINE_COLUMNS,
|
||||||
|
ORDER_LINE_COLUMNS,
|
||||||
DELIVERY_LINE_COLUMNS,
|
DELIVERY_LINE_COLUMNS,
|
||||||
emptyLine,
|
emptyLine,
|
||||||
lineFromApi,
|
lineFromApi,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
|
|||||||
import deliveryAutomat from '../../automats/deliveryAutomat';
|
import deliveryAutomat from '../../automats/deliveryAutomat';
|
||||||
import quoteAutomat from '../../automats/quoteAutomat';
|
import quoteAutomat from '../../automats/quoteAutomat';
|
||||||
import orderAutomat from '../../automats/orderAutomat';
|
import orderAutomat from '../../automats/orderAutomat';
|
||||||
|
import { netFromGross } from '../../utils/price';
|
||||||
import {
|
import {
|
||||||
GREEN_PANEL,
|
GREEN_PANEL,
|
||||||
ORANGE_PANEL,
|
ORANGE_PANEL,
|
||||||
@@ -87,6 +88,7 @@ export function emptyLine() {
|
|||||||
tax: '',
|
tax: '',
|
||||||
rebate: '',
|
rebate: '',
|
||||||
net: '',
|
net: '',
|
||||||
|
gross: '',
|
||||||
name: '',
|
name: '',
|
||||||
pu: '',
|
pu: '',
|
||||||
unit: '',
|
unit: '',
|
||||||
@@ -98,6 +100,16 @@ export function emptyLine() {
|
|||||||
export function lineFromApi(row) {
|
export function lineFromApi(row) {
|
||||||
const netVal = row.netprice != null ? String(row.netprice) : row.net != null ? String(row.net) : '';
|
const netVal = row.netprice != null ? String(row.netprice) : row.net != null ? String(row.net) : '';
|
||||||
const taxVal = row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : '';
|
const taxVal = row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : '';
|
||||||
|
const netNum = parseNum(netVal);
|
||||||
|
const taxNum = parseNum(taxVal);
|
||||||
|
let grossVal = '';
|
||||||
|
if (row.gross != null && row.gross !== '') {
|
||||||
|
grossVal = String(row.gross);
|
||||||
|
} else if (netNum != null) {
|
||||||
|
const rate = taxNum ?? 0;
|
||||||
|
const g = Math.round((netNum * (1 + rate / 100) + Number.EPSILON) * 100) / 100;
|
||||||
|
grossVal = String(g);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
key: `line-${row.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`}`,
|
key: `line-${row.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`}`,
|
||||||
@@ -106,6 +118,7 @@ export function lineFromApi(row) {
|
|||||||
tax: taxVal,
|
tax: taxVal,
|
||||||
rebate: row.rebate == null ? '' : String(row.rebate),
|
rebate: row.rebate == null ? '' : String(row.rebate),
|
||||||
net: netVal,
|
net: netVal,
|
||||||
|
gross: grossVal,
|
||||||
name: row.name ?? '',
|
name: row.name ?? '',
|
||||||
pu: row.pu == null ? '' : String(row.pu),
|
pu: row.pu == null ? '' : String(row.pu),
|
||||||
unit: row.unit ?? '',
|
unit: row.unit ?? '',
|
||||||
@@ -136,7 +149,7 @@ export function hasArticleLines(lines) {
|
|||||||
const sku = String(l.sku ?? '').trim();
|
const sku = String(l.sku ?? '').trim();
|
||||||
const name = String(l.name ?? '').trim();
|
const name = String(l.name ?? '').trim();
|
||||||
const amount = String(l.amount ?? '').trim();
|
const amount = String(l.amount ?? '').trim();
|
||||||
const net = String(l.net ?? l.netprice ?? '').trim();
|
const net = String(l.net ?? l.netprice ?? l.gross ?? '').trim();
|
||||||
return Boolean(sku || name || amount || net);
|
return Boolean(sku || name || amount || net);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -153,14 +166,19 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
|||||||
const rawAmount = getFirstVal(line.amount, line.count);
|
const rawAmount = getFirstVal(line.amount, line.count);
|
||||||
const amount = parseNum(rawAmount);
|
const amount = parseNum(rawAmount);
|
||||||
const rawNet = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto);
|
const rawNet = getFirstVal(line.net, line.netprice, line.netPrice, line.eknetto);
|
||||||
const net = parseNum(rawNet);
|
let net = parseNum(rawNet);
|
||||||
const rawTax = getFirstVal(line.tax, line.taxrate, line.taxRate, line.mwst);
|
const rawTax = getFirstVal(line.tax, line.taxrate, line.taxRate, line.mwst);
|
||||||
const tax = parseNum(rawTax);
|
const tax = parseNum(rawTax);
|
||||||
|
const rawGross = getFirstVal(line.gross);
|
||||||
|
const gross = parseNum(rawGross);
|
||||||
|
if (net == null && gross != null && tax != null) {
|
||||||
|
net = netFromGross(gross, tax);
|
||||||
|
}
|
||||||
const rebate = parseNum(getFirstVal(line.rebate));
|
const rebate = parseNum(getFirstVal(line.rebate));
|
||||||
const rawPu = getFirstVal(line.pu, line.vpe);
|
const rawPu = getFirstVal(line.pu, line.vpe);
|
||||||
const pu = parseNum(rawPu);
|
const pu = parseNum(rawPu);
|
||||||
const blank =
|
const blank =
|
||||||
!sku && !name && amount == null && net == null && tax == null && rebate == null;
|
!sku && !name && amount == null && net == null && gross == null && tax == null && rebate == null;
|
||||||
if (blank) continue;
|
if (blank) continue;
|
||||||
|
|
||||||
const missing = [];
|
const missing = [];
|
||||||
@@ -479,11 +497,23 @@ export async function buildManifestBody(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LINE_COLUMNS = [
|
export const LINE_COLUMNS = [
|
||||||
{ id: 'amount', label: 'Amount', width: 88, align: 'right', type: 'number' },
|
{ id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' },
|
||||||
{ id: 'sku', label: 'SKU', width: 140, align: 'left', type: 'text' },
|
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
|
||||||
{ id: 'tax', label: 'Tax', width: 80, align: 'right', type: 'number' },
|
{ id: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' },
|
||||||
{ id: 'rebate', label: 'Rebate', width: 88, align: 'right', type: 'number' },
|
{ id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' },
|
||||||
{ id: 'net', label: 'Net', width: 104, align: 'right', type: 'number' },
|
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' },
|
||||||
|
{ id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ORDER_LINE_COLUMNS = [
|
||||||
|
{ id: 'pos', label: 'Pos', width: 44, align: 'center' },
|
||||||
|
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
|
||||||
|
{ id: 'name', label: 'Name', width: 200, align: 'left', type: 'text' },
|
||||||
|
{ id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' },
|
||||||
|
{ id: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' },
|
||||||
|
{ id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' },
|
||||||
|
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' },
|
||||||
|
{ id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DELIVERY_LINE_COLUMNS = [
|
export const DELIVERY_LINE_COLUMNS = [
|
||||||
@@ -506,7 +536,8 @@ export const DELIVERY_LINE_COLUMNS = [
|
|||||||
* @param {ManifestKind} kind Manifest kind.
|
* @param {ManifestKind} kind Manifest kind.
|
||||||
* @returns {{get: Function, create: Function, update: Function, delete: Function}}
|
* @returns {{get: Function, create: Function, update: Function, delete: Function}}
|
||||||
*/
|
*/
|
||||||
export function apiFor(kind) {
|
export function apiFor(cfgOrKind) {
|
||||||
|
const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind;
|
||||||
if (kind === 'delivery') return deliveryApi;
|
if (kind === 'delivery') return deliveryApi;
|
||||||
if (kind === 'quote') return quoteApi;
|
if (kind === 'quote') return quoteApi;
|
||||||
if (kind === 'order') return orderApi;
|
if (kind === 'order') return orderApi;
|
||||||
@@ -515,9 +546,10 @@ export function apiFor(kind) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load party options for Autocomplete / list name lookup.
|
* Load party options for Autocomplete / list name lookup.
|
||||||
* @param {ManifestKind} kind
|
* @param {ManifestKind|object} cfgOrKind
|
||||||
*/
|
*/
|
||||||
export async function loadParties(kind) {
|
export async function loadParties(cfgOrKind) {
|
||||||
|
const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind;
|
||||||
if (kind === 'delivery') {
|
if (kind === 'delivery') {
|
||||||
if (supplierAutomat.state.status === 'ready') {
|
if (supplierAutomat.state.status === 'ready') {
|
||||||
return sortSuppliers(supplierAutomat.state.suppliers);
|
return sortSuppliers(supplierAutomat.state.suppliers);
|
||||||
@@ -532,16 +564,21 @@ export async function loadParties(kind) {
|
|||||||
return filterCustomerEntities(entities, groups);
|
return filterCustomerEntities(entities, groups);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function partyDisplayName(kind, party) {
|
export function partyDisplayName(cfgOrKind, party) {
|
||||||
if (!party) return '';
|
if (!party) return '';
|
||||||
|
const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind;
|
||||||
if (kind === 'delivery') return String(party.name ?? '');
|
if (kind === 'delivery') return String(party.name ?? '');
|
||||||
return entityDisplayName(party);
|
return entityDisplayName(party);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ManifestKind} kind
|
* @param {ManifestKind|object} cfgOrKind
|
||||||
*/
|
*/
|
||||||
export function manifestConfig(kind) {
|
export function manifestConfig(cfgOrKind) {
|
||||||
|
if (cfgOrKind && typeof cfgOrKind === 'object' && cfgOrKind.kind && cfgOrKind.api) {
|
||||||
|
return cfgOrKind;
|
||||||
|
}
|
||||||
|
const kind = typeof cfgOrKind === 'string' ? cfgOrKind : cfgOrKind?.kind;
|
||||||
if (kind === 'delivery') {
|
if (kind === 'delivery') {
|
||||||
return {
|
return {
|
||||||
kind: 'delivery',
|
kind: 'delivery',
|
||||||
@@ -641,7 +678,7 @@ export function manifestConfig(kind) {
|
|||||||
listAutomat: orderAutomat,
|
listAutomat: orderAutomat,
|
||||||
partiesByIdFromList: (parties) =>
|
partiesByIdFromList: (parties) =>
|
||||||
Object.fromEntries(parties.map((row) => [row.id, row])),
|
Object.fromEntries(parties.map((row) => [row.id, row])),
|
||||||
columns: LINE_COLUMNS,
|
columns: ORDER_LINE_COLUMNS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error(`Unknown manifest kind: ${kind}`);
|
throw new Error(`Unknown manifest kind: ${kind}`);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { ManifestList } from '../../components/manifest';
|
import { OrderList } from '../../components/manifest';
|
||||||
|
|
||||||
class AuftragBestellungPage extends PureComponent {
|
class AuftragBestellungPage extends PureComponent {
|
||||||
render() {
|
render() {
|
||||||
return <ManifestList kind="order" />;
|
return <OrderList />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user