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 ? ' *' : ''} {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 && ( )} ); } }