# `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')`) > **Related**: [`categoryViewAutomat`](./categoryViewAutomat.md) — the keyed per-category article views, extracted into their own module. --- ## 1. Overview & Purpose `categoryAutomat` manages the hierarchical category catalog tree for the active company (`/kategorien/*`), category breadcrumbs, category images and POS category visibility flags. It owns the tree and all category/article **mutations**; the per-category *article views* live in [`categoryViewAutomat.js`](../src/automats/categoryViewAutomat.js) and are attached to this automat via `attachCategoryTree()`. ### Core Responsibilities - **Hierarchical Category Tree**: Normalized `id → node` dictionary (`categories`) representing the parent-child relational tree. - **Bulk View Preload**: after `GET /api/category/tree` succeeds, it triggers `loadAllCategoryViews()` so every category's articles are already cached when a route mounts. - **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 matching category views' article lists. - **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; /** True while the post-tree preload of all category views is running. */ viewsLoading: boolean; } ``` ### Initial State ```js const initialState = { companyId: null, languageId: null, categories: null, rootCategoryId: null, renameMode: false, treeStatus: 'idle', error: null, viewsLoading: false, }; ``` --- ## 3. Category View Automats (delegated) The keyed per-category article views are **no longer defined in this file**. They are implemented in [`categoryViewAutomat.js`](../src/automats/categoryViewAutomat.js) and re-exported here for backward compatibility: | Symbol | Source | |---|---| | `categoryViews` | `categoryViewAutomat.js` | | `getOrCreateCategoryAutomat({ slugPath })` | `categoryViewAutomat.js` | | `invalidateAllCategoryViews()` | `categoryViewAutomat.js` | | `disposeAllCategoryViewAutomats()` | `categoryViewAutomat.js` | The following facades remain on the tree automat and delegate to `categoryViews`: ### `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`. - **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. See [categoryViewAutomat.md](./categoryViewAutomat.md) for the full view API, including `loadAllCategoryViews()`. --- ## 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, updates state, and then **awaits `loadAllCategoryViews()`** with `viewsLoading: true → false`. Individual category failures are logged as a warning and do not fail the tree load. - **`loadAllCategoryViews(): Promise<{ loaded: number, failed: Array<{ slug, error }> }>`**: Re-runs the bulk view preload on demand (idempotent — already cached views resolve immediately). - **`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.