u
This commit is contained in:
@@ -482,6 +482,21 @@ export class ApiClient {
|
|||||||
/** DELETE /api/article/{sku}/property/{propertyId} */
|
/** DELETE /api/article/{sku}/property/{propertyId} */
|
||||||
deleteProperty: (sku, propertyId) =>
|
deleteProperty: (sku, propertyId) =>
|
||||||
this.#json('DELETE', `/api/article/${enc(sku)}/property/${enc(propertyId)}`),
|
this.#json('DELETE', `/api/article/${enc(sku)}/property/${enc(propertyId)}`),
|
||||||
|
/** POST /api/article/{sku}/father */
|
||||||
|
toggleFather: (sku, body) =>
|
||||||
|
this.#json('POST', `/api/article/${enc(sku)}/father`, { body }),
|
||||||
|
/** GET /api/article/{sku}/children */
|
||||||
|
getChildren: (sku) =>
|
||||||
|
this.#json('GET', `/api/article/${enc(sku)}/children`),
|
||||||
|
/** POST /api/article/{sku}/children */
|
||||||
|
addChild: (sku, body) =>
|
||||||
|
this.#json('POST', `/api/article/${enc(sku)}/children`, { body }),
|
||||||
|
/** DELETE /api/article/{sku}/children/{childSku} */
|
||||||
|
removeChild: (sku, childSku, { deleteArticle = false } = {}) =>
|
||||||
|
this.#json(
|
||||||
|
'DELETE',
|
||||||
|
`/api/article/${enc(sku)}/children/${enc(childSku)}${deleteArticle ? '?deleteArticle=true' : ''}`
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.posterminal = {
|
this.posterminal = {
|
||||||
|
|||||||
@@ -479,6 +479,46 @@ export const article = {
|
|||||||
{ method: 'DELETE' }
|
{ method: 'DELETE' }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/article/:sku/father
|
||||||
|
* Body: { isFather: boolean, attributeNameId?: number }
|
||||||
|
*/
|
||||||
|
toggleFather(sku, body) {
|
||||||
|
return http(`/api/article/${encodeURIComponent(sku)}/father`, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/article/:sku/children → { children: [...] }
|
||||||
|
*/
|
||||||
|
getChildren(sku) {
|
||||||
|
return http(`/api/article/${encodeURIComponent(sku)}/children`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/article/:sku/children
|
||||||
|
* Body: { attributeValueId?: number, value?: string, name?: string }
|
||||||
|
*/
|
||||||
|
addChild(sku, body) {
|
||||||
|
return http(`/api/article/${encodeURIComponent(sku)}/children`, {
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/article/:sku/children/:childSku
|
||||||
|
*/
|
||||||
|
removeChild(sku, childSku, options = {}) {
|
||||||
|
const qs = options?.deleteArticle ? '?deleteArticle=true' : '';
|
||||||
|
return http(
|
||||||
|
`/api/article/${encodeURIComponent(sku)}/children/${encodeURIComponent(childSku)}${qs}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
ARTICLE_TAB,
|
ARTICLE_TAB,
|
||||||
DEFAULT_ARTICLE_TAB,
|
DEFAULT_ARTICLE_TAB,
|
||||||
artikelDetailPath,
|
artikelDetailPath,
|
||||||
articleShowsCompoundTab,
|
|
||||||
articleSku,
|
articleSku,
|
||||||
articleTabFromPathname,
|
articleTabFromPathname,
|
||||||
readPreferredArticleTab,
|
readPreferredArticleTab,
|
||||||
@@ -27,7 +26,6 @@ import { matchPath } from 'react-router-dom';
|
|||||||
import { PanelTile } from 'dashgui/panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from 'dashgui/panels/panelTileLayout';
|
import { PANEL_TILE_BORDER_RADIUS, responsiveColumnSpan } from 'dashgui/panels/panelTileLayout';
|
||||||
import ArticleAttributesTab from './tabs/ArticleAttributesTab';
|
import ArticleAttributesTab from './tabs/ArticleAttributesTab';
|
||||||
import ArticleCompoundTab from './tabs/ArticleCompoundTab';
|
|
||||||
import ArticleHistoryTab from './tabs/ArticleHistoryTab';
|
import ArticleHistoryTab from './tabs/ArticleHistoryTab';
|
||||||
import ArticleLabelTab from './tabs/ArticleLabelTab';
|
import ArticleLabelTab from './tabs/ArticleLabelTab';
|
||||||
import ArticleSeoTab from './tabs/ArticleSeoTab';
|
import ArticleSeoTab from './tabs/ArticleSeoTab';
|
||||||
@@ -73,14 +71,6 @@ const ALWAYS_TABS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const COMPOUND_TAB = {
|
|
||||||
id: ARTICLE_TAB.compound,
|
|
||||||
label: 'Compound Editor',
|
|
||||||
color: '#e9d5ff',
|
|
||||||
Icon: AccountTreeIcon,
|
|
||||||
help: 'Stückliste und Unterartikel für diesen Set-Artikel verwalten',
|
|
||||||
};
|
|
||||||
|
|
||||||
const SETTINGS_TAB = {
|
const SETTINGS_TAB = {
|
||||||
id: ARTICLE_TAB.settings,
|
id: ARTICLE_TAB.settings,
|
||||||
label: 'Einstellungen',
|
label: 'Einstellungen',
|
||||||
@@ -89,13 +79,8 @@ const SETTINGS_TAB = {
|
|||||||
help: 'Artikeleinstellungen, Einheiten, Steuersatz und Löschen',
|
help: 'Artikeleinstellungen, Einheiten, Steuersatz und Löschen',
|
||||||
};
|
};
|
||||||
|
|
||||||
function visibleTabs(article) {
|
function visibleTabs() {
|
||||||
const tabs = [...ALWAYS_TABS];
|
return [...ALWAYS_TABS, SETTINGS_TAB];
|
||||||
if (articleShowsCompoundTab(article)) {
|
|
||||||
tabs.push(COMPOUND_TAB);
|
|
||||||
}
|
|
||||||
tabs.push(SETTINGS_TAB);
|
|
||||||
return tabs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readRouteTab() {
|
function readRouteTab() {
|
||||||
@@ -185,8 +170,6 @@ class ArticleTileTab extends PureComponent {
|
|||||||
return <ArticleLabelTab article={articleData} />;
|
return <ArticleLabelTab article={articleData} />;
|
||||||
case ARTICLE_TAB.attributes:
|
case ARTICLE_TAB.attributes:
|
||||||
return <ArticleAttributesTab article={articleData} sku={articleData.sku} />;
|
return <ArticleAttributesTab article={articleData} sku={articleData.sku} />;
|
||||||
case ARTICLE_TAB.compound:
|
|
||||||
return <ArticleCompoundTab />;
|
|
||||||
case ARTICLE_TAB.settings:
|
case ARTICLE_TAB.settings:
|
||||||
return (
|
return (
|
||||||
<ArticleSettingsTab
|
<ArticleSettingsTab
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ export const ARTICLE_TAB = {
|
|||||||
seo: 'seo',
|
seo: 'seo',
|
||||||
label: 'label',
|
label: 'label',
|
||||||
attributes: 'attributes',
|
attributes: 'attributes',
|
||||||
compound: 'compound',
|
|
||||||
settings: 'settings',
|
settings: 'settings',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,22 +30,8 @@ export function articleSku(article) {
|
|||||||
return sku || null;
|
return sku || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function articleShowsCompoundTab(article) {
|
export function articleTabIds() {
|
||||||
return (
|
return [...BASE_ARTICLE_TAB_IDS, ARTICLE_TAB.settings];
|
||||||
article?.articleListCount > 0 ||
|
|
||||||
article?.optionsCount > 0 ||
|
|
||||||
article?.childCount > 0 ||
|
|
||||||
article?.fatherArticleId != null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function articleTabIds(article) {
|
|
||||||
const ids = [...BASE_ARTICLE_TAB_IDS];
|
|
||||||
if (articleShowsCompoundTab(article)) {
|
|
||||||
ids.push(ARTICLE_TAB.compound);
|
|
||||||
}
|
|
||||||
ids.push(ARTICLE_TAB.settings);
|
|
||||||
return ids;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isArticleTabId(tab) {
|
export function isArticleTabId(tab) {
|
||||||
|
|||||||
@@ -1,64 +1,729 @@
|
|||||||
import { Component, PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
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 FormControl from '@mui/material/FormControl';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
import Link from '@mui/material/Link';
|
import Link from '@mui/material/Link';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import Typography from '@mui/material/Typography';
|
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 PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||||
|
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
||||||
import { PanelTile } from 'dashgui/panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { artikelDetailPath } from '../articleRoute';
|
import { artikelDetailPath } from '../articleRoute';
|
||||||
|
import { article as articleApi } from '../../../api';
|
||||||
class CountLine extends Component {
|
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
|
||||||
render() {
|
import artikelAutomat from '../../../automats/artikelAutomat';
|
||||||
const { label, count } = this.props;
|
import { notifyError } from '../../../utils/notify';
|
||||||
if (count == null) return null;
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
return (
|
|
||||||
<Typography variant="body2">
|
|
||||||
{label}: {count}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RelationLine extends Component {
|
|
||||||
render() {
|
|
||||||
const { label, active, children } = this.props;
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" sx={{ mb: 0.5 }}>
|
|
||||||
{label}:{' '}
|
|
||||||
{active ? (
|
|
||||||
children ?? 'ja'
|
|
||||||
) : (
|
|
||||||
<Typography component="span" variant="body2" color="text.secondary">
|
|
||||||
nein
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ArticleViewLists extends PureComponent {
|
export default class ArticleViewLists extends PureComponent {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
const initialAttributes = selectAttributes() || [];
|
||||||
|
this.state = {
|
||||||
|
attributes: initialAttributes,
|
||||||
|
assignedAttributes: [],
|
||||||
|
busy: false,
|
||||||
|
isActivating: false,
|
||||||
|
selectedAttributeId: '',
|
||||||
|
selectedChildValueId: '',
|
||||||
|
newChildText: '',
|
||||||
|
isCustomChildValue: false,
|
||||||
|
|
||||||
|
// Remove dialog
|
||||||
|
removeTargetChild: null, // { sku, attributeValue }
|
||||||
|
};
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attributeAutomat.state.status === 'idle') {
|
||||||
|
void attributeAutomat.actions.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.article?.sku) {
|
||||||
|
void this.loadAssignedAttributes(this.props.article.sku);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.article?.sku !== this.props.article?.sku) {
|
||||||
|
if (this.props.article?.sku) {
|
||||||
|
void this.loadAssignedAttributes(this.props.article.sku);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._cancelled = true;
|
||||||
|
this.unsubAttr?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
getApi() {
|
||||||
|
return this.context?.article || articleApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStartActivating = async () => {
|
||||||
|
this.setState({ isActivating: true });
|
||||||
|
await this.loadAssignedAttributes(this.props.article?.sku);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleEnableFather = async () => {
|
||||||
|
const { article } = this.props;
|
||||||
|
const { selectedAttributeId, assignedAttributes, busy } = this.state;
|
||||||
|
if (busy || !article?.sku || !selectedAttributeId) 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.toggleFather(article.sku, {
|
||||||
|
isFather: 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, isActivating: false });
|
||||||
|
} catch (err) {
|
||||||
|
if (this._cancelled) return;
|
||||||
|
notifyError(err);
|
||||||
|
this.setState({ busy: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleDisableFather = 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.toggleFather(article.sku, { isFather: 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { article: articleData } = this.props;
|
const { article: articleData } = this.props;
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
assignedAttributes,
|
||||||
|
busy,
|
||||||
|
isActivating,
|
||||||
|
selectedAttributeId,
|
||||||
|
selectedChildValueId,
|
||||||
|
newChildText,
|
||||||
|
isCustomChildValue,
|
||||||
|
removeTargetChild,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (!articleData) return null;
|
||||||
|
|
||||||
|
const isChild = articleData.fatherArticleId != null || Boolean(articleData.fatherSku);
|
||||||
|
const isFather = Boolean(articleData.isFather);
|
||||||
|
const children = Array.isArray(articleData.children) ? articleData.children : [];
|
||||||
|
|
||||||
|
// Attributes assigned to this article that cannot be selected for father mode
|
||||||
|
const assignedAttributeIds = new Set(
|
||||||
|
(assignedAttributes || []).map((a) => a.attributeId)
|
||||||
|
);
|
||||||
|
const selectableAttributes = attributes.filter(
|
||||||
|
(a) => !assignedAttributeIds.has(a.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find father attribute definition & values
|
||||||
|
const fatherAttrDef = isFather
|
||||||
|
? attributes.find((a) => a.id === articleData.fatherAttributeNameId)
|
||||||
|
: null;
|
||||||
|
const allFatherValues = fatherAttrDef?.values || [];
|
||||||
|
const assignedValueIds = new Set(
|
||||||
|
children.map((c) => c.attributeValueId).filter((id) => id != null)
|
||||||
|
);
|
||||||
|
const availableFatherValues = allFatherValues.filter((v) => !assignedValueIds.has(v.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTile wide={1} minHeight={200} color="#fbcfe8">
|
<PanelTile wide={1} minHeight={200} color="#fbcfe8">
|
||||||
<RelationLine label="Vaterartikel" active={articleData.childCount > 0}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 1 }}>
|
||||||
{articleData.childCount}
|
{/* If article is a Child / Variant of another article, show link to its father */}
|
||||||
</RelationLine>
|
{isChild && articleData.fatherSku ? (
|
||||||
<RelationLine
|
<Box
|
||||||
label="Kindartikel"
|
sx={{
|
||||||
active={articleData.fatherArticleId != null}
|
display: 'flex',
|
||||||
>
|
alignItems: 'center',
|
||||||
{articleData.fatherSku ? (
|
justifyContent: 'space-between',
|
||||||
<Link
|
px: 1,
|
||||||
component={RouterLink}
|
py: 0.5,
|
||||||
to={artikelDetailPath(articleData.fatherSku)}
|
borderRadius: 1,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{articleData.fatherSku}
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||||
</Link>
|
<AccountTreeIcon sx={{ color: 'text.secondary', fontSize: 16 }} />
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Variante von:
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to={artikelDetailPath(articleData.fatherSku)}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': { textDecoration: 'underline' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{articleData.fatherSku}
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
</RelationLine>
|
|
||||||
<CountLine label="Optionen" count={articleData.optionsCount} />
|
{/* Father Mode or Activation (supported on both root and child articles) */}
|
||||||
<CountLine label="Stücklisten" count={articleData.articleListCount} />
|
{isFather ? (
|
||||||
<CountLine label="Varianten" count={articleData.childCount} />
|
<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 }}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, fontSize: '0.88rem' }}>
|
||||||
|
Vaterartikel
|
||||||
|
</Typography>
|
||||||
|
<Chip
|
||||||
|
label={articleData.fatherAttributeName || fatherAttrDef?.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.handleDisableFather}
|
||||||
|
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 || availableFatherValues.length === 0 ? (
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
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,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
'& .MuiInputBase-input': { py: 0.5, px: 1, fontSize: '0.82rem' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormControl size="small" sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Select
|
||||||
|
size="small"
|
||||||
|
value={selectedChildValueId}
|
||||||
|
displayEmpty
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.setState({ selectedChildValueId: e.target.value })}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
height: 32,
|
||||||
|
'& .MuiSelect-select': { py: 0.5, px: 1 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled>
|
||||||
|
<em>Variante wählen...</em>
|
||||||
|
</MenuItem>
|
||||||
|
{availableFatherValues.map((v) => (
|
||||||
|
<MenuItem key={v.id} value={v.id} sx={{ fontSize: '0.82rem' }}>
|
||||||
|
{v.value}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title="Variante hinzufügen">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
disabled={
|
||||||
|
busy ||
|
||||||
|
(isCustomChildValue || availableFatherValues.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 */}
|
||||||
|
{allFatherValues.length > 0 && availableFatherValues.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,
|
||||||
|
maxHeight: 160,
|
||||||
|
overflowY: 'auto',
|
||||||
|
pr: 0.25,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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.4,
|
||||||
|
borderRadius: 1,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{child.attributeValue || child.name || child.sku}
|
||||||
|
</Typography>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to={artikelDetailPath(child.sku)}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
color: 'text.secondary',
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': { textDecoration: 'underline' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{child.sku}
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => this.handleOpenRemoveDialog(child)}
|
||||||
|
sx={{
|
||||||
|
p: 0.25,
|
||||||
|
color: 'text.secondary',
|
||||||
|
'&:hover': { color: 'error.main' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteOutlinedIcon sx={{ fontSize: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
/* Case 3: Regular Article (neither father nor child) */
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
Vaterartikel:
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
nein
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{!isActivating ? (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
onClick={this.handleStartActivating}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.78rem',
|
||||||
|
textTransform: 'none',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderColor: 'divider',
|
||||||
|
py: 0.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Als Vaterartikel aktivieren
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 1,
|
||||||
|
p: 1,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 1,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectableAttributes.length === 0 ? (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<FormControl size="small" fullWidth>
|
||||||
|
<InputLabel id="activate-attr-label" sx={{ fontSize: '0.8rem' }}>
|
||||||
|
Merkmal für Varianten
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="activate-attr-label"
|
||||||
|
label="Merkmal für Varianten"
|
||||||
|
value={selectedAttributeId}
|
||||||
|
size="small"
|
||||||
|
onChange={(e) => this.setState({ selectedAttributeId: e.target.value })}
|
||||||
|
sx={{ fontSize: '0.82rem', height: 32 }}
|
||||||
|
>
|
||||||
|
{attributes.map((a) => {
|
||||||
|
const isAssigned = assignedAttributeIds.has(a.id);
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={a.id}
|
||||||
|
value={a.id}
|
||||||
|
disabled={isAssigned}
|
||||||
|
sx={{ fontSize: '0.82rem' }}
|
||||||
|
>
|
||||||
|
{a.name}
|
||||||
|
{isAssigned ? ' (bereits zugewiesen)' : ''}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 0.75, justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => this.setState({ isActivating: false })}
|
||||||
|
sx={{ fontSize: '0.75rem', py: 0.25 }}
|
||||||
|
>
|
||||||
|
Abbrechen
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
disabled={
|
||||||
|
busy ||
|
||||||
|
!selectedAttributeId ||
|
||||||
|
assignedAttributeIds.has(Number(selectedAttributeId)) ||
|
||||||
|
selectableAttributes.length === 0
|
||||||
|
}
|
||||||
|
onClick={this.handleEnableFather}
|
||||||
|
sx={{ fontSize: '0.75rem', py: 0.25 }}
|
||||||
|
>
|
||||||
|
{busy ? <CircularProgress size={14} color="inherit" /> : 'Aktivieren'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Counts Line at Bottom */}
|
||||||
|
{(articleData.optionsCount != null || articleData.articleListCount != null) && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: 'auto',
|
||||||
|
pt: 0.75,
|
||||||
|
borderTop: '1px solid',
|
||||||
|
borderColor: 'rgba(0,0,0,0.08)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{articleData.optionsCount != null && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Optionen: {articleData.optionsCount}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{articleData.articleListCount != null && (
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Stücklisten: {articleData.articleListCount}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Remove Child Confirmation Dialog */}
|
||||||
|
<Dialog
|
||||||
|
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>
|
||||||
|
</Dialog>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Component } from 'react';
|
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
|
|
||||||
export default class ArticleCompoundTab extends Component {
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Compound Editor
|
|
||||||
</Typography>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user