Files
dashfront/src/components/article/boxes/ArticleViewLists.jsx
2026-10-05 17:13:52 +02:00

1843 lines
65 KiB
JavaScript

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 (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, flex: 1, minHeight: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<LayersIcon sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
Stückliste
</Typography>
{isStueckliste && (
<Chip
label={`${stuecklisteItems.length} Komp.`}
size="small"
color="primary"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 600 }}
/>
)}
</Box>
{isStueckliste ? (
<Tooltip title="Stückliste auflösen">
<span>
<IconButton
size="small"
onClick={() => this.setState({ confirmClearType: 'stueckliste' })}
disabled={busy}
sx={{ p: 0.4, color: 'text.secondary', '&:hover': { color: 'error.main' } }}
>
<PowerSettingsNewIcon fontSize="small" sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
) : (
<Button
size="small"
color="inherit"
disabled={busy}
onClick={() =>
this.setState({
activeCreatorMode: null,
stuecklisteTargetArticle: null,
stuecklisteCount: 1,
})
}
>
Abbrechen
</Button>
)}
</Box>
{/* Add Component Row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
<ArticleSearch
size="small"
label="Artikel"
placeholder="Artikel suchen..."
value={stuecklisteTargetArticle}
disabled={busy}
excludeSkus={[article?.sku, ...stuecklisteItems.map((item) => item.component_sku || item.sku)]}
onSelect={(val) => this.setState({ stuecklisteTargetArticle: val })}
sx={{ flex: 1, minWidth: 0 }}
/>
<TextInput
type="number"
label="Menge"
value={stuecklisteCount}
disabled={busy}
onChange={(e) =>
this.setState({
stuecklisteCount: Math.max(1, parseInt(e.target.value, 10) || 1),
})
}
slotProps={{
htmlInput: {
min: 1,
style: { textAlign: 'center' },
},
}}
sx={{ width: 72, flexShrink: 0 }}
/>
<Tooltip title="Komponente hinzufügen">
<span>
<IconButton
size="small"
color="primary"
disabled={busy || !stuecklisteTargetArticle}
onClick={this.handleAddItemToStueckliste}
sx={{
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': { bgcolor: 'primary.dark' },
'&.Mui-disabled': {
bgcolor: 'action.disabledBackground',
color: 'action.disabled',
},
p: 0.6,
flexShrink: 0,
}}
>
{busy ? <CircularProgress size={16} color="inherit" /> : <AddIcon fontSize="small" />}
</IconButton>
</span>
</Tooltip>
</Box>
{/* Items List */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 0.5,
mt: 0.5,
}}
>
{stuecklisteItems.map((item) => (
<Box
key={item.id || item.sku}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1,
py: 0.4,
borderRadius: 1,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Chip
label={`${item.count}x`}
size="small"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 700, px: 0.25 }}
/>
<Box sx={{ minWidth: 0, display: 'flex', flexDirection: 'column' }}>
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontSize: '0.82rem',
lineHeight: 1.2,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{item.name || item.sku}
</Typography>
<Link
component={RouterLink}
to={artikelDetailPath(item.sku)}
sx={{
fontSize: '0.72rem',
color: 'text.secondary',
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
{item.sku}
</Link>
</Box>
</Box>
<IconButton
size="small"
disabled={busy}
onClick={() => this.handleRemoveStuecklisteItem(item.sku)}
sx={{
p: 0.25,
color: 'text.secondary',
'&:hover': { color: 'error.main' },
}}
>
<DeleteOutlinedIcon sx={{ fontSize: 16 }} />
</IconButton>
</Box>
))
}
</Box>
</Box>
);
}
renderOptionsContent(optionsItems, selectableOtherArticles, hasOptions = true) {
const { article } = this.props;
const { busy, optionTargetArticle, optionCount, optionMandated } = this.state;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, flex: 1, minHeight: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PlaylistAddCheckIcon sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
Optionen
</Typography>
{hasOptions && (
<Chip
label={`${optionsItems.length} Opt.`}
size="small"
color="primary"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 600 }}
/>
)}
</Box>
{hasOptions ? (
<Tooltip title="Optionen entfernen">
<span>
<IconButton
size="small"
onClick={() => this.setState({ confirmClearType: 'options' })}
disabled={busy}
sx={{ p: 0.4, color: 'text.secondary', '&:hover': { color: 'error.main' } }}
>
<PowerSettingsNewIcon fontSize="small" sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
) : (
<Button
size="small"
color="inherit"
disabled={busy}
onClick={() =>
this.setState({
activeCreatorMode: null,
optionTargetArticle: null,
optionCount: 1,
optionMandated: false,
})
}
>
Abbrechen
</Button>
)}
</Box>
{/* Add Option Row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
<ArticleSearch
size="small"
label="Option"
placeholder="Option suchen..."
value={optionTargetArticle}
disabled={busy}
excludeSkus={[article?.sku, ...optionsItems.map((item) => item.component_sku || item.sku)]}
onSelect={(val) => this.setState({ optionTargetArticle: val })}
sx={{ flex: 1, minWidth: 0 }}
/>
<TextInput
type="number"
label="Menge"
value={optionCount}
disabled={busy}
onChange={(e) =>
this.setState({
optionCount: Math.max(1, parseInt(e.target.value, 10) || 1),
})
}
slotProps={{
htmlInput: {
min: 1,
style: { textAlign: 'center' },
},
}}
sx={{ width: 72, flexShrink: 0 }}
/>
<Tooltip title="Option hinzufügen">
<span>
<IconButton
size="small"
color="primary"
disabled={busy || !optionTargetArticle}
onClick={this.handleAddOption}
sx={{
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': { bgcolor: 'primary.dark' },
'&.Mui-disabled': {
bgcolor: 'action.disabledBackground',
color: 'action.disabled',
},
p: 0.6,
flexShrink: 0,
}}
>
{busy ? <CircularProgress size={16} color="inherit" /> : <AddIcon fontSize="small" />}
</IconButton>
</span>
</Tooltip>
</Box>
{/* Mandated checkbox toggle */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', mt: -0.5 }}>
<FormControlLabel
control={
<Checkbox
size="small"
checked={optionMandated}
onChange={(e) => this.setState({ optionMandated: e.target.checked })}
sx={{ p: 0.5 }}
/>
}
label={
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.74rem' }}>
Fest
</Typography>
}
sx={{ ml: 0.5 }}
/>
</Box>
{/* Options List */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 0.5,
mt: 0.25,
}}
>
{optionsItems.map((opt) => (
<Box
key={opt.id || opt.sku}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1,
py: 0.4,
borderRadius: 1,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ minWidth: 0, display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Chip
label={`${opt.count}x`}
size="small"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 700, px: 0.25 }}
/>
<Box sx={{ minWidth: 0, display: 'flex', flexDirection: 'column' }}>
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontSize: '0.82rem',
lineHeight: 1.2,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{opt.name || opt.sku}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
<Link
component={RouterLink}
to={artikelDetailPath(opt.sku)}
sx={{
fontSize: '0.72rem',
color: 'text.secondary',
textDecoration: 'none',
'&:hover': { textDecoration: 'underline' },
}}
>
{opt.sku}
</Link>
{opt.mandated ? (
<Chip
label="Fest"
size="small"
color="warning"
variant="outlined"
sx={{ height: 16, fontSize: '0.65rem', fontWeight: 600 }}
/>
) : null}
</Box>
</Box>
</Box>
<IconButton
size="small"
disabled={busy}
onClick={() => this.handleRemoveOption(opt.sku)}
sx={{
p: 0.25,
color: 'text.secondary',
'&:hover': { color: 'error.main' },
}}
>
<DeleteOutlinedIcon sx={{ fontSize: 16 }} />
</IconButton>
</Box>
))
}
</Box>
</Box>
);
}
renderCompoundManager(
isStueckliste,
hasOptions,
stuecklisteItems,
optionsItems,
selectableOtherArticles
) {
const { activeListTab } = this.state;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, flex: 1, minHeight: 0 }}>
<Tabs
value={activeListTab}
onChange={(_e, val) => 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,
},
}}
>
<Tab
value="stueckliste"
label={isStueckliste ? `Stückliste (${stuecklisteItems.length})` : '+ Stückliste'}
icon={<LayersIcon sx={{ fontSize: 16 }} />}
iconPosition="start"
/>
<Tab
value="options"
label={hasOptions ? `Optionen (${optionsItems.length})` : '+ Optionen'}
icon={<PlaylistAddCheckIcon sx={{ fontSize: 16 }} />}
iconPosition="start"
/>
</Tabs>
{activeListTab === 'stueckliste'
? this.renderStuecklisteContent(stuecklisteItems, selectableOtherArticles, isStueckliste)
: this.renderOptionsContent(optionsItems, selectableOtherArticles, hasOptions)}
</Box>
);
}
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 (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minHeight: 0 }}>
{/* Header: Variation [Grün] für [Farbe] */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.6, flexWrap: 'wrap' }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
Variation
</Typography>
{variationValue ? (
<Chip
label={variationValue}
size="small"
color="primary"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 600 }}
/>
) : null}
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
für
</Typography>
<Chip
label={parentAttrName}
size="small"
variant="outlined"
color="primary"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 600 }}
/>
</Box>
{/* Link Vater */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.4, mt: 0.25 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
Vaterartikel:
</Typography>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1,
py: 0.6,
borderRadius: 1,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
transition: 'all 0.15s ease-in-out',
'&:hover': {
borderColor: 'primary.main',
boxShadow: SHADOW_CARD,
},
}}
>
<Link
component={RouterLink}
to={artikelDetailPath(parentSku)}
sx={{
minWidth: 0,
flex: 1,
display: 'flex',
flexDirection: 'column',
textDecoration: 'none',
color: 'inherit',
py: 0.25,
cursor: 'pointer',
'&:hover .link-title': {
color: 'primary.main',
},
'&:hover .link-sku': {
color: 'primary.dark',
textDecoration: 'underline',
},
}}
>
<Typography
className="link-title"
variant="body2"
sx={{
fontWeight: 600,
fontSize: '0.86rem',
lineHeight: 1.3,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: 'text.primary',
transition: 'color 0.15s ease',
}}
>
{parentArticle?.name || parentSku}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.2 }}>
<Typography
className="link-sku"
variant="caption"
sx={{
fontSize: '0.78rem',
fontWeight: 600,
color: 'primary.main',
fontFamily: 'monospace',
letterSpacing: '0.02em',
}}
>
{parentSku}
</Typography>
<OpenInNewIcon sx={{ fontSize: 13, color: 'primary.main', opacity: 0.8 }} />
</Box>
</Link>
</Box>
</Box>
{/* Link(s) Geschwister */}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.4, mt: 0.5 }}>
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
Geschwister:
</Typography>
{siblings.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 0.5, fontSize: '0.8rem' }}>
Keine weiteren Varianten vorhanden.
</Typography>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{siblings.map((sibling) => (
<Box
key={sibling.id || sibling.sku}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1,
py: 0.6,
borderRadius: 1,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
transition: 'all 0.15s ease-in-out',
'&:hover': {
borderColor: 'primary.main',
boxShadow: SHADOW_CARD,
},
}}
>
<Link
component={RouterLink}
to={artikelDetailPath(sibling.sku)}
sx={{
minWidth: 0,
flex: 1,
display: 'flex',
flexDirection: 'column',
textDecoration: 'none',
color: 'inherit',
py: 0.25,
cursor: 'pointer',
'&:hover .child-name': {
color: 'primary.main',
},
'&:hover .child-sku': {
color: 'primary.dark',
textDecoration: 'underline',
},
}}
>
<Typography
className="child-name"
variant="body2"
sx={{
fontWeight: 600,
fontSize: '0.86rem',
lineHeight: 1.3,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: 'text.primary',
transition: 'color 0.15s ease',
}}
>
{sibling.attributeValue || sibling.name || sibling.sku}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.2 }}>
<Typography
className="child-sku"
variant="caption"
sx={{
fontSize: '0.78rem',
fontWeight: 600,
color: 'primary.main',
fontFamily: 'monospace',
letterSpacing: '0.02em',
}}
>
{sibling.sku}
</Typography>
<OpenInNewIcon sx={{ fontSize: 13, color: 'primary.main', opacity: 0.8 }} />
</Box>
</Link>
</Box>
))}
</Box>
)}
</Box>
</Box>
);
}
renderStandardView(selectableAttributes, assignedAttributeIds) {
const { busy, activeCreatorMode, selectedAttributeId } = this.state;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{activeCreatorMode === 'parent' ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<AccountTreeIcon sx={{ color: 'primary.main', fontSize: 18 }} />
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
Vaterartikel aktivieren
</Typography>
</Box>
<Button
size="small"
color="inherit"
disabled={busy}
onClick={() => this.setState({ activeCreatorMode: null, selectedAttributeId: '' })}
>
Abbrechen
</Button>
</Box>
{selectableAttributes.length === 0 ? (
<Typography variant="caption" color="text.secondary">
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
</Typography>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, mt: 0.5 }}>
<TextInput
select
fullWidth
label="Merkmal für Varianten"
value={selectedAttributeId}
onChange={(e) => this.setState({ selectedAttributeId: e.target.value })}
sx={{ flex: 1, minWidth: 0 }}
>
<MenuItem value="" disabled>
<em>Merkmal wählen...</em>
</MenuItem>
{selectableAttributes.map((a) => (
<MenuItem key={a.id} value={a.id}>
{a.name}
</MenuItem>
))}
</TextInput>
<Button
variant="contained"
disabled={
busy ||
!selectedAttributeId ||
assignedAttributeIds.has(Number(selectedAttributeId)) ||
selectableAttributes.length === 0
}
onClick={this.handleEnableParent}
sx={{ height: 38, flexShrink: 0 }}
>
{busy ? <CircularProgress size={14} color="inherit" /> : 'Aktivieren'}
</Button>
</Box>
)}
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}>
<Button
variant="outlined"
color="primary"
startIcon={<AccountTreeIcon sx={{ fontSize: 16 }} />}
onClick={() => this.setState({ activeCreatorMode: 'parent' })}
>
Als Vaterartikel aktivieren
</Button>
<Button
variant="outlined"
color="primary"
startIcon={<LayersIcon sx={{ fontSize: 16 }} />}
onClick={() =>
this.setState({ activeCreatorMode: 'stueckliste', activeListTab: 'stueckliste' })
}
>
Als Stückliste anlegen
</Button>
<Button
variant="outlined"
color="primary"
startIcon={<PlaylistAddCheckIcon sx={{ fontSize: 16 }} />}
onClick={() =>
this.setState({ activeCreatorMode: 'options', activeListTab: 'options' })
}
>
Optionen hinzufügen
</Button>
</Box>
)}
</Box>
);
}
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 (
<PanelTile wide={1} minHeight={200} color={MAGENTA_PANEL}>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 1 }}>
{/* ── Mode 1: Parent Article ──────────────────────────────────────── */}
{isParent ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.75, flex: 1, minHeight: 0 }}>
{isChild && parentSku ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.5 }}>
<AccountTreeIcon sx={{ color: 'text.secondary', fontSize: 16 }} />
<Typography variant="caption" color="text.secondary">
Übergeordnete Variante von:
</Typography>
<Link
component={RouterLink}
to={artikelDetailPath(parentSku)}
sx={{
fontWeight: 600,
fontSize: '0.82rem',
color: 'primary.main',
textDecoration: 'none',
fontFamily: 'monospace',
'&:hover': { textDecoration: 'underline' },
}}
>
{parentSku}
</Link>
</Box>
) : null}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
Vater für Auswahl-Merkmal
</Typography>
<Chip
label={articleData.parentAttributeName || parentAttrDef?.name || 'Varianten'}
size="small"
color="primary"
sx={{ height: 20, fontSize: '0.72rem', fontWeight: 600 }}
/>
</Box>
{children.length === 0 ? (
<Tooltip title="Vaterartikel-Modus deaktivieren">
<span>
<IconButton
size="small"
onClick={this.handleDisableParent}
disabled={busy}
sx={{ p: 0.4, color: 'text.secondary', '&:hover': { color: 'error.main' } }}
>
<PowerSettingsNewIcon fontSize="small" sx={{ fontSize: 16 }} />
</IconButton>
</span>
</Tooltip>
) : null}
</Box>
{/* Add Child Variant Row */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
{isCustomChildValue || availableParentValues.length === 0 ? (
<TextInput
label="Neuer Wert"
placeholder="Neuer Wert..."
value={newChildText}
disabled={busy}
onChange={(e) => this.setState({ newChildText: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
void this.handleAddChild();
}
}}
sx={{ flex: 1, minWidth: 0 }}
/>
) : (
<TextInput
select
label="Variante"
value={selectedChildValueId}
disabled={busy}
onChange={(e) => this.setState({ selectedChildValueId: e.target.value })}
sx={{ flex: 1, minWidth: 0 }}
>
<MenuItem value="" disabled>
<em>Variante wählen...</em>
</MenuItem>
{availableParentValues.map((v) => (
<MenuItem key={v.id} value={v.id}>
{v.value}
</MenuItem>
))}
</TextInput>
)}
<Tooltip title="Variante hinzufügen">
<span>
<IconButton
size="small"
color="primary"
disabled={
busy ||
(isCustomChildValue || availableParentValues.length === 0
? !newChildText.trim()
: !selectedChildValueId)
}
onClick={this.handleAddChild}
sx={{
bgcolor: 'primary.main',
color: 'primary.contrastText',
'&:hover': { bgcolor: 'primary.dark' },
'&.Mui-disabled': {
bgcolor: 'action.disabledBackground',
color: 'action.disabled',
},
p: 0.6,
flexShrink: 0,
}}
>
{busy ? <CircularProgress size={16} color="inherit" /> : <AddIcon fontSize="small" />}
</IconButton>
</span>
</Tooltip>
</Box>
{/* Toggle custom value / predefined value if values exist */}
{allParentValues.length > 0 && availableParentValues.length > 0 ? (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: -0.25 }}>
<Link
component="button"
type="button"
variant="caption"
onClick={() =>
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'}
</Link>
</Box>
) : null}
{/* Children List */}
<Box
sx={{
display: 'flex',
flexDirection: 'column',
gap: 0.5,
mt: 0.5,
}}
>
{children.length === 0 ? (
<Typography variant="body2" color="text.secondary" sx={{ py: 1, fontSize: '0.8rem' }}>
Noch keine Varianten angelegt.
</Typography>
) : (
children.map((child) => (
<Box
key={child.id || child.sku}
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1,
py: 0.6,
borderRadius: 1,
bgcolor: 'background.paper',
border: '1px solid',
borderColor: 'divider',
transition: 'all 0.15s ease-in-out',
'&:hover': {
borderColor: 'primary.main',
boxShadow: SHADOW_CARD,
},
}}
>
<Link
component={RouterLink}
to={artikelDetailPath(child.sku)}
sx={{
minWidth: 0,
flex: 1,
display: 'flex',
flexDirection: 'column',
textDecoration: 'none',
color: 'inherit',
py: 0.25,
mr: 1,
cursor: 'pointer',
'&:hover .child-name': {
color: 'primary.main',
},
'&:hover .child-sku': {
color: 'primary.dark',
textDecoration: 'underline',
},
}}
>
<Typography
className="child-name"
variant="body2"
sx={{
fontWeight: 600,
fontSize: '0.86rem',
lineHeight: 1.3,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: 'text.primary',
transition: 'color 0.15s ease',
}}
>
{child.attributeValue || child.name || child.sku}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.2 }}>
<Typography
className="child-sku"
variant="caption"
sx={{
fontSize: '0.78rem',
fontWeight: 600,
color: 'primary.main',
fontFamily: 'monospace',
letterSpacing: '0.02em',
display: 'inline-flex',
alignItems: 'center',
gap: 0.35,
}}
>
{child.sku}
</Typography>
<OpenInNewIcon sx={{ fontSize: 13, color: 'primary.main', opacity: 0.8 }} />
</Box>
</Link>
<IconButton
size="small"
disabled={busy}
onClick={() => this.handleOpenRemoveDialog(child)}
title="Variante entfernen"
sx={{
p: 0.35,
color: 'text.secondary',
'&:hover': { color: 'error.main' },
}}
>
<DeleteOutlinedIcon sx={{ fontSize: 17 }} />
</IconButton>
</Box>
))
)}
</Box>
</Box>
) : isChild && parentSku ? (
/* ── Mode 2: Child Variant Article ───────────────────────────────── */
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flex: 1, minHeight: 0 }}>
{this.renderChildView(
parentSku,
parentArticle,
attributes,
assignedAttributes
)}
<Box sx={{ mt: 0.5, pt: 1, borderTop: '1px solid', borderColor: 'divider' }}>
{isStueckliste ||
hasOptions ||
activeCreatorMode === 'stueckliste' ||
activeCreatorMode === 'options'
? this.renderCompoundManager(
isStueckliste,
hasOptions,
stuecklisteItems,
optionsItems,
selectableOtherArticles
)
: this.renderStandardView(selectableAttributes, assignedAttributeIds)}
</Box>
</Box>
) : 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)
)}
</Box>
{/* Remove Child Confirmation Dialog */}
<AppDialog
open={Boolean(removeTargetChild)}
onClose={this.handleCloseRemoveDialog}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ pb: 1, fontSize: '1rem', fontWeight: 600 }}>
Variante entfernen
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ fontSize: '0.88rem' }}>
Möchten Sie die Variante{' '}
<strong>
„{removeTargetChild?.attributeValue || removeTargetChild?.name || removeTargetChild?.sku}“
</strong>{' '}
({removeTargetChild?.sku}) vom Vaterartikel trennen oder den Artikel vollständig löschen?
</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 2, pb: 2, gap: 0.5, flexWrap: 'wrap' }}>
<Button size="small" onClick={this.handleCloseRemoveDialog} color="inherit">
Abbrechen
</Button>
<Button
size="small"
variant="outlined"
startIcon={<LinkOffIcon />}
onClick={() => void this.handleConfirmRemoveChild(false)}
>
Nur trennen
</Button>
<Button
size="small"
variant="contained"
color="error"
startIcon={<DeleteOutlinedIcon />}
onClick={() => void this.handleConfirmRemoveChild(true)}
>
Vollständig löschen
</Button>
</DialogActions>
</AppDialog>
{/* Clear Stückliste / Optionen Confirmation Dialog */}
<AppDialog
open={Boolean(confirmClearType)}
onClose={() => this.setState({ confirmClearType: null })}
maxWidth="xs"
fullWidth
>
<DialogTitle sx={{ pb: 1, fontSize: '1rem', fontWeight: 600 }}>
{confirmClearType === 'stueckliste' ? 'Stückliste auflösen?' : 'Optionen entfernen?'}
</DialogTitle>
<DialogContent>
<DialogContentText sx={{ fontSize: '0.88rem' }}>
{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?')}
</DialogContentText>
</DialogContent>
<DialogActions sx={{ px: 2, pb: 2 }}>
<Button size="small" onClick={() => this.setState({ confirmClearType: null })} color="inherit">
Abbrechen
</Button>
<Button
size="small"
variant="contained"
color="error"
onClick={() => {
if (confirmClearType === 'stueckliste') {
void this.handleClearStueckliste();
} else if (confirmClearType === 'options') {
void this.handleClearOptions();
}
}}
>
Auflösen
</Button>
</DialogActions>
</AppDialog>
</PanelTile>
);
}
}