feat(lieferung/neu): PDF upload with SSE streaming and result dialog

- Add PDF-Upload button to ManifestHeader (delivery forms only)
- New ManifestPdfDialog: live SSE event log, structured basedata,
  and line-items table streamed from POST /api/delivery/parsePDF
- ApiClient.delivery.parsePdfSse: multipart/form-data upload,
  async generator yielding typed SSE events (status, llm_call,
  tool_call, usage, progress, result, error)
- ManifestForm.handlePdfUpload: drives SSE generator, updates
  dialog state in real-time; dialog opens immediately on upload
- Fix MUI icon imports (CheckCircle, ErrorOutlined)
This commit is contained in:
seb
2026-09-15 08:50:16 +02:00
parent 4df5110253
commit dbf978d015
9 changed files with 2064 additions and 33 deletions

View File

@@ -1,7 +1,10 @@
import { PureComponent } from 'react';
import { PureComponent, createRef } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import Button from '@mui/material/Button';
import CircularProgress from '@mui/material/CircularProgress';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import UploadFileIcon from '@mui/icons-material/UploadFile';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { PanelTile } from 'dashgui/panels';
import { manifestConfig, partyDisplayName } from './manifestConfig';
@@ -9,13 +12,27 @@ import { manifestConfig, partyDisplayName } from './manifestConfig';
class ManifestHeader extends PureComponent {
constructor(props) {
super(props);
this.fileInputRef = createRef();
this.handlePartyChange = this.handlePartyChange.bind(this);
this.handleUploadClick = this.handleUploadClick.bind(this);
this.handleFileChange = this.handleFileChange.bind(this);
}
handlePartyChange(_event, option) {
this.props.onPartyChange(option?.id ?? null);
}
handleUploadClick() {
this.fileInputRef.current?.click();
}
handleFileChange(event) {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
this.props.onPdfUpload?.(file);
}
render() {
const {
kind,
@@ -29,6 +46,8 @@ class ManifestHeader extends PureComponent {
number,
externalNumber,
saving,
pdfUploading,
onPdfUpload,
onDateChange,
onNumberChange,
onExternalNumberChange,
@@ -44,13 +63,13 @@ class ManifestHeader extends PureComponent {
return (
<PanelTile wide={3} minHeight={160} color={headerColor} title="Kopfdaten">
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2}>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} alignItems={{ xs: 'stretch', sm: 'center' }}>
<TextField
size="small"
type="date"
label={dateLabel}
value={date}
disabled={saving}
disabled={saving || pdfUploading}
onChange={(e) => onDateChange(e.target.value)}
autoComplete="off"
slotProps={{
@@ -64,7 +83,7 @@ class ManifestHeader extends PureComponent {
options={parties}
value={selected}
loading={Boolean(partiesLoading)}
disabled={saving}
disabled={saving || pdfUploading}
getOptionLabel={(option) => partyDisplayName(kind, option)}
isOptionEqualToValue={(option, value) => option.id === value.id}
onChange={this.handlePartyChange}
@@ -88,7 +107,7 @@ class ManifestHeader extends PureComponent {
size="small"
label="Nummer"
value={number}
disabled={saving || useExternalNumber}
disabled={saving || pdfUploading || useExternalNumber}
onChange={onNumberChange}
sx={{ minWidth: 180, flex: 1 }}
/>
@@ -97,15 +116,37 @@ class ManifestHeader extends PureComponent {
size="small"
label="Referenz"
value={externalNumber ?? ''}
disabled={saving}
disabled={saving || pdfUploading}
onChange={onExternalNumberChange}
sx={{ minWidth: 180, flex: 1 }}
/>
) : null}
{onPdfUpload ? (
<Button
variant="outlined"
size="small"
startIcon={pdfUploading ? <CircularProgress size={16} color="inherit" /> : <UploadFileIcon />}
disabled={saving || pdfUploading}
onClick={this.handleUploadClick}
sx={{ whiteSpace: 'nowrap', height: 40, alignSelf: { xs: 'stretch', sm: 'center' }, px: 2 }}
>
{pdfUploading ? 'Wird analysiert…' : 'PDF hochladen'}
</Button>
) : null}
</Stack>
{onPdfUpload ? (
<input
ref={this.fileInputRef}
type="file"
accept="application/pdf,.pdf"
style={{ display: 'none' }}
onChange={this.handleFileChange}
/>
) : null}
</PanelTile>
);
}
}
export default ManifestHeader;