This commit is contained in:
seb
2026-10-14 14:46:44 +02:00
parent 49f8f2f93f
commit 4188d8f46f
6 changed files with 597 additions and 26 deletions

View File

@@ -233,3 +233,307 @@ export function getManifestDraftAutomat(kind) {
if (kind === 'order') return orderDraftAutomat;
return null;
}
/**
* Computes detailed diff between current manifest draft/edit state and baseline snapshot.
*
* @param {object} current
* @param {object|null} baseline
* @returns {{ isDirty: boolean, fields: { date: boolean, partyId: boolean, externalNumber: boolean, number: boolean, lines: boolean, attachments: boolean }, dirtyLineKeys: string[], summary: string[] }}
*/
export function getManifestDirtyDetails(current, baseline) {
if (!baseline) {
return {
isDirty: false,
fields: {
date: false,
partyId: false,
externalNumber: false,
number: false,
lines: false,
attachments: false,
},
dirtyLineKeys: [],
summary: [],
};
}
const fields = {
date: (current.date || '') !== (baseline.date || ''),
partyId:
(current.partyId != null && current.partyId !== '' ? Number(current.partyId) : null) !==
(baseline.partyId != null && baseline.partyId !== '' ? Number(baseline.partyId) : null),
externalNumber:
String(current.externalNumber || '').trim() !== String(baseline.externalNumber || '').trim(),
number: String(current.number || '').trim() !== String(baseline.number || '').trim(),
lines: false,
attachments: false,
};
const dirtyLineKeys = [];
const curLines = current.lines || [];
const baseLines = baseline.lines || [];
const baseLinesByKey = new Map();
for (let i = 0; i < baseLines.length; i++) {
const bl = baseLines[i];
if (bl.key) baseLinesByKey.set(bl.key, bl);
if (bl.id != null) baseLinesByKey.set(`id-${bl.id}`, bl);
}
if (curLines.length !== baseLines.length) {
fields.lines = true;
}
for (let i = 0; i < curLines.length; i++) {
const cl = curLines[i];
const key = cl.key ?? `idx-${i}`;
const bl =
(cl.key && baseLinesByKey.get(cl.key)) ||
(cl.id != null && baseLinesByKey.get(`id-${cl.id}`)) ||
baseLines[i];
if (!bl) {
if (cl.sku || cl.name || (cl.amount && cl.amount !== '1') || cl.net || cl.gross) {
dirtyLineKeys.push(key);
fields.lines = true;
}
} else {
const lineChanged =
String(cl.sku ?? '').trim() !== String(bl.sku ?? '').trim() ||
String(cl.name ?? '').trim() !== String(bl.name ?? '').trim() ||
String(cl.amount ?? '').trim() !== String(bl.amount ?? '').trim() ||
String(cl.net ?? cl.netprice ?? '').trim() !== String(bl.net ?? bl.netprice ?? '').trim() ||
String(cl.gross ?? '').trim() !== String(bl.gross ?? '').trim() ||
String(cl.rebate ?? '').trim() !== String(bl.rebate ?? '').trim() ||
String(cl.tax ?? cl.taxrate ?? '').trim() !== String(bl.tax ?? bl.taxrate ?? '').trim() ||
String(cl.pu ?? '').trim() !== String(bl.pu ?? '').trim() ||
String(cl.unit ?? '').trim() !== String(bl.unit ?? '').trim();
if (lineChanged) {
dirtyLineKeys.push(key);
fields.lines = true;
}
}
}
// Check attachments
const curAtts = current.attachments || [];
const baseAtts = baseline.attachments || [];
if (curAtts.length !== baseAtts.length) {
fields.attachments = true;
} else {
for (const a of curAtts) {
if (!a.isPersisted || a.isDirty) {
fields.attachments = true;
break;
}
}
}
const summary = [];
if (fields.date) summary.push('Datum');
if (fields.partyId) summary.push(current.kind === 'delivery' ? 'Lieferant' : 'Kunde');
if (fields.externalNumber) summary.push('Referenz');
if (fields.number) summary.push('Nummer');
if (fields.lines) {
const count = dirtyLineKeys.length;
summary.push(count > 0 ? `${count} Artikelzeile${count === 1 ? '' : 'n'}` : 'Artikelzeilen');
}
if (fields.attachments) summary.push('Dateien');
const isDirty = summary.length > 0;
return {
isDirty,
fields,
dirtyLineKeys,
summary,
};
}
/**
* Compares current manifest draft/edit state against a baseline snapshot.
*
* @param {object} current
* @param {object|null} baseline
* @returns {boolean} True if any field or line has changed from baseline
*/
export function checkManifestDirty(current, baseline) {
return getManifestDirtyDetails(current, baseline).isDirty;
}
/**
* Creates an Automat managing the single last-edited manifest for that kind.
*
* @param {{ kind: 'quote'|'order', name: string }} options
* @returns {Automat}
*/
export function createManifestEditAutomat({ kind, name }) {
let automat;
const emptyFields = {
date: false,
partyId: false,
externalNumber: false,
number: false,
lines: false,
attachments: false,
};
const actions = {
loadDoc(id, doc) {
const date = dateInputValue(doc.date);
const partyId = doc.partyId ?? null;
const number = doc.number ?? '';
const externalNumber = doc.externalNumber != null ? String(doc.externalNumber) : '';
const lines = Array.isArray(doc.lines) && doc.lines.length > 0 ? doc.lines : [emptyLine()];
const attachments = Array.isArray(doc.attachments) ? doc.attachments : [];
const baseline = {
date,
partyId,
number,
externalNumber,
lines: lines.map((l) => ({ ...l })),
attachments: [...attachments],
};
automat.setState({
kind,
mode: 'edit',
id,
date,
partyId,
number,
externalNumber,
lines,
attachments,
baseline,
isDirty: false,
dirtyFields: { ...emptyFields },
dirtyLineKeys: [],
dirtySummary: [],
});
},
update(updates) {
const next = { ...automat.state, ...updates };
const dirtyInfo = getManifestDirtyDetails(next, automat.state.baseline);
automat.setState({
...updates,
isDirty: dirtyInfo.isDirty,
dirtyFields: dirtyInfo.fields,
dirtyLineKeys: dirtyInfo.dirtyLineKeys,
dirtySummary: dirtyInfo.summary,
});
},
restoreBaseline() {
if (!automat.state.baseline) return;
const base = automat.state.baseline;
automat.setState({
date: base.date,
partyId: base.partyId,
number: base.number,
externalNumber: base.externalNumber,
lines: base.lines.map((l) => ({ ...l })),
attachments: [...base.attachments],
isDirty: false,
dirtyFields: { ...emptyFields },
dirtyLineKeys: [],
dirtySummary: [],
});
},
commitSaved(savedDoc) {
const date = dateInputValue(savedDoc.date);
const partyId = savedDoc.partyId ?? null;
const number = savedDoc.number ?? automat.state.number;
const externalNumber = savedDoc.externalNumber != null ? String(savedDoc.externalNumber) : '';
const lines = Array.isArray(savedDoc.lines) && savedDoc.lines.length > 0 ? savedDoc.lines : [emptyLine()];
const attachments = Array.isArray(savedDoc.attachments) ? savedDoc.attachments : automat.state.attachments;
const baseline = {
date,
partyId,
number,
externalNumber,
lines: lines.map((l) => ({ ...l })),
attachments: [...attachments],
};
automat.setState({
date,
partyId,
number,
externalNumber,
lines,
attachments,
baseline,
isDirty: false,
dirtyFields: { ...emptyFields },
dirtyLineKeys: [],
dirtySummary: [],
});
},
reset() {
automat.setState({
kind,
mode: 'edit',
id: null,
date: '',
partyId: null,
number: '',
externalNumber: '',
lines: [emptyLine()],
attachments: [],
baseline: null,
isDirty: false,
dirtyFields: { ...emptyFields },
dirtyLineKeys: [],
dirtySummary: [],
});
},
};
automat = new Automat(
{
kind,
mode: 'edit',
id: null,
date: '',
partyId: null,
number: '',
externalNumber: '',
lines: [emptyLine()],
attachments: [],
baseline: null,
isDirty: false,
dirtyFields: { ...emptyFields },
dirtyLineKeys: [],
dirtySummary: [],
},
actions,
{ name, persist: false },
);
return automat;
}
/** Shared singleton automat for the single last edited quote. */
export const quoteEditAutomat = createManifestEditAutomat({
kind: 'quote',
name: 'quote-edit',
});
/**
* Returns the edit automat for a given manifest kind.
* @param {'quote'|'order'} kind
* @returns {Automat|null}
*/
export function getManifestEditAutomat(kind) {
if (kind === 'quote') return quoteEditAutomat;
return null;
}