238 lines
10 KiB
Markdown
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.
|