This commit is contained in:
seb
2026-09-29 18:50:27 +02:00
parent 1b9c39e975
commit d8f4727f32
6 changed files with 772 additions and 96 deletions

View File

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

View File

@@ -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' }
);
},
}; };
/** /**

View File

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

View File

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

View File

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

View File

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