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);