u
This commit is contained in:
237
docs/deliveryEditorAutomat.md
Normal file
237
docs/deliveryEditorAutomat.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user