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: (