u
This commit is contained in:
@@ -117,6 +117,17 @@ const actions = {
|
|||||||
await actions.load();
|
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.
|
* Reorder attribute names.
|
||||||
* @param {number[]} attributenameIds
|
* @param {number[]} attributenameIds
|
||||||
@@ -157,6 +168,17 @@ const actions = {
|
|||||||
await actions.load();
|
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.
|
* Reorder values of an attribute.
|
||||||
* @param {number} attributeId
|
* @param {number} attributeId
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ const actions = {
|
|||||||
await brandApi.delete(id);
|
await brandApi.delete(id);
|
||||||
updateStateWithBrands(brandAutomat.state.brands.filter((b) => b.id !== 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
|
// #region addPic
|
||||||
async addImage(id, file) {
|
async addImage(id, file) {
|
||||||
const res = await brandApi.addImage(id, file);
|
const res = await brandApi.addImage(id, file);
|
||||||
|
|||||||
@@ -97,6 +97,17 @@ const actions = {
|
|||||||
await actions.load();
|
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.
|
* Reorder property names.
|
||||||
* @param {number[]} propertynameIds
|
* @param {number[]} propertynameIds
|
||||||
|
|||||||
@@ -22,16 +22,21 @@ import {
|
|||||||
class ArtikelMarkenSearchField extends PureComponent {
|
class ArtikelMarkenSearchField extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
const brands = brandAutomat.state.brands || [];
|
||||||
this.state = {
|
this.state = {
|
||||||
input: brandAutomat.state.filter ?? '',
|
input: brandAutomat.state.filter ?? '',
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
deletingUnused: false,
|
||||||
|
unusedCount: brands.filter((b) => (b.articleCount ?? 0) === 0).length,
|
||||||
};
|
};
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
this.handleDeleteUnused = this.handleDeleteUnused.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
this.unsub = brandAutomat.subscribe(this, (state) => ({
|
||||||
input: state.filter,
|
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() {
|
render() {
|
||||||
const { inputRef } = this.props;
|
const { inputRef } = this.props;
|
||||||
const { input, submitting } = this.state;
|
const { input, submitting, deletingUnused, unusedCount } = this.state;
|
||||||
const canSubmit = Boolean(input.trim()) && !submitting;
|
const canSubmit = Boolean(input.trim()) && !submitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,6 +115,12 @@ class ArtikelMarkenSearchField extends PureComponent {
|
|||||||
value={input}
|
value={input}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
onChange={(next) => this.onInputChange(next)}
|
onChange={(next) => this.onInputChange(next)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
void this.handleSubmit();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -97,6 +129,15 @@ class ArtikelMarkenSearchField extends PureComponent {
|
|||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</Button>
|
</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>
|
</Box>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,16 +46,19 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
attributes: initialAttributes,
|
attributes: initialAttributes,
|
||||||
attrFilter: '',
|
attrFilter: '',
|
||||||
attrSubmitting: false,
|
attrSubmitting: false,
|
||||||
|
attrDeletingUnused: false,
|
||||||
attrPage: 1,
|
attrPage: 1,
|
||||||
selectedAttributeId: initialSelectedAttrId,
|
selectedAttributeId: initialSelectedAttrId,
|
||||||
|
|
||||||
valFilter: '',
|
valFilter: '',
|
||||||
valSubmitting: false,
|
valSubmitting: false,
|
||||||
|
valDeletingUnused: false,
|
||||||
valPage: 1,
|
valPage: 1,
|
||||||
|
|
||||||
properties: initialProperties,
|
properties: initialProperties,
|
||||||
propFilter: '',
|
propFilter: '',
|
||||||
propSubmitting: false,
|
propSubmitting: false,
|
||||||
|
propDeletingUnused: false,
|
||||||
propPage: 1,
|
propPage: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,6 +76,9 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
this.handleCreateAttribute = this.handleCreateAttribute.bind(this);
|
this.handleCreateAttribute = this.handleCreateAttribute.bind(this);
|
||||||
this.handleCreateValue = this.handleCreateValue.bind(this);
|
this.handleCreateValue = this.handleCreateValue.bind(this);
|
||||||
this.handleCreateProperty = this.handleCreateProperty.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() {
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
attrStatus,
|
attrStatus,
|
||||||
@@ -426,10 +503,13 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
selectedAttributeId,
|
selectedAttributeId,
|
||||||
attrFilter,
|
attrFilter,
|
||||||
attrSubmitting,
|
attrSubmitting,
|
||||||
|
attrDeletingUnused,
|
||||||
valFilter,
|
valFilter,
|
||||||
valSubmitting,
|
valSubmitting,
|
||||||
|
valDeletingUnused,
|
||||||
propFilter,
|
propFilter,
|
||||||
propSubmitting,
|
propSubmitting,
|
||||||
|
propDeletingUnused,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (attrStatus === 'loading' && propStatus === 'loading' && !properties.length && !attributes.length) {
|
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.filter((p) => p.name.toLowerCase().includes(propFilterTrimmed.toLowerCase()))
|
||||||
: properties;
|
: 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 (
|
return (
|
||||||
<TabbedPanelTileGrid onChange={this.handleTabChange}>
|
<TabbedPanelTileGrid onChange={this.handleTabChange}>
|
||||||
{/* Tab 1: Auswahl-Merkmale & Werte */}
|
{/* Tab 1: Auswahl-Merkmale & Werte */}
|
||||||
@@ -469,11 +553,23 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
{/* Tile 1: Auswahl-Merkmale */}
|
{/* Tile 1: Auswahl-Merkmale */}
|
||||||
<PanelTile wide={2} minHeight={0} color={BLUE_PANEL}>
|
<PanelTile wide={2} minHeight={0} color={BLUE_PANEL}>
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||||
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||||
Auswahl-Merkmale
|
<Typography variant="subtitle1" sx={{ fontWeight: 600, noWrap: true }}>
|
||||||
</Typography>
|
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>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<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 */}
|
{/* Tile 2: Werte für das gewählte Auswahl-Merkmal */}
|
||||||
<PanelTile wide={2} minHeight={0} color={PURPLE_PANEL}>
|
<PanelTile wide={2} minHeight={0} color={PURPLE_PANEL}>
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||||
<LabelImportantIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<LabelImportantIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||||
{selectedAttr ? `Werte für Auswahl-Merkmal „${selectedAttr.name}“` : 'Werte'}
|
<Typography variant="subtitle1" sx={{ fontWeight: 600, noWrap: true }}>
|
||||||
</Typography>
|
{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>
|
</Box>
|
||||||
|
|
||||||
{!selectedAttr ? (
|
{!selectedAttr ? (
|
||||||
@@ -604,11 +712,23 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
{/* Tile 3: Text-Merkmale */}
|
{/* Tile 3: Text-Merkmale */}
|
||||||
<PanelTile wide={2} minHeight={0} color={ORANGE_PANEL}>
|
<PanelTile wide={2} minHeight={0} color={ORANGE_PANEL}>
|
||||||
<Stack spacing={1.5}>
|
<Stack spacing={1.5}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||||
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||||
Text-Merkmale
|
<Typography variant="subtitle1" sx={{ fontWeight: 600, noWrap: true }}>
|
||||||
</Typography>
|
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>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
@@ -637,28 +757,20 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{filteredProperties.length === 0 ? (
|
<FillSpace
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
items={filteredProperties}
|
||||||
{propFilterTrimmed
|
totalCount={properties.length}
|
||||||
? `Keine Treffer für „${propFilterTrimmed}“.`
|
minPageSize={8}
|
||||||
: 'Noch keine Text-Merkmale angelegt.'}
|
maxPageSize={8}
|
||||||
</Typography>
|
listRef={this.propListRef}
|
||||||
) : (
|
onPageChange={(page) => this.setState({ propPage: page })}
|
||||||
<FillSpace
|
renderItem={(p) => (
|
||||||
items={filteredProperties}
|
<ArtikelMerkmalText
|
||||||
totalCount={properties.length}
|
key={p.id}
|
||||||
minPageSize={8}
|
property={p}
|
||||||
maxPageSize={8}
|
/>
|
||||||
listRef={this.propListRef}
|
)}
|
||||||
onPageChange={(page) => this.setState({ propPage: page })}
|
/>
|
||||||
renderItem={(p) => (
|
|
||||||
<ArtikelMerkmalText
|
|
||||||
key={p.id}
|
|
||||||
property={p}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
|
|||||||
Reference in New Issue
Block a user