u
This commit is contained in:
235
src/automats/manifestDraftAutomat.js
Normal file
235
src/automats/manifestDraftAutomat.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* @file manifestDraftAutomat.js
|
||||
* @description Out-of-tree state automats for in-progress quotes and orders (quoteDraftAutomat, orderDraftAutomat).
|
||||
* Preserves draft state across view navigation and syncs bidirectionally with VFS virtual draft files.
|
||||
*/
|
||||
|
||||
import { Automat } from 'react-automat';
|
||||
import { selectCompanyTaxrate } from './companyAutomat';
|
||||
import artikelAutomat, { getArticle } from './artikelAutomat';
|
||||
import {
|
||||
emptyLine,
|
||||
lineFromApi,
|
||||
dateInputValue,
|
||||
parseNum,
|
||||
} from '../components/manifest/manifestConfig';
|
||||
|
||||
/**
|
||||
* Creates the initial empty draft state for a manifest kind.
|
||||
*
|
||||
* @param {'quote'|'order'} kind
|
||||
* @returns {object} Initial state
|
||||
*/
|
||||
export function createInitialManifestDraftState(kind) {
|
||||
return {
|
||||
kind,
|
||||
mode: 'create',
|
||||
id: null,
|
||||
date: '',
|
||||
partyId: null,
|
||||
number: '',
|
||||
externalNumber: '',
|
||||
lines: [emptyLine()],
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Automat instance managing draft state for a manifest kind.
|
||||
*
|
||||
* @param {{ kind: 'quote'|'order', name: string }} options
|
||||
* @returns {Automat}
|
||||
*/
|
||||
export function createManifestDraftAutomat({ kind, name }) {
|
||||
let automat;
|
||||
|
||||
const actions = {
|
||||
/** @param {string} date */
|
||||
setDate(date) {
|
||||
automat.setState({ date: dateInputValue(date) });
|
||||
},
|
||||
|
||||
/** @param {number|string|null} partyId */
|
||||
setPartyId(partyId) {
|
||||
automat.setState({ partyId: partyId != null ? partyId : null });
|
||||
},
|
||||
|
||||
/** @param {string} externalNumber */
|
||||
setExternalNumber(externalNumber) {
|
||||
automat.setState({ externalNumber: externalNumber != null ? String(externalNumber) : '' });
|
||||
},
|
||||
|
||||
/** @param {string} number */
|
||||
setNumber(number) {
|
||||
automat.setState({ number: number != null ? String(number) : '' });
|
||||
},
|
||||
|
||||
/** @param {Array<object>} lines */
|
||||
setLines(lines) {
|
||||
const list = Array.isArray(lines) && lines.length > 0 ? lines : [emptyLine()];
|
||||
automat.setState({ lines: list });
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates an existing line by key.
|
||||
* @param {string} key
|
||||
* @param {string|object} patchOrField
|
||||
* @param {*} [value]
|
||||
*/
|
||||
updateLine(key, patchOrField, value) {
|
||||
const current = automat.state.lines || [];
|
||||
const next = current.map((line) => {
|
||||
if (line.key !== key) return line;
|
||||
if (typeof patchOrField === 'object' && patchOrField != null) {
|
||||
return { ...line, ...patchOrField };
|
||||
}
|
||||
return { ...line, [patchOrField]: value };
|
||||
});
|
||||
automat.setState({ lines: next });
|
||||
},
|
||||
|
||||
/** @param {object} [line] */
|
||||
addLine(line = null) {
|
||||
const current = automat.state.lines || [];
|
||||
const newLine = line ? { ...emptyLine(), ...line } : emptyLine();
|
||||
automat.setState({ lines: [...current, newLine] });
|
||||
},
|
||||
|
||||
/** @param {string} key */
|
||||
removeLine(key) {
|
||||
const current = automat.state.lines || [];
|
||||
const next = current.filter((l) => l.key !== key);
|
||||
automat.setState({ lines: next.length > 0 ? next : [emptyLine()] });
|
||||
},
|
||||
|
||||
removeAllLines() {
|
||||
automat.setState({ lines: [emptyLine()] });
|
||||
},
|
||||
|
||||
/** @param {Array<object>} attachments */
|
||||
setAttachments(attachments) {
|
||||
automat.setState({ attachments: Array.isArray(attachments) ? attachments : [] });
|
||||
},
|
||||
|
||||
/** Resets draft state back to clean initial state. */
|
||||
reset() {
|
||||
automat.setState(createInitialManifestDraftState(kind));
|
||||
},
|
||||
|
||||
/**
|
||||
* Applies full manifest update (from external agent, VFS, or SSE).
|
||||
* @param {object} content
|
||||
*/
|
||||
setManifest(content) {
|
||||
if (!content) return;
|
||||
const manifest = content.manifest || content;
|
||||
const updates = {};
|
||||
|
||||
if (manifest.date !== undefined) {
|
||||
updates.date = dateInputValue(manifest.date);
|
||||
}
|
||||
const party = manifest.partyId ?? manifest.entityId ?? manifest.party_id ?? manifest.supplierId;
|
||||
if (party !== undefined) {
|
||||
updates.partyId = party != null ? party : null;
|
||||
}
|
||||
const ext = manifest.externalNumber ?? manifest.external_number;
|
||||
if (ext !== undefined) {
|
||||
updates.externalNumber = ext != null ? String(ext) : '';
|
||||
}
|
||||
|
||||
const rawLines = manifest.items ?? manifest.lines ?? manifest.lineitems;
|
||||
if (Array.isArray(rawLines)) {
|
||||
const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19';
|
||||
const lines = rawLines.map((row) => {
|
||||
const mapped = lineFromApi(row);
|
||||
if (!mapped.amount) mapped.amount = '1';
|
||||
if (!mapped.tax) {
|
||||
mapped.tax = normTax;
|
||||
mapped.taxrate = normTax;
|
||||
}
|
||||
if (!mapped.rebate) mapped.rebate = '0';
|
||||
if (!mapped.name && mapped.sku) {
|
||||
const art = getArticle(mapped.sku);
|
||||
if (art?.name) mapped.name = art.name;
|
||||
}
|
||||
return mapped;
|
||||
});
|
||||
updates.lines = lines.length > 0 ? lines : [emptyLine()];
|
||||
|
||||
for (const l of updates.lines) {
|
||||
if (l.sku) {
|
||||
void artikelAutomat.actions.loadArticle(l.sku).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
automat.setState(updates);
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes current draft state into VFS draft manifest format.
|
||||
* @returns {object} Manifest payload
|
||||
*/
|
||||
getManifest() {
|
||||
const state = automat.state;
|
||||
const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19';
|
||||
|
||||
const formattedLines = (state.lines || [])
|
||||
.filter((l) => Boolean(l && (l.sku || l.name || l.net || l.gross)))
|
||||
.map((l) => ({
|
||||
sku: l.sku || null,
|
||||
name: l.name || null,
|
||||
amount: parseNum(l.amount) ?? (l.amount ? Number(l.amount) : 1),
|
||||
net: parseNum(l.net) ?? parseNum(l.netprice) ?? null,
|
||||
gross: parseNum(l.gross) ?? null,
|
||||
taxrate: parseNum(l.tax) ?? parseNum(l.taxrate) ?? Number(normTax),
|
||||
rebate: parseNum(l.rebate) ?? 0,
|
||||
pu: parseNum(l.pu) ?? 1,
|
||||
unit: l.unit || null,
|
||||
}));
|
||||
|
||||
return {
|
||||
manifest: {
|
||||
kind,
|
||||
mode: 'create',
|
||||
id: null,
|
||||
date: state.date || null,
|
||||
partyId: state.partyId ?? null,
|
||||
externalNumber: state.externalNumber || null,
|
||||
lines: formattedLines,
|
||||
items: formattedLines,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
automat = new Automat(createInitialManifestDraftState(kind), actions, {
|
||||
name,
|
||||
persist: false,
|
||||
});
|
||||
|
||||
return automat;
|
||||
}
|
||||
|
||||
/** Shared singleton automat for quote draft. */
|
||||
export const quoteDraftAutomat = createManifestDraftAutomat({
|
||||
kind: 'quote',
|
||||
name: 'quote-draft',
|
||||
});
|
||||
|
||||
/** Shared singleton automat for order draft. */
|
||||
export const orderDraftAutomat = createManifestDraftAutomat({
|
||||
kind: 'order',
|
||||
name: 'order-draft',
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the draft automat for a given manifest kind.
|
||||
* @param {'quote'|'order'} kind
|
||||
* @returns {Automat|null}
|
||||
*/
|
||||
export function getManifestDraftAutomat(kind) {
|
||||
if (kind === 'quote') return quoteDraftAutomat;
|
||||
if (kind === 'order') return orderDraftAutomat;
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user