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:
seb
2026-09-16 10:06:19 +02:00
parent 7135cdb5fe
commit 0130ee6516
10 changed files with 665 additions and 128 deletions

View File

@@ -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 }}>