u
This commit is contained in:
278
docs/categoryAutomat.md
Normal file
278
docs/categoryAutomat.md
Normal 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.
|
||||
Reference in New Issue
Block a user