This commit is contained in:
seb
2026-09-26 07:09:36 +02:00
parent e5b43361cc
commit dcca972ff8
4 changed files with 451 additions and 258 deletions

View File

@@ -1,22 +1,12 @@
import { 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 Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import { enqueueSnackbar } from 'notistack';
import BackendError from '../BackendError';
import { RecordList } from 'dashgui/lists';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { ledger, delivery } from '../../api';
import { backendErrorFrom } from '../../utils/backendError';
import { notifyError } from '../../utils/notify';
import { fetchDeliveryLedgerSummary } from '../../utils/deliveryLedger';
import BookDeliveryDialog from './BookDeliveryDialog';
import withNavigate from '../withNavigate';
import DeliveryList from './DeliveryList';
import {
formatDate,
loadParties,
@@ -38,23 +28,19 @@ class ManifestList extends PureComponent {
docs: [],
partiesById: {},
busyId: null,
ledgerSummaries: {},
bookingDoc: null,
bookingOpen: false,
};
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.handleOpenBookDialog = this.handleOpenBookDialog.bind(this);
this.handleCloseBookDialog = this.handleCloseBookDialog.bind(this);
this.handleBookSuccess = this.handleBookSuccess.bind(this);
}
componentDidMount() {
this._cancelled = false;
void this.load();
if (this.props.kind !== 'delivery') {
void this.load();
}
}
componentWillUnmount() {
@@ -76,58 +62,16 @@ class ManifestList extends PureComponent {
docs: sortedDocs,
partiesById: cfg.partiesByIdFromList(parties),
});
if (this.props.kind === 'delivery') {
void this.loadLedgerSummaries(sortedDocs);
}
} catch (err) {
if (this._cancelled) return;
this.setState({ status: 'failed', error: backendErrorFrom(err) });
}
}
async loadLedgerSummaries(docs) {
if (!Array.isArray(docs) || docs.length === 0) return;
const ledgerApi = this.context?.ledger || ledger;
const deliveryApi = this.context?.delivery || delivery;
for (const doc of docs) {
if (this._cancelled) return;
this.setState((prev) => ({
ledgerSummaries: {
...prev.ledgerSummaries,
[doc.id]: prev.ledgerSummaries[doc.id] || { status: 'loading' },
},
}));
fetchDeliveryLedgerSummary(doc.id, { ledgerApi, deliveryApi })
.then((summary) => {
if (this._cancelled) return;
this.setState((prev) => ({
ledgerSummaries: {
...prev.ledgerSummaries,
[doc.id]: summary,
},
}));
})
.catch((err) => {
if (this._cancelled) return;
console.error(`[ManifestList] Ledger summary failed for delivery ${doc.id}:`, err);
this.setState((prev) => ({
ledgerSummaries: {
...prev.ledgerSummaries,
[doc.id]: { status: 'error', error: err.message },
},
}));
});
}
}
handleRetry() {
void this.load();
}
/** Same path as AppShell sub-nav: live useNavigate via withNavigate. */
go(to) {
const { navigate } = this.props;
if (typeof navigate === 'function') {
@@ -145,24 +89,6 @@ class ManifestList extends PureComponent {
this.go(this.cfg.editPath(doc.id));
}
handleOpenBookDialog(doc) {
this.setState({ bookingDoc: doc, bookingOpen: true });
}
handleCloseBookDialog() {
this.setState({ bookingDoc: null, bookingOpen: false });
}
handleBookSuccess(updatedSummary) {
if (!updatedSummary?.deliveryId) return;
this.setState((prev) => ({
ledgerSummaries: {
...prev.ledgerSummaries,
[updatedSummary.deliveryId]: updatedSummary,
},
}));
}
async handleDelete(doc) {
if (this.state.busyId != null) return;
if (!window.confirm(this.cfg.confirmDelete(doc.number))) return;
@@ -173,14 +99,9 @@ class ManifestList extends PureComponent {
await this.context[cfg.api].delete(doc.id);
const list = await this.context[cfg.updateCache]();
if (this._cancelled) return;
this.setState((prev) => {
const nextSummaries = { ...prev.ledgerSummaries };
delete nextSummaries[doc.id];
return {
docs: sortDocs(list),
busyId: null,
ledgerSummaries: nextSummaries,
};
this.setState({
docs: sortDocs(list),
busyId: null,
});
enqueueSnackbar(cfg.deletedMsg, { variant: 'success' });
} catch (err) {
@@ -198,141 +119,12 @@ class ManifestList extends PureComponent {
);
}
renderLedgerSummary(row) {
const summary = this.state.ledgerSummaries[row.id];
if (!summary || summary.status === 'loading') {
return (
<Box sx={{ display: 'inline-flex', alignItems: 'center', gap: 0.75 }}>
<CircularProgress size={14} color="inherit" sx={{ opacity: 0.6 }} />
<Typography variant="caption" color="text.secondary">
Prüfe...
</Typography>
</Box>
);
}
if (summary.status === 'error') {
return (
<Tooltip title={summary.error || 'Fehler beim Laden des Lagerstatus'}>
<Chip size="small" label="Statusfehler" color="error" variant="outlined" />
</Tooltip>
);
}
if (summary.status === 'empty') {
return (
<Typography variant="caption" color="text.secondary">
Keine Positionen
</Typography>
);
}
if (summary.status === 'booked') {
return (
<Tooltip
title={`${summary.totalPositions} Positionen (${summary.bookedQuantity} Stk.) vollständig ins Lager gebucht`}
>
<Chip
size="small"
icon={<CheckCircleOutlineOutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
label={`Gebucht (${summary.bookedPositions}/${summary.totalPositions})`}
color="success"
variant="outlined"
onClick={(e) => {
e.stopPropagation();
this.handleOpenBookDialog(row);
}}
sx={{ fontWeight: 600, cursor: 'pointer' }}
/>
</Tooltip>
);
}
if (summary.status === 'partial') {
return (
<Box
sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}
onClick={(e) => e.stopPropagation()}
>
<Tooltip
title={`${summary.bookedPositions} von ${summary.totalPositions} Positionen gebucht (${summary.bookedQuantity} / ${summary.totalQuantity} Stk.)`}
>
<Chip
size="small"
icon={<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 16 } }} />}
label={`Teilweise (${summary.bookedPositions}/${summary.totalPositions})`}
color="warning"
variant="outlined"
onClick={() => this.handleOpenBookDialog(row)}
sx={{ fontWeight: 600, cursor: 'pointer' }}
/>
</Tooltip>
<Button
size="small"
variant="contained"
color="warning"
onClick={() => this.handleOpenBookDialog(row)}
sx={{
py: 0.2,
px: 1,
minHeight: 26,
textTransform: 'none',
fontSize: '0.8125rem',
fontWeight: 600,
}}
>
Rest buchen
</Button>
</Box>
);
}
// Default: 'unbooked'
return (
<Box
sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}
onClick={(e) => e.stopPropagation()}
>
<Tooltip
title={`Noch nicht ins Lager gebucht (${summary.totalPositions} Positionen, ${summary.totalQuantity} Stk.)`}
>
<Chip
size="small"
label={`Offen (${summary.totalPositions} Pos.)`}
variant="outlined"
onClick={() => this.handleOpenBookDialog(row)}
sx={{
color: 'text.secondary',
borderColor: 'divider',
fontWeight: 500,
cursor: 'pointer',
}}
/>
</Tooltip>
<Button
size="small"
variant="contained"
color="primary"
startIcon={<Inventory2OutlinedIcon sx={{ fontSize: 16 }} />}
onClick={() => this.handleOpenBookDialog(row)}
sx={{
py: 0.2,
px: 1.2,
minHeight: 26,
textTransform: 'none',
fontSize: '0.8125rem',
fontWeight: 600,
}}
>
Ins Lager buchen
</Button>
</Box>
);
}
render() {
const { status, error, docs, busyId, bookingDoc, bookingOpen, ledgerSummaries } = this.state;
if (this.props.kind === 'delivery') {
return <DeliveryList {...this.props} />;
}
const { status, error, docs, busyId } = this.state;
const cfg = this.cfg;
const partyField = cfg.partyField;
@@ -357,43 +149,22 @@ class ManifestList extends PureComponent {
},
);
if (this.props.kind === 'delivery') {
columns.push({
key: 'ledgerStatus',
label: 'Lagerbuchung',
render: (row) => this.renderLedgerSummary(row),
});
}
return (
<>
<RecordList
status={status}
error={<BackendError error={error} onRetry={this.handleRetry} />}
rows={docs}
columns={columns}
createLabel={cfg.createButton}
onCreate={this.handleCreate}
onEdit={this.handleEdit}
onDelete={this.handleDelete}
deleteDisabled={busyId != null}
emptyMessage={cfg.emptyList}
color={cfg.listColor}
getEditLabel={(row) => `„${row.number}“ bearbeiten`}
getDeleteLabel={(row) => `„${row.number}“ löschen`}
/>
{this.props.kind === 'delivery' && (
<BookDeliveryDialog
open={bookingOpen}
delivery={bookingDoc}
summary={bookingDoc ? ledgerSummaries[bookingDoc.id] : null}
partyName={bookingDoc ? this.partyName(bookingDoc[partyField]) : null}
onClose={this.handleCloseBookDialog}
onBookSuccess={this.handleBookSuccess}
/>
)}
</>
<RecordList
status={status}
error={<BackendError error={error} onRetry={this.handleRetry} />}
rows={docs}
columns={columns}
createLabel={cfg.createButton}
onCreate={this.handleCreate}
onEdit={this.handleEdit}
onDelete={this.handleDelete}
deleteDisabled={busyId != null}
emptyMessage={cfg.emptyList}
color={cfg.listColor}
getEditLabel={(row) => `„${row.number}“ bearbeiten`}
getDeleteLabel={(row) => `„${row.number}“ löschen`}
/>
);
}
}