This commit is contained in:
seb
2026-09-23 17:24:33 +02:00
parent 0391e845f0
commit b331f8f535
17 changed files with 2352 additions and 202 deletions

278
docs/categoryAutomat.md Normal file
View File

@@ -0,0 +1,278 @@
# `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.