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

11 KiB

categoryAutomat

Source File: src/automats/categoryAutomat.js
Registration Key: 'category' (Automat.get('category') / window.__AUTOMATS__.get('category'))


1. Overview & Purpose

categoryAutomat manages the hierarchical category catalog tree for the active company (/kategorien/*), category breadcrumbs, category images, POS category visibility flags, and dynamic per-route category article view automats.

Core Responsibilities

  • Hierarchical Category Tree: Normalized id → node dictionary (categories) representing the parent-child relational tree.
  • Dynamic Per-Route Category View Automats: Instantiates lightweight route-scoped automats (category-view:<slug>) that load category articles on-demand with automatic cache cleanup and disposal.
  • Mutations & Tree Reordering: Category creation, batch creation, sibling reordering (sort), drag-and-drop hierarchy moves, and in-place renaming.
  • Article Insertion & Synchronous UI Reflection: When articles are created within a category, categoryAutomat immediately patches the active route automat's article list.
  • Media Management: Uploading and deleting category cover images and updating AVIF full and thumbnail hashes (fullHash, thumbHash).
  • POS Integration: Toggling POS visibility and hardware terminal IDs per category.

2. State Structure

interface CategoryNode {
  id: number | string;
  name: string;
  parentId: number | string | null;
  sort: number;
  pos?: boolean;
  fullHash?: string | null;
  thumbHash?: string | null;
  thumbW?: number | null;
  thumbH?: number | null;
  posterminalIds?: Array<number>;
  childCount?: number;
  [key: string]: any;
}

interface CategoryState {
  companyId: number | string | null;
  languageId: string | null;
  /** Normalized category map keyed by string ID. */
  categories: Record<string, CategoryNode> | null;
  /** Root (home) category ID for the current company. */
  rootCategoryId: number | string | null;
  /** Whether category tiles are in rename/edit mode. */
  renameMode: boolean;
  /** Category tree loading status. */
  treeStatus: 'idle' | 'loading' | 'succeeded' | 'failed';
  /** Last tree error message. */
  error: string | null;
}

Initial State

const initialState = {
  companyId: null,
  languageId: null,
  categories: null,
  rootCategoryId: null,
  renameMode: false,
  treeStatus: 'idle',
  error: null,
};

3. Dynamic Category View Automats (KeyedAutomat)

Category views are powered by a KeyedAutomat instance (categoryViews) indexed by route slug path (e.g. '' for root, 'getraenke', 'getraenke/softdrinks').

categoryAutomat.key(slugPath): KeyHandle

Returns a scoped KeyHandle proxy for the given route slug.

  • Loader: Resolves slugPath → categoryId from the tree, fetches articles via GET /api/category/:id/articles, and derives childCategories (all child categories having the current as their root/parent).
  • Handle State: { data: { slug, categoryId, articles, childCategories }, loaded: boolean, loading: boolean, error: string | null }.
  • handle.load(options?): Deduplicated background fetch across concurrent mounts.
  • handle.read(): React Suspense-compatible read (throws in-flight promise if unready).
  • handle.reload(): Re-fetches articles and child categories.
  • handle.subscribe(target, selector): Fine-grained subscription to this slug view with zero cross-talk between different category routes.

invalidateAllCategoryViews(): void

Triggers reload() across all active slug view handles.

disposeAllCategoryViewAutomats(): void

Clears all active view handles and frees memory on company switch or logout.


4. Actions & Methods

Access actions via categoryAutomat.actions.<method>().

Tree Operations

  • loadTree(): Promise<void>: Reads rootCategoryId from companyAutomat, queries GET /api/category/tree, parses the response into a normalized dictionary, and updates state.
  • setRenameMode(enabled: boolean): void: Toggles inline renaming mode for the category explorer.
  • reset(): void: Disposes all route automats and resets tree state to initialState.

Category Mutations

  • createCategory({ parentId, name }): Promise<CategoryNode>: Creates a category via POST /api/category and inserts it into categories.
  • createCategories({ parentId, names }): Promise<CategoryNode[]>: Batch-creates multiple categories under parentId.
  • renameCategory({ categoryId, name }): Promise<CategoryNode>: Updates a category name via PATCH /api/category/:id/name. Protects root category from being renamed.
  • reorderCategories({ parentId, categoryIds }): Promise<number[]>: Updates sibling display ordering via POST /api/category/:parentId/reorder.
  • moveCategory({ categoryId, parentId }): Promise<object>: Moves a category under a new parent via PATCH /api/category/:id/move.
  • deleteCategory(categoryId): Promise<number|string>: Deletes a category via DELETE /api/category/:id and removes it from state.
  • toggleCategoryPos({ categoryId, pos }): Promise<CategoryNode>: Toggles POS visibility via PATCH /api/category/:id/pos.

Article Mutations within Categories

  • createArticle({ parentId, name }): Promise<object>: Dispatches POST /api/article and appends the new article to the active category view automat's articles list immediately.
  • createArticles({ parentId, names }): Promise<object[]>: Batch creates articles and updates the active route view.
  • removeArticleBySku(sku: string): void: Removes an article from all live category view article lists when deleted.

Category Media

  • uploadCategoryImage({ categoryId, file }): Promise<object>: Uploads an image blob to POST /api/category/:id/image and patches fullHash, thumbHash, thumbW, and thumbH in the category node.
  • deleteCategoryImage(categoryId): Promise<object>: Deletes the category image via DELETE /api/category/:id/image.

Hardware POS Terminals

  • patchCategoryPosterminalIds({ categoryId, terminalId, enabled }): void: Updates the posterminalIds array on a category node locally.

5. Exported Tree Utilities

Pure helper functions re-exported from ../utils/categoryUtils:

  • ROOT_CATEGORY_NAME: Constant for the root category.
  • getCategoryBreadcrumbs(categories, categoryId): Array<CategoryNode>: Traverses parent pointers from node to root.
  • getCategoryChildren(categories, parentId): Array<CategoryNode>: Returns all immediate child nodes sorted by sort.
  • categoryDisplayName(category): string: Formats category display label.
  • categorySlug(name): string: Generates a clean URL slug.
  • normalizeCategoryNode(raw): CategoryNode: Normalizes API category payload.

6. Cross-Automat Coordination

categoryAutomat automatically stays in sync with companyAutomat and authAutomat:

// Sync with companyAutomat:
companyAutomat.subscribe((company) => {
  const id = company.current?.id ?? null;
  const rootCategoryId = company.current?.rootCategoryId ?? null;
  if (id && rootCategoryId && id !== _lastCompanyId) {
    _lastCompanyId = id;
    disposeAllCategoryViewAutomats();
    void categoryAutomat.actions.loadTree();
  }
  if (!id && _lastCompanyId) {
    _lastCompanyId = null;
    categoryAutomat.actions.reset();
  }
});

// Reset on logout:
authAutomat.subscribe((auth) => {
  if (auth.status === 'anon') {
    _lastCompanyId = null;
    categoryAutomat.actions.reset();
  }
});

7. Usage Examples

A. React PureComponent Category Tree Browser

import { PureComponent } from 'react';
import categoryAutomat, { getCategoryChildren, getCategoryBreadcrumbs } from '../automats/categoryAutomat';

export class CategoryBrowser extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      categories: categoryAutomat.state.categories,
      rootCategoryId: categoryAutomat.state.rootCategoryId,
      treeStatus: categoryAutomat.state.treeStatus,
    };
  }

  componentDidMount() {
    this.unsub = categoryAutomat.subscribe(this, ['categories', 'rootCategoryId', 'treeStatus']);
  }

  componentWillUnmount() {
    this.unsub?.();
  }

  render() {
    const { categories, rootCategoryId, treeStatus } = this.state;
    if (treeStatus === 'loading' || !categories) return <div>Lade Kategorien...</div>;

    const currentParentId = this.props.categoryId ?? rootCategoryId;
    const subcategories = getCategoryChildren(categories, currentParentId);
    const breadcrumbs = getCategoryBreadcrumbs(categories, currentParentId);

    return (
      <div>
        <nav>
          {breadcrumbs.map((b) => (
            <span key={b.id}>{b.name} / </span>
          ))}
        </nav>
        <ul>
          {subcategories.map((cat) => (
            <li key={cat.id}>{cat.name}</li>
          ))}
        </ul>
      </div>
    );
  }
}

B. Consuming a Per-Route Category View with categoryAutomat.key()

import { PureComponent } from 'react';
import categoryAutomat from '../automats/categoryAutomat';

export class CategoryArticlesList extends PureComponent {
  constructor(props) {
    super(props);
    this.handle = categoryAutomat.key(props.slugPath);
    this.state = {
      articles: this.handle.state.data?.articles ?? [],
      childCategories: this.handle.state.data?.childCategories ?? [],
      loading: this.handle.state.loading,
    };
  }

  componentDidMount() {
    this.unsub = this.handle.subscribe(this, (item) => ({
      articles: item.data?.articles ?? [],
      childCategories: item.data?.childCategories ?? [],
      loading: item.loading,
    }));
    this.handle.load();
  }

  componentWillUnmount() {
    this.unsub?.();
  }

  render() {
    const { articles, childCategories, loading } = this.state;
    if (loading) return <div>Lade Artikel in dieser Kategorie...</div>;
    return (
      <div>
        <h3>Unterkategorien ({childCategories.length})</h3>
        <ul>
          {childCategories.map((cat) => (
            <li key={cat.id}>{cat.name}</li>
          ))}
        </ul>
        <h3>Artikel ({articles.length})</h3>
        <ul>
          {articles.map((art) => (
            <li key={art.sku}>{art.name} ({art.sku})</li>
          ))}
        </ul>
      </div>
    );
  }
}

8. Best Practices & Gotchas

  1. Root category cannot be renamed or deleted: The backend returns HTTP 400 if you attempt to rename or delete the company root category.
  2. Reverse index updates: When creating articles with categoryAutomat.actions.createArticle, any active route automat matching the parent ID is updated immediately.
  3. Use normalized helpers: Always use getCategoryChildren(categories, parentId) rather than manual filtering; it ensures correct sort order.