This commit is contained in:
seb
2026-10-16 13:10:58 +02:00
parent 7bc1da4de6
commit d851c6e1e9
10 changed files with 326 additions and 445 deletions

View File

@@ -36,7 +36,6 @@ import {
ArticleSearchDialog,
BookDeliveryDialog,
} from '../../components/manifest';
import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog';
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
import { article as articleApi, delivery as deliveryApi } from '../../api';
import { getCategoryState } from '../../automats/categoryAutomat';
@@ -68,9 +67,6 @@ class LieferungNeuPage extends PureComponent {
partiesLoading: true,
saving: false,
pdfUploading: false,
pdfDialogOpen: false,
pdfDialogData: null,
pdfFileName: '',
textEditorOpen: false,
textEditorAttachment: null,
viewerPosition: userSettingsAutomat.state.viewerPosition,
@@ -105,8 +101,6 @@ class LieferungNeuPage extends PureComponent {
this.handlePdfUpload = this.handlePdfUpload.bind(this);
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.handleTextEditorPositionChange = this.handleTextEditorPositionChange.bind(this);
@@ -159,10 +153,6 @@ 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;
@@ -301,7 +291,6 @@ class LieferungNeuPage extends PureComponent {
handleCreateSupplierFromPdf(supplierName) {
const name = (supplierName ?? this.state.unmatchedSupplierName ?? '').trim();
this.handleClosePdfDialog();
const target = `/lieferung/lieferanten?name=${encodeURIComponent(name)}&returnTo=/lieferung/neu`;
this.props.navigate(target, { state: { name, returnTo: '/lieferung/neu' } });
}
@@ -635,9 +624,6 @@ class LieferungNeuPage extends PureComponent {
};
this.automat.actions.addAttachment(textAttachment);
}
// 3. Run SSE parsing with backend (POST /api/delivery/parse)
await this.runParse(file);
}
async handlePdfRescan(attachment) {
@@ -697,8 +683,6 @@ class LieferungNeuPage extends PureComponent {
console.error('Fehler beim Aktualisieren der Textdatei:', txtErr);
}
}
await this.runParse(attachment.file);
}
handlePdfDelete(attachment) {
@@ -706,227 +690,6 @@ class LieferungNeuPage extends PureComponent {
this.automat.actions.removeAttachment(attachment.id);
}
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, aborted: false },
});
const appendMessage = (msg) => {
if (this._cancelled || abortController.signal.aborted) return;
this.setState((prev) => ({
pdfDialogData: {
...prev.pdfDialogData,
messages: [...(prev.pdfDialogData?.messages ?? []), msg],
},
}));
};
try {
for await (const { event, data } of deliveryApi.parseSse(file, { signal: abortController.signal })) {
if (this._cancelled || abortController.signal.aborted) return;
switch (event) {
case 'status':
appendMessage({ type: 'status', text: data.message ?? String(data) });
break;
case 'llm_call':
appendMessage({
type: 'llm_call',
text: `LLM-Aufruf: ${data.model} (${data.promptSize} tokens, ${data.messages} msg)`,
});
break;
case 'tool_call':
appendMessage({ type: 'tool_call', text: `Tool-Aufruf: ${data.name}` });
break;
case 'usage':
appendMessage({
type: 'usage',
text: `Token-Verbrauch: ${data.input} in / ${data.cached} cached / ${data.output} out — $${(data.cost ?? 0).toFixed(4)}`,
});
break;
case 'progress':
this.setState((prev) => ({
pdfDialogData: { ...prev.pdfDialogData, result: data },
}));
break;
case 'result':
case 'done': {
const result = data?.delivery ?? data;
const rawItems = Array.isArray(result?.lines)
? result.lines
: Array.isArray(result?.items?.items)
? result.items.items
: null;
const newLines = Array.isArray(rawItems)
? rawItems.map((l, idx) => {
const taxVal = (l.taxrate ?? l.tax ?? l.taxRate ?? l.mwst) != null
? String(l.taxrate ?? l.tax ?? l.taxRate ?? l.mwst)
: '';
const netVal = (l.netprice ?? l.net ?? l.netPrice ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : null)) != null
? String(l.netprice ?? l.net ?? l.netPrice ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : ''))
: '';
return {
key: l.key ?? `${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 8)}`,
name: l.name ?? l.articleName ?? null,
sku: l.sku ?? l.liefArtNr ?? '',
amount: (l.amount ?? l.count) != null ? String(l.amount ?? l.count) : '',
pu: (l.pu ?? l.vpe) != null ? String(l.pu ?? l.vpe) : '',
unit: l.unit ?? '',
taxrate: taxVal,
tax: taxVal,
netprice: netVal,
net: netVal,
};
})
: null;
this.setState((prev) => ({
pdfDialogData: { ...prev.pdfDialogData, streaming: false, result },
}));
const docDate = result?.date ?? result?.basedata?.date;
let supplierId = result?.supplierId ?? result?.basedata?.kLieferant;
const extNum = result?.externalNumber ?? result?.basedata?.manifestNr;
const docType = result?.type ?? result?.basedata?.manifestType;
const notes = Array.isArray(result?.notes) ? result.notes : [];
const parsedSupplierName = (
result?.supplierName ??
result?.basedata?.lieferantName ??
result?.lieferantName ??
result?.basedata?.supplierName ??
''
).trim();
// If supplierId is null, try matching supplier by name from loaded delivery entities
if (supplierId == null && parsedSupplierName && Array.isArray(this.state.deliveryEntities)) {
const nameToMatch = parsedSupplierName.toLowerCase();
const matched = this.state.deliveryEntities.find(
(e) =>
e.name &&
(e.name.trim().toLowerCase() === nameToMatch ||
e.name.toLowerCase().includes(nameToMatch) ||
nameToMatch.includes(e.name.toLowerCase())),
);
if (matched) supplierId = matched.id;
}
const unmatchedSupplierName = supplierId == null && parsedSupplierName ? parsedSupplierName : null;
this.setState({ unmatchedSupplierName });
this.automat.actions.setUnmatchedSupplierName(unmatchedSupplierName);
if (docDate) this.automat.actions.setDate(docDate);
if (supplierId != null) this.automat.actions.setDeliveryEntity(supplierId);
if (extNum != null) {
this.automat.actions.setExternalNumber(String(extNum));
}
if (docType) this.automat.actions.setType(docType);
if (newLines && newLines.length > 0) {
this.automat.actions.setLineitems(newLines);
}
if (notes.length > 0) {
this.automat.actions.setNotes(notes);
}
// Supply whole article list to GET /supplier/{id}/articles
const effectiveSupplierId = supplierId ?? this.automat.state.deliveryEntity ?? this.automat.state.partyId;
const effectiveLines = (newLines && newLines.length > 0) ? newLines : this.automat.state.lines;
if (effectiveSupplierId != null && effectiveLines && effectiveLines.length > 0) {
this.automat.actions.fetchSupplierArticles(effectiveSupplierId, effectiveLines);
}
break;
}
case 'error':
this.setState((prev) => ({
pdfDialogData: {
...prev.pdfDialogData,
streaming: false,
error: data.message ?? 'Analyse fehlgeschlagen.',
},
}));
break;
default:
break;
}
}
} 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,
streaming: false,
error: err.message ?? 'Netzwerkfehler bei Analyse.',
},
}));
} finally {
if (this._parseAbortController === abortController) {
this._parseAbortController = null;
}
if (!this._cancelled) {
this.setState((prev) => ({
pdfUploading: false,
pdfDialogData: prev.pdfDialogData
? { ...prev.pdfDialogData, streaming: false }
: null,
}));
}
}
}
async runPdfParse(file) {
await this.runParse(file);
}
async runTxtParse(file) {
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() {
if (this._parseAbortController) {
this._parseAbortController.abort();
this._parseAbortController = null;
}
this.setState({ pdfDialogOpen: false, pdfUploading: false });
}
async ensureAttachmentFile(attachment) {
if (!attachment) return null;
const hasContent =
@@ -1127,7 +890,7 @@ class LieferungNeuPage extends PureComponent {
isViewerOpen() {
return Boolean(
(this.state.textEditorOpen && this.state.textEditorAttachment) ||
(this.state.pdfViewerOpen && this.state.pdfViewerAttachment),
(this.state.pdfViewerOpen && this.state.pdfViewerAttachment),
);
}
@@ -1244,9 +1007,6 @@ class LieferungNeuPage extends PureComponent {
partiesLoading,
saving,
pdfUploading,
pdfDialogOpen,
pdfDialogData,
pdfFileName,
deliveryEntities,
date,
partyId,
@@ -1376,15 +1136,7 @@ class LieferungNeuPage extends PureComponent {
</PanelTile>
)}
</PanelTileGrid>
<ManifestPdfDialog
open={pdfDialogOpen}
filename={pdfFileName}
data={pdfDialogData}
unmatchedSupplierName={this.state.unmatchedSupplierName}
onCreateSupplier={this.handleCreateSupplierFromPdf}
onClose={this.handleClosePdfDialog}
onAbort={this.handleAbortParse}
/>
<ArticleSearchDialog
open={this.state.articleSearchDialogOpen}
@@ -1456,8 +1208,8 @@ class LieferungNeuPage extends PureComponent {
status === 'booked'
? 'success.light'
: status === 'partial'
? 'warning.light'
: 'divider',
? 'warning.light'
: 'divider',
}}
>
<Stack spacing={2}>
@@ -1554,8 +1306,8 @@ class LieferungNeuPage extends PureComponent {
{status === 'booked'
? 'Buchungsdetails anzeigen'
: status === 'partial'
? 'Restliche Mengen einbuchen'
: 'Ins Lager buchen'}
? 'Restliche Mengen einbuchen'
: 'Ins Lager buchen'}
</Button>
</Box>