feat(manifest): improve PDF extraction workflow and fix delivery lines rendering
- Fix SSE 'result' event handling in delivery creation and stop circular progress - Directly populate deliveryEditorSlice with parsed header, line items, and notes - Simplify ManifestPdfDialog to display LLM protocol log and status only - Render PDF analysis notes at the bottom of the delivery page - Keep article lines visible when imported items exist even if partyId is not yet selected - Bind handleEdit and handleDelete in ManifestList constructor to fix row click TypeError - Support delivery columns (name, pu, netprice, taxrate) in ManifestForm and lineFromApi - Remove obsolete Unit column from delivery lines
This commit is contained in:
@@ -176,7 +176,16 @@ class ManifestForm extends PureComponent {
|
||||
handleLineChange(key, field, value) {
|
||||
this.setState((prev) => ({
|
||||
lines: prev.lines.map((line) =>
|
||||
line.key === key ? { ...line, [field]: value } : line,
|
||||
line.key === key
|
||||
? {
|
||||
...line,
|
||||
[field]: value,
|
||||
...(field === 'net' ? { netprice: value } : {}),
|
||||
...(field === 'netprice' ? { net: value } : {}),
|
||||
...(field === 'tax' ? { taxrate: value } : {}),
|
||||
...(field === 'taxrate' ? { tax: value } : {}),
|
||||
}
|
||||
: line,
|
||||
),
|
||||
}));
|
||||
}
|
||||
@@ -260,7 +269,14 @@ class ManifestForm extends PureComponent {
|
||||
},
|
||||
}));
|
||||
} finally {
|
||||
if (!this._cancelled) this.setState({ pdfUploading: false });
|
||||
if (!this._cancelled) {
|
||||
this.setState((prev) => ({
|
||||
pdfUploading: false,
|
||||
pdfDialogData: prev.pdfDialogData
|
||||
? { ...prev.pdfDialogData, streaming: false }
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +374,7 @@ class ManifestForm extends PureComponent {
|
||||
|
||||
return (
|
||||
<ManifestLines
|
||||
columns={cfg.columns}
|
||||
lines={lines}
|
||||
saving={saving}
|
||||
linesColor={cfg.linesColor}
|
||||
|
||||
@@ -24,7 +24,7 @@ class ManifestFormLine extends PureComponent {
|
||||
size="small"
|
||||
fullWidth
|
||||
type={type}
|
||||
value={line[id]}
|
||||
value={line[id] ?? ''}
|
||||
disabled={saving}
|
||||
autoComplete="off"
|
||||
onChange={(e) => onChange(line.key, id, e.target.value)}
|
||||
|
||||
@@ -50,10 +50,10 @@ class ManifestLines extends PureComponent {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{lines.map((line) => (
|
||||
{lines.map((line, index) => (
|
||||
<ManifestFormLine
|
||||
key={line.key}
|
||||
line={line}
|
||||
key={line.key ?? index}
|
||||
line={line.key != null ? line : { ...line, key: index }}
|
||||
columns={columns}
|
||||
saving={saving}
|
||||
onChange={onLineChange}
|
||||
|
||||
@@ -31,6 +31,8 @@ class ManifestList extends PureComponent {
|
||||
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);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
@@ -8,14 +8,7 @@ import Dialog from '@mui/material/Dialog';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
@@ -64,7 +57,7 @@ function EventLog({ messages, streaming }) {
|
||||
<Collapse in={expanded}>
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: 180,
|
||||
maxHeight: 280,
|
||||
overflowY: 'auto',
|
||||
border: 1,
|
||||
borderColor: 'divider',
|
||||
@@ -98,73 +91,6 @@ function EventLog({ messages, streaming }) {
|
||||
);
|
||||
}
|
||||
|
||||
function BaseDataSection({ basedata }) {
|
||||
if (!basedata) return null;
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ display: 'block', mb: 0.5 }}>
|
||||
Kopfdaten
|
||||
</Typography>
|
||||
<Stack direction="row" flexWrap="wrap" gap={1}>
|
||||
{basedata.date && <Chip size="small" label={`Datum: ${basedata.date}`} />}
|
||||
{basedata.manifestNr && <Chip size="small" label={`Nr: ${basedata.manifestNr}`} />}
|
||||
{basedata.manifestType && <Chip size="small" label={`Typ: ${basedata.manifestType}`} />}
|
||||
{basedata.lieferantName && <Chip size="small" label={`Lieferant: ${basedata.lieferantName}`} />}
|
||||
{basedata.kLieferant != null && <Chip size="small" label={`kLieferant: ${basedata.kLieferant}`} />}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ItemsSection({ items }) {
|
||||
if (!items?.items?.length) return null;
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ display: 'block', mb: 0.5 }}>
|
||||
Positionen ({items.items.length})
|
||||
{items.pagesFrom != null && (
|
||||
<span style={{ fontWeight: 400, marginLeft: 4 }}>
|
||||
— Seiten {items.pagesFrom}–{items.pagesTo}
|
||||
</span>
|
||||
)}
|
||||
</Typography>
|
||||
<Box sx={{ overflowX: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Table size="small" padding="none" sx={{ minWidth: 520 }}>
|
||||
<TableHead>
|
||||
<TableRow sx={{ bgcolor: 'action.hover' }}>
|
||||
{['Pos.', 'Artikel', 'LiefArtNr', 'Menge', 'VPE', 'Einheit', 'EK-Netto', 'MwSt'].map((h) => (
|
||||
<TableCell key={h} sx={{ px: 1, py: 0.5, fontSize: '0.72rem', fontWeight: 700, whiteSpace: 'nowrap' }}>
|
||||
{h}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.items.map((item, i) => (
|
||||
<TableRow key={i} hover>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem' }}>{item.position ?? i + 1}</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', maxWidth: 180, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{item.articleName ?? '–'}
|
||||
</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', fontFamily: 'monospace' }}>{item.liefArtNr ?? '–'}</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right' }}>{item.count}</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right' }}>{item.vpe ?? 1}</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem' }}>{item.unit ?? '–'}</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right', fontFamily: 'monospace' }}>
|
||||
{item.eknetto != null ? item.eknetto.toFixed(2) : '–'}
|
||||
</TableCell>
|
||||
<TableCell sx={{ px: 1, py: 0.4, fontSize: '0.75rem', textAlign: 'right' }}>
|
||||
{item.mwst != null ? `${item.mwst} %` : '–'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
||||
if (!open) return null;
|
||||
|
||||
@@ -175,7 +101,7 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={streaming ? undefined : onClose}
|
||||
maxWidth="md"
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
aria-labelledby="pdf-dialog-title"
|
||||
>
|
||||
@@ -200,7 +126,7 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="subtitle1" component="div" fontWeight={600}>
|
||||
{streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyseergebnis'}
|
||||
{streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyse abgeschlossen'}
|
||||
</Typography>
|
||||
{filename ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
@@ -220,47 +146,22 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) {
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers sx={{ p: 2 }}>
|
||||
<Stack spacing={2}>
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<Alert severity="error" icon={<ErrorOutlinedIcon />}>
|
||||
<Typography variant="body2" fontWeight={600}>{error}</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<Alert severity="error" icon={<ErrorOutlinedIcon />} sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>{error}</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Live log */}
|
||||
<EventLog messages={messages} streaming={streaming} />
|
||||
{/* Live log */}
|
||||
<EventLog messages={messages} streaming={streaming} />
|
||||
|
||||
{/* Notes */}
|
||||
{result?.notes?.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={600} sx={{ display: 'block', mb: 0.5 }}>
|
||||
Hinweise
|
||||
</Typography>
|
||||
<Stack spacing={0.5}>
|
||||
{result.notes.map((note, i) => (
|
||||
<Alert key={i} severity="warning" sx={{ py: 0, '& .MuiAlert-message': { fontSize: '0.8rem' } }}>
|
||||
{note}
|
||||
</Alert>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Basedata + Items */}
|
||||
{hasResult && (
|
||||
<>
|
||||
<Divider />
|
||||
<BaseDataSection basedata={result.basedata} />
|
||||
<ItemsSection items={result.items} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* No result yet and no error: show waiting state */}
|
||||
{!hasResult && !error && !streaming && (
|
||||
<Alert severity="info">Keine Daten extrahiert.</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
{/* Success notification */}
|
||||
{hasResult && !streaming && !error && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
Daten erfolgreich in das Formular übernommen.
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ p: 1.5, px: 2 }}>
|
||||
|
||||
@@ -7,6 +7,7 @@ export { withManifestId } from './withManifestId';
|
||||
export {
|
||||
manifestConfig,
|
||||
LINE_COLUMNS,
|
||||
DELIVERY_LINE_COLUMNS,
|
||||
emptyLine,
|
||||
lineFromApi,
|
||||
buildManifestBody,
|
||||
|
||||
@@ -69,17 +69,27 @@ export function emptyLine() {
|
||||
tax: '',
|
||||
rebate: '',
|
||||
net: '',
|
||||
name: '',
|
||||
pu: '',
|
||||
unit: '',
|
||||
netprice: '',
|
||||
taxrate: '',
|
||||
};
|
||||
}
|
||||
|
||||
export function lineFromApi(row) {
|
||||
return {
|
||||
key: `line-${row.id}`,
|
||||
key: `line-${row.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`}`,
|
||||
amount: row.amount == null ? '' : String(row.amount),
|
||||
sku: row.sku ?? '',
|
||||
tax: row.tax == null ? '' : String(row.tax),
|
||||
rebate: row.rebate == null ? '' : String(row.rebate),
|
||||
net: row.net == null ? '' : String(row.net),
|
||||
name: row.name ?? '',
|
||||
pu: row.pu == null ? '' : String(row.pu),
|
||||
unit: row.unit ?? '',
|
||||
netprice: row.netprice != null ? String(row.netprice) : row.net != null ? String(row.net) : '',
|
||||
taxrate: row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,8 +105,8 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
||||
for (const line of lines) {
|
||||
const sku = String(line.sku ?? '').trim();
|
||||
const amount = parseNum(line.amount);
|
||||
const net = parseNum(line.net);
|
||||
const tax = parseNum(line.tax);
|
||||
const net = parseNum(line.net ?? line.netprice);
|
||||
const tax = parseNum(line.tax ?? line.taxrate);
|
||||
const rebate = parseNum(line.rebate);
|
||||
const blank =
|
||||
!sku && amount == null && net == null && tax == null && rebate == null;
|
||||
@@ -106,6 +116,9 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
||||
}
|
||||
const row = { sku, amount, net, tax };
|
||||
if (rebate != null) row.rebate = rebate;
|
||||
if (line.name) row.name = line.name;
|
||||
if (line.pu != null) row.pu = line.pu;
|
||||
if (line.unit) row.unit = line.unit;
|
||||
outLines.push(row);
|
||||
}
|
||||
if (requireAtLeastOne && outLines.length === 0) {
|
||||
@@ -157,6 +170,15 @@ export const LINE_COLUMNS = [
|
||||
{ id: 'net', label: 'Net', width: 104, align: 'right', type: 'number' },
|
||||
];
|
||||
|
||||
export const DELIVERY_LINE_COLUMNS = [
|
||||
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
|
||||
{ id: 'name', label: 'Name', width: 220, align: 'left', type: 'text' },
|
||||
{ id: 'amount', label: 'Amount', width: 88, align: 'right', type: 'number' },
|
||||
{ id: 'pu', label: 'PU', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'taxrate', label: 'Tax', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'netprice', label: 'Net', width: 104, align: 'right', type: 'number' },
|
||||
];
|
||||
|
||||
/** @typedef {'delivery'|'quote'|'order'} ManifestKind */
|
||||
|
||||
/**
|
||||
@@ -220,6 +242,7 @@ export function manifestConfig(kind) {
|
||||
// Delivery uses a server-allocated read-only number plus an optional
|
||||
// caller-supplied external reference.
|
||||
externalNumber: true,
|
||||
columns: DELIVERY_LINE_COLUMNS,
|
||||
};
|
||||
}
|
||||
if (kind === 'quote') {
|
||||
@@ -253,6 +276,7 @@ export function manifestConfig(kind) {
|
||||
listPromise: (ctx) => ctx.quotes ?? ctx.quotePromise,
|
||||
partiesByIdFromList: (parties) =>
|
||||
Object.fromEntries(parties.map((row) => [row.id, row])),
|
||||
columns: LINE_COLUMNS,
|
||||
};
|
||||
}
|
||||
if (kind === 'order') {
|
||||
@@ -286,6 +310,7 @@ export function manifestConfig(kind) {
|
||||
listPromise: (ctx) => ctx.orders ?? ctx.orderPromise,
|
||||
partiesByIdFromList: (parties) =>
|
||||
Object.fromEntries(parties.map((row) => [row.id, row])),
|
||||
columns: LINE_COLUMNS,
|
||||
};
|
||||
}
|
||||
throw new Error(`Unknown manifest kind: ${kind}`);
|
||||
|
||||
Reference in New Issue
Block a user