10 KiB
deliveryEditorAutomat
Source File:
src/automats/deliveryEditorAutomat.js
Registration Keys:'delivery-draft'(draft editor) /'delivery-{docId}'(saved delivery editors)
1. Overview & Purpose
deliveryEditorAutomat powers the inbound delivery reception, receipt reconciliation, and invoice editor (/lieferung/neu and /lieferung/:deliveryId). It manages complex multi-line receipts, OCR attachments, and automatic matching of supplier articles to internal catalog SKUs.
Core Responsibilities
- Multi-Instance Architecture: Supports an unsaved draft editor (
deliveryDraftAutomat) as well as isolated, named automat instances for each open delivery (getDeliveryEditorAutomat(docId)). - Line Item Management: Adding, editing, reordering, and deleting delivery lineitems (
name,sku,amount,pu,unit,netprice,taxrate). - Supplier Article Auto-Matching: As lines are typed or parsed from receipts, automatically queries
GET /api/supplier/:id/articlesand indexes them by supplier SKU, internal SKU, and barcode EANs. - Unlinking & Re-matching: Supports unlinking mistaken supplier-article associations (
unlinkSupplierArticle) and refreshing lines. - Attachment & Receipt Pipelines: Manages uploaded PDFs and image receipts, OCR parsing state, and attachment previews.
- Reactive Navigation Badges: Used by
src/nav.jsto show live uncommitted line badges on the sidebar navigation items.
2. State Structure
interface DeliveryLine {
key: string | number; // Stable client-side unique identifier
name: string | null; // Article description
sku: string | number | null; // Supplier SKU or internal SKU
amount: number | null; // Delivered quantity
pu: number | null; // Price unit (e.g. 1, 100)
unit: string | null; // Unit of measure (e.g. 'Stück', 'kg')
netprice: number | null; // Net unit price
taxrate: number | null; // VAT rate (e.g. 19.0)
}
interface SupplierArticle {
id?: string | number;
sku?: string | number | null;
article?: {
sku?: string | number | null;
eans?: Array<string | number>;
articleean?: Array<string | number>;
};
}
interface DeliveryEditorState {
deliveryEntities: Array<object>; // Available suppliers/parties
id: string | number | null;
deliveryId: string | number | null; // Alias of id
date: string | null; // ISO delivery date string
deliveryEntity: string | number | null; // Selected supplier ID
partyId: string | number | null; // Alias of deliveryEntity
externalNumber: string | null; // Supplier invoice / delivery slip number
external_number: string | null; // Alias of externalNumber
number: string | null; // Internal receipt number
type: string | null;
lines: Array<DeliveryLine>; // Editable line items
lineitems: Array<DeliveryLine>; // Alias of lines
notes: Array<any>; // Delivery notes
attachments: Array<object>; // PDF/image attachments
files: Array<object>; // Alias of attachments
supplierArticles: Array<SupplierArticle>; // Loaded supplier articles
supplierArticlesMap: Record<string, SupplierArticle>; // Lookup map by SKU / EAN
supplierArticlesByLineKey: Record<string, SupplierArticle | null>; // Matched article per line
supplierArticlesLoading: boolean; // Pending article lookup
loadingLineKeys: Record<string, boolean>; // Per-line loading state
busySupplierArticleLineKeys: Record<string, boolean>; // Lines with pending unlinks
supplierArticlesError: string | null;
unmatchedSupplierName: string | null; // Parsed supplier name without match
}
3. Factory & Instance Resolution
createDeliveryEditorAutomat(options?: { name?: string } | string): Automat<DeliveryEditorState>
Factory function creating a new delivery editor automat or retrieving an existing one by name.
deliveryDraftAutomat / deliveryEditorAutomat
The singleton automat with name 'delivery-draft' used for creating new, unsaved deliveries (/lieferung/neu).
getDeliveryEditorAutomat(docId: string | number | null): Automat<DeliveryEditorState>
Returns deliveryDraftAutomat when docId is null/undefined. If docId is provided, returns or instantiates 'delivery-${docId}'.
4. Helper Utilities
createEmptyLineitem(line?: Partial<DeliveryLine>): DeliveryLine: Returns a normalized lineitem with a generated uniquekey.buildSupplierArticlesMap(articles, existingMap?): Record<string, SupplierArticle>: Indexes articles by supplier SKU, internal SKU, and EANs (storing both original and lowercased keys for case-insensitive lookup).
5. Actions & Methods
Actions can be invoked directly from the automat instance:
Header & Supplier Details
setId(id): SetsidanddeliveryId.setDeliveryEntities(entities): Sets available supplier entities dropdown list.setDelivery(delivery): Replaces the full delivery state, normalizes aliases (lines/lineitems,attachments/files), and immediately triggersfetchSupplierArticlesif a supplier is set.setDate(date): Sets delivery date.setDeliveryEntity(entityId)/setPartyId(partyId): Sets the active supplier and refreshes supplier articles for the current lines.setExternalNumber(externalNumber): Sets external supplier invoice/slip number.setNumber(number): Sets internal receipt number.setType(type): Sets delivery document type.setNotes(notes): Sets delivery notes array.setUnmatchedSupplierName(name): Sets OCR-extracted supplier name before an entity is matched.
Line Items
setLines(lines)/setLineitems(lines): Sets the entire lines array.addLineitem(line?)/addLine(line?): Appends a new line item.updateLineitem(payload)/updateLine(payload): Updates line fields identified byindexorkey.removeLineitem(payload)/removeLine(payload): Deletes a line by index or key, and cleans up its matched supplier article entry.
Supplier Article Matching & Unlinking
fetchSupplierArticles(targetEntityId?, targetLines?): Promise<void>: Fetches all supplier articles for the unique SKUs across the lines and builds the matching map.fetchSupplierArticleForSku(sku, lineKey?): Promise<void>: Loads and matches a supplier article for a single line SKU.unlinkSupplierArticle(internalSku, assignmentId, lineKey?): Promise<void>: CallsDELETE /api/article/:sku/supplier/:idto unassign an incorrect supplier article match, cleans up the map, and re-resolves the line.
Attachments & Resets
addAttachment(attachment): Appends a file/receipt attachment.setAttachments(attachments): Replaces the attachments array.updateAttachment(payload): Updates attachment fields byid.removeAttachment(idOrPayload): Removes an attachment by ID or index.resetDelivery(): Clears delivery fields while preserving available supplier entities.resetDeliveryEditor(): Resets the complete state back toinitialState.
6. Exported Selectors
Plain selectors operating on deliveryEditorAutomat.state by default:
selectDeliveryId(state?)
selectDeliveryEntities(state?)
selectDeliveryDate(state?)
selectDeliveryEntity(state?)
selectUnmatchedSupplierName(state?)
selectDeliveryExternalNumber(state?)
selectDeliveryNumber(state?)
selectDeliveryType(state?)
selectDeliveryLineitems(state?)
selectDeliveryNotes(state?)
selectDeliveryAttachments(state?)
selectSupplierArticles(state?)
selectSupplierArticlesMap(state?)
selectSupplierArticlesByLineKey(state?)
selectSupplierArticlesLoading(state?)
selectLoadingLineKeys(state?)
selectBusySupplierArticleLineKeys(state?)
7. Usage Examples
A. React PureComponent Delivery Lines Table
import { PureComponent } from 'react';
import deliveryEditorAutomat from '../automats/deliveryEditorAutomat';
export class DeliveryLinesTable extends PureComponent {
constructor(props) {
super(props);
this.state = {
lines: deliveryEditorAutomat.state.lines,
matches: deliveryEditorAutomat.state.supplierArticlesByLineKey,
};
}
componentDidMount() {
this.unsub = deliveryEditorAutomat.subscribe(this, (state) => ({
lines: state.lines,
matches: state.supplierArticlesByLineKey,
}));
}
componentWillUnmount() {
this.unsub?.();
}
handleAddLine = () => {
deliveryEditorAutomat.actions.addLineitem({ amount: 1 });
};
handleSkuChange = (key, sku) => {
deliveryEditorAutomat.actions.updateLineitem({ key, sku });
deliveryEditorAutomat.actions.fetchSupplierArticleForSku(sku, key);
};
render() {
const { lines, matches } = this.state;
return (
<div>
<table>
<thead>
<tr>
<th>SKU</th>
<th>Artikelname</th>
<th>Menge</th>
<th>Zugeordneter Artikel</th>
</tr>
</thead>
<tbody>
{lines.map((l) => (
<tr key={l.key}>
<td>
<input
value={l.sku ?? ''}
onChange={(e) => this.handleSkuChange(l.key, e.target.value)}
/>
</td>
<td>{l.name}</td>
<td>{l.amount}</td>
<td>{matches[l.key]?.article?.sku ?? 'Nicht zugeordnet'}</td>
</tr>
))}
</tbody>
</table>
<button onClick={this.handleAddLine}>+ Position hinzufügen</button>
</div>
);
}
}
8. Best Practices & Gotchas
- Always use line
key: Line indices shift when rows are deleted or reordered. Usel.keygenerated bycreateEmptyLineitem()for React keys and lookup indices. - Draft vs Saved Delivery: Always use
getDeliveryEditorAutomat(deliveryId)when editing an existing delivery so edits don't contaminate the/lieferung/neudraft state. - Sidebar badge count:
src/nav.jsdirectly inspectsdeliveryEditorAutomat.state.lines.lengthto render draft item badges.