Files
dashfront/src/components/article/tabs/ArticleHistoryTab.jsx
2026-10-23 23:11:05 +02:00

931 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { PureComponent, createRef } 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 Link from '@mui/material/Link';
import ListItemButton from '@mui/material/ListItemButton';
import Skeleton from '@mui/material/Skeleton';
import Typography from '@mui/material/Typography';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import { enqueueSnackbar } from 'notistack';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import FillSpace from 'dashgui/FillSpace';
import BackendError from '../../BackendError';
import { getArticleHistoryAutomat } from '../../../automats/articleDetailAutomat';
import sseAutomat from '../../../automats/sseAutomat';
import shipmentAutomat from '../../../automats/shipmentAutomat';
import orderAutomat from '../../../automats/orderAutomat';
import entityAutomat from '../../../automats/entityAutomat';
import brandAutomat from '../../../automats/brandAutomat';
import { shipment as shipmentApi, order as orderApi } from '../../../api';
import { ShipmentDetailDialog } from '../../manifest';
import SkeleHashImage from '../../settings/SkeleHashImage';
import withNavigate from '../../withNavigate';
import { backendErrorFrom } from '../../../utils/backendError';
import { notifyError } from '../../../utils/notify';
import { DEFAULT_CHUNK_SIZE as CHUNK_SIZE } from '../../../automats/createChunkListAutomat';
function formatDateTime(val) {
if (!val) return '–';
try {
const d = new Date(val);
if (isNaN(d.getTime())) return String(val);
return d.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
} catch {
return String(val);
}
}
function decodeMeta(meta) {
if (!meta) return null;
if (typeof meta === 'object') return meta;
try {
return JSON.parse(meta);
} catch {
return null;
}
}
// Dummy history entry for layout and FillSpace row measurement
export function createDummyTestArticleHistory() {
return {
id: 999999,
date: '2030-10-30T12:00:00Z',
text: 'Preis geändert von 10,00 € auf 12,50 € durch Benutzer Max Mustermann für Lieferschein LS-123456',
};
}
const rowGridSx = {
display: 'grid',
gridTemplateColumns: 'minmax(150px, max-content) minmax(260px, 1fr)',
gridTemplateRows: '0px 1fr',
rowGap: 0,
columnGap: 1.5,
alignItems: 'center',
width: '100%',
boxSizing: 'border-box',
};
const ellipsisSx = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
const measuringCellSx = {
visibility: 'hidden',
height: 0,
boxSizing: 'border-box',
pointerEvents: 'none',
textTransform: 'none',
letterSpacing: 'normal',
};
const headerCellSx = {
color: 'text.secondary',
typography: 'caption',
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.04em',
userSelect: 'none',
};
function renderInvisibleDummyRow(dummy) {
const d = dummy || createDummyTestArticleHistory();
return (
<>
<Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}>{formatDateTime(d.date)}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}>{d.text}</Typography>
</Box>
</>
);
}
/**
* Ambient row context for `ArticleHistoryRow` callbacks, mirroring the pattern in `QuoteList`.
*/
const rowContext = {
headerItem: null,
onOpenShipment: null,
onOpenOrder: null,
onOpenCustomer: null,
onOpenBrand: null,
};
// MARK: <ArticleHistoryRow/>
class ArticleHistoryRow extends PureComponent {
renderLink(label, onClick) {
return (
<Link
component="button"
type="button"
variant="body2"
onClick={(e) => {
e.stopPropagation();
onClick?.(e);
}}
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>
);
}
collectSpans(text, meta) {
const spans = [];
if (!text) return spans;
const { onOpenShipment, onOpenOrder, onOpenCustomer, onOpenBrand } = rowContext;
// 1. Lieferschein
const shipmentNumber =
meta?.shipmentNumber || (meta?.type === 'shipment' ? meta?.manifestNumber : null);
const regexShipment =
!shipmentNumber && text ? text.match(/Lieferschein\s+([A-Za-z0-9\-_]+)/) : null;
const effectiveShipmentNumber = shipmentNumber || regexShipment?.[1] || null;
if (effectiveShipmentNumber) {
const shipmentId = meta?.shipmentId ?? meta?.manifestId ?? meta?.id ?? null;
const targetPattern = `Lieferschein ${effectiveShipmentNumber}`;
let idx = text.indexOf(targetPattern);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + targetPattern.length,
node: this.renderLink(targetPattern, () =>
onOpenShipment?.(effectiveShipmentNumber, shipmentId),
),
});
} else {
idx = text.indexOf(effectiveShipmentNumber);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + effectiveShipmentNumber.length,
node: this.renderLink(effectiveShipmentNumber, () =>
onOpenShipment?.(effectiveShipmentNumber, shipmentId),
),
});
}
}
}
// 2. Auftrag (Order)
const orderNumber =
meta?.orderNumber || (meta?.type === 'order' ? meta?.number : null);
const regexOrder =
!orderNumber && text ? text.match(/Auftrag\s+([A-Za-z0-9\-_]+)/) : null;
const effectiveOrderNumber = orderNumber || regexOrder?.[1] || null;
if (effectiveOrderNumber) {
const orderId = meta?.orderId ?? null;
const targetPattern = `Auftrag ${effectiveOrderNumber}`;
let idx = text.indexOf(targetPattern);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + targetPattern.length,
node: this.renderLink(targetPattern, () =>
onOpenOrder?.(orderId, effectiveOrderNumber),
),
});
} else {
idx = text.indexOf(effectiveOrderNumber);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + effectiveOrderNumber.length,
node: this.renderLink(effectiveOrderNumber, () =>
onOpenOrder?.(orderId, effectiveOrderNumber),
),
});
}
}
}
// 3. Kunde (Customer)
const customerName =
meta?.customerName || (text.match(/Kunde:\s*"([^"]+)"/)?.[1] ?? null);
if (customerName) {
const inQuotes = `"${customerName}"`;
let idx = text.indexOf(inQuotes);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + inQuotes.length,
node: this.renderLink(inQuotes, () => onOpenCustomer?.(customerName)),
});
} else {
idx = text.indexOf(customerName);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + customerName.length,
node: this.renderLink(customerName, () => onOpenCustomer?.(customerName)),
});
}
}
}
// 4. Marke (Brand)
const brandNames = [meta?.newBrandName, meta?.oldBrandName].filter(Boolean);
for (const bName of brandNames) {
const inQuotes = `"${bName}"`;
let idx = text.indexOf(inQuotes);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + inQuotes.length,
node: this.renderLink(inQuotes, () => onOpenBrand?.(bName)),
});
} else {
idx = text.indexOf(bName);
if (idx !== -1) {
spans.push({
start: idx,
end: idx + bName.length,
node: this.renderLink(bName, () => onOpenBrand?.(bName)),
});
}
}
}
return spans;
}
renderEntrySpans(text, meta) {
if (!text) return null;
const allSpans = this.collectSpans(text, meta);
if (allSpans.length === 0) return text;
allSpans.sort((a, b) => a.start - b.start || (b.end - b.start) - (a.end - a.start));
const nonOverlapping = [];
let lastEnd = 0;
for (const span of allSpans) {
if (span.start >= lastEnd) {
nonOverlapping.push(span);
lastEnd = span.end;
}
}
const fragments = [];
let cur = 0;
for (let i = 0; i < nonOverlapping.length; i++) {
const span = nonOverlapping[i];
if (span.start > cur) {
fragments.push(text.slice(cur, span.start));
}
fragments.push(<span key={i}>{span.node}</span>);
cur = span.end;
}
if (cur < text.length) {
fragments.push(text.slice(cur));
}
return fragments;
}
renderSkelehashThumb(meta) {
if (!meta?.skelehash) return null;
return (
<Box
component="span"
sx={{
display: 'inline-block',
width: 14,
height: 14,
lineHeight: 0,
m: 0,
my: 0,
p: 0,
py: 0,
mr: 0.5,
borderRadius: '2px',
overflow: 'hidden',
flexShrink: 0,
verticalAlign: 'middle',
}}
>
<SkeleHashImage
hash={meta.skelehash}
sx={{ width: 14, height: 14, display: 'block' }}
/>
</Box>
);
}
render() {
const { item, disabled, headerItem } = this.props;
const dummy = headerItem || rowContext.headerItem;
if (!item) {
if (disabled) {
return (
<ListItemButton
disabled
divider
sx={{
...rowGridSx,
height: 32,
minHeight: 32,
maxHeight: 32,
px: 1.5,
py: 0,
opacity: 0,
pointerEvents: 'none',
}}
>
<Typography variant="body2" sx={{ fontSize: '0.8125rem' }}>&nbsp;</Typography>
</ListItemButton>
);
}
return (
<ListItemButton
disabled
divider
sx={{ ...rowGridSx, height: 32, minHeight: 32, maxHeight: 32, px: 1.5, py: 0 }}
>
{renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2 }}>
<Skeleton variant="text" width="85%" height={16} />
</Box>
<Box sx={{ gridColumn: 2, gridRow: 2 }}>
<Skeleton variant="text" width="95%" height={16} />
</Box>
</ListItemButton>
);
}
const meta = decodeMeta(item.meta);
return (
<ListItemButton
divider
disabled={disabled}
sx={{ ...rowGridSx, height: 32, minHeight: 32, maxHeight: 32, px: 1.5, py: 0 }}
>
{renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0 }}>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontVariantNumeric: 'tabular-nums', fontSize: '0.8125rem', ...ellipsisSx }}
>
{formatDateTime(item.date || item.createdAt)}
</Typography>
</Box>
<Box
sx={{
gridColumn: 2,
gridRow: 2,
minWidth: 0,
display: 'flex',
alignItems: 'center',
overflow: 'hidden',
}}
>
{this.renderSkelehashThumb(meta)}
<Typography
variant="body2"
component="div"
sx={{ ...ellipsisSx, display: 'inline', width: '100%', fontSize: '0.8125rem' }}
>
{this.renderEntrySpans(item.text, meta)}
</Typography>
</Box>
</ListItemButton>
);
}
}
// MARK: <ArticleHistorySearchField/>
class ArticleHistorySearchField extends PureComponent {
constructor(props) {
super(props);
const { automat } = props;
const s = automat.state;
const stats = automat.getChunkStats?.();
this.state = {
input: s.filter ?? '',
seekMonths: s.seekMonths ?? 0,
seekYears: s.seekYears ?? 0,
negativeSeekMonths: s.negativeSeekMonths ?? 0,
lruChunksCount: stats?.lruChunksCount || 0,
maxLruChunks: stats?.maxLruChunks || 50,
filterCachesCount: stats?.filterCaches?.length || 1,
};
}
componentDidMount() {
this.subscribeAutomat(this.props.automat);
}
componentDidUpdate(prevProps) {
if (prevProps.automat !== this.props.automat) {
this.unsubscribeAutomat();
const { automat } = this.props;
const s = automat.state;
const stats = automat.getChunkStats?.();
this.setState({
input: s.filter ?? '',
seekMonths: s.seekMonths ?? 0,
seekYears: s.seekYears ?? 0,
negativeSeekMonths: s.negativeSeekMonths ?? 0,
lruChunksCount: stats?.lruChunksCount || 0,
maxLruChunks: stats?.maxLruChunks || 50,
filterCachesCount: stats?.filterCaches?.length || 1,
});
this.subscribeAutomat(automat);
}
}
componentWillUnmount() {
this.unsubscribeAutomat();
}
subscribeAutomat(automat) {
this.unsub = automat.subscribe(this, (state) => ({
input: state.filter ?? '',
seekMonths: state.seekMonths ?? 0,
seekYears: state.seekYears ?? 0,
negativeSeekMonths: state.negativeSeekMonths ?? 0,
}));
if (import.meta.env.DEV) {
this.unsubStats = automat.subscribeChunkStats?.((stats) => {
this.setState({
lruChunksCount: stats.lruChunksCount,
maxLruChunks: stats.maxLruChunks,
filterCachesCount: stats.filterCaches?.length || 1,
});
});
}
}
unsubscribeAutomat() {
this.unsub?.();
this.unsub = null;
this.unsubStats?.();
this.unsubStats = null;
}
render() {
const { inputRef, automat } = this.props;
const { input, seekMonths, seekYears } = this.state;
const monthActive = seekMonths > 0;
const monthLabel = !monthActive ? '-1 Month' : `-${seekMonths} Month${seekMonths > 1 ? 's' : ''}`;
const yearActive = seekYears > 0;
const yearLabel = !yearActive ? '-1 Year' : `-${seekYears} Year${seekYears > 1 ? 's' : ''}`;
return (
<Box
ref={inputRef}
sx={{
display: 'flex',
flexDirection: 'column',
gap: 1.25,
pb: 1.5,
}}
>
{/* Top row: search field */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<SingleLineTextEdit
sx={{ flex: 1 }}
size="small"
search
clear
value={input}
placeholder="In Artikel-Historie suchen…"
onChange={(next) => automat.actions.filter(next)}
/>
</Box>
{/* Filter Bar: Negativeseek (-1 Month, -1 Year, Reset) & Chunk-Cache Monitor */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
flexWrap: 'wrap',
gap: 1.5,
}}
>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.25,
borderRadius: 2,
bgcolor: 'background.paper',
boxShadow: 1,
}}
>
<Button
size="small"
variant={monthActive ? 'contained' : 'outlined'}
color={monthActive ? 'primary' : 'inherit'}
onClick={() => automat.actions.addSeekMonth()}
sx={{
minWidth: 88,
px: 1.25,
py: 0.25,
height: 28,
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'none',
boxSizing: 'border-box',
border: '1px solid',
borderColor: monthActive ? 'primary.main' : 'divider',
whiteSpace: 'nowrap',
}}
>
{monthLabel}
</Button>
<Button
size="small"
variant={yearActive ? 'contained' : 'outlined'}
color={yearActive ? 'primary' : 'inherit'}
onClick={() => automat.actions.addSeekYear()}
sx={{
minWidth: 78,
px: 1.25,
py: 0.25,
height: 28,
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'none',
boxSizing: 'border-box',
border: '1px solid',
borderColor: yearActive ? 'primary.main' : 'divider',
whiteSpace: 'nowrap',
}}
>
{yearLabel}
</Button>
<Button
size="small"
variant="text"
color="inherit"
disabled={!monthActive && !yearActive}
onClick={() => automat.actions.resetNegativeSeek()}
sx={{
minWidth: 'auto',
px: 1,
py: 0.25,
height: 28,
fontSize: '0.75rem',
fontWeight: 600,
textTransform: 'none',
}}
>
Reset
</Button>
</Box>
{import.meta.env.DEV && (
<Chip
size="small"
icon={<ViewModuleIcon sx={{ fontSize: 16 }} />}
label={
this.state.lruChunksCount > 0
? `Chunk-Cache (${this.state.lruChunksCount}/${this.state.maxLruChunks || 50})`
: 'Chunk-Cache'
}
onClick={() =>
window.dispatchEvent(
new CustomEvent('dash:toggle-chunk-monitor', {
detail: { target: automat.name },
}),
)
}
sx={{
cursor: 'pointer',
height: 28,
fontWeight: 600,
fontSize: '0.75rem',
bgcolor: 'background.paper',
boxShadow: 1,
'&:hover': { bgcolor: 'action.hover' },
}}
/>
)}
</Box>
</Box>
);
}
}
// MARK: <ArticleHistoryTab/>
class ArticleHistoryTab extends PureComponent {
constructor(props) {
super(props);
this.headerItem = createDummyTestArticleHistory();
this.automat = getArticleHistoryAutomat(props.sku);
this.state = {
items: this.automat.getItems(),
unfilteredTotal: this.automat.state.unfilteredTotal ?? 0,
filterKey: this.automat.getFilterKey?.() ?? '',
status: this.automat.state.status,
error: this.automat.state.error,
selectedShipment: null,
loadingShipment: false,
};
this.searchRef = createRef();
this.handlePageChange = this.handlePageChange.bind(this);
this.handleRetry = this.handleRetry.bind(this);
this.handleCloseShipment = this.handleCloseShipment.bind(this);
this.handleOpenShipment = this.handleOpenShipment.bind(this);
this.handleOpenOrder = this.handleOpenOrder.bind(this);
this.handleOpenCustomer = this.handleOpenCustomer.bind(this);
this.handleOpenBrand = this.handleOpenBrand.bind(this);
}
componentDidMount() {
this.watch(this.props.sku);
if (entityAutomat.state.status === 'idle') {
void entityAutomat.actions.load().catch(() => {});
}
if (brandAutomat.state.status === 'idle') {
void brandAutomat.actions.load().catch(() => {});
}
}
componentDidUpdate(prevProps) {
if (prevProps.sku !== this.props.sku) {
this.watch(this.props.sku);
}
}
componentWillUnmount() {
this.unsub?.();
this.unsub = null;
this.unwatchSignal?.();
this.unwatchSignal = null;
}
watch(sku) {
this.unsub?.();
this.unwatchSignal?.();
this.automat = getArticleHistoryAutomat(sku);
this.setState({
items: this.automat.getItems(),
unfilteredTotal: this.automat.state.unfilteredTotal ?? 0,
filterKey: this.automat.getFilterKey?.() ?? '',
status: this.automat.state.status,
error: this.automat.state.error,
});
this.unsub = this.automat.subscribe(this, (state) => ({
items: state.items,
unfilteredTotal: state.unfilteredTotal ?? 0,
filterKey: this.automat.getFilterKey(state),
status: state.status,
error: state.error,
}));
this.unwatchSignal = sseAutomat.actions.watchArticle(String(sku), () => {
void this.automat.actions.reload().catch(() => {});
});
}
handlePageChange(page, meta) {
const pageSize = meta?.pageSize ?? 10;
const startIndex = (page - 1) * pageSize;
const items = this.state.items;
const maxIndex = items ? items.length - 1 : startIndex;
const endIndex = Math.min(maxIndex, startIndex + pageSize - 1);
const startChunk = Math.floor(startIndex / CHUNK_SIZE);
const endChunk = Math.floor(endIndex / CHUNK_SIZE);
this.automat.requireChunk(startChunk, endChunk);
}
handleRetry() {
void this.automat.actions.requireChunk(0, 0, { immediate: true }).catch(() => {});
}
handleCloseShipment() {
this.setState({ selectedShipment: null });
}
async handleOpenShipment(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);
}
}
async handleOpenOrder(orderId, orderNumber) {
if (orderId) {
this.props.navigate(`/auftrag/bestellung/${orderId}`);
return;
}
if (!orderNumber) return;
const cached = orderAutomat.state.items?.find(
(o) => o && String(o.number).toLowerCase() === String(orderNumber).toLowerCase(),
);
if (cached?.id) {
this.props.navigate(`/auftrag/bestellung/${cached.id}`);
return;
}
try {
const res = await orderApi.list({ search: orderNumber });
const orders = Array.isArray(res?.orders) ? res.orders : [];
const match = orders.find(
(o) => String(o.number).toLowerCase() === String(orderNumber).toLowerCase(),
) || orders[0];
if (match?.id) {
this.props.navigate(`/auftrag/bestellung/${match.id}`);
} else {
this.props.navigate(`/auftrag/bestellung?search=${encodeURIComponent(orderNumber)}`);
}
} catch {
this.props.navigate(`/auftrag/bestellung?search=${encodeURIComponent(orderNumber)}`);
}
}
async handleOpenCustomer(customerName) {
if (!customerName) return;
const target = String(customerName).trim().toLowerCase();
const findInItems = (items) => (items || []).find((c) => {
const full = [c.firstName, c.name].filter(Boolean).join(' ').trim().toLowerCase();
const nameOnly = String(c.name || '').trim().toLowerCase();
return full === target || nameOnly === target;
});
let match = findInItems(entityAutomat.state.items);
if (!match && entityAutomat.state.status !== 'loading') {
try {
const loaded = await entityAutomat.actions.load();
match = findInItems(loaded || entityAutomat.state.items);
} catch {}
}
if (match?.id) {
this.props.navigate(`/kunde/${match.id}`);
} else {
this.props.navigate(`/kunde?search=${encodeURIComponent(customerName)}`);
}
}
handleOpenBrand(brandName) {
if (!brandName) return;
brandAutomat.actions.filter(brandName);
this.props.navigate('/artikel/marken');
}
render() {
const { items, unfilteredTotal, filterKey, status, error, selectedShipment } = this.state;
const isFailed = items === null && status === 'failed';
rowContext.onOpenShipment = this.handleOpenShipment;
rowContext.onOpenOrder = this.handleOpenOrder;
rowContext.onOpenCustomer = this.handleOpenCustomer;
rowContext.onOpenBrand = this.handleOpenBrand;
rowContext.headerItem = this.headerItem;
return (
<Box sx={{ width: '100%', position: 'relative' }}>
<ArticleHistorySearchField
inputRef={this.searchRef}
automat={this.automat}
/>
{isFailed ? (
<Box sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(error)} onRetry={this.handleRetry} />
</Box>
) : (
<Box sx={{ width: '100%', position: 'relative' }}>
<FillSpace
aboveRef={[this.searchRef]}
reserveBottom={16}
minPageSize={20}
minHeight={0}
resetKey={filterKey}
items={items || []}
totalCount={items ? items.length : 0}
unfilteredTotal={unfilteredTotal}
headerItem={this.headerItem}
renderItem={ArticleHistoryRow}
disableWheel
header={
<Box
sx={{
...rowGridSx,
height: 28,
minHeight: 28,
px: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
{renderInvisibleDummyRow(this.headerItem)}
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Datum</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Änderung / Ereignis</Box>
</Box>
}
onPageChange={this.handlePageChange}
/>
{items === null && (
<Box
sx={{
position: 'absolute',
top: 52,
left: 0,
right: 0,
bottom: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 2,
}}
>
<CircularProgress size={32} />
</Box>
)}
</Box>
)}
{selectedShipment && (
<ShipmentDetailDialog
open={Boolean(selectedShipment)}
shipment={selectedShipment}
onClose={this.handleCloseShipment}
onNavigate={this.props.navigate}
/>
)}
</Box>
);
}
}
export default withNavigate(ArticleHistoryTab);