# `categoryAutomat` > **Source File**: [`src/automats/categoryAutomat.js`](file:///home/seb/src/dashfront/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:`) 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 ```ts 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; childCount?: number; [key: string]: any; } interface CategoryState { companyId: number | string | null; languageId: string | null; /** Normalized category map keyed by string ID. */ categories: Record | 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 ```js 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.()`. ### Tree Operations - **`loadTree(): Promise`**: 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`**: Creates a category via `POST /api/category` and inserts it into `categories`. - **`createCategories({ parentId, names }): Promise`**: Batch-creates multiple categories under `parentId`. - **`renameCategory({ categoryId, name }): Promise`**: Updates a category name via `PATCH /api/category/:id/name`. Protects root category from being renamed. - **`reorderCategories({ parentId, categoryIds }): Promise`**: Updates sibling display ordering via `POST /api/category/:parentId/reorder`. - **`moveCategory({ categoryId, parentId }): Promise`**: Moves a category under a new parent via `PATCH /api/category/:id/move`. - **`deleteCategory(categoryId): Promise`**: Deletes a category via `DELETE /api/category/:id` and removes it from state. - **`toggleCategoryPos({ categoryId, pos }): Promise`**: Toggles POS visibility via `PATCH /api/category/:id/pos`. ### Article Mutations within Categories - **`createArticle({ parentId, name }): Promise`**: Dispatches `POST /api/article` and appends the new article to the active category view automat's `articles` list immediately. - **`createArticles({ parentId, names }): Promise`**: 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`**: Uploads an image blob to `POST /api/category/:id/image` and patches `fullHash`, `thumbHash`, `thumbW`, and `thumbH` in the category node. - **`deleteCategoryImage(categoryId): Promise`**: 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`**: Traverses parent pointers from node to root. - **`getCategoryChildren(categories, parentId): Array`**: 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`: ```js // 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 ```jsx 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
Lade Kategorien...
; const currentParentId = this.props.categoryId ?? rootCategoryId; const subcategories = getCategoryChildren(categories, currentParentId); const breadcrumbs = getCategoryBreadcrumbs(categories, currentParentId); return (
    {subcategories.map((cat) => (
  • {cat.name}
  • ))}
); } } ``` ### B. Consuming a Per-Route Category View with `categoryAutomat.key()` ```jsx 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
Lade Artikel in dieser Kategorie...
; return (

Unterkategorien ({childCategories.length})

    {childCategories.map((cat) => (
  • {cat.name}
  • ))}

Artikel ({articles.length})

    {articles.map((art) => (
  • {art.name} ({art.sku})
  • ))}
); } } ``` --- ## 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.