diff --git a/src/automats/attributeAutomat.js b/src/automats/attributeAutomat.js
index 60c0b3c..994dbc1 100644
--- a/src/automats/attributeAutomat.js
+++ b/src/automats/attributeAutomat.js
@@ -117,6 +117,17 @@ const actions = {
await actions.load();
},
+ /**
+ * Delete multiple attribute names.
+ * @param {number[]} ids
+ */
+ async deleteAttributes(ids) {
+ for (const id of ids) {
+ await attributenameApi.delete(id);
+ }
+ await actions.load();
+ },
+
/**
* Reorder attribute names.
* @param {number[]} attributenameIds
@@ -157,6 +168,17 @@ const actions = {
await actions.load();
},
+ /**
+ * Delete multiple attribute values.
+ * @param {number[]} ids
+ */
+ async deleteValues(ids) {
+ for (const id of ids) {
+ await attributevalueApi.delete(id);
+ }
+ await actions.load();
+ },
+
/**
* Reorder values of an attribute.
* @param {number} attributeId
diff --git a/src/automats/brandAutomat.js b/src/automats/brandAutomat.js
index 846bb0b..eca68c4 100644
--- a/src/automats/brandAutomat.js
+++ b/src/automats/brandAutomat.js
@@ -113,6 +113,14 @@ const actions = {
await brandApi.delete(id);
updateStateWithBrands(brandAutomat.state.brands.filter((b) => b.id !== id));
},
+ async removeBrands(ids) {
+ const idSet = new Set(ids);
+ for (const id of ids) {
+ await brandApi.delete(id);
+ }
+ updateStateWithBrands(brandAutomat.state.brands.filter((b) => !idSet.has(b.id)));
+ await actions.load(true);
+ },
// #region addPic
async addImage(id, file) {
const res = await brandApi.addImage(id, file);
diff --git a/src/automats/propertyAutomat.js b/src/automats/propertyAutomat.js
index 88de378..570a488 100644
--- a/src/automats/propertyAutomat.js
+++ b/src/automats/propertyAutomat.js
@@ -97,6 +97,17 @@ const actions = {
await actions.load();
},
+ /**
+ * Delete multiple property names.
+ * @param {number[]} ids
+ */
+ async deleteProperties(ids) {
+ for (const id of ids) {
+ await propertynameApi.delete(id);
+ }
+ await actions.load();
+ },
+
/**
* Reorder property names.
* @param {number[]} propertynameIds
diff --git a/src/routes/artikel/marken.jsx b/src/routes/artikel/marken.jsx
index d28ebea..397c6a2 100644
--- a/src/routes/artikel/marken.jsx
+++ b/src/routes/artikel/marken.jsx
@@ -22,16 +22,21 @@ import {
class ArtikelMarkenSearchField extends PureComponent {
constructor(props) {
super(props);
+ const brands = brandAutomat.state.brands || [];
this.state = {
input: brandAutomat.state.filter ?? '',
submitting: false,
+ deletingUnused: false,
+ unusedCount: brands.filter((b) => (b.articleCount ?? 0) === 0).length,
};
this.handleSubmit = this.handleSubmit.bind(this);
+ this.handleDeleteUnused = this.handleDeleteUnused.bind(this);
}
componentDidMount() {
this.unsub = brandAutomat.subscribe(this, (state) => ({
input: state.filter,
+ unusedCount: (state.brands || []).filter((b) => (b.articleCount ?? 0) === 0).length,
}));
}
@@ -60,9 +65,30 @@ class ArtikelMarkenSearchField extends PureComponent {
}
}
+ async handleDeleteUnused() {
+ if (this.state.deletingUnused) return;
+ const allBrands = brandAutomat.state.brands || [];
+ const unused = allBrands.filter((b) => (b.articleCount ?? 0) === 0);
+ if (!unused.length) return;
+
+ if (!window.confirm(`Alle ${unused.length} ungenutzten Marken wirklich löschen?`)) {
+ return;
+ }
+
+ this.setState({ deletingUnused: true });
+ try {
+ await brandAutomat.actions.removeBrands(unused.map((b) => b.id));
+ enqueueSnackbar(`${unused.length} ungenutzte Marken gelöscht.`, { variant: 'success' });
+ } catch (err) {
+ notifyError(err);
+ } finally {
+ this.setState({ deletingUnused: false });
+ }
+ }
+
render() {
const { inputRef } = this.props;
- const { input, submitting } = this.state;
+ const { input, submitting, deletingUnused, unusedCount } = this.state;
const canSubmit = Boolean(input.trim()) && !submitting;
return (
@@ -89,6 +115,12 @@ class ArtikelMarkenSearchField extends PureComponent {
value={input}
disabled={submitting}
onChange={(next) => this.onInputChange(next)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ void this.handleSubmit();
+ }
+ }}
/>
+
);
diff --git a/src/routes/artikel/merkmale.jsx b/src/routes/artikel/merkmale.jsx
index 8428726..e6b9df2 100644
--- a/src/routes/artikel/merkmale.jsx
+++ b/src/routes/artikel/merkmale.jsx
@@ -46,16 +46,19 @@ class ArtikelMerkmalePage extends PureComponent {
attributes: initialAttributes,
attrFilter: '',
attrSubmitting: false,
+ attrDeletingUnused: false,
attrPage: 1,
selectedAttributeId: initialSelectedAttrId,
valFilter: '',
valSubmitting: false,
+ valDeletingUnused: false,
valPage: 1,
properties: initialProperties,
propFilter: '',
propSubmitting: false,
+ propDeletingUnused: false,
propPage: 1,
};
@@ -73,6 +76,9 @@ class ArtikelMerkmalePage extends PureComponent {
this.handleCreateAttribute = this.handleCreateAttribute.bind(this);
this.handleCreateValue = this.handleCreateValue.bind(this);
this.handleCreateProperty = this.handleCreateProperty.bind(this);
+ this.handleDeleteUnusedAttributes = this.handleDeleteUnusedAttributes.bind(this);
+ this.handleDeleteUnusedValues = this.handleDeleteUnusedValues.bind(this);
+ this.handleDeleteUnusedProperties = this.handleDeleteUnusedProperties.bind(this);
}
componentDidMount() {
@@ -416,6 +422,77 @@ class ArtikelMerkmalePage extends PureComponent {
}
}
+ async handleDeleteUnusedAttributes() {
+ const { attributes, attrDeletingUnused } = this.state;
+ if (attrDeletingUnused) return;
+ const unused = attributes.filter((a) => (a.articleCount ?? 0) === 0);
+ if (!unused.length) return;
+
+ if (!window.confirm(`Alle ${unused.length} ungenutzten Auswahl-Merkmale wirklich löschen?`)) {
+ return;
+ }
+
+ this.setState({ attrDeletingUnused: true });
+ try {
+ await attributeAutomat.actions.deleteAttributes(unused.map((a) => a.id));
+ enqueueSnackbar(`${unused.length} ungenutzte Auswahl-Merkmale gelöscht.`, { variant: 'success' });
+ } catch (err) {
+ notifyError(err);
+ } finally {
+ if (!this._cancelled) {
+ this.setState({ attrDeletingUnused: false });
+ }
+ }
+ }
+
+ async handleDeleteUnusedValues() {
+ const { attributes, selectedAttributeId, valDeletingUnused } = this.state;
+ if (valDeletingUnused) return;
+ const selectedAttr = attributes.find((a) => a.id === selectedAttributeId) ?? attributes[0] ?? null;
+ if (!selectedAttr) return;
+ const unused = (selectedAttr.values || []).filter((v) => (v.articleCount ?? 0) === 0);
+ if (!unused.length) return;
+
+ if (!window.confirm(`Alle ${unused.length} ungenutzten Werte wirklich löschen?`)) {
+ return;
+ }
+
+ this.setState({ valDeletingUnused: true });
+ try {
+ await attributeAutomat.actions.deleteValues(unused.map((v) => v.id));
+ enqueueSnackbar(`${unused.length} ungenutzte Werte gelöscht.`, { variant: 'success' });
+ } catch (err) {
+ notifyError(err);
+ } finally {
+ if (!this._cancelled) {
+ this.setState({ valDeletingUnused: false });
+ }
+ }
+ }
+
+ async handleDeleteUnusedProperties() {
+ const { properties, propDeletingUnused } = this.state;
+ if (propDeletingUnused) return;
+ const unused = properties.filter((p) => (p.articleCount ?? 0) === 0);
+ if (!unused.length) return;
+
+ if (!window.confirm(`Alle ${unused.length} ungenutzten Text-Merkmale wirklich löschen?`)) {
+ return;
+ }
+
+ this.setState({ propDeletingUnused: true });
+ try {
+ await propertyAutomat.actions.deleteProperties(unused.map((p) => p.id));
+ enqueueSnackbar(`${unused.length} ungenutzte Text-Merkmale gelöscht.`, { variant: 'success' });
+ } catch (err) {
+ notifyError(err);
+ } finally {
+ if (!this._cancelled) {
+ this.setState({ propDeletingUnused: false });
+ }
+ }
+ }
+
render() {
const {
attrStatus,
@@ -426,10 +503,13 @@ class ArtikelMerkmalePage extends PureComponent {
selectedAttributeId,
attrFilter,
attrSubmitting,
+ attrDeletingUnused,
valFilter,
valSubmitting,
+ valDeletingUnused,
propFilter,
propSubmitting,
+ propDeletingUnused,
} = this.state;
if (attrStatus === 'loading' && propStatus === 'loading' && !properties.length && !attributes.length) {
@@ -462,6 +542,10 @@ class ArtikelMerkmalePage extends PureComponent {
? properties.filter((p) => p.name.toLowerCase().includes(propFilterTrimmed.toLowerCase()))
: properties;
+ const unusedAttrCount = attributes.filter((a) => (a.articleCount ?? 0) === 0).length;
+ const unusedValCount = (selectedValues || []).filter((v) => (v.articleCount ?? 0) === 0).length;
+ const unusedPropCount = properties.filter((p) => (p.articleCount ?? 0) === 0).length;
+
return (
{/* Tab 1: Auswahl-Merkmale & Werte */}
@@ -469,11 +553,23 @@ class ArtikelMerkmalePage extends PureComponent {
{/* Tile 1: Auswahl-Merkmale */}
-
-
-
- Auswahl-Merkmale
-
+
+
+
+
+ Auswahl-Merkmale
+
+
+
@@ -532,11 +628,23 @@ class ArtikelMerkmalePage extends PureComponent {
{/* Tile 2: Werte für das gewählte Auswahl-Merkmal */}
-
-
-
- {selectedAttr ? `Werte für Auswahl-Merkmal „${selectedAttr.name}“` : 'Werte'}
-
+
+
+
+
+ {selectedAttr ? `Werte für Auswahl-Merkmal „${selectedAttr.name}“` : 'Werte'}
+
+
+
{!selectedAttr ? (
@@ -604,11 +712,23 @@ class ArtikelMerkmalePage extends PureComponent {
{/* Tile 3: Text-Merkmale */}
-
-
-
- Text-Merkmale
-
+
+
+
+
+ Text-Merkmale
+
+
+
@@ -637,28 +757,20 @@ class ArtikelMerkmalePage extends PureComponent {
- {filteredProperties.length === 0 ? (
-
- {propFilterTrimmed
- ? `Keine Treffer für „${propFilterTrimmed}“.`
- : 'Noch keine Text-Merkmale angelegt.'}
-
- ) : (
- this.setState({ propPage: page })}
- renderItem={(p) => (
-
- )}
- />
- )}
+ this.setState({ propPage: page })}
+ renderItem={(p) => (
+
+ )}
+ />