import { PureComponent } from 'react'; import Box from '@mui/material/Box'; import CircularProgress from '@mui/material/CircularProgress'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import { PanelTileGrid } from 'dashgui/panels'; import { delivery as deliveryApi, quote as quoteApi } from '../../api'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; import withNavigate from '../withNavigate'; import ManifestHeader from './ManifestHeader'; import ManifestLines from './ManifestLines'; import ManifestPdfDialog from './ManifestPdfDialog'; import ArticleSearchDialog from './ArticleSearchDialog'; import artikelAutomat, { getArticle } from '../../automats/artikelAutomat'; import entityAutomat from '../../automats/entityAutomat'; import supplierAutomat from '../../automats/supplierAutomat'; import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat'; import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price'; import { buildManifestBody, restoreAttachmentsFromApi, dateInputValue, emptyLine, lineFromApi, loadParties, manifestConfig, apiFor, parseNum, } from './manifestConfig'; import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber'; import { getManifestDraftAutomat, getManifestEditAutomat, } from '../../automats/manifestDraftAutomat'; class ManifestForm extends PureComponent { constructor(props) { super(props); this.cfg = manifestConfig(props.kind); const initialParties = props.kind === 'delivery' ? (supplierAutomat.state.status === 'ready' ? (supplierAutomat.state.suppliers ?? []) : []) : (entityAutomat.state.status === 'ready' ? (entityAutomat.state.items ?? []) : []); const partiesReady = props.kind === 'delivery' ? supplierAutomat.state.status === 'ready' : entityAutomat.state.status === 'ready'; this.draftAutomat = props.mode === 'create' ? getManifestDraftAutomat(props.kind) : null; this.editAutomat = props.mode === 'edit' ? getManifestEditAutomat(props.kind) : null; const initialDraft = this.draftAutomat?.state; const initialEdit = (this.editAutomat && String(this.editAutomat.state?.id) === String(props.docId)) ? this.editAutomat.state : null; const initialDoc = initialDraft || initialEdit; this.state = { status: props.mode === 'edit' && !initialEdit ? 'loading' : (partiesReady ? 'ready' : 'loadingParties'), error: null, parties: initialParties, partiesLoading: !partiesReady, date: initialDoc?.date || '', partyId: initialDoc?.partyId ?? null, number: initialDoc?.number || '', externalNumber: initialDoc?.externalNumber || '', lines: initialDoc?.lines?.length > 0 ? initialDoc.lines : [emptyLine()], attachments: initialDoc?.attachments || [], isDirty: Boolean(initialEdit?.isDirty), dirtyFields: initialEdit?.dirtyFields || null, dirtyLineKeys: initialEdit?.dirtyLineKeys || [], dirtyLineFields: initialEdit?.dirtyLineFields || {}, dirtySummary: initialEdit?.dirtySummary || [], saving: false, pdfUploading: false, pdfCreating: false, pdfDialogOpen: false, pdfDialogData: null, pdfFileName: '', articleSearchDialogOpen: false, articleSearchLine: null, }; this._cancelled = false; this.handleRetry = this.handleRetry.bind(this); this.handleRestore = this.handleRestore.bind(this); this.handlePartyChange = this.handlePartyChange.bind(this); this.handleDateChange = this.handleDateChange.bind(this); this.handleNumberChange = this.handleNumberChange.bind(this); this.handleExternalNumberChange = this.handleExternalNumberChange.bind(this); this.handleLineChange = this.handleLineChange.bind(this); this.handleSkuBlur = this.handleSkuBlur.bind(this); this.handleAddLine = this.handleAddLine.bind(this); this.handleAddArticleLine = this.handleAddArticleLine.bind(this); this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this); this.handleCloseArticleSearch = this.handleCloseArticleSearch.bind(this); this.handleAssignArticleForLine = this.handleAssignArticleForLine.bind(this); this.handleRemoveLine = this.handleRemoveLine.bind(this); this.handleMoveLine = this.handleMoveLine.bind(this); this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this); this.handleSave = this.handleSave.bind(this); this.handlePdfUpload = this.handlePdfUpload.bind(this); this.handleCreatePdf = this.handleCreatePdf.bind(this); this.handleAttachmentDelete = this.handleAttachmentDelete.bind(this); this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this); this.handleAbortParse = this.handleAbortParse.bind(this); } componentDidMount() { this._cancelled = false; if (this.props.kind === 'delivery') { this.unsubParties = supplierAutomat.subscribe(this, (state) => ({ parties: state.status === 'ready' ? (state.suppliers ?? []) : [], partiesLoading: state.status === 'loading', })); } else { this.unsubParties = entityAutomat.subscribe(this, (state) => ({ parties: state.items ?? [], partiesLoading: state.status === 'loading', })); } if (companyAutomat.state.status === 'idle') { void companyAutomat.actions.load(); } this.unsubCompany = companyAutomat.subscribe(this, (state) => { const taxrate = state.current?.taxrate; if (taxrate != null) { this.setState((prev) => { if ( prev.lines.length === 1 && !prev.lines[0].sku && !prev.lines[0].name && !prev.lines[0].net && prev.lines[0].tax !== String(taxrate) ) { return { lines: [ { ...prev.lines[0], tax: String(taxrate), taxrate: String(taxrate), }, ], }; } return null; }); } }); if (this.props.mode === 'edit') { void this.loadEdit(); } else { void this.loadPartiesOnly(); } if (this.draftAutomat) { this.unsubDraft = this.draftAutomat.subscribe(this, (state) => ({ date: state.date || '', partyId: state.partyId, externalNumber: state.externalNumber || '', lines: state.lines?.length > 0 ? state.lines : [emptyLine()], attachments: state.attachments || [], })); } if (this.editAutomat) { this.unsubEdit = this.editAutomat.subscribe(this, (state) => { if (String(state.id) === String(this.props.docId)) { return { date: state.date || '', partyId: state.partyId, externalNumber: state.externalNumber || '', lines: state.lines?.length > 0 ? state.lines : [emptyLine()], attachments: state.attachments || [], isDirty: Boolean(state.isDirty), dirtyFields: state.dirtyFields, dirtyLineKeys: state.dirtyLineKeys, dirtyLineFields: state.dirtyLineFields, dirtySummary: state.dirtySummary, }; } return null; }); } } componentDidUpdate(prevProps, prevState) { if ( this.props.mode === 'edit' && prevProps.docId !== this.props.docId ) { void this.loadEdit(); } if (this.props.mode === 'create' && this.draftAutomat) { if ( prevState.date !== this.state.date || prevState.partyId !== this.state.partyId || prevState.externalNumber !== this.state.externalNumber || prevState.lines !== this.state.lines || prevState.attachments !== this.state.attachments ) { this.draftAutomat.setState({ date: this.state.date, partyId: this.state.partyId, externalNumber: this.state.externalNumber, lines: this.state.lines, attachments: this.state.attachments, }); } } if ( this.props.mode === 'edit' && this.editAutomat && String(this.editAutomat.state.id) === String(this.props.docId) ) { if ( prevState.date !== this.state.date || prevState.partyId !== this.state.partyId || prevState.externalNumber !== this.state.externalNumber || prevState.lines !== this.state.lines ) { this.editAutomat.actions.update({ date: this.state.date, partyId: this.state.partyId, externalNumber: this.state.externalNumber, lines: this.state.lines, }); const { isDirty, dirtyFields, dirtyLineKeys, dirtyLineFields, dirtySummary } = this.editAutomat.state; if ( this.state.isDirty !== isDirty || this.state.dirtyFields !== dirtyFields || this.state.dirtyLineFields !== dirtyLineFields || this.state.dirtySummary !== dirtySummary ) { this.setState({ isDirty, dirtyFields, dirtyLineKeys, dirtyLineFields, dirtySummary }); } } } } componentWillUnmount() { this._cancelled = true; this.unsubDraft?.(); this.unsubEdit?.(); this.unsubParties?.(); this.unsubCompany?.(); if (this._parseAbortController) { this._parseAbortController.abort(); this._parseAbortController = null; } } async loadPartiesOnly() { if (this.state.parties.length === 0) { this.setState({ status: 'loadingParties', error: null, partiesLoading: true }); } try { const parties = await loadParties(this.props.kind); if (this._cancelled) return; this.setState({ status: 'ready', parties, partiesLoading: false, }); } catch (err) { if (this._cancelled) return; if (this.state.parties.length === 0) { this.setState({ status: 'failed', error: backendErrorFrom(err), partiesLoading: false, }); } notifyError(err); } } async loadEdit() { const { docId, kind } = this.props; const cfg = this.cfg; if (docId == null) { this.setState({ status: 'failed', error: backendErrorFrom(new Error(cfg.invalidId)), }); return; } // If editAutomat already holds this exact document in memory, preserve in-progress edits if ( this.editAutomat && String(this.editAutomat.state.id) === String(docId) && this.editAutomat.state.baseline != null ) { void this.loadPartiesOnly(); this.setState({ status: 'ready', isDirty: Boolean(this.editAutomat.state.isDirty), dirtyFields: this.editAutomat.state.dirtyFields, dirtyLineKeys: this.editAutomat.state.dirtyLineKeys, dirtyLineFields: this.editAutomat.state.dirtyLineFields, dirtySummary: this.editAutomat.state.dirtySummary, date: this.editAutomat.state.date, partyId: this.editAutomat.state.partyId, number: this.editAutomat.state.number, externalNumber: this.editAutomat.state.externalNumber, lines: this.editAutomat.state.lines, attachments: this.editAutomat.state.attachments, }); return; } this.setState({ status: 'loading', error: null, partiesLoading: true }); try { const [docRes, parties] = await Promise.all([ apiFor(cfg).get(docId), loadParties(kind), ]); if (this._cancelled) return; requireObject(docRes, cfg.getPathLabel); requireObject(docRes[cfg.docField], cfg.docField); const doc = docRes[cfg.docField]; const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`); const lines = apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()]; // Restore attachments if available const apiFiles = doc.files ?? []; const attachments = await restoreAttachmentsFromApi(apiFiles); const loadedDoc = { date: dateInputValue(doc.date), partyId: doc[cfg.partyField], number: doc.number ?? '', externalNumber: doc.externalNumber != null ? String(doc.externalNumber) : '', lines, attachments, }; if (this.editAutomat) { this.editAutomat.actions.loadDoc(docId, loadedDoc); } this.setState({ status: 'ready', parties, partiesLoading: false, ...loadedDoc, isDirty: false, dirtyFields: null, dirtyLineKeys: [], dirtyLineFields: {}, dirtySummary: [], }); } catch (err) { if (this._cancelled) return; this.setState({ status: 'failed', error: backendErrorFrom(err), partiesLoading: false, }); } } handleRetry() { if (this.props.mode === 'edit') void this.loadEdit(); else void this.loadPartiesOnly(); } handleRestore() { if (!this.editAutomat) return; this.editAutomat.actions.restoreBaseline(); const state = this.editAutomat.state; this.setState({ date: state.date || '', partyId: state.partyId, number: state.number || '', externalNumber: state.externalNumber || '', lines: state.lines?.length > 0 ? state.lines : [emptyLine()], attachments: state.attachments || [], isDirty: false, dirtyFields: state.dirtyFields, dirtyLineKeys: state.dirtyLineKeys, dirtyLineFields: state.dirtyLineFields, dirtySummary: state.dirtySummary, }); enqueueSnackbar('Änderungen verworfen und ursprünglicher Stand wiederhergestellt.', { variant: 'info' }); } handlePartyChange(partyId) { const { mode } = this.props; const { requirePartyBeforeLines } = this.cfg; if (mode === 'create' && requirePartyBeforeLines && partyId == null) { this.setState({ partyId: null, lines: [emptyLine()] }); return; } this.setState({ partyId }); } handleDateChange(date) { this.setState({ date }); } handleNumberChange(number) { this.setState({ number }); } handleExternalNumberChange(externalNumber) { this.setState({ externalNumber }); } handleLineChange(key, field, value) { this.setState((prev) => ({ lines: prev.lines.map((line) => { if (line.key !== key) return line; if (field === 'gross') { const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0; const rebateVal = parseNum(line.rebate) ?? 0; const grossNum = parseNum(value); let updatedNet = line.net; if (grossNum != null) { const netNum = netFromGross(grossNum, taxVal, rebateVal); if (netNum != null) { updatedNet = String(netNum); } } else if (value === '') { updatedNet = ''; } return { ...line, gross: value, net: updatedNet, netprice: updatedNet, }; } if (field === 'net' || field === 'netprice') { const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0; const rebateVal = parseNum(line.rebate) ?? 0; const netNum = parseNum(value); let updatedGross = line.gross; if (netNum != null) { const grossNum = grossFromNet(netNum, taxVal, rebateVal); updatedGross = grossNum != null ? String(grossNum) : ''; } else if (value === '') { updatedGross = ''; } return { ...line, [field]: value, net: value, netprice: value, gross: updatedGross, }; } if (field === 'tax' || field === 'taxrate') { const taxNum = parseNum(value) ?? 0; const rebateVal = parseNum(line.rebate) ?? 0; const netNum = parseNum(line.net ?? line.netprice); let updatedGross = line.gross; if (netNum != null) { const grossNum = grossFromNet(netNum, taxNum, rebateVal); updatedGross = grossNum != null ? String(grossNum) : ''; } return { ...line, [field]: value, tax: value, taxrate: value, gross: updatedGross, }; } if (field === 'rebate') { const rebateNum = parseNum(value) ?? 0; const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0; const netNum = parseNum(line.net ?? line.netprice); let updatedGross = line.gross; if (netNum != null) { const grossNum = grossFromNet(netNum, taxVal, rebateNum); updatedGross = grossNum != null ? String(grossNum) : ''; } return { ...line, rebate: value, gross: updatedGross, }; } return { ...line, [field]: value, }; }), })); } async handleSkuBlur(key, sku) { const trimmed = String(sku ?? '').trim(); if (!trimmed) return; try { const art = getArticle(trimmed) || (await artikelAutomat.actions.loadArticle(trimmed)); if (art && !this._cancelled) { this.setState((prev) => ({ lines: prev.lines.map((l) => { if (l.key !== key) return l; const netVal = art.price != null ? String(art.price) : art.vknetto != null ? String(art.vknetto) : art.net != null ? String(art.net) : ''; const taxVal = art.taxrate != null ? String(art.taxrate) : art.tax != null ? String(art.tax) : ''; const netNum = parseNum(netVal); const taxNum = parseNum(taxVal); const rebateNum = parseNum(l.rebate) ?? 0; const artGross = getArticleGross(art, taxNum ?? 0); let grossVal = ''; if (artGross != null && rebateNum === 0) { grossVal = String(artGross); } else if (netNum != null) { grossVal = String(grossFromNet(netNum, taxNum ?? 0, rebateNum)); } else if (artGross != null) { grossVal = String(Math.round((artGross * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100); } const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; const finalTax = art.taxrate != null ? String(art.taxrate) : art.tax != null ? String(art.tax) : l.tax || normTax; return { ...l, amount: l.amount || '1', sku: art.sku || l.sku, name: art.name || l.name || '', tax: finalTax, taxrate: finalTax, rebate: l.rebate || '0', net: l.net || netVal, netprice: l.netprice || netVal, gross: l.gross || grossVal, unit: art.unit || l.unit || 'Stück', }; }), })); } } catch { // not in catalog or load error; preserve user entry } } handleAddLine() { this.setState((prev) => ({ lines: [...prev.lines, emptyLine()] })); } handleAddArticleLine(article) { if (!article) return; const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; const netVal = article.price != null ? String(article.price) : article.vknetto != null ? String(article.vknetto) : article.net != null ? String(article.net) : ''; const taxVal = article.taxrate != null ? String(article.taxrate) : article.tax != null ? String(article.tax) : normTax; const netNum = parseNum(netVal); const taxNum = parseNum(taxVal); const artGross = getArticleGross(article, taxNum ?? 0); let grossVal = ''; if (artGross != null) { grossVal = String(artGross); } else if (netNum != null) { const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100; grossVal = String(g); } const newLine = { key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, amount: '1', sku: article.sku || '', name: article.name || '', tax: taxVal, taxrate: taxVal, net: netVal, netprice: netVal, gross: grossVal, rebate: '0', pu: '1', unit: article.unit || 'Stück', }; this.setState((prev) => { if (prev.lines.length === 1 && !prev.lines[0].sku && !prev.lines[0].name) { return { lines: [newLine] }; } return { lines: [...prev.lines, newLine] }; }); } handleOpenArticleSearchForLine(line) { this.setState({ articleSearchDialogOpen: true, articleSearchLine: line, }); } handleCloseArticleSearch() { this.setState({ articleSearchDialogOpen: false, articleSearchLine: null, }); } handleAssignArticleForLine(article, targetLine) { if (!article) return; if (!targetLine) { this.handleAddArticleLine(article); this.setState({ articleSearchDialogOpen: false, articleSearchLine: null, }); return; } const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; const netVal = article.price != null ? String(article.price) : article.vknetto != null ? String(article.vknetto) : article.net != null ? String(article.net) : ''; const taxVal = article.taxrate != null ? String(article.taxrate) : article.tax != null ? String(article.tax) : normTax; const netNum = parseNum(netVal); const taxNum = parseNum(taxVal); const rebateNum = parseNum(targetLine.rebate) ?? 0; const artGross = getArticleGross(article, taxNum ?? 0); let grossVal = ''; if (artGross != null && rebateNum === 0) { grossVal = String(artGross); } else if (netNum != null) { grossVal = String(grossFromNet(netNum, taxNum ?? 0, rebateNum)); } else if (artGross != null) { grossVal = String(Math.round((artGross * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100); } this.setState((prev) => ({ lines: prev.lines.map((l) => { if (l.key !== targetLine.key) return l; return { ...l, amount: l.amount || '1', sku: article.sku || l.sku, name: article.name || l.name, tax: taxVal || l.tax || normTax, taxrate: taxVal || l.taxrate || normTax, rebate: l.rebate || '0', net: netVal || l.net, netprice: netVal || l.netprice, gross: grossVal || l.gross, unit: article.unit || l.unit || 'Stück', }; }), articleSearchDialogOpen: false, articleSearchLine: null, })); } handleRemoveLine(key) { this.setState((prev) => { const next = prev.lines.filter((line) => line.key !== key); return { lines: next.length > 0 ? next : [emptyLine()] }; }); } handleMoveLine(key, delta) { this.setState((prev) => { const fromIndex = prev.lines.findIndex((l) => l.key === key); if (fromIndex < 0) return null; const toIndex = fromIndex + delta; if (toIndex < 0 || toIndex >= prev.lines.length) return null; const next = [...prev.lines]; const [item] = next.splice(fromIndex, 1); next.splice(toIndex, 0, item); return { lines: next, isDirty: true }; }); } handleRemoveAllLines() { this.setState({ lines: [] }); } 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, aborted: false }, }); const appendMessage = (msg) => { if (this._cancelled || abortController.signal.aborted) return; this.setState((prev) => ({ pdfDialogData: { ...prev.pdfDialogData, messages: [...(prev.pdfDialogData?.messages ?? []), msg], }, })); }; try { const gen = deliveryApi.parsePdfSse; if (!gen) throw new Error('parsePdfSse nicht verfügbar.'); for await (const { event, data } of gen(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': this.setState((prev) => ({ pdfDialogData: { ...prev.pdfDialogData, result: data, streaming: false }, })); break; case 'error': this.setState((prev) => ({ pdfDialogData: { ...prev.pdfDialogData, error: data.message ?? String(data), streaming: false }, })); return; 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; } notifyError(err); this.setState((prev) => ({ pdfDialogData: { ...(prev.pdfDialogData ?? {}), error: err.message || String(err), streaming: false, }, })); } finally { if (this._parseAbortController === abortController) { this._parseAbortController = null; } if (!this._cancelled) { this.setState((prev) => ({ pdfUploading: false, pdfDialogData: prev.pdfDialogData ? { ...prev.pdfDialogData, streaming: false } : null, })); } } } 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 handleCreatePdf(e) { e?.preventDefault?.(); e?.stopPropagation?.(); const { docId } = this.props; if (this.state.pdfCreating || this.state.saving) return; if (docId == null) return; this.setState({ pdfCreating: true }); try { const res = await quoteApi.createPdf(docId); requireObject(res, 'POST /api/quote/:id/pdf'); const quoteRes = await quoteApi.get(docId); if (quoteRes?.quote?.files && !this._cancelled) { const attachments = await restoreAttachmentsFromApi( quoteRes.quote.files, this.state.attachments, ); this.setState({ attachments }); if (this.editAutomat) { this.editAutomat.actions.setAttachments?.(attachments); } } enqueueSnackbar('Angebot-PDF erfolgreich erstellt.', { variant: 'success' }); } catch (err) { if (!this._cancelled) { notifyError(err); } } finally { if (!this._cancelled) { this.setState({ pdfCreating: false }); } } } async handleAttachmentDelete(attachment) { if (!attachment) return; const fileId = attachment.fileId ?? attachment.id; const { docId } = this.props; const nextAttachments = (this.state.attachments || []).filter( (a) => (a.fileId ?? a.id) !== fileId && a !== attachment, ); this.setState({ attachments: nextAttachments }); if (this.editAutomat) { this.editAutomat.actions.setAttachments?.(nextAttachments); } if (docId != null && fileId != null && attachment.isPersisted) { try { const api = apiFor(this.cfg); if (typeof api.deleteFile === 'function') { await api.deleteFile(docId, fileId); enqueueSnackbar('Anhang gelöscht.', { variant: 'success' }); } } catch (err) { notifyError(err); } } } async handleSave() { const { mode, docId } = this.props; const cfg = this.cfg; if (this.state.saving) return; if (mode === 'edit' && docId == null) return; const requireAtLeastOne = mode === 'create'; let body; try { body = await buildManifestBody( { number: this.state.number, externalNumber: this.state.externalNumber, date: this.state.date, partyId: this.state.partyId, lines: this.state.lines, attachments: this.state.attachments, }, { kind: cfg.kind, partyField: cfg.partyField, requireAtLeastOne, externalNumber: cfg.externalNumber, }, ); } catch (err) { notifyError(err); return; } this.setState({ saving: true }); const api = apiFor(cfg); try { if (mode === 'create') { const data = await api.create(body); requireObject(data, cfg.postPathLabel); requireObject(data[cfg.docField], cfg.docField); requireDefined(data[cfg.docField].id, `${cfg.docField}.id`); this.draftAutomat?.actions?.reset(); await cfg.listAutomat.actions.reload(); if (this._cancelled) return; enqueueSnackbar(cfg.createdMsg, { variant: 'success' }); this.props.navigate(cfg.editPath(data[cfg.docField].id)); return; } const data = await api.update(docId, body); requireObject(data, cfg.patchPathLabel); requireObject(data[cfg.docField], cfg.docField); requireDefined(data[cfg.docField].id, `${cfg.docField}.id`); await cfg.listAutomat.actions.reload(); if (this._cancelled) return; const doc = data[cfg.docField]; const apiLines = requireArray(doc.lines, `${cfg.docField}.lines`); const savedDoc = { date: dateInputValue(doc.date), partyId: doc[cfg.partyField], number: doc.number ?? '', externalNumber: doc.externalNumber != null ? String(doc.externalNumber) : '', lines: apiLines.length > 0 ? apiLines.map(lineFromApi) : [emptyLine()], attachments: this.state.attachments, }; if (this.editAutomat && String(this.editAutomat.state.id) === String(docId)) { this.editAutomat.actions.commitSaved(savedDoc); } this.setState({ ...savedDoc, isDirty: false, dirtyFields: null, dirtyLineKeys: [], dirtyLineFields: {}, dirtySummary: [], saving: false, }); enqueueSnackbar(cfg.savedMsg, { variant: 'success' }); } catch (err) { if (this._cancelled) return; notifyError(err); this.setState({ saving: false }); } } renderArticles() { const cfg = this.cfg; const { lines, saving, dirtyLineFields } = this.state; return ( void this.handleSave()} /> ); } render() { const { kind } = this.props; const cfg = this.cfg; const { status, error, parties, partiesLoading, date, partyId, number, externalNumber, lines, saving, pdfUploading, pdfDialogOpen, pdfDialogData, pdfFileName, } = this.state; if (status === 'loading') { return ( ); } if (status === 'failed') { return ; } return ( <> {status === 'ready' ? this.renderArticles() : null} ); } } export default withNavigate(ManifestForm);