/** * @file deliveryEditorAutomat.js * @description State engine for inbound goods receipt creation and invoice * reconciliation (lineitems, attachments, PDF/receipt OCR, and supplier article matching). * * Upstream Triggers: * - UI / Delivery Editor: User actions in `/lieferung/neu` or `/lieferung/:id` * (`addLine`, `updateLine`, `removeLine`, `fetchSupplierArticles`, * `addAttachment`, `resetDelivery`, etc.). * * Downstream Triggers: * - Navigation badges in `src/nav.js` subscribe to draft line counts. */ import { Automat } from 'react-automat'; import { article as articleApi, supplier as supplierApi } from '../api'; import { todayInputValue } from '../components/manifest/manifestConfig'; /** * @typedef {Object} DeliveryLine * @property {string|number} key Stable editor key. * @property {string|null} name Article name. * @property {string|number|null} sku Supplier or internal article SKU. * @property {number|null} amount Ordered amount. * @property {number|null} pu Price unit. * @property {string|null} unit Unit of measure. * @property {number|null} netprice Net price. * @property {number|null} taxrate Tax rate. */ /** * @typedef {Object} SupplierArticle * @property {string|number} [id] Supplier-article identifier. * @property {string|number|null} [sku] Supplier SKU. * @property {{sku?: string|number|null, eans?: Array, articleean?: Array}} [article] * Linked internal article. */ /** * @typedef {Object} DeliveryEditorState * @property {Array} deliveryEntities Available suppliers or delivery parties. * @property {string|number|null} id Delivery identifier. * @property {string|number|null} deliveryId Alias of `id`. * @property {string|null} date Delivery date. * @property {string|number|null} deliveryEntity Selected delivery party. * @property {string|number|null} partyId Alias of `deliveryEntity`. * @property {string|null} externalNumber External delivery number. * @property {string|null} external_number API-compatible alias of `externalNumber`. * @property {string|null} number Internal delivery number. * @property {string|null} type Delivery type. * @property {Array} lines Editable lines. * @property {Array} lineitems Alias of `lines`. * @property {Array<*>} notes Delivery notes. * @property {Array} attachments Delivery attachments. * @property {Array} files Alias of `attachments`. * @property {Array} supplierArticles Loaded supplier articles. * @property {Object} supplierArticlesMap Articles indexed by SKU or EAN. * @property {Object} supplierArticlesByLineKey Matched article per line. * @property {boolean} supplierArticlesLoading Whether any supplier article request is pending. * @property {Object} loadingLineKeys Pending lookup state per line. * @property {Object} busySupplierArticleLineKeys Lines with a pending unlink request. * @property {string|null} supplierArticlesError Last supplier article error. * @property {string|null} unmatchedSupplierName Parsed supplier name without a matching entity. */ function parseNum(value) { if (value == null || value === '') return null; const cleaned = String(value).trim().replace('%', '').trim().replace(',', '.'); if (cleaned === '') return null; const n = Number(cleaned); return Number.isFinite(n) ? n : null; } /** * Normalizes a partial line into an editor line with a stable key. * * @param {Partial} [line] Initial line values. * @returns {DeliveryLine} Normalized line. */ export function createEmptyLineitem(line = {}) { let netVal = null; if (line.netprice != null || line.net != null || line.netPrice != null) { const netRaw = line.netprice ?? line.net ?? line.netPrice; netVal = netRaw != null ? (parseNum(netRaw) ?? netRaw) : null; } else if (line.eknetto != null && line.eknetto !== '') { const parsedCent = parseNum(line.eknetto); netVal = parsedCent != null ? Math.round((parsedCent / 100) * 10000) / 10000 : null; } const taxRaw = line.taxrate ?? line.tax ?? line.mwst ?? line.taxRate ?? null; const amountRaw = line.amount ?? line.count ?? line.qty ?? line.quantity ?? null; const puRaw = line.pu ?? line.vpe ?? null; const taxVal = taxRaw != null ? (parseNum(taxRaw) ?? taxRaw) : null; const amountVal = amountRaw != null ? (parseNum(amountRaw) ?? amountRaw) : null; const puVal = puRaw != null ? (parseNum(puRaw) ?? puRaw) : null; return { key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, name: line.name ?? line.articleName ?? line.title ?? null, sku: line.sku ?? line.liefArtNr ?? line.articleNumber ?? null, amount: amountVal, pu: puVal, unit: line.unit ?? null, netprice: netVal, net: netVal, taxrate: taxVal, tax: taxVal, }; } /** * Indexes supplier articles by supplier SKU, internal SKU, and EAN. Text keys * are stored both verbatim and lower-cased for case-insensitive lookup. * * @param {Array} [articles] Articles to index. * @param {Object} [existingMap] Existing index to extend. * @returns {Object} Article lookup map. */ export function buildSupplierArticlesMap(articles = [], existingMap = {}) { const map = { ...existingMap }; for (const item of articles) { if (!item) continue; // Map by supplier SKU if (item.sku != null) { const skuStr = String(item.sku).trim(); if (skuStr) { map[skuStr] = item; map[skuStr.toLowerCase()] = item; } } // Map by internal article SKU if (item.article?.sku != null) { const artSku = String(item.article.sku).trim(); if (artSku) { map[artSku] = item; map[artSku.toLowerCase()] = item; } } // Map by Eans const eans = item.article?.eans ?? item.article?.articleean ?? []; for (const ean of eans) { if (ean != null) { const eanStr = String(ean).trim(); if (eanStr) { map[eanStr] = item; map[eanStr.toLowerCase()] = item; } } } } return map; } /** @type {Omit} */ export const initialDelivery = { id: null, deliveryId: null, date: todayInputValue(), deliveryEntity: null, partyId: null, externalNumber: null, external_number: null, number: null, type: null, lines: [], lineitems: [], notes: [], attachments: [], files: [], supplierArticles: [], supplierArticlesMap: {}, supplierArticlesByLineKey: {}, supplierArticlesLoading: false, loadingLineKeys: {}, busySupplierArticleLineKeys: {}, supplierArticlesError: null, unmatchedSupplierName: null, }; /** @type {DeliveryEditorState} */ export const initialState = { deliveryEntities: [], ...initialDelivery, }; /** * Creates a delivery editor automat or returns the already registered named * instance. Named instances allow independent state for multiple deliveries. * * @param {{name?: string|null}|string} [options] Automat options or name. * @returns {Automat} Delivery editor automat. */ export function createDeliveryEditorAutomat(options = {}) { const name = typeof options === 'string' ? options : options?.name ?? null; if (name) { const existing = Automat.get(name); if (existing) return existing; } let automat; const actions = { /** @param {string|number|null} id @returns {void} */ setId(id) { automat.setState({ id, deliveryId: id }); }, /** @param {Array} deliveryEntities @returns {void} */ setDeliveryEntities(deliveryEntities) { automat.setState({ deliveryEntities: deliveryEntities ?? [] }); }, /** * Replaces the edited delivery and normalizes API aliases. * @param {Partial} [delivery] * @returns {void} */ setDelivery(delivery = {}) { const rawLines = delivery.lines ?? delivery.lineitems ?? []; const lines = Array.isArray(rawLines) ? rawLines : []; const partyId = delivery.partyId ?? delivery.deliveryEntity ?? null; const extNum = delivery.externalNumber ?? delivery.external_number ?? null; const id = delivery.id ?? delivery.deliveryId ?? null; const notes = Array.isArray(delivery.notes) ? delivery.notes : delivery.notes ? [delivery.notes] : []; const rawAttachments = delivery.attachments ?? delivery.files ?? []; const attachments = Array.isArray(rawAttachments) ? rawAttachments : []; const date = delivery.date !== undefined ? delivery.date : (id != null ? null : (automat.state.date || todayInputValue())); automat.setState({ ...initialDelivery, ...delivery, id, deliveryId: id, date, partyId, deliveryEntity: partyId, externalNumber: extNum, external_number: extNum, lines, lineitems: lines, notes, attachments, files: attachments, }); if (partyId && lines.length > 0) { automat.actions.fetchSupplierArticles(partyId, lines); } }, /** @param {string|null} date @returns {void} */ setDate(date) { automat.setState({ date }); }, /** * Selects a delivery party and refreshes its supplier articles. * @param {string|number|null} entityId * @returns {void} */ setDeliveryEntity(entityId) { const prevId = automat.state.deliveryEntity ?? automat.state.partyId; const idChanged = prevId !== entityId; automat.setState({ deliveryEntity: entityId, partyId: entityId, unmatchedSupplierName: entityId != null ? null : automat.state.unmatchedSupplierName, ...(idChanged ? { supplierArticles: [], supplierArticlesMap: {}, supplierArticlesByLineKey: {}, loadingLineKeys: {}, } : {}), }); if (entityId && idChanged) { automat.actions.fetchSupplierArticles(entityId); } }, /** @param {string|null} name @returns {void} */ setUnmatchedSupplierName(name) { automat.setState({ unmatchedSupplierName: name ?? null }); }, /** @param {string|number|null} partyId @returns {void} */ setPartyId(partyId) { automat.actions.setDeliveryEntity(partyId); }, /** @param {string|null} externalNumber @returns {void} */ setExternalNumber(externalNumber) { automat.setState({ externalNumber, external_number: externalNumber, }); }, /** @param {string|null} number @returns {void} */ setNumber(number) { automat.setState({ number }); }, /** @param {string|null} type @returns {void} */ setType(type) { automat.setState({ type }); }, /** @param {Array} lines @returns {void} */ setLines(lines) { const cleanLines = lines ?? []; automat.setState({ lines: cleanLines, lineitems: cleanLines }); }, /** @param {Array} lines @returns {void} */ setLineitems(lines) { const cleanLines = lines ?? []; automat.setState({ lines: cleanLines, lineitems: cleanLines }); }, /** @param {Array<*>|*} notes @returns {void} */ setNotes(notes) { const cleanNotes = Array.isArray(notes) ? notes : notes ? [notes] : []; automat.setState({ notes: cleanNotes }); }, /** @param {Partial} [line] @returns {void} */ addLineitem(line = {}) { const item = createEmptyLineitem(line); const nextLines = [...(automat.state.lines ?? []), item]; automat.setState({ lines: nextLines, lineitems: nextLines }); }, /** @param {Partial} [line] @returns {void} */ addLine(line = {}) { automat.actions.addLineitem(line); }, /** * Updates a line selected by index or key. * @param {{index?: number, key?: string|number, [key: string]: *}} payload * @returns {void} */ updateLineitem(payload) { const { index, key, ...fields } = payload || {}; const currentLines = automat.state.lines ?? []; let targetIndex = typeof index === 'number' && !Number.isNaN(index) ? index : -1; if (targetIndex === -1 && key != null) { targetIndex = currentLines.findIndex((l) => l.key === key); } if (targetIndex >= 0 && targetIndex < currentLines.length) { const nextLines = [...currentLines]; nextLines[targetIndex] = { ...nextLines[targetIndex], ...fields, }; automat.setState({ lines: nextLines, lineitems: nextLines }); } }, /** @param {{index?: number, key?: string|number, [key: string]: *}} payload @returns {void} */ updateLine(payload) { automat.actions.updateLineitem(payload); }, /** @param {number|string|{key: string|number}} payload @returns {void} */ removeLineitem(payload) { const currentLines = automat.state.lines ?? []; let index = typeof payload === 'number' && !Number.isNaN(payload) ? payload : -1; let targetKey = null; if (index === -1 && payload != null) { const key = typeof payload === 'string' ? payload : payload?.key; if (key != null) { index = currentLines.findIndex((l) => l.key === key); targetKey = key; } } if (index >= 0 && index < currentLines.length) { targetKey = targetKey ?? currentLines[index]?.key; const nextLines = currentLines.filter((_, i) => i !== index); const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) }; if (targetKey != null) delete byLineKey[targetKey]; automat.setState({ lines: nextLines, lineitems: nextLines, supplierArticlesByLineKey: byLineKey, }); } }, /** @param {number|string|{key: string|number}} payload @returns {void} */ removeLine(payload) { automat.actions.removeLineitem(payload); }, /** * Moves a lineitem from one index to another. * @param {number} fromIndex Source index. * @param {number} toIndex Target index. * @returns {void} */ moveLineitem(fromIndex, toIndex) { const currentLines = automat.state.lines ?? []; if (fromIndex < 0 || fromIndex >= currentLines.length || toIndex < 0 || toIndex >= currentLines.length) return; const nextLines = [...currentLines]; const [item] = nextLines.splice(fromIndex, 1); nextLines.splice(toIndex, 0, item); automat.setState({ lines: nextLines, lineitems: nextLines, }); }, /** Clears all lines from the editor. @returns {void} */ clearLineitems() { automat.setState({ lines: [], lineitems: [], supplierArticlesByLineKey: {}, }); }, /** Clears all lines from the editor. @returns {void} */ clearLines() { automat.actions.clearLineitems(); }, /** * Loads supplier articles for all current SKUs and rebuilds line matches. * @param {string|number|null} [targetEntityId] Supplier to query. * @param {Array} [targetLines] Lines whose SKUs should be queried. * @returns {Promise} */ async fetchSupplierArticles(targetEntityId, targetLines) { const entityId = targetEntityId ?? automat.state.deliveryEntity ?? automat.state.partyId; if (!entityId) { automat.setState({ supplierArticles: [], supplierArticlesMap: {}, supplierArticlesByLineKey: {}, supplierArticlesLoading: false, loadingLineKeys: {}, }); return; } const lines = targetLines ?? automat.state.lines ?? []; const skus = Array.from( new Set( lines .map((l) => (l.sku != null ? String(l.sku).trim() : '')) .filter(Boolean), ), ); automat.setState({ supplierArticlesLoading: true, supplierArticlesError: null, }); try { const res = await supplierApi.getArticles(entityId); const articles = Array.isArray(res?.articles) ? res.articles : []; const map = buildSupplierArticlesMap(articles); const byLineKey = {}; for (const line of lines) { if (line.key != null) { const rawSku = line.sku != null ? String(line.sku).trim() : ''; byLineKey[line.key] = rawSku ? (map[rawSku.toLowerCase()] || map[rawSku] || null) : null; } } automat.setState({ supplierArticles: articles, supplierArticlesMap: map, supplierArticlesByLineKey: byLineKey, supplierArticlesLoading: false, loadingLineKeys: {}, }); } catch (err) { console.error('Fehler beim Abrufen der Lieferanten-Artikel:', err); automat.setState({ supplierArticlesLoading: false, supplierArticlesError: err?.message || 'Fehler beim Laden der Artikel', }); } }, /** * Loads and matches a supplier article for one line SKU. * @param {string|number|null} sku SKU to query. * @param {string|number} [lineKey] Editor line key receiving the match. * @returns {Promise} */ async fetchSupplierArticleForSku(sku, lineKey) { const entityId = automat.state.deliveryEntity ?? automat.state.partyId; const trimmed = sku != null ? String(sku).trim() : ''; if (!entityId || !trimmed) { if (lineKey != null) { const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) }; byLineKey[lineKey] = null; automat.setState({ supplierArticlesByLineKey: byLineKey }); } return; } const prevLoading = automat.state.loadingLineKeys || {}; automat.setState({ loadingLineKeys: lineKey != null ? { ...prevLoading, [lineKey]: true } : prevLoading, }); try { const res = await supplierApi.getArticles(entityId, [trimmed]); const articles = Array.isArray(res?.articles) ? res.articles : []; const nextMap = buildSupplierArticlesMap(articles, automat.state.supplierArticlesMap); const matched = nextMap[trimmed.toLowerCase()] || nextMap[trimmed] || articles[0] || null; const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) }; if (lineKey != null) { byLineKey[lineKey] = matched; } const currentList = automat.state.supplierArticles || []; const mergedList = [...currentList]; for (const a of articles) { if (!mergedList.some((existing) => existing.id === a.id)) { mergedList.push(a); } } const nextLoading = { ...(automat.state.loadingLineKeys || {}) }; if (lineKey != null) delete nextLoading[lineKey]; automat.setState({ supplierArticles: mergedList, supplierArticlesMap: nextMap, supplierArticlesByLineKey: byLineKey, loadingLineKeys: nextLoading, supplierArticlesLoading: Object.keys(nextLoading).length > 0, }); } catch (err) { console.error('Fehler beim Abrufen des Artikels für SKU:', err); const nextLoading = { ...(automat.state.loadingLineKeys || {}) }; if (lineKey != null) delete nextLoading[lineKey]; automat.setState({ loadingLineKeys: nextLoading, supplierArticlesLoading: Object.keys(nextLoading).length > 0, }); } }, /** * Removes the supplier-article assignment behind a matched line by * `DELETE /api/article/{sku}/supplier/{id}`, then re-resolves the line. * * @param {string|number} internalSku Internal article SKU. * @param {string|number} assignmentId Article-supplier assignment ID. * @param {string|number} [lineKey] Editor line key showing the match. * @returns {Promise} */ async unlinkSupplierArticle(internalSku, assignmentId, lineKey) { const entityId = automat.state.deliveryEntity ?? automat.state.partyId; const sku = internalSku != null ? String(internalSku).trim() : ''; if (!sku || assignmentId == null) { throw new Error('Artikelsku und Zuordnungs-ID sind erforderlich.'); } if (lineKey != null) { automat.setState({ busySupplierArticleLineKeys: { ...(automat.state.busySupplierArticleLineKeys || {}), [lineKey]: true, }, }); } try { await articleApi.deleteSupplier(sku, assignmentId); const clearedMap = { ...(automat.state.supplierArticlesMap || {}) }; for (const [mapKey, item] of Object.entries(clearedMap)) { if (item != null && String(item.id) === String(assignmentId)) { delete clearedMap[mapKey]; } } const nextList = (automat.state.supplierArticles || []).filter( (item) => item != null && String(item.id) !== String(assignmentId), ); const byLineKey = { ...(automat.state.supplierArticlesByLineKey || {}) }; if (lineKey != null) byLineKey[lineKey] = null; automat.setState({ supplierArticles: nextList, supplierArticlesMap: clearedMap, supplierArticlesByLineKey: byLineKey, }); if (lineKey != null && entityId) { const line = (automat.state.lines || []).find((l) => l.key === lineKey); const rawSku = line?.sku != null ? String(line.sku).trim() : ''; if (rawSku) { await automat.actions.fetchSupplierArticleForSku(rawSku, lineKey); } } } finally { if (lineKey != null) { const nextBusy = { ...(automat.state.busySupplierArticleLineKeys || {}) }; delete nextBusy[lineKey]; automat.setState({ busySupplierArticleLineKeys: nextBusy }); } } }, /** @param {Object} attachment @returns {void} */ addAttachment(attachment) { const id = attachment.id ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const item = { ...attachment, id }; const next = [...(automat.state.attachments ?? []), item]; automat.setState({ attachments: next, files: next }); }, /** @param {Array} attachments @returns {void} */ setAttachments(attachments) { const clean = Array.isArray(attachments) ? attachments : []; automat.setState({ attachments: clean, files: clean }); }, /** @param {{id: string|number, [key: string]: *}} payload @returns {void} */ updateAttachment(payload) { const { id, ...fields } = payload || {}; const current = automat.state.attachments ?? []; const idx = current.findIndex((a) => a.id === id || String(a.id) === String(id)); if (idx !== -1) { const next = [...current]; next[idx] = { ...next[idx], ...fields }; automat.setState({ attachments: next, files: next }); } }, /** @param {string|number|{id?: string|number, fileId?: string|number}} idOrPayload @returns {void} */ removeAttachment(idOrPayload) { const current = automat.state.attachments ?? []; const targetId = typeof idOrPayload === 'object' && idOrPayload !== null ? (idOrPayload.id ?? idOrPayload.fileId) : idOrPayload; const hasMatch = current.some((a) => a.id === targetId || String(a.id) === String(targetId)); let next; if (hasMatch) { next = current.filter((a) => a.id !== targetId && String(a.id) !== String(targetId)); } else if (typeof idOrPayload === 'number' && idOrPayload >= 0 && idOrPayload < current.length) { next = current.filter((_, idx) => idx !== idOrPayload); } else { next = current.filter((a) => a.id !== targetId && String(a.id) !== String(targetId)); } automat.setState({ attachments: next, files: next }); }, /** Resets delivery fields while retaining the available entities. @returns {void} */ resetDelivery() { automat.setState({ ...initialDelivery, date: todayInputValue(), }); }, /** Resets the complete editor state. @returns {void} */ resetDeliveryEditor() { automat.setState({ ...initialState, date: todayInputValue(), }); }, /** * Loads a delivery manifest (e.g. from draft_delivery_manifest tool) into the editor. * @param {Object} manifestData * @returns {void} */ loadDraftManifest(manifestData) { if (!manifestData || typeof manifestData !== 'object') return; const data = manifestData.manifest ?? manifestData; const basedata = data.basedata ?? data.baseData ?? data; const rawItems = data.items?.items ?? data.items ?? data.lines ?? data.lineitems ?? []; const itemsList = Array.isArray(rawItems) ? rawItems : []; const partyId = basedata.lieferant_id ?? basedata.lieferantId ?? basedata.partyId ?? basedata.deliveryEntity ?? basedata.supplier_id ?? basedata.supplierId ?? null; const supplierName = basedata.lieferantName ?? basedata.supplierName ?? basedata.unmatchedSupplierName ?? null; const extNum = basedata.manifestNr ?? basedata.manifestNumber ?? basedata.externalNumber ?? basedata.external_number ?? basedata.invoiceNumber ?? basedata.invoice_number ?? null; const date = basedata.date ?? automat.state.date ?? todayInputValue(); const type = basedata.manifestType ?? basedata.type ?? 'rechnung'; let matchedPartyId = partyId != null && partyId !== '' ? partyId : null; if (!matchedPartyId && supplierName && automat.state.deliveryEntities?.length > 0) { const normName = String(supplierName).trim().toLowerCase(); const found = automat.state.deliveryEntities.find( (e) => (e.name && String(e.name).trim().toLowerCase() === normName) || (e.company && String(e.company).trim().toLowerCase() === normName) ); if (found?.id != null) { matchedPartyId = found.id; } } const lines = itemsList.map((item, idx) => createEmptyLineitem({ ...item, key: item.key ?? `manifest-${Date.now()}-${idx}-${Math.random().toString(36).slice(2, 6)}`, }) ); // Preserve already attached files in the editor so loading a draft manifest does not wipe them out. const currentAttachments = automat.state.attachments?.length > 0 ? automat.state.attachments : (Array.isArray(automat.state.files) && automat.state.files.length > 0 ? automat.state.files : []); const rawManifestAttachments = data.attachments ?? data.files ?? basedata.attachments ?? basedata.files ?? null; const manifestAttachments = Array.isArray(rawManifestAttachments) ? rawManifestAttachments : []; let attachments = currentAttachments; if (currentAttachments.length === 0) { attachments = manifestAttachments; } else if (manifestAttachments.length > 0) { const existingKeys = new Set( currentAttachments.flatMap((a) => [ a.id != null ? String(a.id) : null, a.name ? String(a.name).trim().toLowerCase() : null, ]).filter(Boolean) ); const newAttachments = manifestAttachments.filter((a) => { const idKey = a.id != null ? String(a.id) : null; const nameKey = a.name ? String(a.name).trim().toLowerCase() : null; if (idKey && existingKeys.has(idKey)) return false; if (nameKey && existingKeys.has(nameKey)) return false; return true; }); if (newAttachments.length > 0) { attachments = [...currentAttachments, ...newAttachments]; } } automat.actions.setDelivery({ id: null, deliveryId: null, date, partyId: matchedPartyId, deliveryEntity: matchedPartyId, unmatchedSupplierName: matchedPartyId != null ? null : supplierName, externalNumber: extNum, external_number: extNum, type, lines, lineitems: lines, attachments, files: attachments, }); }, /** Serializes the delivery into a VFS draft manifest format. @returns {object} */ getManifest() { const state = automat.state; const rawLines = state.lines ?? state.lineitems ?? []; const lines = rawLines .filter((l) => Boolean(l && (l.sku || l.name || l.amount != null || l.netprice != null || l.net != null))) .map((l) => ({ sku: l.sku || null, name: l.name || null, amount: l.amount != null ? Number(l.amount) : null, pu: l.pu != null ? Number(l.pu) : 1, unit: l.unit || null, netprice: l.netprice != null ? Number(l.netprice) : (l.net != null ? Number(l.net) : null), taxrate: l.taxrate != null ? Number(l.taxrate) : (l.tax != null ? Number(l.tax) : null), })); return { manifest: { kind: 'delivery', id: null, date: state.date || null, partyId: state.partyId ?? state.deliveryEntity ?? null, externalNumber: state.externalNumber ?? state.external_number ?? null, lines, items: lines, notes: state.notes ?? [], attachments: (state.attachments ?? []).map((a) => ({ name: a.name, type: a.type })), }, }; }, }; automat = new Automat(initialState, actions, { name, persist: false, }); return automat; } /** Shared automat used for new, unsaved deliveries. */ export const deliveryDraftAutomat = createDeliveryEditorAutomat({ name: 'delivery-draft' }); /** Backward-compatible alias of {@link deliveryDraftAutomat}. */ export const deliveryEditorAutomat = deliveryDraftAutomat; /** * Returns the draft automat or the named automat belonging to a saved delivery. * * @param {string|number|null} docId Delivery identifier; `null` selects the draft. * @returns {Automat} Delivery-specific editor automat. */ export function getDeliveryEditorAutomat(docId) { if (docId == null) { return deliveryDraftAutomat; } const name = `delivery-${docId}`; let automat = Automat.get(name); if (!automat) { automat = createDeliveryEditorAutomat({ name }); } return automat; } export const { setId, setDeliveryEntities, setDelivery, loadDraftManifest, setDate, setDeliveryEntity, setPartyId, setExternalNumber, setNumber, setType, setLines, setLineitems, setNotes, addLineitem, addLine, updateLineitem, updateLine, removeLineitem, removeLine, clearLineitems, clearLines, fetchSupplierArticles, fetchSupplierArticleForSku, unlinkSupplierArticle, addAttachment, setAttachments, updateAttachment, removeAttachment, setUnmatchedSupplierName, resetDelivery, resetDeliveryEditor, } = deliveryEditorAutomat.actions; // Selectors /** @param {DeliveryEditorState} [state] @returns {string|number|null} */ export const selectDeliveryId = (state = deliveryEditorAutomat.state) => state.deliveryId ?? state.id; /** @param {DeliveryEditorState} [state] @returns {Array} */ export const selectDeliveryEntities = (state = deliveryEditorAutomat.state) => state.deliveryEntities; /** @param {DeliveryEditorState} [state] @returns {string|null} */ export const selectDeliveryDate = (state = deliveryEditorAutomat.state) => state.date; /** @param {DeliveryEditorState} [state] @returns {string|number|null} */ export const selectDeliveryEntity = (state = deliveryEditorAutomat.state) => state.deliveryEntity ?? state.partyId; /** @param {DeliveryEditorState} [state] @returns {string|null} */ export const selectUnmatchedSupplierName = (state = deliveryEditorAutomat.state) => state.unmatchedSupplierName; /** @param {DeliveryEditorState} [state] @returns {string|null} */ export const selectDeliveryExternalNumber = (state = deliveryEditorAutomat.state) => state.externalNumber ?? state.external_number; /** @param {DeliveryEditorState} [state] @returns {string|null} */ export const selectDeliveryNumber = (state = deliveryEditorAutomat.state) => state.number; /** @param {DeliveryEditorState} [state] @returns {string|null} */ export const selectDeliveryType = (state = deliveryEditorAutomat.state) => state.type; /** @param {DeliveryEditorState} [state] @returns {Array} */ export const selectDeliveryLineitems = (state = deliveryEditorAutomat.state) => state.lines ?? state.lineitems; /** @param {DeliveryEditorState} [state] @returns {Array<*>} */ export const selectDeliveryNotes = (state = deliveryEditorAutomat.state) => state.notes; /** @param {DeliveryEditorState} [state] @returns {Array} */ export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments; /** @param {DeliveryEditorState} [state] @returns {Array} */ export const selectSupplierArticles = (state = deliveryEditorAutomat.state) => state.supplierArticles; /** @param {DeliveryEditorState} [state] @returns {Object} */ export const selectSupplierArticlesMap = (state = deliveryEditorAutomat.state) => state.supplierArticlesMap; /** @param {DeliveryEditorState} [state] @returns {Object} */ export const selectSupplierArticlesByLineKey = (state = deliveryEditorAutomat.state) => state.supplierArticlesByLineKey; /** @param {DeliveryEditorState} [state] @returns {boolean} */ export const selectSupplierArticlesLoading = (state = deliveryEditorAutomat.state) => state.supplierArticlesLoading; /** @param {DeliveryEditorState} [state] @returns {Object} */ export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys; /** @param {DeliveryEditorState} [state] @returns {Object} */ export const selectBusySupplierArticleLineKeys = (state = deliveryEditorAutomat.state) => state.busySupplierArticleLineKeys; export default deliveryEditorAutomat;