u
This commit is contained in:
@@ -4,10 +4,19 @@ import TableBody from '@mui/material/TableBody';
|
|||||||
import TableCell from '@mui/material/TableCell';
|
import TableCell from '@mui/material/TableCell';
|
||||||
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 Link from '@mui/material/Link';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { articleHistoryAutomat } from '../../../automats/articleDetailAutomat';
|
import { articleHistoryAutomat } from '../../../automats/articleDetailAutomat';
|
||||||
import sseAutomat from '../../../automats/sseAutomat';
|
import sseAutomat from '../../../automats/sseAutomat';
|
||||||
|
import shipmentAutomat from '../../../automats/shipmentAutomat';
|
||||||
|
import { shipment as shipmentApi } from '../../../api';
|
||||||
|
import { ShipmentDetailDialog } from '../../manifest';
|
||||||
|
import withNavigate from '../../withNavigate';
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
import ResultsPagination from '../ResultsPagination';
|
import ResultsPagination from '../ResultsPagination';
|
||||||
|
import {
|
||||||
|
SCRIM_SOFT,
|
||||||
|
} from '../../../colors';
|
||||||
|
|
||||||
const PAGE_SIZE = 20;
|
const PAGE_SIZE = 20;
|
||||||
|
|
||||||
@@ -15,9 +24,15 @@ function formatDate(iso) {
|
|||||||
return new Date(iso).toLocaleString('de-DE');
|
return new Date(iso).toLocaleString('de-DE');
|
||||||
}
|
}
|
||||||
|
|
||||||
import {
|
function decodeMeta(meta) {
|
||||||
SCRIM_SOFT,
|
if (!meta) return null;
|
||||||
} from '../../../colors';
|
if (typeof meta === 'object') return meta;
|
||||||
|
try {
|
||||||
|
return JSON.parse(meta);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const HISTORY_DATE_CELL_BORDER = `1px solid ${SCRIM_SOFT}`;
|
const HISTORY_DATE_CELL_BORDER = `1px solid ${SCRIM_SOFT}`;
|
||||||
|
|
||||||
@@ -30,11 +45,17 @@ const dateCellSx = {
|
|||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ArticleHistoryTab extends PureComponent {
|
class ArticleHistoryTab extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { all: [], skip: 0 };
|
this.state = {
|
||||||
|
all: [],
|
||||||
|
skip: 0,
|
||||||
|
selectedShipment: null,
|
||||||
|
loadingShipment: false,
|
||||||
|
};
|
||||||
this.handlePageChange = this.handlePageChange.bind(this);
|
this.handlePageChange = this.handlePageChange.bind(this);
|
||||||
|
this.handleCloseShipment = this.handleCloseShipment.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -88,8 +109,122 @@ export default class ArticleHistoryTab extends PureComponent {
|
|||||||
this.setState({ skip: (page - 1) * PAGE_SIZE });
|
this.setState({ skip: (page - 1) * PAGE_SIZE });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCloseShipment() {
|
||||||
|
this.setState({ selectedShipment: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenShipment = async (shipmentNumber, shipmentId = null) => {
|
||||||
|
if (shipmentId) {
|
||||||
|
this.setState({ selectedShipment: { id: shipmentId, number: shipmentNumber } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!shipmentNumber) return;
|
||||||
|
|
||||||
|
this.setState({ loadingShipment: true });
|
||||||
|
try {
|
||||||
|
const cached = shipmentAutomat.state.items?.find(
|
||||||
|
(s) => s && String(s.number).toLowerCase() === String(shipmentNumber).toLowerCase()
|
||||||
|
);
|
||||||
|
if (cached?.id) {
|
||||||
|
this.setState({ selectedShipment: cached, loadingShipment: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await shipmentApi.list({ search: shipmentNumber });
|
||||||
|
const shipments = Array.isArray(res?.shipments) ? res.shipments : [];
|
||||||
|
const match = shipments.find(
|
||||||
|
(s) => String(s.number).toLowerCase() === String(shipmentNumber).toLowerCase()
|
||||||
|
) || shipments[0];
|
||||||
|
|
||||||
|
if (match?.id) {
|
||||||
|
this.setState({ selectedShipment: match, loadingShipment: false });
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar(`Lieferschein „${shipmentNumber}“ nicht gefunden.`, { variant: 'warning' });
|
||||||
|
this.setState({ loadingShipment: false });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ loadingShipment: false });
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderEntryText(entry) {
|
||||||
|
const meta = decodeMeta(entry.meta);
|
||||||
|
const shipmentNumber = meta?.shipmentNumber || (meta?.type === 'shipment' ? meta?.manifestNumber : null);
|
||||||
|
const regexMatch = !shipmentNumber && entry.text ? entry.text.match(/Lieferschein\s+([A-Za-z0-9\-_]+)/) : null;
|
||||||
|
const effectiveShipmentNumber = shipmentNumber || regexMatch?.[1] || null;
|
||||||
|
|
||||||
|
if (!effectiveShipmentNumber || !entry.text) {
|
||||||
|
return entry.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shipmentId = meta?.shipmentId ?? meta?.manifestId ?? meta?.id ?? null;
|
||||||
|
const targetPattern = `Lieferschein ${effectiveShipmentNumber}`;
|
||||||
|
const idx = entry.text.indexOf(targetPattern);
|
||||||
|
|
||||||
|
const linkElement = (label) => (
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={() => this.handleOpenShipment(effectiveShipmentNumber, shipmentId)}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
verticalAlign: 'baseline',
|
||||||
|
textAlign: 'left',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: 'primary.main',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
border: 'none',
|
||||||
|
background: 'none',
|
||||||
|
display: 'inline',
|
||||||
|
'&:hover': {
|
||||||
|
color: 'primary.dark',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idx !== -1) {
|
||||||
|
const before = entry.text.slice(0, idx);
|
||||||
|
const after = entry.text.slice(idx + targetPattern.length);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{before}
|
||||||
|
{linkElement(targetPattern)}
|
||||||
|
{after}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const numIdx = entry.text.indexOf(effectiveShipmentNumber);
|
||||||
|
if (numIdx !== -1) {
|
||||||
|
const before = entry.text.slice(0, numIdx);
|
||||||
|
const after = entry.text.slice(numIdx + effectiveShipmentNumber.length);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{before}
|
||||||
|
{linkElement(effectiveShipmentNumber)}
|
||||||
|
{after}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{linkElement(`Lieferschein ${effectiveShipmentNumber}`)} {entry.text}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { all, skip } = this.state;
|
const { all, skip, selectedShipment } = this.state;
|
||||||
const total = all.length;
|
const total = all.length;
|
||||||
const history = this.pageOf(all, skip);
|
const history = this.pageOf(all, skip);
|
||||||
|
|
||||||
@@ -98,54 +233,69 @@ export default class ArticleHistoryTab extends PureComponent {
|
|||||||
const showPagination = total > PAGE_SIZE;
|
const showPagination = total > PAGE_SIZE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<>
|
||||||
size="small"
|
<Table
|
||||||
sx={{
|
size="small"
|
||||||
'& td': { borderBottom: 'none', bgcolor: 'transparent' },
|
sx={{
|
||||||
'& th': { bgcolor: 'transparent' },
|
'& td': { borderBottom: 'none', bgcolor: 'transparent' },
|
||||||
}}
|
'& th': { bgcolor: 'transparent' },
|
||||||
>
|
}}
|
||||||
<TableHead>
|
>
|
||||||
<TableRow>
|
<TableHead>
|
||||||
<TableCell
|
<TableRow>
|
||||||
sx={{
|
|
||||||
...dateDividerSx,
|
|
||||||
width: '1%',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
verticalAlign: 'middle',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Datum
|
|
||||||
</TableCell>
|
|
||||||
<TableCell
|
|
||||||
sx={dateCellSx}
|
|
||||||
>
|
|
||||||
{showPagination ? (
|
|
||||||
<ResultsPagination
|
|
||||||
offset={skip}
|
|
||||||
totalCount={total}
|
|
||||||
pageSize={PAGE_SIZE}
|
|
||||||
pageCount={pageCount}
|
|
||||||
currentPage={currentPage}
|
|
||||||
onPageChange={this.handlePageChange}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{history.map((entry) => (
|
|
||||||
<TableRow key={entry.id}>
|
|
||||||
<TableCell
|
<TableCell
|
||||||
sx={{ verticalAlign: 'top', whiteSpace: 'nowrap', width: '1%' }}
|
sx={{
|
||||||
|
...dateDividerSx,
|
||||||
|
width: '1%',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{formatDate(entry.date)}
|
Datum
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={dateCellSx}
|
||||||
|
>
|
||||||
|
{showPagination ? (
|
||||||
|
<ResultsPagination
|
||||||
|
offset={skip}
|
||||||
|
totalCount={total}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
pageCount={pageCount}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onPageChange={this.handlePageChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ whiteSpace: 'pre-wrap' }}>{entry.text}</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHead>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{history.map((entry) => (
|
||||||
|
<TableRow key={entry.id}>
|
||||||
|
<TableCell
|
||||||
|
sx={{ verticalAlign: 'top', whiteSpace: 'nowrap', width: '1%' }}
|
||||||
|
>
|
||||||
|
{formatDate(entry.date)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{this.renderEntryText(entry)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{selectedShipment && (
|
||||||
|
<ShipmentDetailDialog
|
||||||
|
open={Boolean(selectedShipment)}
|
||||||
|
shipment={selectedShipment}
|
||||||
|
onClose={this.handleCloseShipment}
|
||||||
|
onNavigate={this.props.navigate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withNavigate(ArticleHistoryTab);
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
|
|||||||
import supplierAutomat from '../../automats/supplierAutomat';
|
import supplierAutomat from '../../automats/supplierAutomat';
|
||||||
import orderAutomat from '../../automats/orderAutomat';
|
import orderAutomat from '../../automats/orderAutomat';
|
||||||
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
||||||
|
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||||
|
import sseAutomat from '../../automats/sseAutomat';
|
||||||
|
import OrderLedgerPanel from './OrderLedgerPanel';
|
||||||
|
import { fetchOrderLedgerEntries } from '../../utils/orderLedger';
|
||||||
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
|
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
|
||||||
import { resolveSalesPrice } from '../../utils/articleDetail';
|
import { resolveSalesPrice } from '../../utils/articleDetail';
|
||||||
import {
|
import {
|
||||||
@@ -102,6 +106,7 @@ class ManifestForm extends PureComponent {
|
|||||||
shipments: initialEdit?.shipments || [],
|
shipments: initialEdit?.shipments || [],
|
||||||
payments: initialEdit?.payments || [],
|
payments: initialEdit?.payments || [],
|
||||||
shippings: initialEdit?.shippings || [],
|
shippings: initialEdit?.shippings || [],
|
||||||
|
ledgerEntries: [],
|
||||||
orderTotal: initialEdit?.orderTotal ?? null,
|
orderTotal: initialEdit?.orderTotal ?? null,
|
||||||
paidTotal: initialEdit?.paidTotal ?? null,
|
paidTotal: initialEdit?.paidTotal ?? null,
|
||||||
paid: Boolean(initialEdit?.paid),
|
paid: Boolean(initialEdit?.paid),
|
||||||
@@ -217,8 +222,20 @@ class ManifestForm extends PureComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (warehouseAutomat.state.status === 'idle') {
|
||||||
|
void warehouseAutomat.actions.load();
|
||||||
|
}
|
||||||
|
|
||||||
if (this.props.mode === 'edit') {
|
if (this.props.mode === 'edit') {
|
||||||
void this.loadEdit();
|
void this.loadEdit();
|
||||||
|
if (this.props.kind === 'order') {
|
||||||
|
this.unsubSseLedger = sseAutomat?.actions?.on?.('ledger', () => {
|
||||||
|
void this.reloadLedger();
|
||||||
|
});
|
||||||
|
this.unsubSseStock = sseAutomat?.actions?.on?.('stock', () => {
|
||||||
|
void this.reloadLedger();
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
void this.loadPartiesOnly();
|
void this.loadPartiesOnly();
|
||||||
void ensureDocumentNumber(this.props.kind).catch(() => {});
|
void ensureDocumentNumber(this.props.kind).catch(() => {});
|
||||||
@@ -359,12 +376,28 @@ class ManifestForm extends PureComponent {
|
|||||||
this.unsubParties?.();
|
this.unsubParties?.();
|
||||||
this.unsubGroups?.();
|
this.unsubGroups?.();
|
||||||
this.unsubCompany?.();
|
this.unsubCompany?.();
|
||||||
|
this.unsubSseLedger?.();
|
||||||
|
this.unsubSseStock?.();
|
||||||
if (this._parseAbortController) {
|
if (this._parseAbortController) {
|
||||||
this._parseAbortController.abort();
|
this._parseAbortController.abort();
|
||||||
this._parseAbortController = null;
|
this._parseAbortController = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reloadLedger() {
|
||||||
|
if (this.props.kind !== 'order' || this.props.mode !== 'edit' || !this.props.docId) return;
|
||||||
|
try {
|
||||||
|
const ledgerEntries = await fetchOrderLedgerEntries(this.props.docId, {
|
||||||
|
shipments: this.state.shipments,
|
||||||
|
lines: this.state.lines,
|
||||||
|
});
|
||||||
|
if (this._cancelled) return;
|
||||||
|
this.setState({ ledgerEntries });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[ManifestForm] Failed to reload ledger entries:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadPartiesOnly() {
|
async loadPartiesOnly() {
|
||||||
if (this.state.parties.length === 0) {
|
if (this.state.parties.length === 0) {
|
||||||
this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
|
this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
|
||||||
@@ -464,6 +497,18 @@ class ManifestForm extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ledgerEntries = [];
|
||||||
|
if (kind === 'order') {
|
||||||
|
try {
|
||||||
|
ledgerEntries = await fetchOrderLedgerEntries(docId, {
|
||||||
|
shipments,
|
||||||
|
lines: doc.lines,
|
||||||
|
});
|
||||||
|
} catch (lErr) {
|
||||||
|
console.warn('[ManifestForm] Failed to load ledger entries:', lErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadedDoc = {
|
const loadedDoc = {
|
||||||
date: dateInputValue(doc.date),
|
date: dateInputValue(doc.date),
|
||||||
partyId: doc[cfg.partyField],
|
partyId: doc[cfg.partyField],
|
||||||
@@ -492,6 +537,7 @@ class ManifestForm extends PureComponent {
|
|||||||
parties,
|
parties,
|
||||||
partiesLoading: false,
|
partiesLoading: false,
|
||||||
...loadedDoc,
|
...loadedDoc,
|
||||||
|
ledgerEntries,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
dirtyFields: null,
|
dirtyFields: null,
|
||||||
dirtyLineKeys: [],
|
dirtyLineKeys: [],
|
||||||
@@ -1409,6 +1455,9 @@ class ManifestForm extends PureComponent {
|
|||||||
navigate={this.props.navigate}
|
navigate={this.props.navigate}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{status === 'ready' && kind === 'order' && this.props.mode === 'edit' ? (
|
||||||
|
<OrderLedgerPanel entries={this.state.ledgerEntries} />
|
||||||
|
) : null}
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
<ManifestPdfDialog
|
<ManifestPdfDialog
|
||||||
open={pdfDialogOpen}
|
open={pdfDialogOpen}
|
||||||
|
|||||||
96
src/components/manifest/OrderLedgerPanel.jsx
Normal file
96
src/components/manifest/OrderLedgerPanel.jsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { PureComponent } from 'react';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
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 TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { PanelTile } from 'dashgui/panels';
|
||||||
|
import { SURFACE_BASE, SURFACE_RAISED } from '../../colors';
|
||||||
|
|
||||||
|
export default class OrderLedgerPanel extends PureComponent {
|
||||||
|
render() {
|
||||||
|
const { entries = [] } = this.props;
|
||||||
|
if (!Array.isArray(entries) || entries.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelTile
|
||||||
|
wide={4}
|
||||||
|
color={SURFACE_BASE}
|
||||||
|
sx={{
|
||||||
|
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableContainer component={Paper} variant="outlined">
|
||||||
|
<Table size="small">
|
||||||
|
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>ArtNr</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferschein / Beleg</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge verbucht</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Referenz</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{entries.map((entry) => {
|
||||||
|
const isNegative = entry.rawAmount != null && entry.rawAmount < 0;
|
||||||
|
return (
|
||||||
|
<TableRow key={entry.id} hover>
|
||||||
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
|
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||||
|
{entry.sku}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5 }}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{entry.name}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{entry.documentNumber || '–'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{entry.warehouseName}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
align="right"
|
||||||
|
sx={{
|
||||||
|
py: 0.5,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: isNegative ? 'error.main' : 'success.main',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{entry.mengeVerbucht}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5 }}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{entry.transaction_text || '–'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</PanelTile>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -328,7 +328,7 @@ export default class ShipmentDetailDialog extends PureComponent {
|
|||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
shipment: { ...prev.shipment, state: newState },
|
shipment: { ...prev.shipment, state: newState },
|
||||||
}));
|
}));
|
||||||
const label = newState === 'delivered' ? 'Versendet' : newState === 'returned' ? 'Retoure' : 'Neu';
|
const label = newState === 'delivered' ? 'Versendet' : 'Neu';
|
||||||
enqueueSnackbar(`Lieferstatus geändert: ${label}`, { variant: 'success' });
|
enqueueSnackbar(`Lieferstatus geändert: ${label}`, { variant: 'success' });
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
orderAutomat.actions.reload(),
|
orderAutomat.actions.reload(),
|
||||||
@@ -631,20 +631,9 @@ export default class ShipmentDetailDialog extends PureComponent {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{shipment.state === 'delivered' && (
|
{shipment.state === 'delivered' && (
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="warning"
|
|
||||||
disabled={busyAction}
|
|
||||||
onClick={() => this.handleStateChange('returned')}
|
|
||||||
sx={{ textTransform: 'none', fontWeight: 700 }}
|
|
||||||
>
|
|
||||||
{busyAction ? 'Wird gebucht…' : 'Retoure'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{shipment.state === 'returned' && (
|
|
||||||
<Chip
|
<Chip
|
||||||
label="Retoure"
|
label="Versendet"
|
||||||
color="error"
|
color="success"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ fontWeight: 700, height: 28, fontSize: '0.8rem' }}
|
sx={{ fontWeight: 700, height: 28, fontSize: '0.8rem' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -424,12 +424,6 @@ class DeliveryStateMenu extends PureComponent {
|
|||||||
>
|
>
|
||||||
Geliefert (Zugestellt)
|
Geliefert (Zugestellt)
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
|
||||||
selected={currentState === 'returned'}
|
|
||||||
onClick={() => this.handleSelectState('returned')}
|
|
||||||
>
|
|
||||||
Retoure (Retourniert)
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -824,14 +818,6 @@ class ShipmentSearchField extends PureComponent {
|
|||||||
onClick={() => shipmentAutomat.actions.setStateFilter('delivered')}
|
onClick={() => shipmentAutomat.actions.setStateFilter('delivered')}
|
||||||
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
||||||
/>
|
/>
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label="Retoure"
|
|
||||||
color={stateFilter === 'returned' ? 'error' : 'default'}
|
|
||||||
variant={stateFilter === 'returned' ? 'filled' : 'outlined'}
|
|
||||||
onClick={() => shipmentAutomat.actions.setStateFilter('returned')}
|
|
||||||
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Right: Negativeseek controls (-1 Month, -1 Year, Reset) */}
|
{/* Right: Negativeseek controls (-1 Month, -1 Year, Reset) */}
|
||||||
|
|||||||
@@ -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 OrderLedgerPanel } from './OrderLedgerPanel';
|
||||||
export { default as PartyFilterPanel, PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
export { default as PartyFilterPanel, PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||||
export { withManifestId } from './withManifestId';
|
export { withManifestId } from './withManifestId';
|
||||||
export {
|
export {
|
||||||
|
|||||||
184
src/utils/orderLedger.js
Normal file
184
src/utils/orderLedger.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { ledger as defaultLedgerApi, shipment as defaultShipmentApi } from '../api';
|
||||||
|
import warehouseAutomat from '../automats/warehouseAutomat';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches ledger entries for an order by inspecting:
|
||||||
|
* 1. All lines of the order's shipments (each shipment line id is a manifestarticle_id)
|
||||||
|
* 2. All lines of the order itself (in case order lines have direct ledger bookings)
|
||||||
|
*
|
||||||
|
* @param {number|string} orderId
|
||||||
|
* @param {Object} [options]
|
||||||
|
* @param {Array<object>} [options.shipments] Preloaded shipments
|
||||||
|
* @param {Array<object>} [options.lines] Order lines
|
||||||
|
* @param {Object} [options.ledgerApi]
|
||||||
|
* @param {Object} [options.shipmentApi]
|
||||||
|
* @returns {Promise<Array<object>>} Flattened, correlated and sorted ledger entries
|
||||||
|
*/
|
||||||
|
export async function fetchOrderLedgerEntries(orderId, {
|
||||||
|
shipments = null,
|
||||||
|
lines = null,
|
||||||
|
ledgerApi = defaultLedgerApi,
|
||||||
|
shipmentApi = defaultShipmentApi,
|
||||||
|
} = {}) {
|
||||||
|
let shipmentList = shipments;
|
||||||
|
if (!shipmentList && orderId) {
|
||||||
|
try {
|
||||||
|
const res = await shipmentApi.listByOrder(orderId);
|
||||||
|
shipmentList = Array.isArray(res?.shipments) ? res.shipments : [];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[orderLedger] Failed to fetch shipments for order:', err);
|
||||||
|
shipmentList = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure shipments have their lines loaded
|
||||||
|
if (Array.isArray(shipmentList)) {
|
||||||
|
shipmentList = await Promise.all(
|
||||||
|
shipmentList.map(async (sh) => {
|
||||||
|
if (Array.isArray(sh.lines) && sh.lines.length > 0) return sh;
|
||||||
|
if (!sh.id) return sh;
|
||||||
|
try {
|
||||||
|
const detailRes = await shipmentApi.get(sh.id);
|
||||||
|
const detail = detailRes?.shipment ?? detailRes;
|
||||||
|
return detail?.lines ? detail : sh;
|
||||||
|
} catch {
|
||||||
|
return sh;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect target lines to check for ledger bookings
|
||||||
|
const targets = [];
|
||||||
|
const seenIds = new Set();
|
||||||
|
|
||||||
|
for (const sh of shipmentList || []) {
|
||||||
|
const docNum = sh.number || `Lieferschein #${sh.id}`;
|
||||||
|
for (const sLine of sh.lines || []) {
|
||||||
|
if (sLine?.id && !seenIds.has(sLine.id)) {
|
||||||
|
seenIds.add(sLine.id);
|
||||||
|
targets.push({
|
||||||
|
manifestarticleId: sLine.id,
|
||||||
|
sku: sLine.sku || null,
|
||||||
|
name: sLine.name || null,
|
||||||
|
lineAmount: sLine.amount ?? null,
|
||||||
|
documentNumber: docNum,
|
||||||
|
documentType: 'shipment',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const oLine of lines || []) {
|
||||||
|
if (oLine?.id && !seenIds.has(oLine.id)) {
|
||||||
|
seenIds.add(oLine.id);
|
||||||
|
targets.push({
|
||||||
|
manifestarticleId: oLine.id,
|
||||||
|
sku: oLine.sku || null,
|
||||||
|
name: oLine.name || null,
|
||||||
|
lineAmount: oLine.amount ?? null,
|
||||||
|
documentNumber: null,
|
||||||
|
documentType: 'order',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targets.length === 0) return [];
|
||||||
|
|
||||||
|
// Query ledger entries for each manifestarticleId in parallel
|
||||||
|
const results = await Promise.all(
|
||||||
|
targets.map(async (t) => {
|
||||||
|
try {
|
||||||
|
const res = await ledgerApi.listByManifestarticleId(t.manifestarticleId);
|
||||||
|
const entries = Array.isArray(res?.ledger) ? res.ledger : [];
|
||||||
|
return { target: t, entries };
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[orderLedger] Failed to fetch ledger for line ${t.manifestarticleId}:`, err);
|
||||||
|
return { target: t, entries: [] };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const warehouses = warehouseAutomat.state.warehouses || [];
|
||||||
|
const allEntries = [];
|
||||||
|
|
||||||
|
for (const { target, entries } of results) {
|
||||||
|
if (!entries || entries.length === 0) continue;
|
||||||
|
|
||||||
|
const docEntries = entries.filter((e) => {
|
||||||
|
const type = String(e.transaction_type || '').toUpperCase();
|
||||||
|
return type !== 'WAREHOUSE';
|
||||||
|
});
|
||||||
|
const warehouseEntries = entries.filter((e) => {
|
||||||
|
const type = String(e.transaction_type || '').toUpperCase();
|
||||||
|
return type === 'WAREHOUSE';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (docEntries.length > 0) {
|
||||||
|
docEntries.forEach((docEntry, idx) => {
|
||||||
|
const matchingWh =
|
||||||
|
warehouseEntries.find(
|
||||||
|
(w) =>
|
||||||
|
w.warehouse_id != null &&
|
||||||
|
(w.transaction_text === docEntry.transaction_text ||
|
||||||
|
Math.abs(new Date(w.created_at) - new Date(docEntry.created_at)) < 15000)
|
||||||
|
) || warehouseEntries[idx] || null;
|
||||||
|
|
||||||
|
const warehouseId = matchingWh?.warehouse_id ?? docEntry.warehouse_id ?? null;
|
||||||
|
const wh = warehouseId != null
|
||||||
|
? warehouses.find((w) => Number(w.id) === Number(warehouseId))
|
||||||
|
: null;
|
||||||
|
const warehouseName = wh?.name || (warehouseId != null ? `Lager #${warehouseId}` : '–');
|
||||||
|
|
||||||
|
const rawAmount = matchingWh?.amount ?? docEntry.amount;
|
||||||
|
const mengeVerbucht = rawAmount != null
|
||||||
|
? (Number(rawAmount) > 0 ? `+${rawAmount}` : String(rawAmount))
|
||||||
|
: '–';
|
||||||
|
|
||||||
|
allEntries.push({
|
||||||
|
id: docEntry.id,
|
||||||
|
created_at: docEntry.created_at,
|
||||||
|
sku: docEntry.sku || target.sku || '–',
|
||||||
|
name: target.name || '–',
|
||||||
|
lineAmount: target.lineAmount != null ? String(target.lineAmount) : '–',
|
||||||
|
documentNumber: target.documentNumber,
|
||||||
|
warehouseName,
|
||||||
|
warehouseId,
|
||||||
|
mengeVerbucht,
|
||||||
|
rawAmount: rawAmount != null ? Number(rawAmount) : null,
|
||||||
|
transaction_type: docEntry.transaction_type,
|
||||||
|
transaction_text: docEntry.transaction_text || '–',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else if (warehouseEntries.length > 0) {
|
||||||
|
for (const w of warehouseEntries) {
|
||||||
|
const wh = w.warehouse_id != null
|
||||||
|
? warehouses.find((item) => Number(item.id) === Number(w.warehouse_id))
|
||||||
|
: null;
|
||||||
|
const warehouseName = wh?.name || (w.warehouse_id != null ? `Lager #${w.warehouse_id}` : '–');
|
||||||
|
const rawAmount = w.amount;
|
||||||
|
const mengeVerbucht = rawAmount != null
|
||||||
|
? (Number(rawAmount) > 0 ? `+${rawAmount}` : String(rawAmount))
|
||||||
|
: '–';
|
||||||
|
|
||||||
|
allEntries.push({
|
||||||
|
id: w.id,
|
||||||
|
created_at: w.created_at,
|
||||||
|
sku: w.sku || target.sku || '–',
|
||||||
|
name: target.name || '–',
|
||||||
|
lineAmount: target.lineAmount != null ? String(target.lineAmount) : '–',
|
||||||
|
documentNumber: target.documentNumber,
|
||||||
|
warehouseName,
|
||||||
|
warehouseId: w.warehouse_id,
|
||||||
|
mengeVerbucht,
|
||||||
|
rawAmount: rawAmount != null ? Number(rawAmount) : null,
|
||||||
|
transaction_type: w.transaction_type,
|
||||||
|
transaction_text: w.transaction_text || '–',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
||||||
|
return allEntries;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user