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

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/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

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:

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

  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.