import { PureComponent } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Checkbox from '@mui/material/Checkbox';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import AppDialog from '../../AppDialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import FormControlLabel from '@mui/material/FormControlLabel';
import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
import MenuItem from '@mui/material/MenuItem';
import Tab from '@mui/material/Tab';
import Tabs from '@mui/material/Tabs';
import TextInput from '../../TextInput';
import ArticleSearch from '../ArticleSearch';
import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import AddIcon from '@mui/icons-material/Add';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import LayersIcon from '@mui/icons-material/Layers';
import PlaylistAddCheckIcon from '@mui/icons-material/PlaylistAddCheck';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import LinkOffIcon from '@mui/icons-material/LinkOff';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { PanelTile } from 'dashgui/panels';
import { enqueueSnackbar } from 'notistack';
import { artikelDetailPath } from '../articleRoute';
import { article as articleApi } from '../../../api';
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
import { notifyError } from '../../../utils/notify';
import articleIndexAutomat from '../../../automats/articleIndexAutomat';
import {
MAGENTA_PANEL,
SHADOW_CARD,
} from '../../../colors';
export default class ArticleViewLists extends PureComponent {
constructor(props) {
super(props);
const initialAttributes = selectAttributes() || [];
const hasStueckliste = Boolean(
props.article?.articlelistId != null || props.article?.articlelist?.length > 0
);
const hasOptions = Boolean(
props.article?.optionslistId != null || props.article?.options?.length > 0
);
let activeListTab = 'stueckliste';
if (!hasStueckliste && hasOptions) {
activeListTab = 'options';
}
this.state = {
attributes: initialAttributes,
assignedAttributes: [],
allArticles: [],
busy: false,
// Active sub-tab for compound article: 'stueckliste' | 'options'
activeListTab,
// Creator mode for standard article: null | 'parent' | 'stueckliste' | 'options'
activeCreatorMode: null,
// Parent mode state
selectedAttributeId: '',
selectedChildValueId: '',
newChildText: '',
isCustomChildValue: false,
// Stückliste item add state
stuecklisteTargetArticle: null,
stuecklisteCount: 1,
// Options item add state
optionTargetArticle: null,
optionCount: 1,
optionMandated: false,
// Confirmation dialogs
removeTargetChild: null, // { sku, attributeValue }
confirmClearType: null, // 'stueckliste' | 'options' | null
};
this._cancelled = false;
}
componentDidMount() {
this._cancelled = false;
this.unsubAttr = attributeAutomat.subscribe((state) => {
if (this._cancelled) return;
this.setState((prev) => {
const assignedIds = new Set(prev.assignedAttributes.map((a) => a.attributeId));
const selectable = state.attributes.filter((a) => !assignedIds.has(a.id));
let nextSelectedId = prev.selectedAttributeId;
if ((!nextSelectedId || assignedIds.has(Number(nextSelectedId))) && selectable[0]) {
nextSelectedId = selectable[0].id;
}
return {
attributes: state.attributes,
selectedAttributeId: nextSelectedId,
};
});
});
this.unsubArtikel = artikelAutomat.subscribe(() => {
if (this._cancelled) return;
this.forceUpdate();
});
if (attributeAutomat.state.status === 'idle') {
void attributeAutomat.actions.load();
}
if (this.props.article?.sku) {
void this.loadAssignedAttributes(this.props.article.sku);
}
if (this.props.article?.parentSku) {
void artikelAutomat.actions.loadArticle(this.props.article.parentSku);
}
void this.loadAllArticles();
}
componentDidUpdate(prevProps) {
if (prevProps.article?.sku !== this.props.article?.sku) {
if (this.props.article?.sku) {
void this.loadAssignedAttributes(this.props.article.sku);
}
if (this.props.article?.parentSku) {
void artikelAutomat.actions.loadArticle(this.props.article.parentSku);
}
const nextArticle = this.props.article;
const hasStueckliste = Boolean(
nextArticle?.articlelistId != null || nextArticle?.articlelist?.length > 0
);
const hasOptions = Boolean(
nextArticle?.optionslistId != null || nextArticle?.options?.length > 0
);
let activeListTab = 'stueckliste';
if (!hasStueckliste && hasOptions) {
activeListTab = 'options';
}
if (this.state.allArticles.length === 0) {
void this.loadAllArticles();
}
this.setState({
activeListTab,
activeCreatorMode: null,
stuecklisteTargetArticle: null,
stuecklisteCount: 1,
optionTargetArticle: null,
optionCount: 1,
optionMandated: false,
});
}
}
componentWillUnmount() {
this._cancelled = true;
this.unsubAttr?.();
this.unsubArtikel?.();
}
getApi() {
return articleApi;
}
async loadAllArticles() {
try {
const articles = await articleIndexAutomat.actions.ensureLoaded();
if (!this._cancelled) {
this.setState({ allArticles: articles });
}
} catch (err) {
console.error('Failed to load article index:', err);
}
}
async loadAssignedAttributes(sku) {
if (!sku) return;
try {
const api = this.getApi();
const res = await api.getAttributeValues(sku);
if (this._cancelled) return;
const assigned = Array.isArray(res?.attributevalues) ? res.attributevalues : [];
this.setState((prev) => {
const assignedIds = new Set(assigned.map((a) => a.attributeId));
const selectable = prev.attributes.filter((a) => !assignedIds.has(a.id));
let nextSelectedId = prev.selectedAttributeId;
if ((!nextSelectedId || assignedIds.has(Number(nextSelectedId))) && selectable[0]) {
nextSelectedId = selectable[0].id;
} else if (selectable.length === 0) {
nextSelectedId = '';
}
return {
assignedAttributes: assigned,
selectedAttributeId: nextSelectedId,
};
});
} catch {
// Ignore
}
}
// ── Parent Actions ──────────────────────────────────────────────────────────
handleEnableParent = async () => {
const { article } = this.props;
const { selectedAttributeId, assignedAttributes, busy } = this.state;
if (busy || !article?.sku || !selectedAttributeId) return;
const isStueckliste = article.articlelistId != null || (article.articleListCount ?? 0) > 0;
const hasOptions = article.optionslistId != null || (article.optionsCount ?? 0) > 0;
if (isStueckliste) {
enqueueSnackbar('Ein Stücklisten-Artikel kann kein Vaterartikel sein.', { variant: 'error' });
return;
}
if (hasOptions) {
enqueueSnackbar('Ein Artikel mit Optionen kann kein Vaterartikel sein.', { variant: 'error' });
return;
}
const assignedIds = new Set(assignedAttributes.map((a) => a.attributeId));
if (assignedIds.has(Number(selectedAttributeId))) {
enqueueSnackbar('Dieses Merkmal ist dem Artikel bereits zugewiesen.', { variant: 'warning' });
return;
}
this.setState({ busy: true });
try {
const api = this.getApi();
const res = await api.toggleParent(article.sku, {
isParent: true,
attributeNameId: Number(selectedAttributeId),
});
if (this._cancelled) return;
if (res?.article) {
artikelAutomat.actions.setArticle(res.article);
} else {
await artikelAutomat.actions.reloadArticle(article.sku);
}
enqueueSnackbar('Vaterartikel-Modus aktiviert.', { variant: 'success' });
this.setState({ busy: false, activeCreatorMode: null });
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleDisableParent = async () => {
const { article } = this.props;
const { busy } = this.state;
if (busy || !article?.sku) return;
this.setState({ busy: true });
try {
const api = this.getApi();
const res = await api.toggleParent(article.sku, { isParent: false });
if (this._cancelled) return;
if (res?.article) {
artikelAutomat.actions.setArticle(res.article);
} else {
await artikelAutomat.actions.reloadArticle(article.sku);
}
enqueueSnackbar('Vaterartikel-Modus deaktiviert.', { variant: 'success' });
this.setState({ busy: false });
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleAddChild = async () => {
const { article } = this.props;
const { selectedChildValueId, newChildText, isCustomChildValue, busy } = this.state;
if (busy || !article?.sku) return;
const payload = {};
if (isCustomChildValue) {
const val = newChildText.trim();
if (!val) return;
payload.value = val;
} else {
if (!selectedChildValueId) return;
payload.attributeValueId = Number(selectedChildValueId);
}
this.setState({ busy: true });
try {
const api = this.getApi();
await api.addChild(article.sku, payload);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Variante erfolgreich angelegt.', { variant: 'success' });
this.setState({
busy: false,
selectedChildValueId: '',
newChildText: '',
});
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleOpenRemoveDialog = (child) => {
this.setState({ removeTargetChild: child });
};
handleCloseRemoveDialog = () => {
this.setState({ removeTargetChild: null });
};
handleConfirmRemoveChild = async (deletePermanently) => {
const { article } = this.props;
const { removeTargetChild, busy } = this.state;
if (busy || !article?.sku || !removeTargetChild?.sku) return;
const childSku = removeTargetChild.sku;
this.setState({ busy: true, removeTargetChild: null });
try {
const api = this.getApi();
await api.removeChild(article.sku, childSku, {
deleteArticle: deletePermanently,
});
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar(
deletePermanently ? 'Variante vollständig gelöscht.' : 'Variante vom Vaterartikel getrennt.',
{ variant: 'success' }
);
this.setState({ busy: false });
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
// ── Stückliste Actions ──────────────────────────────────────────────────────
handleAddItemToStueckliste = async () => {
const { article } = this.props;
const { stuecklisteTargetArticle, stuecklisteCount, busy } = this.state;
if (busy || !article?.sku || !stuecklisteTargetArticle?.sku) return;
if (stuecklisteTargetArticle.sku === article.sku) {
enqueueSnackbar('Ein Artikel kann sich nicht selbst als Komponente enthalten.', { variant: 'error' });
return;
}
const isTargetParent = Boolean(
stuecklisteTargetArticle.isParent ||
stuecklisteTargetArticle.is_parent ||
getArticle(stuecklisteTargetArticle.sku)?.isParent
);
if (isTargetParent) {
enqueueSnackbar('Ein Vaterartikel kann nicht als Komponente einer Stückliste hinzugefügt werden.', { variant: 'error' });
return;
}
const currentItems = Array.isArray(article.articlelist) ? article.articlelist : [];
const count = Math.max(1, Number(stuecklisteCount) || 1);
const existingIndex = currentItems.findIndex((i) => i.sku === stuecklisteTargetArticle.sku);
const nextItems =
existingIndex >= 0
? currentItems.map((item, idx) =>
idx === existingIndex ? { ...item, count: item.count + count } : item
)
: [
...currentItems,
{
articleId: stuecklisteTargetArticle.id,
sku: stuecklisteTargetArticle.sku,
count,
sort: currentItems.length,
},
];
this.setState({ busy: true });
try {
const api = this.getApi();
await api.setArticlelist(article.sku, nextItems);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Komponente hinzugefügt.', { variant: 'success' });
this.setState({
busy: false,
stuecklisteTargetArticle: null,
stuecklisteCount: 1,
activeCreatorMode: null,
activeListTab: 'stueckliste',
});
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleRemoveStuecklisteItem = async (itemSku) => {
const { article } = this.props;
const { busy } = this.state;
if (busy || !article?.sku) return;
const currentItems = Array.isArray(article.articlelist) ? article.articlelist : [];
const remainingItems = currentItems
.filter((i) => i.sku !== itemSku)
.map((item, idx) => ({
articleId: item.articleId,
sku: item.sku,
count: item.count,
sort: idx,
}));
this.setState({ busy: true });
try {
const api = this.getApi();
await api.setArticlelist(article.sku, remainingItems);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Komponente entfernt.', { variant: 'success' });
this.setState({ busy: false });
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleClearStueckliste = async () => {
const { article } = this.props;
const { busy } = this.state;
if (busy || !article?.sku) return;
this.setState({ busy: true, confirmClearType: null });
try {
const api = this.getApi();
await api.setArticlelist(article.sku, []);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Stückliste aufgelöst.', { variant: 'success' });
this.setState({
busy: false,
activeListTab: 'options',
});
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
// ── Optionen Actions ────────────────────────────────────────────────────────
handleAddOption = async () => {
const { article } = this.props;
const { optionTargetArticle, optionCount, optionMandated, busy } = this.state;
if (busy || !article?.sku || !optionTargetArticle?.sku) return;
if (optionTargetArticle.sku === article.sku) {
enqueueSnackbar('Ein Artikel kann sich nicht selbst als Option enthalten.', { variant: 'error' });
return;
}
const isTargetParent = Boolean(
optionTargetArticle.isParent ||
optionTargetArticle.is_parent ||
getArticle(optionTargetArticle.sku)?.isParent
);
if (isTargetParent) {
enqueueSnackbar('Ein Vaterartikel kann nicht als Option hinzugefügt werden.', { variant: 'error' });
return;
}
const currentOptions = Array.isArray(article.options) ? article.options : [];
const count = Math.max(1, Number(optionCount) || 1);
const existingIndex = currentOptions.findIndex((o) => o.sku === optionTargetArticle.sku);
const nextOptions =
existingIndex >= 0
? currentOptions.map((opt, idx) =>
idx === existingIndex
? { ...opt, count: opt.count + count, mandated: Boolean(optionMandated) }
: opt
)
: [
...currentOptions,
{
articleId: optionTargetArticle.id,
sku: optionTargetArticle.sku,
count,
sort: currentOptions.length,
mandated: Boolean(optionMandated),
},
];
this.setState({ busy: true });
try {
const api = this.getApi();
await api.setOptionslist(article.sku, nextOptions);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Option hinzugefügt.', { variant: 'success' });
this.setState({
busy: false,
optionTargetArticle: null,
optionCount: 1,
optionMandated: false,
activeCreatorMode: null,
activeListTab: 'options',
});
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleRemoveOption = async (optionSku) => {
const { article } = this.props;
const { busy } = this.state;
if (busy || !article?.sku) return;
const currentOptions = Array.isArray(article.options) ? article.options : [];
const remainingOptions = currentOptions
.filter((o) => o.sku !== optionSku)
.map((opt, idx) => ({
articleId: opt.articleId,
sku: opt.sku,
count: opt.count,
sort: idx,
mandated: Boolean(opt.mandated),
}));
this.setState({ busy: true });
try {
const api = this.getApi();
await api.setOptionslist(article.sku, remainingOptions);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Option entfernt.', { variant: 'success' });
this.setState({ busy: false });
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
handleClearOptions = async () => {
const { article } = this.props;
const { busy } = this.state;
if (busy || !article?.sku) return;
this.setState({ busy: true, confirmClearType: null });
try {
const api = this.getApi();
await api.setOptionslist(article.sku, []);
if (this._cancelled) return;
await artikelAutomat.actions.reloadArticle(article.sku);
if (this._cancelled) return;
enqueueSnackbar('Optionen entfernt.', { variant: 'success' });
this.setState({
busy: false,
activeListTab: 'stueckliste',
});
} catch (err) {
if (this._cancelled) return;
notifyError(err);
this.setState({ busy: false });
}
};
renderStuecklisteContent(stuecklisteItems, selectableOtherArticles, isStueckliste = true) {
const { article } = this.props;
const { busy, stuecklisteTargetArticle, stuecklisteCount } = this.state;
return (
Stückliste
{isStueckliste && (
)}
{isStueckliste ? (
this.setState({ confirmClearType: 'stueckliste' })}
disabled={busy}
sx={{ p: 0.4, color: 'text.secondary', '&:hover': { color: 'error.main' } }}
>
) : (
)}
{/* Add Component Row */}
item.component_sku || item.sku)]}
onSelect={(val) => this.setState({ stuecklisteTargetArticle: val })}
sx={{ flex: 1, minWidth: 0 }}
/>
this.setState({
stuecklisteCount: Math.max(1, parseInt(e.target.value, 10) || 1),
})
}
slotProps={{
htmlInput: {
min: 1,
style: { textAlign: 'center' },
},
}}
sx={{ width: 72, flexShrink: 0 }}
/>
{busy ? : }
{/* Items List */}
{stuecklisteItems.map((item) => (
{item.name || item.sku}
{item.sku}
this.handleRemoveStuecklisteItem(item.sku)}
sx={{
p: 0.25,
color: 'text.secondary',
'&:hover': { color: 'error.main' },
}}
>
))
}
);
}
renderOptionsContent(optionsItems, selectableOtherArticles, hasOptions = true) {
const { article } = this.props;
const { busy, optionTargetArticle, optionCount, optionMandated } = this.state;
return (
Optionen
{hasOptions && (
)}
{hasOptions ? (
this.setState({ confirmClearType: 'options' })}
disabled={busy}
sx={{ p: 0.4, color: 'text.secondary', '&:hover': { color: 'error.main' } }}
>
) : (
)}
{/* Add Option Row */}
item.component_sku || item.sku)]}
onSelect={(val) => this.setState({ optionTargetArticle: val })}
sx={{ flex: 1, minWidth: 0 }}
/>
this.setState({
optionCount: Math.max(1, parseInt(e.target.value, 10) || 1),
})
}
slotProps={{
htmlInput: {
min: 1,
style: { textAlign: 'center' },
},
}}
sx={{ width: 72, flexShrink: 0 }}
/>
{busy ? : }
{/* Mandated checkbox toggle */}
this.setState({ optionMandated: e.target.checked })}
sx={{ p: 0.5 }}
/>
}
label={
Fest
}
sx={{ ml: 0.5 }}
/>
{/* Options List */}
{optionsItems.map((opt) => (
{opt.name || opt.sku}
{opt.sku}
{opt.mandated ? (
) : null}
this.handleRemoveOption(opt.sku)}
sx={{
p: 0.25,
color: 'text.secondary',
'&:hover': { color: 'error.main' },
}}
>
))
}
);
}
renderCompoundManager(
isStueckliste,
hasOptions,
stuecklisteItems,
optionsItems,
selectableOtherArticles
) {
const { activeListTab } = this.state;
return (
this.setState({ activeListTab: val })}
sx={{
minHeight: 32,
mb: 0.5,
borderBottom: '1px solid',
borderColor: 'divider',
'& .MuiTab-root': {
minHeight: 32,
py: 0.25,
px: 1.5,
textTransform: 'none',
fontSize: '0.8rem',
fontWeight: 600,
},
}}
>
}
iconPosition="start"
/>
}
iconPosition="start"
/>
{activeListTab === 'stueckliste'
? this.renderStuecklisteContent(stuecklisteItems, selectableOtherArticles, isStueckliste)
: this.renderOptionsContent(optionsItems, selectableOtherArticles, hasOptions)}
);
}
renderChildView(parentSku, parentArticle, attributes, assignedAttributes) {
const { article: articleData } = this.props;
const parentAttrName =
parentArticle?.parentAttributeName ||
(parentArticle?.parentAttributeNameId
? attributes.find((a) => a.id === parentArticle.parentAttributeNameId)?.name
: null) ||
'Varianten';
const selfInParent = (parentArticle?.children || []).find(
(c) => c.sku === articleData.sku || c.id === articleData.id
);
const assignedMatch = (assignedAttributes || []).find(
(a) =>
(parentArticle?.parentAttributeNameId != null &&
Number(a.attributeId) === Number(parentArticle.parentAttributeNameId)) ||
(a.attributeName &&
parentAttrName &&
String(a.attributeName).toLowerCase() === String(parentAttrName).toLowerCase())
);
const variationValue =
selfInParent?.attributeValue ||
assignedMatch?.attributeValue ||
null;
const siblings = (parentArticle?.children || []).filter(
(c) => c.sku !== articleData.sku && c.id !== articleData.id
);
return (
{/* Header: Variation [Grün] für [Farbe] */}
Variation
{variationValue ? (
) : null}
für
{/* Link Vater */}
Vaterartikel:
{parentArticle?.name || parentSku}
{parentSku}
{/* Link(s) Geschwister */}
Geschwister:
{siblings.length === 0 ? (
Keine weiteren Varianten vorhanden.
) : (
{siblings.map((sibling) => (
{sibling.attributeValue || sibling.name || sibling.sku}
{sibling.sku}
))}
)}
);
}
renderStandardView(selectableAttributes, assignedAttributeIds) {
const { busy, activeCreatorMode, selectedAttributeId } = this.state;
return (
{activeCreatorMode === 'parent' ? (
Vaterartikel aktivieren
{selectableAttributes.length === 0 ? (
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
) : (
this.setState({ selectedAttributeId: e.target.value })}
sx={{ flex: 1, minWidth: 0 }}
>
{selectableAttributes.map((a) => (
))}
)}
) : (
}
onClick={() => this.setState({ activeCreatorMode: 'parent' })}
>
Als Vaterartikel aktivieren
}
onClick={() =>
this.setState({ activeCreatorMode: 'stueckliste', activeListTab: 'stueckliste' })
}
>
Als Stückliste anlegen
}
onClick={() =>
this.setState({ activeCreatorMode: 'options', activeListTab: 'options' })
}
>
Optionen hinzufügen
)}
);
}
render() {
const { article: articleData } = this.props;
const {
attributes,
assignedAttributes,
allArticles,
busy,
activeCreatorMode,
selectedChildValueId,
newChildText,
isCustomChildValue,
removeTargetChild,
confirmClearType,
} = this.state;
if (!articleData) return null;
const isChild = articleData.parentArticleId != null || Boolean(articleData.parentSku);
const isParent = Boolean(articleData.isParent);
const children = Array.isArray(articleData.children) ? articleData.children : [];
const stuecklisteItems = Array.isArray(articleData.articlelist) ? articleData.articlelist : [];
const isStueckliste = articleData.articlelistId != null || stuecklisteItems.length > 0;
const optionsItems = Array.isArray(articleData.options) ? articleData.options : [];
const hasOptions = articleData.optionslistId != null || optionsItems.length > 0;
// Attributes assigned to this article that cannot be selected for parent mode
const assignedAttributeIds = new Set(
(assignedAttributes || []).map((a) => a.attributeId)
);
const selectableAttributes = attributes.filter(
(a) => !assignedAttributeIds.has(a.id)
);
// Filter available articles for Autocomplete (cannot select self, and cannot select parent articles)
const selectableOtherArticles = allArticles.filter(
(a) => a.sku !== articleData.sku && !a.isParent && !a.is_parent
);
// Find parent attribute definition & values
const parentAttrDef = isParent
? attributes.find((a) => a.id === articleData.parentAttributeNameId)
: null;
const allParentValues = parentAttrDef?.values || [];
const assignedValueIds = new Set(
children.map((c) => c.attributeValueId).filter((id) => id != null)
);
const availableParentValues = allParentValues.filter((v) => !assignedValueIds.has(v.id));
const parentSku = articleData.parentSku;
const parentArticle = parentSku ? getArticle(parentSku) : null;
return (
{/* ── Mode 1: Parent Article ──────────────────────────────────────── */}
{isParent ? (
{isChild && parentSku ? (
Übergeordnete Variante von:
{parentSku}
) : null}
Vater für Auswahl-Merkmal
{children.length === 0 ? (
) : null}
{/* Add Child Variant Row */}
{isCustomChildValue || availableParentValues.length === 0 ? (
this.setState({ newChildText: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void this.handleAddChild();
}
}}
sx={{ flex: 1, minWidth: 0 }}
/>
) : (
this.setState({ selectedChildValueId: e.target.value })}
sx={{ flex: 1, minWidth: 0 }}
>
{availableParentValues.map((v) => (
))}
)}
{busy ? : }
{/* Toggle custom value / predefined value if values exist */}
{allParentValues.length > 0 && availableParentValues.length > 0 ? (
this.setState((prev) => ({
isCustomChildValue: !prev.isCustomChildValue,
selectedChildValueId: '',
newChildText: '',
}))
}
sx={{ fontSize: '0.72rem', color: 'text.secondary', textDecoration: 'none' }}
>
{isCustomChildValue ? 'Aus Liste wählen' : 'Neuen Wert tippen'}
) : null}
{/* Children List */}
{children.length === 0 ? (
Noch keine Varianten angelegt.
) : (
children.map((child) => (
{child.attributeValue || child.name || child.sku}
{child.sku}
this.handleOpenRemoveDialog(child)}
title="Variante entfernen"
sx={{
p: 0.35,
color: 'text.secondary',
'&:hover': { color: 'error.main' },
}}
>
))
)}
) : isChild && parentSku ? (
/* ── Mode 2: Child Variant Article ───────────────────────────────── */
{this.renderChildView(
parentSku,
parentArticle,
attributes,
assignedAttributes
)}
{isStueckliste ||
hasOptions ||
activeCreatorMode === 'stueckliste' ||
activeCreatorMode === 'options'
? this.renderCompoundManager(
isStueckliste,
hasOptions,
stuecklisteItems,
optionsItems,
selectableOtherArticles
)
: this.renderStandardView(selectableAttributes, assignedAttributeIds)}
) : isStueckliste ||
hasOptions ||
activeCreatorMode === 'stueckliste' ||
activeCreatorMode === 'options' ? (
/* ── Mode 3: Combined Compound Manager (Stückliste & Optionen) ───── */
this.renderCompoundManager(
isStueckliste,
hasOptions,
stuecklisteItems,
optionsItems,
selectableOtherArticles
)
) : (
/* ── Mode 4: Standard Article ────────────────────────────────────── */
this.renderStandardView(selectableAttributes, assignedAttributeIds)
)}
{/* Remove Child Confirmation Dialog */}
Variante entfernen
Möchten Sie die Variante{' '}
„{removeTargetChild?.attributeValue || removeTargetChild?.name || removeTargetChild?.sku}“
{' '}
({removeTargetChild?.sku}) vom Vaterartikel trennen oder den Artikel vollständig löschen?
}
onClick={() => void this.handleConfirmRemoveChild(false)}
>
Nur trennen
}
onClick={() => void this.handleConfirmRemoveChild(true)}
>
Vollständig löschen
{/* Clear Stückliste / Optionen Confirmation Dialog */}
this.setState({ confirmClearType: null })}
maxWidth="xs"
fullWidth
>
{confirmClearType === 'stueckliste' ? 'Stückliste auflösen?' : 'Optionen entfernen?'}
{confirmClearType === 'stueckliste'
? (hasOptions
? 'Möchten Sie alle Komponenten aus dieser Stückliste entfernen? Die Optionen bleiben dabei erhalten.'
: 'Möchten Sie alle Komponenten aus dieser Stückliste entfernen und den Artikel wieder als Standard-Artikel führen?')
: (isStueckliste
? 'Möchten Sie alle Optionen von diesem Artikel entfernen? Die Stückliste bleibt dabei erhalten.'
: 'Möchten Sie alle Optionen von diesem Artikel entfernen und ihn wieder als Standard-Artikel führen?')}
);
}
}