u
This commit is contained in:
@@ -36,7 +36,8 @@ import {
|
||||
ArticleSearchDialog,
|
||||
BookDeliveryDialog,
|
||||
} from '../../components/manifest';
|
||||
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
|
||||
import { loadParties, apiFor, resolveLineSkus } from '../../components/manifest/manifestConfig';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
||||
import { getCategoryState } from '../../automats/categoryAutomat';
|
||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||
@@ -79,6 +80,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
ledgerSummary: null,
|
||||
bookDeliveryDialogOpen: false,
|
||||
bookDeliveryPrefillLineId: null,
|
||||
warehouses: warehouseAutomat.state.warehouses || [],
|
||||
};
|
||||
this._cancelled = false;
|
||||
this.handleRetry = this.handleRetry.bind(this);
|
||||
@@ -123,6 +125,10 @@ class LieferungNeuPage extends PureComponent {
|
||||
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
|
||||
viewerPosition: state.viewerPosition,
|
||||
}));
|
||||
this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({
|
||||
warehouses: state.warehouses,
|
||||
}));
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
this._cancelled = false;
|
||||
if (this.props.docId != null) {
|
||||
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
|
||||
@@ -161,6 +167,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
this.unsubscribe?.();
|
||||
this._settingsUnsub?.();
|
||||
this._warehouseUnsub?.();
|
||||
}
|
||||
|
||||
async loadPartiesOnly() {
|
||||
@@ -1265,27 +1272,42 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
|
||||
renderLedgerPanel() {
|
||||
const { ledgerSummary } = this.state;
|
||||
const { ledgerSummary, warehouses = [] } = this.state;
|
||||
if (!ledgerSummary) return null;
|
||||
|
||||
const {
|
||||
status,
|
||||
totalPositions,
|
||||
bookedPositions,
|
||||
totalQuantity,
|
||||
bookedQuantity,
|
||||
lines,
|
||||
} = ledgerSummary;
|
||||
const { lines } = ledgerSummary;
|
||||
const delivery = this.state.delivery || this.automat?.state?.delivery;
|
||||
const supplierArticlesMap = this.state.supplierArticlesMap || {};
|
||||
|
||||
const allEntries = [];
|
||||
if (Array.isArray(lines)) {
|
||||
for (const line of lines) {
|
||||
if (Array.isArray(line.ledgerEntries)) {
|
||||
const { supplierSku, ownSku } = resolveLineSkus(line, delivery, supplierArticlesMap);
|
||||
const lineName = line.name || line.article?.name || line.description || '–';
|
||||
const deliveredAmount = line.amount ?? line.count ?? line.targetAmount ?? '–';
|
||||
|
||||
for (const entry of line.ledgerEntries) {
|
||||
const hasWarehouse = entry.warehouse_id != null;
|
||||
const wh = hasWarehouse
|
||||
? (warehouses.find((w) => Number(w.id) === Number(entry.warehouse_id)) ||
|
||||
warehouseAutomat.state.warehouses?.find((w) => Number(w.id) === Number(entry.warehouse_id)))
|
||||
: null;
|
||||
const warehouseName = wh?.name || (hasWarehouse ? `Lager #${entry.warehouse_id}` : '–');
|
||||
|
||||
const rawBookedAmount = entry.warehouse_amount ?? (hasWarehouse ? entry.amount : null);
|
||||
const mengeEingebucht = rawBookedAmount != null
|
||||
? (Number(rawBookedAmount) > 0 ? `+${rawBookedAmount}` : String(rawBookedAmount))
|
||||
: '–';
|
||||
|
||||
allEntries.push({
|
||||
...entry,
|
||||
lineSku: line.sku,
|
||||
lineName: line.name,
|
||||
supplierSku,
|
||||
ownSku,
|
||||
name: lineName,
|
||||
deliveredAmount,
|
||||
warehouseName,
|
||||
mengeEingebucht,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1293,190 +1315,85 @@ class LieferungNeuPage extends PureComponent {
|
||||
}
|
||||
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
||||
|
||||
if (!ledgerSummary.hasTrackStock && allEntries.length === 0) return null;
|
||||
if (allEntries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<PanelTile
|
||||
wide={4}
|
||||
title="Lagerstatus (Ledger)"
|
||||
title="Buchungen"
|
||||
color={SURFACE_BASE}
|
||||
sx={{
|
||||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
||||
border: '1px solid',
|
||||
borderColor:
|
||||
status === 'booked'
|
||||
? 'success.light'
|
||||
: status === 'partial'
|
||||
? 'warning.light'
|
||||
: 'divider',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 3,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Status
|
||||
</Typography>
|
||||
{status === 'booked' && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<CheckCircleOutlineOutlinedIcon />}
|
||||
label="Vollständig gebucht"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 600, height: 26 }}
|
||||
/>
|
||||
)}
|
||||
{status === 'partial' && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<Inventory2OutlinedIcon />}
|
||||
label={`Teilweise gebucht (${bookedPositions}/${totalPositions})`}
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 600, height: 26 }}
|
||||
/>
|
||||
)}
|
||||
{status === 'unbooked' && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Offen (${totalPositions} Positionen)`}
|
||||
variant="outlined"
|
||||
sx={{ color: 'text.secondary', borderColor: 'divider', fontWeight: 500, height: 26 }}
|
||||
/>
|
||||
)}
|
||||
{status === 'empty' && (
|
||||
<Chip size="small" label="Keine Artikel" variant="outlined" />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Positionen
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{bookedPositions} / {totalPositions} gebucht
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Menge
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{bookedQuantity} / {totalQuantity} Stk. gebucht
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{totalQuantity > bookedQuantity && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Noch offen
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'warning.dark' }}>
|
||||
{totalQuantity - bookedQuantity} Stk.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{ledgerSummary.hasTrackStock && (
|
||||
<Button
|
||||
variant={status === 'booked' ? 'outlined' : 'contained'}
|
||||
color={status === 'booked' ? 'success' : status === 'partial' ? 'warning' : 'primary'}
|
||||
startIcon={
|
||||
status === 'booked' ? (
|
||||
<CheckCircleOutlineOutlinedIcon />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon />
|
||||
)
|
||||
}
|
||||
onClick={this.handleOpenBookDialog}
|
||||
sx={{ fontWeight: 600, textTransform: 'none' }}
|
||||
>
|
||||
{status === 'booked'
|
||||
? 'Buchungsdetails anzeigen'
|
||||
: status === 'partial'
|
||||
? 'Restliche Mengen einbuchen'
|
||||
: 'Ins Lager buchen'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{allEntries.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', mb: 1, display: 'block' }}>
|
||||
Bisherige Ledger-Buchungen ({allEntries.length}):
|
||||
</Typography>
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Zeitpunkt</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Typ</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>SKU / Artikel</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5 }}>Menge</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Buchungstext</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, fontFamily: 'monospace', fontSize: '0.75rem' }}>Prüfsumme (prev_hash)</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{allEntries.map((entry) => (
|
||||
<TableRow key={entry.id} hover>
|
||||
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption">
|
||||
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={entry.transaction_type}
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600 }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, fontFamily: 'monospace' }}>
|
||||
{entry.lineSku || entry.sku || '–'}
|
||||
</Typography>
|
||||
{entry.lineName && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
|
||||
{entry.lineName}
|
||||
</Typography>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 0.5, fontWeight: 700, color: 'success.main' }}>
|
||||
+{entry.amount}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5 }}>
|
||||
<Typography variant="caption">
|
||||
{entry.transaction_text || '–'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5, fontFamily: 'monospace', fontSize: '0.7rem', color: 'text.secondary' }}>
|
||||
<Tooltip title={entry.prev_hash}>
|
||||
<span>{entry.prev_hash ? `${entry.prev_hash.slice(0, 16)}…` : '–'}</span>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferanten SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge geliefert</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge eingebucht</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Referenz</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{allEntries.map((entry) => (
|
||||
<TableRow key={entry.id} hover>
|
||||
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption">
|
||||
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
|
||||
{entry.supplierSku}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||
{entry.ownSku}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5 }}>
|
||||
<Typography variant="caption">
|
||||
{entry.name}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption">
|
||||
{entry.deliveredAmount}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||
<Typography variant="caption">
|
||||
{entry.warehouseName}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={{
|
||||
py: 0.5,
|
||||
fontWeight: 700,
|
||||
color: entry.mengeEingebucht !== '–' ? 'success.main' : 'text.disabled',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{entry.mengeEingebucht}
|
||||
</TableCell>
|
||||
<TableCell sx={{ py: 0.5 }}>
|
||||
<Typography variant="caption">
|
||||
{entry.transaction_text || '–'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PanelTile>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user