u
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -97,6 +129,15 @@ class ArtikelMarkenSearchField extends PureComponent {
|
||||
>
|
||||
Hinzufügen
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
disabled={unusedCount === 0 || deletingUnused}
|
||||
onClick={() => void this.handleDeleteUnused()}
|
||||
sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Alle ungenutzen löschen
|
||||
</Button>
|
||||
</Box>
|
||||
</PanelTileGrid>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<TabbedPanelTileGrid onChange={this.handleTabChange}>
|
||||
{/* Tab 1: Auswahl-Merkmale & Werte */}
|
||||
@@ -469,11 +553,23 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
{/* Tile 1: Auswahl-Merkmale */}
|
||||
<PanelTile wide={2} minHeight={0} color={BLUE_PANEL}>
|
||||
<Stack spacing={1.5}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
Auswahl-Merkmale
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
|
||||
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, noWrap: true }}>
|
||||
Auswahl-Merkmale
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
disabled={unusedAttrCount === 0 || attrDeletingUnused || attrStatus !== 'ready'}
|
||||
onClick={this.handleDeleteUnusedAttributes}
|
||||
sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Alle ungenutzen löschen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
@@ -532,11 +628,23 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
{/* Tile 2: Werte für das gewählte Auswahl-Merkmal */}
|
||||
<PanelTile wide={2} minHeight={0} color={PURPLE_PANEL}>
|
||||
<Stack spacing={1.5}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<LabelImportantIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
{selectedAttr ? `Werte für Auswahl-Merkmal „${selectedAttr.name}“` : 'Werte'}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
|
||||
<LabelImportantIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, noWrap: true }}>
|
||||
{selectedAttr ? `Werte für Auswahl-Merkmal „${selectedAttr.name}“` : 'Werte'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
disabled={!selectedAttr || unusedValCount === 0 || valDeletingUnused || attrStatus !== 'ready'}
|
||||
onClick={this.handleDeleteUnusedValues}
|
||||
sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Alle ungenutzen löschen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{!selectedAttr ? (
|
||||
@@ -604,11 +712,23 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
{/* Tile 3: Text-Merkmale */}
|
||||
<PanelTile wide={2} minHeight={0} color={ORANGE_PANEL}>
|
||||
<Stack spacing={1.5}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||
Text-Merkmale
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
|
||||
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, noWrap: true }}>
|
||||
Text-Merkmale
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
disabled={unusedPropCount === 0 || propDeletingUnused || propStatus !== 'ready'}
|
||||
onClick={this.handleDeleteUnusedProperties}
|
||||
sx={{ flexShrink: 0, whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Alle ungenutzen löschen
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
@@ -637,28 +757,20 @@ class ArtikelMerkmalePage extends PureComponent {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{filteredProperties.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
||||
{propFilterTrimmed
|
||||
? `Keine Treffer für „${propFilterTrimmed}“.`
|
||||
: 'Noch keine Text-Merkmale angelegt.'}
|
||||
</Typography>
|
||||
) : (
|
||||
<FillSpace
|
||||
items={filteredProperties}
|
||||
totalCount={properties.length}
|
||||
minPageSize={8}
|
||||
maxPageSize={8}
|
||||
listRef={this.propListRef}
|
||||
onPageChange={(page) => this.setState({ propPage: page })}
|
||||
renderItem={(p) => (
|
||||
<ArtikelMerkmalText
|
||||
key={p.id}
|
||||
property={p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FillSpace
|
||||
items={filteredProperties}
|
||||
totalCount={properties.length}
|
||||
minPageSize={8}
|
||||
maxPageSize={8}
|
||||
listRef={this.propListRef}
|
||||
onPageChange={(page) => this.setState({ propPage: page })}
|
||||
renderItem={(p) => (
|
||||
<ArtikelMerkmalText
|
||||
key={p.id}
|
||||
property={p}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
</PanelTile>
|
||||
</PanelTileGrid>
|
||||
|
||||
Reference in New Issue
Block a user