diff --git a/src/components/manifest/DeliveryList.jsx b/src/components/manifest/DeliveryList.jsx
new file mode 100644
index 0000000..0c2cef3
--- /dev/null
+++ b/src/components/manifest/DeliveryList.jsx
@@ -0,0 +1,421 @@
+import { PureComponent } from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import Chip from '@mui/material/Chip';
+import IconButton from '@mui/material/IconButton';
+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 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 BackendError from '../BackendError';
+import { RecordList } from 'dashgui/lists';
+import { StorageManagerContext } from '../../api/storageManagerContext';
+import { delivery } from '../../api';
+import { supplierAutomat } from '../../automats';
+import { backendErrorFrom } from '../../utils/backendError';
+import { notifyError } from '../../utils/notify';
+import BookDeliveryDialog from './BookDeliveryDialog';
+import withNavigate from '../withNavigate';
+import { formatDate, numberDisplay } from './manifestConfig';
+
+function sortDeliveries(docs) {
+ if (!Array.isArray(docs)) return [];
+ return [...docs].sort((a, b) => {
+ const cmp = String(b.date ?? '').localeCompare(String(a.date ?? ''));
+ if (cmp !== 0) return cmp;
+ return Number(b.id ?? 0) - Number(a.id ?? 0);
+ });
+}
+
+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`;
+}
+
+export class DeliveryList extends PureComponent {
+ static contextType = StorageManagerContext;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ status: 'loading',
+ error: null,
+ docs: [],
+ suppliers: supplierAutomat.state?.suppliers || [],
+ busyId: null,
+ 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;
+ this.unsubSupplier = supplierAutomat.subscribe(this, (state) => ({
+ suppliers: state.suppliers || [],
+ }));
+
+ void this.load();
+ }
+
+ componentWillUnmount() {
+ this._cancelled = true;
+ this.unsubSupplier?.();
+ }
+
+ async fetchDeliveries() {
+ if (this.context?.deliveries) {
+ return this.context.deliveries;
+ }
+ if (this.context?.deliveryPromise) {
+ return await this.context.deliveryPromise;
+ }
+ const res = await (this.context?.delivery?.list ? this.context.delivery.list() : delivery.list());
+ return res?.deliveries ?? res ?? [];
+ }
+
+ async load() {
+ this.setState({ status: 'loading', error: null });
+ try {
+ const docs = await this.fetchDeliveries();
+ if (this._cancelled) return;
+ this.setState({
+ status: 'ready',
+ docs: sortDeliveries(docs),
+ });
+ } catch (err) {
+ if (this._cancelled) return;
+ this.setState({ status: 'failed', error: backendErrorFrom(err) });
+ }
+ }
+
+ async handleRetry() {
+ if (this.context?.updateDelivery) {
+ try {
+ await this.context.updateDelivery();
+ } catch {
+ // Will be caught in load()
+ }
+ }
+ void this.load();
+ }
+
+ go(to) {
+ const { navigate } = this.props;
+ if (typeof navigate === 'function') {
+ navigate(to);
+ return;
+ }
+ window.location.assign(to);
+ }
+
+ handleCreate() {
+ this.go('/lieferung/neu');
+ }
+
+ handleEdit(doc) {
+ this.go(`/lieferung/${doc.id}`);
+ }
+
+ handleOpenBookDialog(doc) {
+ this.setState({ bookingDoc: doc, bookingOpen: true });
+ }
+
+ handleCloseBookDialog() {
+ this.setState({ bookingDoc: null, bookingOpen: false });
+ }
+
+ async handleBookSuccess() {
+ this.handleCloseBookDialog();
+ if (this.context?.updateDelivery) {
+ try {
+ const updated = await this.context.updateDelivery();
+ if (this._cancelled) return;
+ this.setState({ docs: sortDeliveries(updated) });
+ return;
+ } catch {
+ // Fall back to load()
+ }
+ }
+ void this.load();
+ }
+
+ async handleDelete(doc) {
+ if (this.state.busyId != null) return;
+ if (!window.confirm(`Lieferung „${doc.number}“ wirklich löschen?`)) return;
+
+ this.setState({ busyId: doc.id });
+ try {
+ await (this.context?.delivery?.delete ? this.context.delivery.delete(doc.id) : delivery.delete(doc.id));
+ if (this.context?.updateDelivery) {
+ await this.context.updateDelivery();
+ }
+ if (this._cancelled) return;
+ await this.load();
+ enqueueSnackbar('Lieferung gelöscht.', { variant: 'success' });
+ } catch (err) {
+ if (this._cancelled) return;
+ notifyError(err);
+ this.setState({ busyId: null });
+ }
+ }
+
+ getSupplierName(supplierId) {
+ if (supplierId == null) return null;
+ const supplier = this.state.suppliers?.find((s) => s.id === supplierId);
+ return supplier?.name || `Lieferant #${supplierId}`;
+ }
+
+ render() {
+ const { status, error, docs, busyId, bookingDoc, bookingOpen, suppliers } = this.state;
+
+ const columns = [
+ {
+ key: 'number',
+ label: 'Nummer',
+ render: (row) => (
+
+ {numberDisplay(row.number)}
+
+ ),
+ },
+ {
+ key: 'createdAt',
+ label: 'Erstellt',
+ render: (row) => (
+
+ {row.createdAt ? formatDate(row.createdAt) : '–'}
+
+ ),
+ },
+ {
+ key: 'date',
+ label: 'Lieferung',
+ render: (row) => (
+
+ {row.date ? formatDate(row.date) : '–'}
+
+ ),
+ },
+ {
+ key: 'totalPositions',
+ label: 'Positionen',
+ render: (row) => {
+ const total = row.ledger?.totalPositions ?? row.ledger?.total ?? 0;
+ return (
+
+ {total}
+
+ );
+ },
+ },
+ {
+ key: 'unbooked',
+ label: 'Ungebucht',
+ render: (row) => {
+ const total = row.ledger?.totalPositions ?? row.ledger?.total ?? 0;
+ const completely = row.ledger?.completelyOnLedgerPositions ?? row.ledger?.booked ?? 0;
+ const unbooked = Math.max(0, total - completely);
+
+ if (total === 0) {
+ return (
+
+ 0
+
+ );
+ }
+
+ if (unbooked === 0) {
+ return (
+
+ }
+ label="0"
+ color="success"
+ variant="outlined"
+ onClick={(e) => {
+ e.stopPropagation();
+ this.handleOpenBookDialog(row);
+ }}
+ sx={{ fontWeight: 600, cursor: 'pointer' }}
+ />
+
+ );
+ }
+
+ return (
+ e.stopPropagation()}
+ >
+
+ }
+ label={String(unbooked)}
+ color="warning"
+ variant="outlined"
+ onClick={() => this.handleOpenBookDialog(row)}
+ sx={{ fontWeight: 600, cursor: 'pointer' }}
+ />
+
+
+
+ );
+ },
+ },
+ {
+ key: 'supplierId',
+ label: 'Lieferant',
+ render: (row) => {
+ const supplier = suppliers?.find((s) => s.id === row.supplierId);
+ const name = supplier?.name || (row.supplierId != null ? `Lieferant #${row.supplierId}` : '–');
+ return (
+
+ {name}
+
+ );
+ },
+ },
+ {
+ key: 'externalNumber',
+ label: 'Referenz',
+ render: (row) => (
+
+ {numberDisplay(row.externalNumber)}
+
+ ),
+ },
+ {
+ key: 'files',
+ label: 'Attachments',
+ render: (row) => {
+ const files = Array.isArray(row.files) ? row.files : [];
+ if (files.length === 0) {
+ return (
+
+ –
+
+ );
+ }
+
+ return (
+ e.stopPropagation()}
+ >
+ {files.map((file, idx) => {
+ const docId = file.manifestId ?? row.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 ? (
+
+ ) : isTxt ? (
+
+ ) : (
+
+ );
+
+ const title = `${file.name}${file.size ? ` (${formatFileSize(file.size)})` : ''}`;
+ const fileUrl = `/api/delivery/${encodeURIComponent(docId)}/file/${encodeURIComponent(fileId)}`;
+
+ return (
+
+ 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}
+
+
+ );
+ })}
+
+ );
+ },
+ },
+ ];
+
+ return (
+ <>
+ }
+ rows={docs}
+ columns={columns}
+ createLabel="Neue Lieferung"
+ onCreate={this.handleCreate}
+ onEdit={this.handleEdit}
+ onDelete={this.handleDelete}
+ deleteDisabled={busyId != null}
+ emptyMessage="Noch keine Lieferungen."
+ color="#fed7aa"
+ getEditLabel={(row) => `„${row.number}“ bearbeiten`}
+ getDeleteLabel={(row) => `„${row.number}“ löschen`}
+ />
+
+
+ >
+ );
+ }
+}
+
+export default withNavigate(DeliveryList);
diff --git a/src/components/manifest/ManifestList.jsx b/src/components/manifest/ManifestList.jsx
index 023d139..8eb10bd 100644
--- a/src/components/manifest/ManifestList.jsx
+++ b/src/components/manifest/ManifestList.jsx
@@ -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 (
-
-
-
- Prüfe...
-
-
- );
- }
-
- if (summary.status === 'error') {
- return (
-
-
-
- );
- }
-
- if (summary.status === 'empty') {
- return (
-
- Keine Positionen
-
- );
- }
-
- if (summary.status === 'booked') {
- return (
-
- }
- label={`Gebucht (${summary.bookedPositions}/${summary.totalPositions})`}
- color="success"
- variant="outlined"
- onClick={(e) => {
- e.stopPropagation();
- this.handleOpenBookDialog(row);
- }}
- sx={{ fontWeight: 600, cursor: 'pointer' }}
- />
-
- );
- }
-
- if (summary.status === 'partial') {
- return (
- e.stopPropagation()}
- >
-
- }
- label={`Teilweise (${summary.bookedPositions}/${summary.totalPositions})`}
- color="warning"
- variant="outlined"
- onClick={() => this.handleOpenBookDialog(row)}
- sx={{ fontWeight: 600, cursor: 'pointer' }}
- />
-
-
-
- );
- }
-
- // Default: 'unbooked'
- return (
- e.stopPropagation()}
- >
-
- this.handleOpenBookDialog(row)}
- sx={{
- color: 'text.secondary',
- borderColor: 'divider',
- fontWeight: 500,
- cursor: 'pointer',
- }}
- />
-
- }
- onClick={() => this.handleOpenBookDialog(row)}
- sx={{
- py: 0.2,
- px: 1.2,
- minHeight: 26,
- textTransform: 'none',
- fontSize: '0.8125rem',
- fontWeight: 600,
- }}
- >
- Ins Lager buchen
-
-
- );
- }
-
render() {
- const { status, error, docs, busyId, bookingDoc, bookingOpen, ledgerSummaries } = this.state;
+ if (this.props.kind === 'delivery') {
+ return ;
+ }
+
+ 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 (
- <>
- }
- 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' && (
-
- )}
- >
+ }
+ 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`}
+ />
);
}
}
diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js
index 543c2b8..d8b44d4 100644
--- a/src/components/manifest/index.js
+++ b/src/components/manifest/index.js
@@ -1,4 +1,5 @@
export { default as ManifestList } from './ManifestList';
+export { default as DeliveryList } from './DeliveryList';
export { default as ManifestForm } from './ManifestForm';
export { default as ManifestHeader } from './ManifestHeader';
export { default as ManifestLines } from './ManifestLines';
diff --git a/src/pages/lieferung/index.jsx b/src/pages/lieferung/index.jsx
index 90ab664..616ef67 100644
--- a/src/pages/lieferung/index.jsx
+++ b/src/pages/lieferung/index.jsx
@@ -1,5 +1,5 @@
import { PureComponent } from 'react';
-import { ManifestList } from '../../components/manifest';
+import { DeliveryList } from '../../components/manifest';
import { setLastDeliveryPath } from '../../automats';
class LieferungListePage extends PureComponent {
@@ -8,7 +8,7 @@ class LieferungListePage extends PureComponent {
}
render() {
- return ;
+ return ;
}
}