From dcca972ff8604e4150d262109a319f833c4c40be Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 26 Sep 2026 07:09:36 +0200 Subject: [PATCH] u --- src/components/manifest/DeliveryList.jsx | 421 +++++++++++++++++++++++ src/components/manifest/ManifestList.jsx | 283 ++------------- src/components/manifest/index.js | 1 + src/pages/lieferung/index.jsx | 4 +- 4 files changed, 451 insertions(+), 258 deletions(-) create mode 100644 src/components/manifest/DeliveryList.jsx 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', - }} - /> - - - - ); - } - 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 ; } }