This commit is contained in:
seb
2026-10-12 01:31:41 +02:00
parent ed2ac2c2c3
commit 0a073f7cf4
5 changed files with 232 additions and 38 deletions

View File

@@ -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

View File

@@ -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);

View 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

View File

@@ -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>
);

View File

@@ -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>