Files
dashfront/src/components/manifest/DeliveryList.jsx
2026-10-17 23:32:21 +02:00

854 lines
30 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 { Component, 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 FormControlLabel from '@mui/material/FormControlLabel';
import IconButton from '@mui/material/IconButton';
import ListItemButton from '@mui/material/ListItemButton';
import Typography from '@mui/material/Typography';
import Switch from '@mui/material/Switch';
import Skeleton from '@mui/material/Skeleton';
import Tooltip from '@mui/material/Tooltip';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import ViewModuleIcon from '@mui/icons-material/ViewModule';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined';
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
import { enqueueSnackbar } from 'notistack';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import FillSpace from 'dashgui/FillSpace';
import { PanelTile, PanelTileGrid } from 'dashgui/panels';
import BackendError from '../BackendError';
import deliveryAutomat, { CHUNK_SIZE } from '../../automats/deliveryAutomat';
import numberAutomat from '../../automats/numberAutomat';
import { delivery as deliveryApi } from '../../api';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import { getMaxDocumentNumber } from '../../utils/documentNumber';
import BookDeliveryDialog from './BookDeliveryDialog';
import withNavigate from '../withNavigate';
import {
formatDate,
loadParties,
manifestConfig,
numberDisplay,
partyDisplayName,
} from './manifestConfig';
const cfg = manifestConfig('delivery');
function formatFileSize(bytes) {
if (bytes == null || Number.isNaN(bytes)) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
// Dummy delivery generator using the active sequence scheme with max digits for layout testing
export function createDummyTestDelivery() {
const maxNumber = getMaxDocumentNumber('delivery');
return {
id: 999999,
number: maxNumber,
externalNumber: 'REF-99999999',
date: '2030-10-30T12:00:00Z',
supplierName: 'Lieferant mit sehr langem Firmennamen GmbH & Co. KG',
positions: 999,
ledger: {
totalPositions: 999,
completelyOnLedgerPositions: 0,
bookedPositions: 0,
unbookedPositions: 999,
isBooked: false,
},
files: [
{
id: 99991,
manifestId: 999999,
fileId: 99991,
name: 'lieferschein.pdf',
mimetype: 'application/pdf',
size: 15420,
},
{
id: 99992,
manifestId: 999999,
fileId: 99992,
name: 'lieferschein.txt',
mimetype: 'text/plain',
size: 2048,
},
],
};
}
const rowGridSx = {
display: 'grid',
gridTemplateColumns: 'max-content max-content 1fr max-content max-content max-content auto',
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',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.04em',
userSelect: 'none',
};
function renderInvisibleDummyRow(dummy) {
const d = dummy || createDummyTestDelivery();
return (
<>
<Box aria-hidden sx={{ gridColumn: 1, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body1" sx={{ fontWeight: 600 }}>{d.number}</Typography>
<Typography variant="body2">{d.externalNumber || 'Nr. / Ref.'}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 2, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body2">{d.date ? formatDate(d.date) : '–'}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 3, gridRow: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Typography variant="body1">{d.supplierName || 'Lieferant'}</Typography>
</Box>
<Box aria-hidden sx={{ gridColumn: 4, gridRow: 1, display: 'inline-flex', alignItems: 'center', gap: 1, whiteSpace: 'nowrap', ...measuringCellSx }}>
<Chip
size="small"
icon={<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
label="999"
sx={{ height: 24, '& .MuiChip-label': { px: 0.5 } }}
/>
<Button
size="small"
variant="contained"
sx={{ py: 0.2, px: 1, minHeight: 24, fontSize: '0.75rem', textTransform: 'none' }}
>
Buchen
</Button>
</Box>
<Box aria-hidden sx={{ gridColumn: 5, gridRow: 1, display: 'flex', justifyContent: 'center', ...measuringCellSx }}>
<Chip size="small" label={d.positions} sx={{ height: 24, '& .MuiChip-label': { px: 0.5 } }} />
</Box>
<Box aria-hidden sx={{ gridColumn: 6, gridRow: 1, display: 'flex', alignItems: 'center', gap: 0.5, ...measuringCellSx }}>
<IconButton size="small" sx={{ p: 0.5, border: '1px solid transparent', borderRadius: 1 }}>
<PictureAsPdfIcon sx={{ fontSize: 18 }} />
</IconButton>
<IconButton size="small" sx={{ p: 0.5, border: '1px solid transparent', borderRadius: 1 }}>
<TextSnippetOutlinedIcon sx={{ fontSize: 18 }} />
</IconButton>
</Box>
<Box aria-hidden sx={{ gridColumn: 7, gridRow: 1, display: 'flex', justifyContent: 'center', ...measuringCellSx }}>
<IconButton sx={{ width: 40, height: 40 }} />
</Box>
</>
);
}
/**
* Ambient context for `DeliveryRow`: FillSpace renders rows as bare elements
* (`<Row item index />`), so the list publishes callbacks and the party map
* here on every render.
*/
const rowContext = {
suppliersById: {},
busyId: null,
onOpen: null,
onDelete: null,
onOpenBookDialog: null,
headerItem: null,
};
// MARK: <DeliveryRow/>
class DeliveryRow extends Component {
supplierName(supplierId, fallbackName) {
if (fallbackName) return fallbackName;
if (supplierId == null) return '–';
const supplier = rowContext.suppliersById[supplierId];
return partyDisplayName('delivery', supplier) || `Lieferant #${supplierId}`;
}
render() {
const { item: delivery, disabled } = this.props;
const dummy = this.props.headerItem || rowContext.headerItem;
// Data-free probe (FillSpace measures the row height with `item` undefined).
if (!delivery) {
if (disabled) {
return (
<ListItemButton disabled divider sx={{ ...rowGridSx, height: 64, minHeight: 64, maxHeight: 64, px: 2, py: 0, opacity: 0, pointerEvents: 'none' }}>
<Typography variant="body2">&nbsp;</Typography>
</ListItemButton>
);
}
return (
<ListItemButton disabled divider sx={{ ...rowGridSx, height: 64, minHeight: 64, maxHeight: 64, px: 2, py: 0 }}>
{renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2 }}>
<Skeleton variant="text" width="90%" height={20} />
<Skeleton variant="text" width="60%" height={16} />
</Box>
<Box sx={{ gridColumn: 2, gridRow: 2 }}><Skeleton variant="text" width="80%" height={20} /></Box>
<Box sx={{ gridColumn: 3, gridRow: 2 }}><Skeleton variant="text" width="70%" height={20} /></Box>
<Box sx={{ gridColumn: 4, gridRow: 2, display: 'inline-flex', alignItems: 'center', gap: 1 }}>
<Skeleton variant="rounded" width={48} height={24} />
<Skeleton variant="rounded" width={56} height={24} />
</Box>
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center' }}><Skeleton variant="rounded" width={28} height={20} /></Box>
<Box sx={{ gridColumn: 6, gridRow: 2, display: 'flex', alignItems: 'center', gap: 0.5 }}><Skeleton variant="circular" width={24} height={24} /></Box>
<Box sx={{ gridColumn: 7, gridRow: 2 }} />
</ListItemButton>
);
}
const { busyId, onOpen, onDelete, onOpenBookDialog } = rowContext;
const files = Array.isArray(delivery.files) ? delivery.files : [];
const totalPositions = Number(delivery.positions || delivery.ledger?.totalPositions || delivery.ledger?.total || 0);
const isBooked = Boolean(delivery.ledger?.isBooked || delivery.isBooked);
const completelyBooked = Number(delivery.ledger?.completelyOnLedgerPositions || delivery.ledger?.bookedPositions || (isBooked ? totalPositions : 0));
const unbooked = isBooked
? 0
: (delivery.ledger?.unbookedPositions != null
? Number(delivery.ledger.unbookedPositions)
: Math.max(0, totalPositions - completelyBooked));
return (
<ListItemButton
divider
sx={{ ...rowGridSx, height: 64, minHeight: 64, maxHeight: 64, px: 2, py: 0 }}
onClick={disabled ? undefined : () => onOpen?.(delivery)}
disabled={disabled}
>
{renderInvisibleDummyRow(dummy)}
<Box sx={{ gridColumn: 1, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<Typography variant="body1" sx={{ fontWeight: 600, ...ellipsisSx }}>
{delivery.number}
</Typography>
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
{numberDisplay(delivery.externalNumber)}
</Typography>
</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<Typography variant="body2" color="text.secondary" sx={ellipsisSx}>
{delivery.date ? formatDate(delivery.date) : '–'}
</Typography>
</Box>
<Box sx={{ gridColumn: 3, gridRow: 2, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<Typography variant="body1" sx={{ fontWeight: 500, ...ellipsisSx }}>
{this.supplierName(delivery.supplierId, delivery.supplierName)}
</Typography>
</Box>
<Box
sx={{ gridColumn: 4, gridRow: 2, display: 'inline-flex', alignItems: 'center', gap: 1 }}
onClick={(e) => e.stopPropagation()}
>
{totalPositions === 0 ? (
<Typography variant="body2" color="text.secondary">0</Typography>
) : unbooked === 0 ? (
<Chip
size="small"
icon={<CheckCircleOutlineOutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
label="Gebucht"
color="success"
variant="outlined"
onClick={() => onOpenBookDialog?.(delivery)}
sx={{ fontWeight: 600, cursor: 'pointer' }}
/>
) : (
<>
<Chip
size="small"
icon={<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
label={String(unbooked)}
color="warning"
variant="outlined"
onClick={() => onOpenBookDialog?.(delivery)}
sx={{ fontWeight: 600, cursor: 'pointer' }}
/>
<Button
size="small"
variant="contained"
color="warning"
onClick={() => onOpenBookDialog?.(delivery)}
sx={{
py: 0.2,
px: 1,
minHeight: 24,
textTransform: 'none',
fontSize: '0.75rem',
fontWeight: 600,
color: '#ffffff',
}}
>
Buchen
</Button>
</>
)}
</Box>
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center' }}>
<Chip
size="small"
label={totalPositions}
title={`${totalPositions} Positionen`}
sx={{
height: 24,
fontSize: '0.75rem',
fontWeight: 600,
fontVariantNumeric: 'tabular-nums',
bgcolor: 'action.hover',
color: totalPositions > 0 ? 'text.primary' : 'text.disabled',
'& .MuiChip-label': { px: 0.5 },
}}
/>
</Box>
<Box
sx={{ gridColumn: 6, gridRow: 2, display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}
onClick={(e) => e.stopPropagation()}
>
{files.length === 0 ? (
<Typography variant="body2" color="text.secondary">–</Typography>
) : (
files.map((file, idx) => {
const docId = file.manifestId ?? delivery.id;
const fileId = file.fileId ?? file.id;
const isPdf =
file.mimetype === 'application/pdf' ||
file.name?.toLowerCase().endsWith('.pdf');
const isTxt =
file.mimetype?.startsWith('text/') ||
file.name?.toLowerCase().endsWith('.txt') ||
file.name?.toLowerCase().endsWith('.csv');
const icon = isPdf ? (
<PictureAsPdfIcon sx={{ fontSize: 18, color: 'error.main' }} />
) : isTxt ? (
<TextSnippetOutlinedIcon sx={{ fontSize: 18, color: 'primary.main' }} />
) : (
<DescriptionOutlinedIcon sx={{ fontSize: 18, color: 'text.secondary' }} />
);
const title = `${file.name}${file.size ? ` (${formatFileSize(file.size)})` : ''}`;
const fileUrl = `/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`;
return (
<Tooltip key={file.id ?? fileId ?? idx} title={title}>
<IconButton
size="small"
aria-label={title}
onClick={() => window.open(fileUrl, '_blank')}
sx={{
p: 0.5,
border: '1px solid',
borderColor: 'divider',
bgcolor: 'background.paper',
borderRadius: 1,
'&:hover': {
bgcolor: 'action.hover',
borderColor: 'primary.main',
},
}}
>
{icon}
</IconButton>
</Tooltip>
);
})
)}
</Box>
<Box sx={{ gridColumn: 7, gridRow: 2, display: 'flex', justifyContent: 'center' }}>
<IconButton
aria-label={`„${delivery.number}“ löschen`}
disabled={busyId != null || disabled}
onClick={(e) => {
e.stopPropagation();
onDelete?.(delivery);
}}
onMouseDown={(e) => e.stopPropagation()}
sx={{ width: 40, height: 40 }}
>
<DeleteOutlinedIcon sx={{ fontSize: 24 }} />
</IconButton>
</Box>
</ListItemButton>
);
}
}
// MARK: <DeliverySearchField/>
class DeliverySearchField extends PureComponent {
constructor(props) {
super(props);
const s = deliveryAutomat.state;
const stats = deliveryAutomat.getChunkStats?.();
this.state = {
input: s.filter ?? '',
onlyUnbooked: s.onlyUnbooked ?? true,
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.unsub = deliveryAutomat.subscribe(this, (state) => ({
input: state.filter ?? '',
onlyUnbooked: state.onlyUnbooked ?? true,
seekMonths: state.seekMonths ?? 0,
seekYears: state.seekYears ?? 0,
negativeSeekMonths: state.negativeSeekMonths ?? 0,
}));
if (import.meta.env.DEV) {
this.unsubStats = deliveryAutomat.subscribeChunkStats?.((stats) => {
this.setState({
lruChunksCount: stats.lruChunksCount,
maxLruChunks: stats.maxLruChunks,
filterCachesCount: stats.filterCaches?.length || 1,
});
});
}
}
componentWillUnmount() {
this.unsub?.();
this.unsubStats?.();
}
render() {
const { inputRef, onCreate } = this.props;
const { input, onlyUnbooked, 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 (
<PanelTileGrid>
<Box
ref={inputRef}
sx={{
gridColumn: { xs: '1 / -1', sm: 'span 2', md: 'span 2' },
display: 'flex',
flexDirection: 'column',
gap: 1.25,
pb: 2,
}}
>
{/* Top row: search field & create button */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<SingleLineTextEdit
sx={{ flex: 1 }}
size="small"
search
clear
value={input}
onChange={(next) => deliveryAutomat.actions.filter(next)}
/>
<Button variant="contained" onClick={onCreate}>
{cfg.createButton}
</Button>
</Box>
{/* Filter Bar: "Nur ungebuchte", Negativeseek (-1 Month, -1 Year, Reset) */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 1.5,
}}
>
{/* Left: Unbooked toggle */}
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 1.5,
px: 1.5,
py: 0.25,
borderRadius: 2,
bgcolor: 'background.paper',
boxShadow: 1,
}}
>
<FormControlLabel
control={
<Switch
size="small"
color="warning"
checked={onlyUnbooked}
onChange={(e) => deliveryAutomat.actions.setOnlyUnbooked(e.target.checked)}
/>
}
label={
<Typography variant="body2" sx={{ fontWeight: onlyUnbooked ? 600 : 400, userSelect: 'none' }}>
Nur ungebuchte
</Typography>
}
sx={{ m: 0 }}
/>
</Box>
{/* Right: Negativeseek controls (-1 Month, -1 Year, Reset) */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<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={() => deliveryAutomat.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={() => deliveryAutomat.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={() => deliveryAutomat.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: 'delivery' } }))}
sx={{
cursor: 'pointer',
height: 28,
fontWeight: 600,
fontSize: '0.75rem',
bgcolor: 'background.paper',
boxShadow: 1,
'&:hover': { bgcolor: 'action.hover' },
}}
/>
)}
</Box>
</Box>
</Box>
</PanelTileGrid>
);
}
}
// MARK: <DeliveryList/>
export class DeliveryList extends PureComponent {
constructor(props) {
super(props);
this.headerItem = createDummyTestDelivery();
this.state = {
items: deliveryAutomat.getItems(),
unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0,
filterKey: deliveryAutomat.getFilterKey?.() ?? '',
suppliersById: {},
busyId: null,
bookingDoc: null,
bookingOpen: false,
};
this.searchRef = createRef();
this._cancelled = false;
this.handleRetry = this.handleRetry.bind(this);
this.handleCreate = this.handleCreate.bind(this);
this.handleOpen = this.handleOpen.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
this.handleOpenBookDialog = this.handleOpenBookDialog.bind(this);
this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this);
this.handleBookSuccess = this.handleBookSuccess.bind(this);
}
componentDidMount() {
this._cancelled = false;
this.unsub = deliveryAutomat.subscribe(this, (state) => ({
items: state.items,
unfilteredTotal: state.unfilteredTotal ?? 0,
filterKey: deliveryAutomat.getFilterKey(state),
}));
if (this.state.items !== deliveryAutomat.state.items) {
this.setState({
items: deliveryAutomat.state.items,
unfilteredTotal: deliveryAutomat.state.unfilteredTotal ?? 0,
filterKey: deliveryAutomat.getFilterKey(deliveryAutomat.state),
});
}
this.unsubNumber = numberAutomat.subscribe(this, () => {
this.headerItem = createDummyTestDelivery();
this.forceUpdate();
});
void numberAutomat.actions.ensureLoaded().catch(() => { });
void this.loadSuppliers();
}
componentWillUnmount() {
this._cancelled = true;
this.unsub?.();
this.unsubNumber?.();
}
async loadSuppliers() {
try {
const parties = await loadParties('delivery');
if (!this._cancelled) {
this.setState({ suppliersById: cfg.partiesByIdFromList(parties) });
}
} catch {
// Non-fatal: names fall back to ids.
}
}
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);
deliveryAutomat.requireChunk(startChunk, endChunk);
}
handleRetry() {
void deliveryAutomat.actions.requireChunk(0, 0, { immediate: true }).catch(() => { });
}
handleCreate() {
this.props.navigate(cfg.createPath);
}
handleOpen(delivery) {
this.props.navigate(cfg.editPath(delivery.id));
}
handleOpenBookDialog(delivery) {
this.setState({ bookingDoc: delivery, bookingOpen: true });
}
handleCloseBookDialog() {
this.setState({ bookingDoc: null, bookingOpen: false });
}
async handleBookSuccess() {
this.handleCloseBookDialog();
try {
await deliveryAutomat.actions.reload();
enqueueSnackbar('Lieferung erfolgreich verbucht.', { variant: 'success' });
} catch (err) {
notifyError(err);
}
}
async handleDelete(delivery) {
if (this.state.busyId != null) return;
if (!window.confirm(cfg.confirmDelete(delivery.number))) return;
this.setState({ busyId: delivery.id });
try {
await deliveryApi.delete(delivery.id);
await deliveryAutomat.actions.reload();
enqueueSnackbar(cfg.deletedMsg, { variant: 'success' });
} catch (err) {
notifyError(err);
} finally {
if (!this._cancelled) this.setState({ busyId: null });
}
}
render() {
const { items, unfilteredTotal, filterKey, suppliersById, busyId, bookingDoc, bookingOpen } = this.state;
rowContext.suppliersById = suppliersById;
rowContext.busyId = busyId;
rowContext.onOpen = this.handleOpen;
rowContext.onDelete = this.handleDelete;
rowContext.onOpenBookDialog = this.handleOpenBookDialog;
rowContext.headerItem = this.headerItem;
if (items === null && deliveryAutomat.state.status === 'failed') {
return (
<>
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
<PanelTileGrid>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ p: 2 }}>
<BackendError error={backendErrorFrom(deliveryAutomat.state.error)} onRetry={this.handleRetry} />
</PanelTile>
</PanelTileGrid>
</>
);
}
const bookingPartyName = bookingDoc
? suppliersById[bookingDoc.supplierId]?.name || (bookingDoc.supplierId ? `Lieferant #${bookingDoc.supplierId}` : null)
: null;
return (
<>
<DeliverySearchField inputRef={this.searchRef} onCreate={this.handleCreate} />
<PanelTileGrid>
<PanelTile wide={3} minHeight={0} color={cfg.listColor} sx={{ pb: 0, position: 'relative' }}>
<FillSpace
aboveRef={[this.searchRef]}
reserveBottom={32}
minPageSize={8}
minHeight={0}
resetKey={filterKey}
items={items || []}
totalCount={items ? items.length : 0}
unfilteredTotal={unfilteredTotal}
headerItem={this.headerItem}
renderItem={DeliveryRow}
virtualScrollbar
header={
<Box
sx={{
...rowGridSx,
height: 36,
minHeight: 36,
px: 2,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
{renderInvisibleDummyRow(this.headerItem)}
<Box sx={{ gridColumn: 1, gridRow: 2, ...headerCellSx }}>Nr. / Ref.</Box>
<Box sx={{ gridColumn: 2, gridRow: 2, ...headerCellSx }}>Lieferung</Box>
<Box sx={{ gridColumn: 3, gridRow: 2, ...headerCellSx }}>Lieferant</Box>
<Box sx={{ gridColumn: 4, gridRow: 2, ...headerCellSx }}>Lagerbuchung</Box>
<Box sx={{ gridColumn: 5, gridRow: 2, display: 'flex', justifyContent: 'center', textAlign: 'center', ...headerCellSx }}>Pos.</Box>
<Box sx={{ gridColumn: 6, gridRow: 2, ...headerCellSx }}>Anhänge</Box>
<Box sx={{ gridColumn: 7, gridRow: 2, ...headerCellSx }} />
</Box>
}
onPageChange={this.handlePageChange}
/>
{items === null && (
<Box
sx={{
position: 'absolute',
top: 52,
left: 0,
right: 0,
bottom: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 2,
}}
>
<CircularProgress size={32} />
</Box>
)}
</PanelTile>
</PanelTileGrid>
<BookDeliveryDialog
open={bookingOpen}
delivery={bookingDoc}
summary={null}
partyName={bookingPartyName}
onClose={this.handleCloseBookDialog}
onBookSuccess={this.handleBookSuccess}
/>
</>
);
}
}
export default withNavigate(DeliveryList);