279 lines
11 KiB
Markdown
279 lines
11 KiB
Markdown
# `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:<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
|
|
|
|
```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<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
|
|
```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.<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`:
|
|
|
|
```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 <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()`
|
|
|
|
```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 <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.
|