From b165f86811030caf82d12f7909c901613844a9d6 Mon Sep 17 00:00:00 2001 From: seb Date: Thu, 8 Oct 2026 18:48:28 +0200 Subject: [PATCH] u --- .agents/skills/backend/SKILL.md | 7 +++- src/api/index.js | 15 ++++--- src/automats/categoryAutomat.js | 31 ++++++++++++++ src/components/CompanySetupDialog.jsx | 15 ++++--- src/components/category/CategoryTree.jsx | 6 ++- .../settings/CompanySettingsPanel.jsx | 6 +-- .../settings/ImportExportSettingsPanel.jsx | 26 ++++++++++++ .../settings/NumberSettingsPanel.jsx | 9 ++-- src/components/tiles/CategoryTile.jsx | 42 ++++++++++++++++--- src/routes/artikel/kategorien.jsx | 12 ++++++ 10 files changed, 139 insertions(+), 30 deletions(-) diff --git a/.agents/skills/backend/SKILL.md b/.agents/skills/backend/SKILL.md index b49a1b6..6e4e698 100644 --- a/.agents/skills/backend/SKILL.md +++ b/.agents/skills/backend/SKILL.md @@ -7,7 +7,7 @@ description: >- # Dash API Routes Overview (All) -Overview of all 191 API endpoints across all modules. +Overview of all 192 API endpoints across all modules. ### Notation @@ -239,7 +239,7 @@ _none_ _none_ - Returns: `200 { languageId: int, tree: { id: int, sort: int, posterminalIds: int[], parentId?: int|null, name?: str|null, slug?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }[], articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T28|null }[] }` #### Delete category — `DELETE /category/{id}` -- Params: `id: int` +- Params: `id: int, query: recursive?: bool` - Returns: `204 void` #### Update category (rename or move) — `PATCH /category/{id}` - Params: `id: int, body: { name?: str, parentId?: int }` @@ -517,6 +517,9 @@ _none_ #### Update property name — `PATCH /propertyname/{id}` - Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }` - Returns: `200 { propertyname: T41 }` +#### Receipt crash dump — `POST /public/telemetry/crash` +- Params: `body: { content: any }` +- Returns: `200 { ok: bool, filename: str }` #### List public articles — `GET /public/{companyId}/articles` - Params: `companyId: int, query: categoryId?: int, search?: str, limit?: int, offset?: int` - Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, net: num|null, taxRate: num|null, gross: num|null, trackStock: bool, brand: { id: int|null, name: str }, fullHash: str|null, thumbW: int|null, thumbH: int|null, thumbHash: str|null }[] }` diff --git a/src/api/index.js b/src/api/index.js index 3f942d3..61b00bf 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -285,8 +285,9 @@ export const article = { * DELETE /api/article/:sku → 204 * Errors: 400 Article has children, 401, 403, 404 Article not found */ - delete(sku) { - return http(`/api/article/${encodeURIComponent(sku)}`, { + delete(sku, { cascade = false } = {}) { + const qs = cascade ? '?cascade=true' : ''; + return http(`/api/article/${encodeURIComponent(sku)}${qs}`, { method: 'DELETE', }); }, @@ -744,9 +745,13 @@ export const category = { }); }, - /** DELETE /api/category/:id → 204 */ - delete(categoryId) { - return http(`/api/category/${categoryId}`, { method: 'DELETE' }); + /** + * DELETE /api/category/:id → 204 + * Query: ?recursive=true (cascades deletion of subcategories and articles) + */ + delete(categoryId, { recursive = false } = {}) { + const qs = recursive ? '?recursive=true' : ''; + return http(`/api/category/${categoryId}${qs}`, { method: 'DELETE' }); }, /** diff --git a/src/automats/categoryAutomat.js b/src/automats/categoryAutomat.js index 6de02e0..dbf41c4 100644 --- a/src/automats/categoryAutomat.js +++ b/src/automats/categoryAutomat.js @@ -44,6 +44,8 @@ import { parseCategoryTreeResponse, normalizeCategoryNode, cleanCategorySlugPath, + getCategoryChildren, + getCategoryDescendantIds, } from '../utils/categoryUtils'; /** @@ -307,6 +309,35 @@ const actions = { return categoryId; }, + /** + * Delete a category and all its descendants recursively. + * Calls DELETE /api/category/:id?recursive=true. + * + * @param {number|string} categoryId + */ + async deleteCategoryRecursive(categoryId) { + const { categories, rootCategoryId } = categoryAutomat.state; + + if (rootCategoryId != null && String(categoryId) === String(rootCategoryId)) { + throw new Error('Root-Kategorie kann nicht gelöscht werden'); + } + + const removedIds = getCategoryDescendantIds(categories, categoryId); + + await categoryApi.delete(categoryId, { recursive: true }); + + if (categories) { + const next = { ...categories }; + for (const id of removedIds) { + delete next[String(id)]; + } + categoryAutomat.setState({ categories: next }); + refreshCategoryViewChildren(next); + } + + return categoryId; + }, + /** * Toggle the POS flag on a category. * @param {{ categoryId: number|string, pos: boolean }} param0 diff --git a/src/components/CompanySetupDialog.jsx b/src/components/CompanySetupDialog.jsx index ecd2913..61427b0 100644 --- a/src/components/CompanySetupDialog.jsx +++ b/src/components/CompanySetupDialog.jsx @@ -22,7 +22,7 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import TextField from '@mui/material/TextField'; +import TextInput from './TextInput'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import Typography from '@mui/material/Typography'; @@ -331,7 +331,7 @@ class CompanySetupDialog extends Component { Neues Unternehmen anlegen ) : null} - this.setState({ name: e.target.value })} - autoComplete="organization" autoFocus={!hasCompanies} /> @@ -361,7 +360,7 @@ class CompanySetupDialog extends Component { - - - - - 0) { + await categoryAutomat.actions.deleteCategoryRecursive(deleteNode.id); + } else { + await categoryAutomat.actions.deleteCategory(deleteNode.id); + } enqueueSnackbar(`Kategorie „${deleteNode.name}“ gelöscht.`, { variant: 'success' }); if (this.props.selectedId != null && String(this.props.selectedId) === String(deleteNode.id)) { this.props.onSelect?.(deleteNode.parentId ?? rootCategoryId); diff --git a/src/components/settings/CompanySettingsPanel.jsx b/src/components/settings/CompanySettingsPanel.jsx index 8edeeeb..f3747f8 100644 --- a/src/components/settings/CompanySettingsPanel.jsx +++ b/src/components/settings/CompanySettingsPanel.jsx @@ -7,7 +7,7 @@ import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import InputAdornment from '@mui/material/InputAdornment'; import Stack from '@mui/material/Stack'; -import TextField from '@mui/material/TextField'; +import TextInput from '../TextInput'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; import { enqueueSnackbar } from 'notistack'; @@ -178,7 +178,7 @@ export default class CompanySettingsPanel extends Component { - - { this.setState({ @@ -628,6 +633,27 @@ export default class ImportExportSettingsPanel extends Component { )} + {importStats.ordersImported != null && ( + + + Aufträge + + + {importStats.ordersImported} + + + {importStats.ordersSkipped ? `+${importStats.ordersSkipped} übersprungen` : 'importiert'} + + + )} )} diff --git a/src/components/settings/NumberSettingsPanel.jsx b/src/components/settings/NumberSettingsPanel.jsx index 82b30d7..6cac791 100644 --- a/src/components/settings/NumberSettingsPanel.jsx +++ b/src/components/settings/NumberSettingsPanel.jsx @@ -10,7 +10,7 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import TextField from '@mui/material/TextField'; +import TextInput from '../TextInput'; import Typography from '@mui/material/Typography'; import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import { enqueueSnackbar } from 'notistack'; @@ -212,16 +212,15 @@ export default class NumberSettingsPanel extends Component { {row.type} - this.handleChange(row.id, 'prefix', e.target.value)} - autoComplete="off" /> - - = 0 && settingsTab < terminalCount; const isPhotoMode = settingsTab === -2; const isDeleteMode = settingsTab === -3; + const isRecursiveDeleteMode = settingsTab === -4; const handleSx = { position: 'absolute', @@ -69,7 +71,7 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick ...handleSx, left: '50%', transform: 'translate(-50%, -50%)', - color: isDeleteMode + color: isDeleteMode || isRecursiveDeleteMode ? 'error.main' : isTerminalMode || isPhotoMode ? 'primary.main' @@ -82,6 +84,8 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick ) : isDeleteMode ? ( + ) : isRecursiveDeleteMode ? ( + ) : ( )} @@ -109,7 +113,7 @@ function TopBand({ onSettingsClick, settingsTab, terminalCount, onPlacementClick * Middle row (1/3): gray band — TextField or terminal toggle. */ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, onTerminalToggle, fullHash }) { - const isActionTab = settingsTab === -2 || settingsTab === -3; + const isActionTab = settingsTab === -2 || settingsTab === -3 || settingsTab === -4; const fillSx = { width: '100%', height: '100%', @@ -121,7 +125,7 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, ...(isActionTab ? { '&:hover': { - bgcolor: settingsTab === -3 ? RED_TINT : 'grey.400', + bgcolor: (settingsTab === -3 || settingsTab === -4) ? RED_TINT : 'grey.400', }, } : {}), @@ -188,6 +192,24 @@ function MiddleBand({ settingsTab, nameInput, terminals, categoryPosterminalIds, ); } + // Show recursive category deletion UI when on recursive delete tab + if (settingsTab === -4) { + return ( + + + + + Rekursiv löschen + + + + ); + } + return ( {nameInput} @@ -277,7 +299,7 @@ class CategoryBackdrop extends PureComponent { * lower half = gray band with chevrons + delete */ export class CategoryTile extends Component { - state = { settingsTab: -1 }; // -1 = rename, -2 = photo, -3 = delete, 0..n-1 = terminal index + state = { settingsTab: -1 }; // -1 = rename, -2 = photo, -3 = delete, -4 = delete recursive, 0..n-1 = terminal index handleClick = () => { this.props.onClick?.(); @@ -303,6 +325,8 @@ export class CategoryTile extends Component { nextTab = -2; } else if (prev.settingsTab === -2) { nextTab = -3; + } else if (prev.settingsTab === -3) { + nextTab = -4; } else { nextTab = -1; } @@ -312,7 +336,7 @@ export class CategoryTile extends Component { handleMiddleBandClick = () => { const { settingsTab } = this.state; - const { onPhotoUpload, onPhotoDelete, fullHash, onDelete } = this.props; + const { onPhotoUpload, onPhotoDelete, fullHash, onDelete, onDeleteRecursive } = this.props; if (settingsTab === -2) { if (fullHash) { onPhotoDelete?.(); @@ -321,6 +345,8 @@ export class CategoryTile extends Component { } } else if (settingsTab === -3) { onDelete?.(); + } else if (settingsTab === -4) { + onDeleteRecursive?.(); } }; @@ -373,6 +399,7 @@ export class CategoryTile extends Component { onPlacementClick, onOpen, onDelete, + onDeleteRecursive, ...rest } = this.props; const { settingsTab } = this.state; @@ -399,7 +426,10 @@ export class CategoryTile extends Component { bottom: '33.33%', left: 0, right: 0, - cursor: settingsTab === -2 || settingsTab === -3 ? 'pointer' : 'default', + cursor: + settingsTab === -2 || settingsTab === -3 || settingsTab === -4 + ? 'pointer' + : 'default', }} > { + const name = cat.name || `Kategorie #${cat.id}`; + if (!window.confirm(`Kategorie „${name}“ und alle Unterkategorien wirklich rekursiv löschen?`)) return; + try { + await categoryAutomat.actions.deleteCategoryRecursive(cat.id); + enqueueSnackbar(`Kategorie „${name}“ und alle Unterkategorien gelöscht.`, { variant: 'success' }); + } catch (err) { + notifyError(err); + } + }; + render() { const { companyId, @@ -495,6 +506,7 @@ class ArtikelKategorienPage extends PureComponent { .catch(notifyError); }, onDelete: () => this.handleDeleteCategory(item), + onDeleteRecursive: () => this.handleDeleteCategoryRecursive(item), nameInput: (