746 lines
27 KiB
JavaScript
746 lines
27 KiB
JavaScript
/**
|
|
* @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';
|
|
|
|
/**
|
|
* @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<string|number>, articleean?: Array<string|number>}} [article]
|
|
* Linked internal article.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} DeliveryEditorState
|
|
* @property {Array<Object>} 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<DeliveryLine>} lines Editable lines.
|
|
* @property {Array<DeliveryLine>} lineitems Alias of `lines`.
|
|
* @property {Array<*>} notes Delivery notes.
|
|
* @property {Array<Object>} attachments Delivery attachments.
|
|
* @property {Array<Object>} files Alias of `attachments`.
|
|
* @property {Array<SupplierArticle>} supplierArticles Loaded supplier articles.
|
|
* @property {Object<string, SupplierArticle>} supplierArticlesMap Articles indexed by SKU or EAN.
|
|
* @property {Object<string, SupplierArticle|null>} supplierArticlesByLineKey Matched article per line.
|
|
* @property {boolean} supplierArticlesLoading Whether any supplier article request is pending.
|
|
* @property {Object<string, boolean>} loadingLineKeys Pending lookup state per line.
|
|
* @property {Object<string, boolean>} 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.
|
|
*/
|
|
|
|
/**
|
|
* Normalizes a partial line into an editor line with a stable key.
|
|
*
|
|
* @param {Partial<DeliveryLine>} [line] Initial line values.
|
|
* @returns {DeliveryLine} Normalized line.
|
|
*/
|
|
export function createEmptyLineitem(line = {}) {
|
|
const netVal = line.netprice ?? line.net ?? null;
|
|
const taxVal = line.taxrate ?? line.tax ?? null;
|
|
return {
|
|
key: line.key ?? `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
name: line.name ?? null,
|
|
sku: line.sku ?? null,
|
|
amount: line.amount ?? null,
|
|
pu: line.pu ?? null,
|
|
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<SupplierArticle>} [articles] Articles to index.
|
|
* @param {Object<string, SupplierArticle>} [existingMap] Existing index to extend.
|
|
* @returns {Object<string, SupplierArticle>} 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<DeliveryEditorState, 'deliveryEntities'>} */
|
|
export const initialDelivery = {
|
|
id: null,
|
|
deliveryId: null,
|
|
date: null,
|
|
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<DeliveryEditorState>} 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<Object>} deliveryEntities @returns {void} */
|
|
setDeliveryEntities(deliveryEntities) {
|
|
automat.setState({ deliveryEntities: deliveryEntities ?? [] });
|
|
},
|
|
|
|
/**
|
|
* Replaces the edited delivery and normalizes API aliases.
|
|
* @param {Partial<DeliveryEditorState>} [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 : [];
|
|
|
|
automat.setState({
|
|
...initialDelivery,
|
|
...delivery,
|
|
id,
|
|
deliveryId: id,
|
|
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<DeliveryLine>} lines @returns {void} */
|
|
setLines(lines) {
|
|
const cleanLines = lines ?? [];
|
|
automat.setState({ lines: cleanLines, lineitems: cleanLines });
|
|
},
|
|
|
|
/** @param {Array<DeliveryLine>} 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<DeliveryLine>} [line] @returns {void} */
|
|
addLineitem(line = {}) {
|
|
const item = createEmptyLineitem(line);
|
|
const nextLines = [...(automat.state.lines ?? []), item];
|
|
automat.setState({ lines: nextLines, lineitems: nextLines });
|
|
},
|
|
|
|
/** @param {Partial<DeliveryLine>} [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);
|
|
},
|
|
|
|
/** 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<DeliveryLine>} [targetLines] Lines whose SKUs should be queried.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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, skus);
|
|
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<void>}
|
|
*/
|
|
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<void>}
|
|
*/
|
|
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<Object>} 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,
|
|
});
|
|
},
|
|
|
|
/** Resets the complete editor state. @returns {void} */
|
|
resetDeliveryEditor() {
|
|
automat.setState({
|
|
...initialState,
|
|
});
|
|
},
|
|
};
|
|
|
|
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<DeliveryEditorState>} 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,
|
|
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<Object>} */
|
|
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<DeliveryLine>} */
|
|
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<Object>} */
|
|
export const selectDeliveryAttachments = (state = deliveryEditorAutomat.state) => state.attachments;
|
|
/** @param {DeliveryEditorState} [state] @returns {Array<SupplierArticle>} */
|
|
export const selectSupplierArticles = (state = deliveryEditorAutomat.state) => state.supplierArticles;
|
|
/** @param {DeliveryEditorState} [state] @returns {Object<string, SupplierArticle>} */
|
|
export const selectSupplierArticlesMap = (state = deliveryEditorAutomat.state) => state.supplierArticlesMap;
|
|
/** @param {DeliveryEditorState} [state] @returns {Object<string, SupplierArticle|null>} */
|
|
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<string, boolean>} */
|
|
export const selectLoadingLineKeys = (state = deliveryEditorAutomat.state) => state.loadingLineKeys;
|
|
/** @param {DeliveryEditorState} [state] @returns {Object<string, boolean>} */
|
|
export const selectBusySupplierArticleLineKeys = (state = deliveryEditorAutomat.state) =>
|
|
state.busySupplierArticleLineKeys;
|
|
|
|
export default deliveryEditorAutomat;
|