import { PureComponent } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import AppDialog from '../AppDialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Chip from '@mui/material/Chip';
import Alert from '@mui/material/Alert';
import Paper from '@mui/material/Paper';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import FormHelperText from '@mui/material/FormHelperText';
import CloseIcon from '@mui/icons-material/Close';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
import WarehouseIcon from '@mui/icons-material/Warehouse';
import AddIcon from '@mui/icons-material/Add';
import { enqueueSnackbar } from 'notistack';
import { notifyError } from '../../utils/notify';
import { formatDate, resolveLineSkus } from './manifestConfig';
import {
fetchDeliveryLedgerSummary,
bookDeliveryLines,
} from '../../utils/deliveryLedger';
import { formatStock } from '../article/searchFormat';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import warehouseAutomat from '../../automats/warehouseAutomat';
import { supplier as supplierApi } from '../../api';
import deliveryEditorAutomat, { buildSupplierArticlesMap } from '../../automats/deliveryEditorAutomat';
import {
SURFACE_BASE,
SURFACE_RAISED,
} from '../../colors';
export default class BookDeliveryDialog extends PureComponent {
constructor(props) {
super(props);
let initialAmounts = {};
if (Array.isArray(props.summary?.lines)) {
for (const line of props.summary.lines) {
if (props.prefillLineId != null) {
initialAmounts[line.id] =
line.id === props.prefillLineId
? line.remainingAmount > 0
? line.remainingAmount
: 0
: 0;
} else {
initialAmounts[line.id] = line.remainingAmount > 0 ? line.remainingAmount : 0;
}
}
}
const initialWarehouses = warehouseAutomat.state.warehouses || [];
const initialSupplierMap =
props.supplierArticlesMap ||
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? {});
this.state = {
summary: props.summary ?? null,
loading: !props.summary,
error: null,
amountsToBook: initialAmounts,
transactionText: props.delivery?.externalNumber || (props.delivery?.number
? `Lieferung ${props.delivery.number}`
: 'Lieferung Wareneingang'),
isBooking: false,
warehouses: initialWarehouses,
selectedWarehouseId: initialWarehouses.length === 1 ? initialWarehouses[0].id : '',
warehouseError: null,
supplierArticlesMap: initialSupplierMap,
enablingTrackStockBySku: {},
};
this.handleAmountChange = this.handleAmountChange.bind(this);
this.handleWarehouseChange = this.handleWarehouseChange.bind(this);
this.handleTransactionTextChange = this.handleTransactionTextChange.bind(this);
this.handleBookAll = this.handleBookAll.bind(this);
this.handleResetAmounts = this.handleResetAmounts.bind(this);
this.handleConfirmBook = this.handleConfirmBook.bind(this);
this.handleEnableTrackStock = this.handleEnableTrackStock.bind(this);
}
loadSupplierArticlesIfNeeded() {
const supplierId = this.props.delivery?.supplierId ?? this.props.delivery?.partyId;
if (supplierId && Object.keys(this.state.supplierArticlesMap).length === 0) {
void supplierApi.get(supplierId).then((res) => {
const articles = res?.supplier?.articles || res?.articles || [];
const map = buildSupplierArticlesMap(articles);
this.setState({ supplierArticlesMap: map });
}).catch(() => {});
}
}
componentDidMount() {
this.unsubWarehouse = warehouseAutomat.subscribe((state) => {
const warehouses = state.warehouses || [];
this.setState((prev) => {
const selectedId =
prev.selectedWarehouseId ||
(warehouses.length === 1 ? warehouses[0].id : '');
return {
warehouses,
selectedWarehouseId: selectedId,
};
});
});
this.unsubArticle = artikelAutomat.subscribe(() => {
this.forceUpdate();
});
void warehouseAutomat.actions.load().catch(() => {});
this.loadSupplierArticlesIfNeeded();
if (this.props.open) {
void this.loadSummary();
}
}
componentWillUnmount() {
this.unsubWarehouse?.();
this.unsubArticle?.();
}
async handleEnableTrackStock(line, targetSku) {
if (!targetSku) return;
this.setState((prev) => ({
enablingTrackStockBySku: {
...prev.enablingTrackStockBySku,
[targetSku]: true,
},
}));
try {
await artikelAutomat.actions.setTrackStock(targetSku, true);
this.setState((prev) => {
if (!prev.summary?.lines) {
return {
enablingTrackStockBySku: {
...prev.enablingTrackStockBySku,
[targetSku]: false,
},
};
}
const updatedLines = prev.summary.lines.map((l) => {
const rawSku = l.sku ? String(l.sku).trim() : '';
const lineOwnSku = l.articleSku || l.article?.sku || '';
if (
l.id === line.id ||
rawSku === targetSku ||
lineOwnSku === targetSku ||
(l.article && String(l.article.sku) === targetSku)
) {
return {
...l,
trackStock: true,
article: l.article
? { ...l.article, track_stock: true }
: { sku: targetSku, track_stock: true },
};
}
return l;
});
return {
summary: {
...prev.summary,
hasTrackStock: true,
lines: updatedLines,
},
enablingTrackStockBySku: {
...prev.enablingTrackStockBySku,
[targetSku]: false,
},
};
});
enqueueSnackbar(`Lagerbestand für „${targetSku}“ aktiviert.`, { variant: 'success' });
} catch (err) {
console.error('[BookDeliveryDialog] Failed to enable track_stock:', err);
enqueueSnackbar(
err?.message || `Fehler beim Aktivieren des Lagerbestands für „${targetSku}“.`,
{ variant: 'error' }
);
this.setState((prev) => ({
enablingTrackStockBySku: {
...prev.enablingTrackStockBySku,
[targetSku]: false,
},
}));
}
}
componentDidUpdate(prevProps) {
if (this.props.open && !prevProps.open) {
const currentWarehouses = this.state.warehouses || warehouseAutomat.state.warehouses || [];
const currentSupplierMap =
this.props.supplierArticlesMap ||
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? this.state.supplierArticlesMap ?? {});
this.setState(
{
summary: this.props.summary ?? null,
loading: !this.props.summary,
error: null,
warehouseError: null,
selectedWarehouseId:
this.state.selectedWarehouseId ||
(currentWarehouses.length === 1 ? currentWarehouses[0].id : ''),
transactionText: this.props.delivery?.externalNumber || (this.props.delivery?.number
? `Lieferung ${this.props.delivery.number}`
: 'Lieferung Wareneingang'),
isBooking: false,
supplierArticlesMap: currentSupplierMap,
},
() => {
void warehouseAutomat.actions.load().catch(() => {});
this.loadSupplierArticlesIfNeeded();
if (this.props.summary) {
this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId);
} else {
void this.loadSummary();
}
}
);
} else if (
this.props.open &&
(this.props.prefillLineId !== prevProps.prefillLineId ||
(this.props.summary && this.props.summary !== prevProps.summary)) &&
!this.state.isBooking
) {
const activeSummary = this.state.summary || this.props.summary;
if (activeSummary) {
this.initAmountsFromSummary(activeSummary, this.props.prefillLineId);
}
}
}
handleWarehouseChange(event) {
const value = event.target.value;
this.setState({
selectedWarehouseId: value ? Number(value) : '',
warehouseError: null,
});
}
async loadSummary() {
const { delivery, prefillLineId } = this.props;
if (!delivery?.id) return;
this.setState({ loading: true, error: null });
try {
const summary = await fetchDeliveryLedgerSummary(delivery.id);
this.initAmountsFromSummary(summary, prefillLineId);
this.setState({ summary, loading: false });
} catch (err) {
console.error('[BookDeliveryDialog] Failed to load ledger summary:', err);
this.setState({ error: err.message || 'Laden fehlgeschlagen', loading: false });
}
}
initAmountsFromSummary(summary, prefillLineId = this.props.prefillLineId) {
const amounts = {};
if (Array.isArray(summary?.lines)) {
for (const line of summary.lines) {
if (prefillLineId != null) {
amounts[line.id] =
line.id === prefillLineId
? line.remainingAmount > 0
? line.remainingAmount
: 0
: 0;
} else {
amounts[line.id] = line.remainingAmount > 0 ? line.remainingAmount : 0;
}
}
}
this.setState({ amountsToBook: amounts });
}
handleAmountChange(lineId, value) {
const num = Math.max(0, Number(value) || 0);
this.setState((prev) => ({
amountsToBook: {
...prev.amountsToBook,
[lineId]: num,
},
}));
}
handleTransactionTextChange(e) {
this.setState({ transactionText: e.target.value });
}
handleBookAll() {
const { summary } = this.state;
if (!summary?.lines) return;
const amounts = {};
for (const line of summary.lines) {
amounts[line.id] = line.remainingAmount > 0 ? line.remainingAmount : 0;
}
this.setState({ amountsToBook: amounts });
}
handleResetAmounts() {
const { summary } = this.state;
if (!summary?.lines) return;
const amounts = {};
for (const line of summary.lines) {
amounts[line.id] = 0;
}
this.setState({ amountsToBook: amounts });
}
async handleConfirmBook() {
const { delivery, onBookSuccess, onClose } = this.props;
const { summary, amountsToBook, transactionText, selectedWarehouseId } = this.state;
if (!summary?.lines) return;
const linesToBook = summary.lines
.map((line) => ({
manifestarticleId: line.id,
sku: line.sku,
name: line.name,
amount: Number(amountsToBook[line.id] || 0),
trackStock: Boolean(line.trackStock),
}))
.filter((l) => l.amount > 0);
if (linesToBook.length === 0) {
enqueueSnackbar('Keine Mengen zum Buchen angegeben.', { variant: 'info' });
return;
}
const hasTracked = linesToBook.some((l) => l.trackStock);
if (hasTracked && !selectedWarehouseId) {
this.setState({ warehouseError: 'Bitte wählen Sie ein Ziellager aus.' });
enqueueSnackbar('Bitte wählen Sie ein Ziellager aus.', { variant: 'warning' });
return;
}
this.setState({ isBooking: true, error: null, warehouseError: null });
try {
await bookDeliveryLines({
delivery: summary.delivery || delivery,
linesToBook,
warehouseId: selectedWarehouseId,
transactionText,
});
const totalBookedQty = linesToBook.reduce((sum, l) => sum + l.amount, 0);
enqueueSnackbar(
`${linesToBook.length} Position${linesToBook.length === 1 ? '' : 'en'} (${totalBookedQty} Stk.) erfolgreich gebucht.`,
{ variant: 'success' }
);
// Refresh summary
const updatedSummary = await fetchDeliveryLedgerSummary(delivery.id);
if (onBookSuccess) {
onBookSuccess(updatedSummary);
}
if (onClose) {
onClose();
}
} catch (err) {
console.error('[BookDeliveryDialog] Booking failed:', err);
notifyError(err);
this.setState({
error: err.message || 'Buchung fehlgeschlagen',
isBooking: false,
});
}
}
renderHeaderInfo({ hasTrackedLinesToBook, isWarehouseMissing } = {}) {
const { delivery, partyName } = this.props;
const {
transactionText,
isBooking,
summary,
warehouses,
selectedWarehouseId,
warehouseError,
} = this.state;
const isFullyBooked = (summary?.lines?.length ?? 0) > 0 && summary?.status === 'booked';
return (
Ziellager{hasTrackedLinesToBook ? ' *' : ''}
}
>
{warehouses.length === 0 ? (
) : (
warehouses.map((wh) => (
))
)}
{warehouseError && (
{warehouseError}
)}
);
}
renderLinesTable() {
const { summary, amountsToBook, isBooking, supplierArticlesMap, enablingTrackStockBySku } = this.state;
const lines = summary?.lines || [];
if (lines.length === 0) {
return (
Diese Lieferung enthält keine Positionen zum Verbuchen.
);
}
return (
Pos.
Lieferanten-SKU
Eigene SKU
EAN
Artikel
Bestand
Liefermenge
Bereits gebucht
Jetzt verbuchen
{lines.map((line, index) => {
const toBook = amountsToBook[line.id] ?? 0;
const isAlreadyFullyBooked = line.isBooked && line.remainingAmount <= 0;
const posNumber = line.pos ?? line.position ?? (index + 1);
const { supplierSku, ownSku } = resolveLineSkus(
line,
this.props.delivery,
supplierArticlesMap
);
const cachedArticle =
line.article ||
(ownSku && ownSku !== '–' ? getArticle(ownSku) : null) ||
(line.sku ? getArticle(line.sku) : null);
const targetSku =
cachedArticle?.sku ||
(ownSku && ownSku !== '–' ? ownSku : null) ||
(line.sku ? String(line.sku).trim() : null);
if (targetSku && !cachedArticle) {
void artikelAutomat.actions.loadArticle(targetSku).catch(() => {});
}
const isTracked =
Boolean(line.trackStock) ||
(cachedArticle?.track_stock !== undefined
? cachedArticle.track_stock !== false &&
cachedArticle.track_stock !== 0 &&
cachedArticle.track_stock !== '0'
: false);
const isEnablingThis = targetSku ? Boolean(enablingTrackStockBySku?.[targetSku]) : false;
let currentStock = null;
if (cachedArticle) {
if (Array.isArray(cachedArticle.warehouses) && cachedArticle.warehouses.length > 0) {
currentStock = cachedArticle.warehouses.reduce(
(sum, wh) => sum + (Number(wh.stock) || 0),
0
);
} else if (cachedArticle.stock != null) {
currentStock = Number(cachedArticle.stock);
}
} else if (line.stock != null) {
currentStock = Number(line.stock);
}
const warehouseBreakdown =
Array.isArray(cachedArticle?.warehouses) && cachedArticle.warehouses.length > 0
? cachedArticle.warehouses
.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock ?? 0)}`)
.join(' • ')
: null;
const eans = line.ean
? [line.ean]
: line.eans || cachedArticle?.eans || cachedArticle?.articleean || [];
const primaryEan = Array.isArray(eans) ? (eans[0] ?? '') : (eans ?? '');
return (
{posNumber}
{supplierSku}
{ownSku}
1 ? eans.join(', ') : undefined}
>
{primaryEan || '–'}
{line.name || '–'}
{isTracked ? (
{currentStock != null ? formatStock(currentStock) : '0'}
) : targetSku ? (
) : (
—
)}
{line.targetAmount}
0 ? 'success.main' : 'text.secondary'}
sx={{ fontWeight: line.bookedAmount > 0 ? 600 : 400 }}
>
{line.bookedAmount}
{isAlreadyFullyBooked && (
)}
{isAlreadyFullyBooked ? (
) : (
this.handleAmountChange(line.id, e.target.value)}
disabled={isBooking}
slotProps={{
htmlInput: {
min: 0,
step: 1,
style: { textAlign: 'right', padding: '4px 8px' },
},
}}
sx={{ width: 90 }}
/>
)}
);
})}
);
}
render() {
const { open, onClose, delivery } = this.props;
const {
summary,
loading,
error,
amountsToBook,
transactionText,
isBooking,
warehouses,
selectedWarehouseId,
warehouseError,
} = this.state;
const lines = summary?.lines || [];
const hasLines = lines.length > 0;
const hasTrackStock = lines.some((l) => l.trackStock);
const linesWithAmountsToBook = lines.filter(
(l) => Number(amountsToBook[l.id]) > 0
);
const hasTrackedLinesToBook = linesWithAmountsToBook.some((l) => l.trackStock);
const prefillLine =
this.props.prefillLineId != null
? lines.find((l) => l.id === this.props.prefillLineId)
: null;
const totalToBook = lines.reduce(
(sum, l) => sum + (Number(amountsToBook[l.id]) || 0),
0
);
const countLinesToBook = linesWithAmountsToBook.length;
const isFullyBooked = hasLines && summary?.status === 'booked';
const selectedWarehouse = warehouses.find((w) => w.id === selectedWarehouseId);
const isWarehouseMissing = hasTrackedLinesToBook && !selectedWarehouseId;
return (
Lieferung verbuchen
{this.renderHeaderInfo({ hasTrackedLinesToBook, isWarehouseMissing })}
{loading ? (
Lade Positionen und Ledger-Status...
) : error ? (
{error}
) : (
<>
{prefillLine && (
Vorausgewählt zum Verbuchen: Position{' '}
{prefillLine.sku ? `SKU ${prefillLine.sku}` : prefillLine.name} ({amountsToBook[prefillLine.id] || 0} Stk.). Andere Positionen stehen auf 0.
)}
{this.renderLinesTable()}
{hasTrackedLinesToBook && warehouses.length === 0 && !isFullyBooked && (
Es ist noch kein Lager eingerichtet. Für lagergeführte Artikel legen Sie bitte unter „Artikel → Lager“ zuerst ein Lager an.
)}
{isFullyBooked ? (
}>
Alle Positionen dieser Lieferung sind bereits vollständig im Ledger verbucht.
) : isWarehouseMissing ? (
Bitte wählen Sie ein Ziellager aus, um die lagergeführten Artikel zu buchen.
) : countLinesToBook === 0 && hasLines ? (
Wählen Sie mindestens eine Menge größer als 0 aus, um Positionen zu verbuchen.
) : null}
>
)}
{!isFullyBooked && hasLines && (
) : (
)
}
sx={{ minWidth: 160, fontWeight: 600 }}
>
{isBooking ? 'Wird gebucht...' : 'Jetzt verbuchen'}
)}
);
}
}