Files
dashfront/docs/deliveryEditorAutomat.md
2026-09-23 17:24:33 +02:00

238 lines
10 KiB
Markdown

# `deliveryEditorAutomat`
> **Source File**: [`src/automats/deliveryEditorAutomat.js`](file:///home/seb/src/dashfront/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/articles` and 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.js` to show live uncommitted line badges on the sidebar navigation items.
---
## 2. State Structure
```ts
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 unique `key`.
- **`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)`**: Sets `id` and `deliveryId`.
- **`setDeliveryEntities(entities)`**: Sets available supplier entities dropdown list.
- **`setDelivery(delivery)`**: Replaces the full delivery state, normalizes aliases (`lines`/`lineitems`, `attachments`/`files`), and immediately triggers `fetchSupplierArticles` if 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 by `index` or `key`.
- **`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>`**: Calls `DELETE /api/article/:sku/supplier/:id` to 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 by `id`.
- **`removeAttachment(idOrPayload)`**: Removes an attachment by ID or index.
- **`resetDelivery()`**: Clears delivery fields while preserving available supplier entities.
- **`resetDeliveryEditor()`**: Resets the complete state back to `initialState`.
---
## 6. Exported Selectors
Plain selectors operating on `deliveryEditorAutomat.state` by default:
```js
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
```jsx
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
1. **Always use line `key`**: Line indices shift when rows are deleted or reordered. Use `l.key` generated by `createEmptyLineitem()` for React keys and lookup indices.
2. **Draft vs Saved Delivery**: Always use `getDeliveryEditorAutomat(deliveryId)` when editing an existing delivery so edits don't contaminate the `/lieferung/neu` draft state.
3. **Sidebar badge count**: `src/nav.js` directly inspects `deliveryEditorAutomat.state.lines.length` to render draft item badges.