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 deliveryAutomat from '../../automats/deliveryAutomat'; 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 { 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() { return deliveryAutomat.actions.ensureLoaded(); } 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() { try { await deliveryAutomat.actions.reload(); } 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(); try { const updated = await deliveryAutomat.actions.reload(); 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 delivery.delete(doc.id); await deliveryAutomat.actions.reload(); 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);