diff --git a/src/api/ApiClient.js b/src/api/ApiClient.js
index 5c3c9b2..31778da 100644
--- a/src/api/ApiClient.js
+++ b/src/api/ApiClient.js
@@ -116,13 +116,14 @@ export class ApiClient {
* Returns an async generator that yields SSE event objects:
* { event: 'status'|'llm_call'|'tool_call'|'usage'|'progress'|'result'|'error', data: object }
*/
- parseSse: async function* (file) {
+ parseSse: async function* (file, { signal } = {}) {
const formData = new FormData();
formData.append('file', file, file.name);
const res = await fetch('/api/delivery/parse', {
method: 'POST',
credentials: 'include',
body: formData,
+ signal,
});
if (!res.ok) {
let message = res.statusText;
@@ -138,28 +139,56 @@ export class ApiClient {
const decoder = new TextDecoder();
let buf = '';
let currentEvent = 'message';
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- buf += decoder.decode(value, { stream: true });
- const lines = buf.split('\n');
- buf = lines.pop(); // keep incomplete last line
- for (const line of lines) {
- if (line.startsWith('event:')) {
- currentEvent = line.slice(6).trim();
- } else if (line.startsWith('data:')) {
- const raw = line.slice(5).trim();
- let data;
- try { data = JSON.parse(raw); } catch { data = raw; }
- yield { event: currentEvent, data };
- currentEvent = 'message';
- }
- // blank line or comment: reset
+
+ const onAbort = () => {
+ try {
+ reader.cancel();
+ } catch { /* ignore */ }
+ };
+ if (signal) {
+ if (signal.aborted) {
+ onAbort();
+ return;
}
+ signal.addEventListener('abort', onAbort, { once: true });
+ }
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buf += decoder.decode(value, { stream: true });
+ const lines = buf.split('\n');
+ buf = lines.pop(); // keep incomplete last line
+ for (const line of lines) {
+ if (line.startsWith('event:')) {
+ currentEvent = line.slice(6).trim();
+ } else if (line.startsWith('data:')) {
+ const raw = line.slice(5).trim();
+ let data;
+ try { data = JSON.parse(raw); } catch { data = raw; }
+ yield { event: currentEvent, data };
+ currentEvent = 'message';
+ }
+ // blank line or comment: reset
+ }
+ }
+ } catch (err) {
+ if (signal?.aborted || err.name === 'AbortError') {
+ return;
+ }
+ throw err;
+ } finally {
+ if (signal) {
+ signal.removeEventListener('abort', onAbort);
+ }
+ try {
+ reader.releaseLock();
+ } catch { /* ignore */ }
}
},
- parsePdfSse(file) {
- return this.parseSse(file);
+ parsePdfSse(file, opts) {
+ return this.parseSse(file, opts);
},
};
diff --git a/src/api/index.js b/src/api/index.js
index 504a3ae..23a8ff7 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -710,13 +710,14 @@ export const delivery = {
* Accepts PDF, TXT or CSV document (max 50 MB)
* Streams SSE events: status, llm_call, tool_call, usage, progress, result, error
*/
- async *parseSse(file) {
+ async *parseSse(file, { signal } = {}) {
const formData = new FormData();
formData.append('file', file, file.name);
const res = await fetch('/api/delivery/parse', {
method: 'POST',
credentials: 'include',
body: formData,
+ signal,
});
if (!res.ok) {
let message = res.statusText;
@@ -732,23 +733,51 @@ export const delivery = {
const decoder = new TextDecoder();
let buf = '';
let currentEvent = 'message';
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- buf += decoder.decode(value, { stream: true });
- const lines = buf.split('\n');
- buf = lines.pop();
- for (const line of lines) {
- if (line.startsWith('event:')) {
- currentEvent = line.slice(6).trim();
- } else if (line.startsWith('data:')) {
- const raw = line.slice(5).trim();
- let data;
- try { data = JSON.parse(raw); } catch { data = raw; }
- yield { event: currentEvent, data };
- currentEvent = 'message';
+
+ const onAbort = () => {
+ try {
+ reader.cancel();
+ } catch { /* ignore */ }
+ };
+ if (signal) {
+ if (signal.aborted) {
+ onAbort();
+ return;
+ }
+ signal.addEventListener('abort', onAbort, { once: true });
+ }
+
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buf += decoder.decode(value, { stream: true });
+ const lines = buf.split('\n');
+ buf = lines.pop();
+ for (const line of lines) {
+ if (line.startsWith('event:')) {
+ currentEvent = line.slice(6).trim();
+ } else if (line.startsWith('data:')) {
+ const raw = line.slice(5).trim();
+ let data;
+ try { data = JSON.parse(raw); } catch { data = raw; }
+ yield { event: currentEvent, data };
+ currentEvent = 'message';
+ }
}
}
+ } catch (err) {
+ if (signal?.aborted || err.name === 'AbortError') {
+ return;
+ }
+ throw err;
+ } finally {
+ if (signal) {
+ signal.removeEventListener('abort', onAbort);
+ }
+ try {
+ reader.releaseLock();
+ } catch { /* ignore */ }
}
},
};
diff --git a/src/components/manifest/ArticleSearchDialog.jsx b/src/components/manifest/ArticleSearchDialog.jsx
new file mode 100644
index 0000000..4b39529
--- /dev/null
+++ b/src/components/manifest/ArticleSearchDialog.jsx
@@ -0,0 +1,344 @@
+import { PureComponent } from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import CircularProgress from '@mui/material/CircularProgress';
+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 IconButton from '@mui/material/IconButton';
+import InputAdornment from '@mui/material/InputAdornment';
+import List from '@mui/material/List';
+import ListItemAvatar from '@mui/material/ListItemAvatar';
+import ListItemButton from '@mui/material/ListItemButton';
+import ListItemText from '@mui/material/ListItemText';
+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 CloseIcon from '@mui/icons-material/Close';
+import SearchIcon from '@mui/icons-material/Search';
+import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
+import LinkIcon from '@mui/icons-material/Link';
+import { StorageManagerContext } from '../../api/storageManagerContext';
+import ArticlePhoto from '../article/ArticlePhoto';
+
+export default class ArticleSearchDialog extends PureComponent {
+ static contextType = StorageManagerContext;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ query: '',
+ articles: [],
+ loading: false,
+ error: null,
+ assigningSku: null,
+ };
+ this.handleQueryChange = this.handleQueryChange.bind(this);
+ this.handleClearQuery = this.handleClearQuery.bind(this);
+ this.handleSelect = this.handleSelect.bind(this);
+ }
+
+ componentDidMount() {
+ if (this.props.open) {
+ this.initFromLine();
+ void this.loadArticles();
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (!prevProps.open && this.props.open) {
+ this.initFromLine();
+ void this.loadArticles();
+ }
+ }
+
+ initFromLine() {
+ const { line } = this.props;
+ const initialQuery = line?.name?.trim() || line?.sku?.trim() || '';
+ this.setState({
+ query: initialQuery,
+ assigningSku: null,
+ error: null,
+ });
+ }
+
+ async loadArticles() {
+ this.setState({ loading: true, error: null });
+ try {
+ let list = [];
+ if (this.context?.articleIndexPromise) {
+ list = await this.context.articleIndexPromise;
+ } else if (this.context?.article?.index) {
+ const res = await this.context.article.index();
+ list = res?.articles || [];
+ }
+ this.setState({ articles: Array.isArray(list) ? list : [], loading: false });
+ } catch (err) {
+ console.error('Fehler beim Laden des Artikel-Index:', err);
+ this.setState({
+ loading: false,
+ error: err?.message || 'Artikel-Index konnte nicht geladen werden.',
+ });
+ }
+ }
+
+ handleQueryChange(e) {
+ this.setState({ query: e.target.value });
+ }
+
+ handleClearQuery() {
+ this.setState({ query: '' });
+ }
+
+ async handleSelect(article) {
+ const { onAssign, line } = this.props;
+ if (!onAssign || !article?.sku) return;
+
+ this.setState({ assigningSku: article.sku, error: null });
+ try {
+ await onAssign(article, line);
+ } catch (err) {
+ this.setState({
+ assigningSku: null,
+ error: err?.message || 'Fehler beim Verknüpfen des Lieferanten-Artikels.',
+ });
+ }
+ }
+
+ render() {
+ const { open, onClose, line } = this.props;
+ const { query, articles, loading, error, assigningSku } = this.state;
+
+ const trimmed = query.trim().toLowerCase();
+ const filtered = trimmed
+ ? articles.filter((a) => {
+ const skuMatch = a.sku?.toLowerCase().includes(trimmed);
+ const nameMatch = a.name?.toLowerCase().includes(trimmed);
+ const eanMatch = Array.isArray(a.eans)
+ ? a.eans.some((e) => String(e).toLowerCase().includes(trimmed))
+ : false;
+ return skuMatch || nameMatch || eanMatch;
+ })
+ : articles.slice(0, 50);
+
+ const rawSku = line?.sku != null ? String(line.sku).trim() : '';
+
+ return (
+
+ );
+ }
+}
diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx
index 02d2f50..527a9e7 100644
--- a/src/components/manifest/ManifestForm.jsx
+++ b/src/components/manifest/ManifestForm.jsx
@@ -59,6 +59,7 @@ class ManifestForm extends PureComponent {
this.handleSave = this.handleSave.bind(this);
this.handlePdfUpload = this.handlePdfUpload.bind(this);
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
+ this.handleAbortParse = this.handleAbortParse.bind(this);
}
componentDidMount() {
@@ -78,6 +79,10 @@ class ManifestForm extends PureComponent {
componentWillUnmount() {
this._cancelled = true;
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ this._parseAbortController = null;
+ }
}
async loadPartiesOnly() {
@@ -210,15 +215,21 @@ class ManifestForm extends PureComponent {
async handlePdfUpload(file) {
if (!file || this.state.pdfUploading) return;
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ }
+ const abortController = new AbortController();
+ this._parseAbortController = abortController;
+
this.setState({
pdfUploading: true,
pdfDialogOpen: true,
pdfFileName: file.name,
- pdfDialogData: { streaming: true, messages: [], result: null, error: null },
+ pdfDialogData: { streaming: true, messages: [], result: null, error: null, aborted: false },
});
const appendMessage = (msg) => {
- if (this._cancelled) return;
+ if (this._cancelled || abortController.signal.aborted) return;
this.setState((prev) => ({
pdfDialogData: {
...prev.pdfDialogData,
@@ -231,8 +242,8 @@ class ManifestForm extends PureComponent {
const gen = this.context?.delivery?.parsePdfSse;
if (!gen) throw new Error('parsePdfSse nicht verfügbar.');
- for await (const { event, data } of this.context.delivery.parsePdfSse(file)) {
- if (this._cancelled) return;
+ for await (const { event, data } of this.context.delivery.parsePdfSse(file, { signal: abortController.signal })) {
+ if (this._cancelled || abortController.signal.aborted) return;
switch (event) {
case 'status':
appendMessage({ type: 'status', text: data.message ?? String(data) });
@@ -267,6 +278,17 @@ class ManifestForm extends PureComponent {
}
} catch (err) {
if (this._cancelled) return;
+ if (abortController.signal.aborted || err.name === 'AbortError') {
+ this.setState((prev) => ({
+ pdfDialogData: {
+ ...prev.pdfDialogData,
+ streaming: false,
+ aborted: true,
+ error: 'Analyse abgebrochen.',
+ },
+ }));
+ return;
+ }
notifyError(err);
this.setState((prev) => ({
pdfDialogData: {
@@ -276,6 +298,9 @@ class ManifestForm extends PureComponent {
},
}));
} finally {
+ if (this._parseAbortController === abortController) {
+ this._parseAbortController = null;
+ }
if (!this._cancelled) {
this.setState((prev) => ({
pdfUploading: false,
@@ -287,9 +312,30 @@ class ManifestForm extends PureComponent {
}
}
+ handleAbortParse() {
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ this._parseAbortController = null;
+ }
+ this.setState((prev) => ({
+ pdfUploading: false,
+ pdfDialogData: prev.pdfDialogData
+ ? {
+ ...prev.pdfDialogData,
+ streaming: false,
+ aborted: true,
+ error: 'Analyse abgebrochen.',
+ }
+ : null,
+ }));
+ }
handleClosePdfDialog() {
- this.setState({ pdfDialogOpen: false });
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ this._parseAbortController = null;
+ }
+ this.setState({ pdfDialogOpen: false, pdfUploading: false });
}
async handleSave() {
@@ -454,6 +500,7 @@ class ManifestForm extends PureComponent {
filename={pdfFileName}
data={pdfDialogData}
onClose={this.handleClosePdfDialog}
+ onAbort={this.handleAbortParse}
/>
>
);
diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx
index 51c06c7..54ef881 100644
--- a/src/components/manifest/ManifestFormLine.jsx
+++ b/src/components/manifest/ManifestFormLine.jsx
@@ -1,4 +1,5 @@
import { Fragment, PureComponent } from 'react';
+import { Link as RouterLink } from 'react-router-dom';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Chip from '@mui/material/Chip';
@@ -13,9 +14,11 @@ import HelpOutlineIcon from '@mui/icons-material/HelpOutlineOutlined';
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined';
import AddIcon from '@mui/icons-material/Add';
+import SearchIcon from '@mui/icons-material/Search';
import { LINE_COLUMNS } from './manifestConfig';
import ArticlePhoto from '../article/ArticlePhoto';
import { formatPrice } from '../../utils/price';
+import { artikelDetailPath } from '../../pages/artikel/articleRoute';
class ManifestFormLine extends PureComponent {
constructor(props) {
@@ -23,6 +26,7 @@ class ManifestFormLine extends PureComponent {
this.handleRemove = this.handleRemove.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleCreateArticle = this.handleCreateArticle.bind(this);
+ this.handleSearchArticle = this.handleSearchArticle.bind(this);
}
handleRemove() {
@@ -42,6 +46,13 @@ class ManifestFormLine extends PureComponent {
}
}
+ handleSearchArticle() {
+ const { line, onSearchArticle } = this.props;
+ if (onSearchArticle) {
+ onSearchArticle(line);
+ }
+ }
+
renderField(column) {
const { id, align, type } = column;
const { line, saving, onChange } = this.props;
@@ -151,6 +162,9 @@ class ManifestFormLine extends PureComponent {
{/* Internal Article SKU */}
{article.sku && (
)}
@@ -196,24 +215,6 @@ class ManifestFormLine extends PureComponent {
)}
- {/* Stock */}
-
-
- {/* Packaging Unit (PU) */}
- {supplierArticle.pu != null && (
-
- )}
-
{/* Stored Purchase Price (Netto) */}
{priceNet != null && (
)}
+ {this.props.onSearchArticle && (
+ }
+ onClick={this.handleSearchArticle}
+ sx={{
+ fontSize: '0.7rem',
+ fontWeight: 600,
+ textTransform: 'none',
+ minWidth: 'auto',
+ py: 0.25,
+ px: 1,
+ color: 'primary.main',
+ '&:hover': {
+ bgcolor: 'primary.50',
+ },
+ }}
+ >
+ Artikel suchen
+
+ )}
);
}
diff --git a/src/components/manifest/ManifestLines.jsx b/src/components/manifest/ManifestLines.jsx
index a9f0105..b6f1912 100644
--- a/src/components/manifest/ManifestLines.jsx
+++ b/src/components/manifest/ManifestLines.jsx
@@ -30,6 +30,7 @@ class ManifestLines extends PureComponent {
loadingLineKeys = {},
onSkuBlur,
onCreateArticle,
+ onSearchArticle,
} = this.props;
return (
@@ -78,6 +79,7 @@ class ManifestLines extends PureComponent {
onRemove={onRemoveLine}
onSkuBlur={onSkuBlur}
onCreateArticle={onCreateArticle}
+ onSearchArticle={onSearchArticle}
/>
);
})}
diff --git a/src/components/manifest/ManifestPdfDialog.jsx b/src/components/manifest/ManifestPdfDialog.jsx
index b155b78..6100e22 100644
--- a/src/components/manifest/ManifestPdfDialog.jsx
+++ b/src/components/manifest/ManifestPdfDialog.jsx
@@ -92,19 +92,21 @@ function EventLog({ messages, streaming }) {
);
}
-export default function ManifestPdfDialog({ open, filename, data, onClose }) {
+export default function ManifestPdfDialog({ open, filename, data, onClose, onAbort }) {
if (!open) return null;
- const { streaming = false, messages = [], result = null, error = null } = data ?? {};
+ const { streaming = false, messages = [], result = null, error = null, aborted = false } = data ?? {};
const hasResult = Boolean(result);
const isTextDoc =
filename?.toLowerCase().endsWith('.txt') || filename?.toLowerCase().endsWith('.csv');
+ const handleAbortOrClose = streaming ? (onAbort || onClose) : onClose;
+
return (
);
diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js
index 201091e..cb4c23a 100644
--- a/src/components/manifest/index.js
+++ b/src/components/manifest/index.js
@@ -6,6 +6,7 @@ export { default as ManifestFormLine } from './ManifestFormLine';
export { default as Attachment } from './Attachment';
export { default as AttachmentList } from './AttachmentList';
export { default as TextEditorDialog } from './TextEditorDialog';
+export { default as ArticleSearchDialog } from './ArticleSearchDialog';
export { withManifestId } from './withManifestId';
export {
manifestConfig,
diff --git a/src/pages/lieferung/neu.jsx b/src/pages/lieferung/neu.jsx
index 4777bde..9b4ded0 100644
--- a/src/pages/lieferung/neu.jsx
+++ b/src/pages/lieferung/neu.jsx
@@ -19,6 +19,7 @@ import {
lineFromApi,
manifestConfig,
TextEditorDialog,
+ ArticleSearchDialog,
} from '../../components/manifest';
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
import { loadParties } from '../../components/manifest/manifestConfig';
@@ -50,6 +51,8 @@ class LieferungNeuPage extends PureComponent {
pdfFileName: '',
textEditorOpen: false,
textEditorAttachment: null,
+ articleSearchDialogOpen: false,
+ articleSearchLine: null,
};
this._cancelled = false;
this.handleRetry = this.handleRetry.bind(this);
@@ -65,6 +68,7 @@ class LieferungNeuPage extends PureComponent {
this.handlePdfRescan = this.handlePdfRescan.bind(this);
this.handlePdfDelete = this.handlePdfDelete.bind(this);
this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this);
+ this.handleAbortParse = this.handleAbortParse.bind(this);
this.handleAttachmentClick = this.handleAttachmentClick.bind(this);
this.handleCloseTextEditor = this.handleCloseTextEditor.bind(this);
this.handleSaveTextEditor = this.handleSaveTextEditor.bind(this);
@@ -72,6 +76,9 @@ class LieferungNeuPage extends PureComponent {
this.ensureAttachmentFile = this.ensureAttachmentFile.bind(this);
this.handleSkuBlur = this.handleSkuBlur.bind(this);
this.handleCreateArticleForLine = this.handleCreateArticleForLine.bind(this);
+ this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this);
+ this.handleCloseArticleSearch = this.handleCloseArticleSearch.bind(this);
+ this.handleAssignArticleForLine = this.handleAssignArticleForLine.bind(this);
}
componentDidMount() {
@@ -106,6 +113,10 @@ class LieferungNeuPage extends PureComponent {
componentWillUnmount() {
this._cancelled = true;
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ this._parseAbortController = null;
+ }
if (this._skuDebounceTimers) {
Object.values(this._skuDebounceTimers).forEach(clearTimeout);
this._skuDebounceTimers = null;
@@ -311,6 +322,67 @@ class LieferungNeuPage extends PureComponent {
}
}
+ handleOpenArticleSearchForLine(line) {
+ this.setState({
+ articleSearchDialogOpen: true,
+ articleSearchLine: line,
+ });
+ }
+
+ handleCloseArticleSearch() {
+ this.setState({
+ articleSearchDialogOpen: false,
+ articleSearchLine: null,
+ });
+ }
+
+ async handleAssignArticleForLine(article, line) {
+ const { partyId } = this.state;
+ if (!partyId) {
+ enqueueSnackbar('Bitte wählen Sie zuerst einen Lieferanten aus.', { variant: 'warning' });
+ return;
+ }
+
+ const internalSku = article?.sku;
+ if (!internalSku) {
+ enqueueSnackbar('Artikel hat keine SKU.', { variant: 'error' });
+ return;
+ }
+
+ const supplierSku = line?.sku;
+
+ try {
+ await this.context.article.addSupplier(internalSku, {
+ entityId: partyId,
+ sku: supplierSku || undefined,
+ pu: line?.pu ? Number(line.pu) : undefined,
+ stock: line?.amount ? Number(line.amount) : undefined,
+ netPrice: line?.netprice ? Number(line.netprice) : undefined,
+ taxRate: line?.taxrate ? Number(line.taxrate) : undefined,
+ });
+
+ if (supplierSku) {
+ this.automat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
+ } else {
+ this.automat.actions.fetchSupplierArticles(partyId);
+ }
+
+ enqueueSnackbar(
+ `Lieferant erfolgreich dem Artikel "${internalSku}" zugeordnet.`,
+ { variant: 'success' },
+ );
+
+ this.setState({
+ articleSearchDialogOpen: false,
+ articleSearchLine: null,
+ });
+ } catch (err) {
+ console.error('Fehler beim Zuordnen des Artikels:', err);
+ notifyError(err);
+ throw err;
+ }
+ }
+
handleAddLine() {
this.automat.actions.addLineitem();
}
@@ -451,15 +523,21 @@ class LieferungNeuPage extends PureComponent {
}
async runParse(file) {
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ }
+ const abortController = new AbortController();
+ this._parseAbortController = abortController;
+
this.setState({
pdfUploading: true,
pdfDialogOpen: true,
pdfFileName: file.name,
- pdfDialogData: { streaming: true, messages: [], result: null, error: null },
+ pdfDialogData: { streaming: true, messages: [], result: null, error: null, aborted: false },
});
const appendMessage = (msg) => {
- if (this._cancelled) return;
+ if (this._cancelled || abortController.signal.aborted) return;
this.setState((prev) => ({
pdfDialogData: {
...prev.pdfDialogData,
@@ -472,8 +550,8 @@ class LieferungNeuPage extends PureComponent {
const parser = this.context?.delivery?.parseSse;
if (!parser) throw new Error('parseSse nicht verfügbar.');
- for await (const { event, data } of parser(file)) {
- if (this._cancelled) return;
+ for await (const { event, data } of parser(file, { signal: abortController.signal })) {
+ if (this._cancelled || abortController.signal.aborted) return;
switch (event) {
case 'status':
appendMessage({ type: 'status', text: data.message ?? String(data) });
@@ -580,6 +658,17 @@ class LieferungNeuPage extends PureComponent {
}
} catch (err) {
if (this._cancelled) return;
+ if (abortController.signal.aborted || err.name === 'AbortError') {
+ this.setState((prev) => ({
+ pdfDialogData: {
+ ...prev.pdfDialogData,
+ streaming: false,
+ aborted: true,
+ error: 'Analyse abgebrochen.',
+ },
+ }));
+ return;
+ }
this.setState((prev) => ({
pdfDialogData: {
...prev.pdfDialogData,
@@ -588,6 +677,9 @@ class LieferungNeuPage extends PureComponent {
},
}));
} finally {
+ if (this._parseAbortController === abortController) {
+ this._parseAbortController = null;
+ }
if (!this._cancelled) {
this.setState((prev) => ({
pdfUploading: false,
@@ -607,8 +699,30 @@ class LieferungNeuPage extends PureComponent {
await this.runParse(file);
}
+ handleAbortParse() {
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ this._parseAbortController = null;
+ }
+ this.setState((prev) => ({
+ pdfUploading: false,
+ pdfDialogData: prev.pdfDialogData
+ ? {
+ ...prev.pdfDialogData,
+ streaming: false,
+ aborted: true,
+ error: 'Analyse abgebrochen.',
+ }
+ : null,
+ }));
+ }
+
handleClosePdfDialog() {
- this.setState({ pdfDialogOpen: false });
+ if (this._parseAbortController) {
+ this._parseAbortController.abort();
+ this._parseAbortController = null;
+ }
+ this.setState({ pdfDialogOpen: false, pdfUploading: false });
}
async ensureAttachmentFile(attachment) {
@@ -929,6 +1043,7 @@ class LieferungNeuPage extends PureComponent {
onRemoveLine={this.handleRemoveLine}
onSave={() => void this.handleSave()}
onCreateArticle={this.handleCreateArticleForLine}
+ onSearchArticle={this.handleOpenArticleSearchForLine}
/>
>
)}
@@ -952,6 +1067,7 @@ class LieferungNeuPage extends PureComponent {
filename={pdfFileName}
data={pdfDialogData}
onClose={this.handleClosePdfDialog}
+ onAbort={this.handleAbortParse}
/>
+
>
);
}