diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx index e39b6fd..29eda31 100644 --- a/src/components/manifest/ManifestForm.jsx +++ b/src/components/manifest/ManifestForm.jsx @@ -176,7 +176,16 @@ class ManifestForm extends PureComponent { handleLineChange(key, field, value) { this.setState((prev) => ({ lines: prev.lines.map((line) => - line.key === key ? { ...line, [field]: value } : line, + line.key === key + ? { + ...line, + [field]: value, + ...(field === 'net' ? { netprice: value } : {}), + ...(field === 'netprice' ? { net: value } : {}), + ...(field === 'tax' ? { taxrate: value } : {}), + ...(field === 'taxrate' ? { tax: value } : {}), + } + : line, ), })); } @@ -260,7 +269,14 @@ class ManifestForm extends PureComponent { }, })); } finally { - if (!this._cancelled) this.setState({ pdfUploading: false }); + if (!this._cancelled) { + this.setState((prev) => ({ + pdfUploading: false, + pdfDialogData: prev.pdfDialogData + ? { ...prev.pdfDialogData, streaming: false } + : null, + })); + } } } @@ -358,6 +374,7 @@ class ManifestForm extends PureComponent { return ( onChange(line.key, id, e.target.value)} diff --git a/src/components/manifest/ManifestLines.jsx b/src/components/manifest/ManifestLines.jsx index a3e9b98..146b6c6 100644 --- a/src/components/manifest/ManifestLines.jsx +++ b/src/components/manifest/ManifestLines.jsx @@ -50,10 +50,10 @@ class ManifestLines extends PureComponent { - {lines.map((line) => ( + {lines.map((line, index) => ( - - Kopfdaten - - - {basedata.date && } - {basedata.manifestNr && } - {basedata.manifestType && } - {basedata.lieferantName && } - {basedata.kLieferant != null && } - - - ); -} - -function ItemsSection({ items }) { - if (!items?.items?.length) return null; - return ( - - - Positionen ({items.items.length}) - {items.pagesFrom != null && ( - - — Seiten {items.pagesFrom}–{items.pagesTo} - - )} - - - - - - {['Pos.', 'Artikel', 'LiefArtNr', 'Menge', 'VPE', 'Einheit', 'EK-Netto', 'MwSt'].map((h) => ( - - {h} - - ))} - - - - {items.items.map((item, i) => ( - - {item.position ?? i + 1} - - {item.articleName ?? '–'} - - {item.liefArtNr ?? '–'} - {item.count} - {item.vpe ?? 1} - {item.unit ?? '–'} - - {item.eknetto != null ? item.eknetto.toFixed(2) : '–'} - - - {item.mwst != null ? `${item.mwst} %` : '–'} - - - ))} - -
-
-
- ); -} - export default function ManifestPdfDialog({ open, filename, data, onClose }) { if (!open) return null; @@ -175,7 +101,7 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) { @@ -200,7 +126,7 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) { )} - {streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyseergebnis'} + {streaming ? 'PDF wird analysiert…' : error ? 'PDF-Analyse fehlgeschlagen' : 'PDF-Analyse abgeschlossen'} {filename ? ( @@ -220,47 +146,22 @@ export default function ManifestPdfDialog({ open, filename, data, onClose }) { - - {/* Error */} - {error && ( - }> - {error} - - )} + {/* Error */} + {error && ( + } sx={{ mb: 2 }}> + {error} + + )} - {/* Live log */} - + {/* Live log */} + - {/* Notes */} - {result?.notes?.length > 0 && ( - - - Hinweise - - - {result.notes.map((note, i) => ( - - {note} - - ))} - - - )} - - {/* Basedata + Items */} - {hasResult && ( - <> - - - - - )} - - {/* No result yet and no error: show waiting state */} - {!hasResult && !error && !streaming && ( - Keine Daten extrahiert. - )} - + {/* Success notification */} + {hasResult && !streaming && !error && ( + + Daten erfolgreich in das Formular übernommen. + + )} diff --git a/src/components/manifest/index.js b/src/components/manifest/index.js index 7cb78e8..d1618cb 100644 --- a/src/components/manifest/index.js +++ b/src/components/manifest/index.js @@ -7,6 +7,7 @@ export { withManifestId } from './withManifestId'; export { manifestConfig, LINE_COLUMNS, + DELIVERY_LINE_COLUMNS, emptyLine, lineFromApi, buildManifestBody, diff --git a/src/components/manifest/manifestConfig.js b/src/components/manifest/manifestConfig.js index 9c1e7fa..201d6f9 100644 --- a/src/components/manifest/manifestConfig.js +++ b/src/components/manifest/manifestConfig.js @@ -69,17 +69,27 @@ export function emptyLine() { tax: '', rebate: '', net: '', + name: '', + pu: '', + unit: '', + netprice: '', + taxrate: '', }; } export function lineFromApi(row) { return { - key: `line-${row.id}`, + key: `line-${row.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`}`, amount: row.amount == null ? '' : String(row.amount), sku: row.sku ?? '', tax: row.tax == null ? '' : String(row.tax), rebate: row.rebate == null ? '' : String(row.rebate), net: row.net == null ? '' : String(row.net), + name: row.name ?? '', + pu: row.pu == null ? '' : String(row.pu), + unit: row.unit ?? '', + netprice: row.netprice != null ? String(row.netprice) : row.net != null ? String(row.net) : '', + taxrate: row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : '', }; } @@ -95,8 +105,8 @@ export function buildLines(lines, { requireAtLeastOne }) { for (const line of lines) { const sku = String(line.sku ?? '').trim(); const amount = parseNum(line.amount); - const net = parseNum(line.net); - const tax = parseNum(line.tax); + const net = parseNum(line.net ?? line.netprice); + const tax = parseNum(line.tax ?? line.taxrate); const rebate = parseNum(line.rebate); const blank = !sku && amount == null && net == null && tax == null && rebate == null; @@ -106,6 +116,9 @@ export function buildLines(lines, { requireAtLeastOne }) { } const row = { sku, amount, net, tax }; if (rebate != null) row.rebate = rebate; + if (line.name) row.name = line.name; + if (line.pu != null) row.pu = line.pu; + if (line.unit) row.unit = line.unit; outLines.push(row); } if (requireAtLeastOne && outLines.length === 0) { @@ -157,6 +170,15 @@ export const LINE_COLUMNS = [ { id: 'net', label: 'Net', width: 104, align: 'right', type: 'number' }, ]; +export const DELIVERY_LINE_COLUMNS = [ + { id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' }, + { id: 'name', label: 'Name', width: 220, align: 'left', type: 'text' }, + { id: 'amount', label: 'Amount', width: 88, align: 'right', type: 'number' }, + { id: 'pu', label: 'PU', width: 80, align: 'right', type: 'number' }, + { id: 'taxrate', label: 'Tax', width: 80, align: 'right', type: 'number' }, + { id: 'netprice', label: 'Net', width: 104, align: 'right', type: 'number' }, +]; + /** @typedef {'delivery'|'quote'|'order'} ManifestKind */ /** @@ -220,6 +242,7 @@ export function manifestConfig(kind) { // Delivery uses a server-allocated read-only number plus an optional // caller-supplied external reference. externalNumber: true, + columns: DELIVERY_LINE_COLUMNS, }; } if (kind === 'quote') { @@ -253,6 +276,7 @@ export function manifestConfig(kind) { listPromise: (ctx) => ctx.quotes ?? ctx.quotePromise, partiesByIdFromList: (parties) => Object.fromEntries(parties.map((row) => [row.id, row])), + columns: LINE_COLUMNS, }; } if (kind === 'order') { @@ -286,6 +310,7 @@ export function manifestConfig(kind) { listPromise: (ctx) => ctx.orders ?? ctx.orderPromise, partiesByIdFromList: (parties) => Object.fromEntries(parties.map((row) => [row.id, row])), + columns: LINE_COLUMNS, }; } throw new Error(`Unknown manifest kind: ${kind}`); diff --git a/src/pages/lieferung/neu.jsx b/src/pages/lieferung/neu.jsx index e6f886d..2ef5e95 100644 --- a/src/pages/lieferung/neu.jsx +++ b/src/pages/lieferung/neu.jsx @@ -1,10 +1,472 @@ import { PureComponent } from 'react'; -import { ManifestForm } from '../../components/manifest'; +import { connect } from 'react-redux'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import CircularProgress from '@mui/material/CircularProgress'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import { enqueueSnackbar } from 'notistack'; +import { PanelTile, PanelTileGrid } from 'dashgui/panels'; +import BackendError from '../../components/BackendError'; +import withNavigate from '../../components/withNavigate'; +import { + ManifestHeader, + ManifestLines, + DELIVERY_LINE_COLUMNS, + buildManifestBody, + manifestConfig, +} from '../../components/manifest'; +import ManifestPdfDialog from '../../components/manifest/ManifestPdfDialog'; +import { loadParties } from '../../components/manifest/manifestConfig'; +import { StorageManagerContext } from '../../api/storageManagerContext'; +import { requireDefined, requireObject } from '../../utils/apiShape'; +import { backendErrorFrom } from '../../utils/backendError'; +import { notifyError } from '../../utils/notify'; +import { + setDeliveryEntities, + setDate, + setDeliveryEntity, + setNumber, + setExternalNumber, + setType, + setLineitems, + setNotes, + addLineitem, + updateLineitem, + removeLineitem, + resetDelivery, + selectDeliveryEntities, + selectDeliveryDate, + selectDeliveryEntity, + selectDeliveryNumber, + selectDeliveryExternalNumber, + selectDeliveryLineitems, + selectDeliveryNotes, +} from '../../store/deliveryEditorSlice'; class LieferungNeuPage extends PureComponent { + static contextType = StorageManagerContext; + + constructor(props) { + super(props); + this.cfg = manifestConfig('delivery'); + this.state = { + status: 'loadingParties', + error: null, + partiesLoading: true, + saving: false, + pdfUploading: false, + pdfDialogOpen: false, + pdfDialogData: null, + pdfFileName: '', + }; + this._cancelled = false; + this.handleRetry = this.handleRetry.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.handleAddLine = this.handleAddLine.bind(this); + this.handleRemoveLine = this.handleRemoveLine.bind(this); + this.handleSave = this.handleSave.bind(this); + this.handlePdfUpload = this.handlePdfUpload.bind(this); + this.handleClosePdfDialog = this.handleClosePdfDialog.bind(this); + } + + componentDidMount() { + this._cancelled = false; + void this.loadPartiesOnly(); + } + + componentWillUnmount() { + this._cancelled = true; + } + + async loadPartiesOnly() { + this.setState({ status: 'loadingParties', error: null, partiesLoading: true }); + try { + const parties = await loadParties('delivery', this.context); + if (this._cancelled) return; + this.props.setDeliveryEntities(parties); + this.setState({ + status: 'ready', + partiesLoading: false, + }); + } catch (err) { + if (this._cancelled) return; + this.setState({ + status: 'failed', + error: backendErrorFrom(err), + partiesLoading: false, + }); + notifyError(err); + } + } + + handleRetry() { + void this.loadPartiesOnly(); + } + + handlePartyChange(partyId) { + this.props.setDeliveryEntity(partyId); + } + + handleDateChange(date) { + this.props.setDate(date); + } + + handleNumberChange(number) { + this.props.setNumber(number); + } + + handleExternalNumberChange(externalNumber) { + this.props.setExternalNumber(externalNumber); + } + + handleLineChange(key, field, value) { + const index = typeof key === 'number' ? key : Number(key); + this.props.updateLineitem({ + index, + [field]: value, + ...(field === 'net' ? { netprice: value } : {}), + ...(field === 'tax' ? { taxrate: value } : {}), + }); + } + + handleAddLine() { + this.props.addLineitem(); + } + + handleRemoveLine(key) { + const index = typeof key === 'number' ? key : Number(key); + this.props.removeLineitem(index); + } + + async handlePdfUpload(file) { + if (!file || this.state.pdfUploading) return; + this.setState({ + pdfUploading: true, + pdfDialogOpen: true, + pdfFileName: file.name, + pdfDialogData: { streaming: true, messages: [], result: null, error: null }, + }); + + const appendMessage = (msg) => { + if (this._cancelled) return; + this.setState((prev) => ({ + pdfDialogData: { + ...prev.pdfDialogData, + messages: [...(prev.pdfDialogData?.messages ?? []), msg], + }, + })); + }; + + try { + 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; + 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) => ({ + 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: (l.taxrate ?? l.tax ?? l.mwst) != null ? String(l.taxrate ?? l.tax ?? l.mwst) : '', + netprice: (l.netprice ?? l.net ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : null)) != null + ? String(l.netprice ?? l.net ?? (l.eknetto != null ? (l.eknetto / 100).toFixed(2) : '')) + : '', + })) + : 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 : []; + + // If supplierId is null, try matching supplier by name from loaded delivery entities + if (supplierId == null && result?.basedata?.lieferantName && Array.isArray(this.props.deliveryEntities)) { + const nameToMatch = result.basedata.lieferantName.trim().toLowerCase(); + const matched = this.props.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; + } + + if (docDate) this.props.setDate(docDate); + if (supplierId != null) this.props.setDeliveryEntity(supplierId); + if (extNum != null) { + this.props.setExternalNumber(String(extNum)); + } + if (docType) this.props.setType(docType); + if (newLines && newLines.length > 0) { + this.props.setLineitems(newLines); + } + if (notes.length > 0) { + this.props.setNotes(notes); + } + break; + } + case 'error': + this.setState((prev) => ({ + pdfDialogData: { + ...prev.pdfDialogData, + streaming: false, + error: data.message ?? 'PDF-Analyse fehlgeschlagen.', + }, + })); + break; + default: + break; + } + } + } catch (err) { + if (this._cancelled) return; + this.setState((prev) => ({ + pdfDialogData: { + ...prev.pdfDialogData, + streaming: false, + error: err.message ?? 'Netzwerkfehler bei PDF-Analyse.', + }, + })); + } finally { + if (!this._cancelled) { + this.setState((prev) => ({ + pdfUploading: false, + pdfDialogData: prev.pdfDialogData + ? { ...prev.pdfDialogData, streaming: false } + : null, + })); + } + } + } + + handleClosePdfDialog() { + this.setState({ pdfDialogOpen: false }); + } + + async handleSave() { + const cfg = this.cfg; + if (this.state.saving) return; + + let body; + try { + body = buildManifestBody( + { + number: this.props.number ?? '', + externalNumber: this.props.externalNumber ?? '', + date: this.props.date ?? '', + partyId: this.props.partyId, + lines: this.props.lines, + }, + { + partyField: cfg.partyField, + requireAtLeastOne: true, + externalNumber: cfg.externalNumber, + }, + ); + } catch (err) { + notifyError(err); + return; + } + + this.setState({ saving: true }); + try { + const data = await this.context[cfg.api].create(body); + requireObject(data, cfg.postPathLabel); + requireObject(data[cfg.docField], cfg.docField); + requireDefined(data[cfg.docField].id, `${cfg.docField}.id`); + await this.context[cfg.updateCache](); + if (this._cancelled) return; + enqueueSnackbar(cfg.createdMsg, { variant: 'success' }); + this.props.resetDelivery(); + this.props.navigate(cfg.editPath(data[cfg.docField].id)); + } catch (err) { + if (this._cancelled) return; + notifyError(err); + this.setState({ saving: false }); + } + } + render() { - return ; + const cfg = this.cfg; + const { + status, + error, + partiesLoading, + saving, + pdfUploading, + pdfDialogOpen, + pdfDialogData, + pdfFileName, + } = this.state; + const { + deliveryEntities, + date, + partyId, + number, + externalNumber, + lines, + notes, + } = this.props; + + if (status === 'loadingParties') { + return ( + + + + ); + } + + if (status === 'failed') { + return ; + } + + return ( + <> + + + {partyId == null && (!lines || lines.length === 0) ? ( + + + {cfg.partyGateMessage} + + + ) : ( + <> + {partyId == null && lines && lines.length > 0 && ( + + + Bitte wählen Sie oben einen Lieferanten aus, um die erfassten Positionen speichern zu können. + + + )} + void this.handleSave()} + /> + + )} + {notes && notes.length > 0 && ( + + + Hinweise zur PDF-Analyse ({notes.length}) + + + {notes.map((note, i) => ( + + {note} + + ))} + + + )} + + + + ); } } -export default LieferungNeuPage; +const mapStateToProps = (state) => ({ + deliveryEntities: selectDeliveryEntities(state), + date: selectDeliveryDate(state), + partyId: selectDeliveryEntity(state), + number: selectDeliveryNumber(state), + externalNumber: selectDeliveryExternalNumber(state), + lines: selectDeliveryLineitems(state), + notes: selectDeliveryNotes(state), +}); + +const mapDispatchToProps = { + setDeliveryEntities, + setDate, + setDeliveryEntity, + setNumber, + setExternalNumber, + setType, + setLineitems, + setNotes, + addLineitem, + updateLineitem, + removeLineitem, + resetDelivery, +}; + +export default connect( + mapStateToProps, + mapDispatchToProps, +)(withNavigate(LieferungNeuPage)); diff --git a/src/store/deliveryEditorSlice.js b/src/store/deliveryEditorSlice.js new file mode 100644 index 0000000..8c1569b --- /dev/null +++ b/src/store/deliveryEditorSlice.js @@ -0,0 +1,127 @@ +import { createSlice } from '@reduxjs/toolkit'; + + +export function createEmptyLineitem(line = {}) { + return { + name: line.name ?? null, + sku: line.sku ?? null, + amount: line.amount ?? null, + pu: line.pu ?? null, + unit: line.unit ?? null, + netprice: line.netprice ?? null, + taxrate: line.taxrate ?? null, + }; +} + +const initialDelivery = { + date: null, + deliveryEntity: null, + external_number: null, + number: null, + type: null, + lineitems: [], + notes: [], +}; + +const initialState = { + // system now delivery entity list + deliveryEntities: [], + // delivery being edited + delivery: initialDelivery, +}; + +const deliveryEditorSlice = createSlice({ + name: 'deliveryEditor', + initialState, + reducers: { + setDeliveryEntities(state, action) { + state.deliveryEntities = action.payload ?? []; + }, + setDelivery(state, action) { + state.delivery = { + ...initialDelivery, + ...action.payload, + }; + }, + setDate(state, action) { + state.delivery.date = action.payload; + }, + setDeliveryEntity(state, action) { + state.delivery.deliveryEntity = action.payload; + }, + setExternalNumber(state, action) { + state.delivery.external_number = action.payload; + }, + setNumber(state, action) { + state.delivery.number = action.payload; + }, + setType(state, action) { + state.delivery.type = action.payload; + }, + setLineitems(state, action) { + state.delivery.lineitems = action.payload ?? []; + }, + setNotes(state, action) { + state.delivery.notes = Array.isArray(action.payload) + ? action.payload + : action.payload ? [action.payload] : []; + }, + addLineitem(state, action) { + state.delivery.lineitems.push(createEmptyLineitem(action.payload)); + }, + updateLineitem(state, action) { + const { index, ...fields } = action.payload; + if (index >= 0 && index < state.delivery.lineitems.length) { + state.delivery.lineitems[index] = { + ...state.delivery.lineitems[index], + ...fields, + }; + } + }, + removeLineitem(state, action) { + const index = action.payload; + if (typeof index === 'number' && index >= 0 && index < state.delivery.lineitems.length) { + state.delivery.lineitems.splice(index, 1); + } + }, + resetDelivery(state) { + state.delivery = { ...initialDelivery }; + }, + resetDeliveryEditor() { + return { + ...initialState, + delivery: { ...initialDelivery }, + }; + }, + }, +}); + +export const { + setDeliveryEntities, + setDelivery, + setDate, + setDeliveryEntity, + setExternalNumber, + setNumber, + setType, + setLineitems, + setNotes, + addLineitem, + updateLineitem, + removeLineitem, + resetDelivery, + resetDeliveryEditor, +} = deliveryEditorSlice.actions; + +// Selectors +export const selectDeliveryEntities = (state) => state.deliveryEditor.deliveryEntities; +export const selectDelivery = (state) => state.deliveryEditor.delivery; +export const selectDeliveryDate = (state) => state.deliveryEditor.delivery.date; +export const selectDeliveryEntity = (state) => state.deliveryEditor.delivery.deliveryEntity; +export const selectDeliveryExternalNumber = (state) => state.deliveryEditor.delivery.external_number; +export const selectDeliveryNumber = (state) => state.deliveryEditor.delivery.number; +export const selectDeliveryType = (state) => state.deliveryEditor.delivery.type; +export const selectDeliveryLineitems = (state) => state.deliveryEditor.delivery.lineitems; +export const selectDeliveryNotes = (state) => state.deliveryEditor.delivery.notes; + +export default deliveryEditorSlice.reducer; diff --git a/src/store/index.js b/src/store/index.js index 3b358cf..a0e78f4 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,6 +5,7 @@ import categoryReducer from './categorySlice'; import companyReducer, { companyLoadListener } from './companySlice'; import posterminalReducer, { posterminalLoadListener } from './posterminalSlice'; import unitReducer, { unitLoadListener } from './unitSlice'; +import deliveryEditorReducer from './deliveryEditorSlice'; const listenerMiddleware = createListenerMiddleware(); companyLoadListener(listenerMiddleware); @@ -52,6 +53,7 @@ export const store = configureStore({ brand: brandReducer, posterminal: posterminalReducer, unit: unitReducer, + deliveryEditor: deliveryEditorReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware()