diff --git a/src/components/article/ArticleUnifyDialog.jsx b/src/components/article/ArticleUnifyDialog.jsx
new file mode 100644
index 0000000..ddfae22
--- /dev/null
+++ b/src/components/article/ArticleUnifyDialog.jsx
@@ -0,0 +1,1214 @@
+import { Component } from 'react';
+import Box from '@mui/material/Box';
+import Button from '@mui/material/Button';
+import DialogTitle from '@mui/material/DialogTitle';
+import DialogContent from '@mui/material/DialogContent';
+import DialogActions from '@mui/material/DialogActions';
+import Table from '@mui/material/Table';
+import TableBody from '@mui/material/TableBody';
+import TableCell from '@mui/material/TableCell';
+import TableHead from '@mui/material/TableHead';
+import TableRow from '@mui/material/TableRow';
+import Typography from '@mui/material/Typography';
+import Chip from '@mui/material/Chip';
+import Radio from '@mui/material/Radio';
+import Checkbox from '@mui/material/Checkbox';
+import IconButton from '@mui/material/IconButton';
+import Tooltip from '@mui/material/Tooltip';
+import Stack from '@mui/material/Stack';
+import CloseIcon from '@mui/icons-material/Close';
+import CallMergeIcon from '@mui/icons-material/CallMerge';
+import QrCodeIcon from '@mui/icons-material/QrCode';
+import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
+import HistoryOutlinedIcon from '@mui/icons-material/HistoryOutlined';
+import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
+
+import AppDialog from '../AppDialog';
+import ArticlePhoto from './ArticlePhoto';
+import { article as articleApi } from '../../api';
+import brandAutomat from '../../automats/brandAutomat';
+import categoryAutomat from '../../automats/categoryAutomat';
+import { formatPrice } from '../../utils/price';
+import { LIGHT_4 } from '../../colors';
+
+export default class ArticleUnifyDialog extends Component {
+ constructor(props) {
+ super(props);
+
+ const left = props.articleLeft || {};
+ const right = props.articleRight || {};
+
+ const leftCategoryIds = left.categoryIds || (Array.isArray(left.categories) ? left.categories.map((c) => c.id || c) : []);
+ const rightCategoryIds = right.categoryIds || (Array.isArray(right.categories) ? right.categories.map((c) => c.id || c) : []);
+ const initialCategories = new Set([...leftCategoryIds, ...rightCategoryIds].map(String));
+
+ const leftPhotos = Array.isArray(left.photos) ? left.photos : Array.isArray(left.articlepic) ? left.articlepic : [];
+ const rightPhotos = Array.isArray(right.photos) ? right.photos : Array.isArray(right.articlepic) ? right.articlepic : [];
+ const initialImages = new Set([
+ ...leftPhotos.map((p) => p.fullHash || p.hash).filter(Boolean),
+ ...rightPhotos.map((p) => p.fullHash || p.hash).filter(Boolean),
+ ]);
+
+ const leftEans = Array.isArray(left.eans) ? left.eans : left.eans ? [left.eans] : [];
+ const rightEans = Array.isArray(right.eans) ? right.eans : right.eans ? [right.eans] : [];
+ const initialEans = new Set([...leftEans, ...rightEans].map((e) => (typeof e === 'object' ? e.ean : String(e))).filter(Boolean));
+
+ const leftOptions = Array.isArray(left.options) ? left.options : [];
+ const rightOptions = Array.isArray(right.options) ? right.options : [];
+ const initialOptions = new Set([
+ ...leftOptions.map((o) => o.sku || o.id),
+ ...rightOptions.map((o) => o.sku || o.id),
+ ]);
+
+ this.state = {
+ loadingDetails: true,
+ leftAttributes: [],
+ rightAttributes: [],
+ leftProperties: [],
+ rightProperties: [],
+ leftSuppliers: [],
+ rightSuppliers: [],
+ leftHistory: [],
+ rightHistory: [],
+
+ // Selection state
+ selectedName: 'left', // 'left' | 'right'
+ selectedBrand: 'left', // 'left' | 'right'
+ selectedSku: 'left', // 'left' | 'right'
+ selectedChildState: left.parentSku ? 'left' : right.parentSku ? 'right' : 'left',
+ selectedCategories: initialCategories,
+ selectedImages: initialImages,
+ selectedEans: initialEans,
+ selectedPrices: { left: true, right: true },
+ selectedStock: 'both', // 'both' | 'left' | 'right'
+ selectedAttributes: new Set(),
+ selectedProperties: new Set(),
+ selectedHistory: { left: true, right: true },
+ selectedSuppliers: new Set(),
+ selectedOptions: initialOptions,
+ };
+ }
+
+ async componentDidMount() {
+ const { articleLeft, articleRight } = this.props;
+ const leftSku = articleLeft?.sku;
+ const rightSku = articleRight?.sku;
+
+ try {
+ const [
+ leftAttrRes,
+ rightAttrRes,
+ leftPropRes,
+ rightPropRes,
+ leftSupRes,
+ rightSupRes,
+ leftHistRes,
+ rightHistRes,
+ ] = await Promise.allSettled([
+ leftSku ? articleApi.getAttributeValues(leftSku) : Promise.resolve({ attributevalues: [] }),
+ rightSku ? articleApi.getAttributeValues(rightSku) : Promise.resolve({ attributevalues: [] }),
+ leftSku ? articleApi.getProperties(leftSku) : Promise.resolve({ properties: [] }),
+ rightSku ? articleApi.getProperties(rightSku) : Promise.resolve({ properties: [] }),
+ leftSku ? articleApi.getSuppliers(leftSku) : Promise.resolve({ suppliers: [] }),
+ rightSku ? articleApi.getSuppliers(rightSku) : Promise.resolve({ suppliers: [] }),
+ leftSku ? articleApi.getBySkuHistory(leftSku, { limit: 20 }) : Promise.resolve({ history: [] }),
+ rightSku ? articleApi.getBySkuHistory(rightSku, { limit: 20 }) : Promise.resolve({ history: [] }),
+ ]);
+
+ const leftAttributes = leftAttrRes.status === 'fulfilled' && Array.isArray(leftAttrRes.value?.attributevalues)
+ ? leftAttrRes.value.attributevalues
+ : [];
+ const rightAttributes = rightAttrRes.status === 'fulfilled' && Array.isArray(rightAttrRes.value?.attributevalues)
+ ? rightAttrRes.value.attributevalues
+ : [];
+ const leftProperties = leftPropRes.status === 'fulfilled' && Array.isArray(leftPropRes.value?.properties)
+ ? leftPropRes.value.properties
+ : [];
+ const rightProperties = rightPropRes.status === 'fulfilled' && Array.isArray(rightPropRes.value?.properties)
+ ? rightPropRes.value.properties
+ : [];
+ const leftSuppliers = leftSupRes.status === 'fulfilled' && Array.isArray(leftSupRes.value?.suppliers)
+ ? leftSupRes.value.suppliers
+ : [];
+ const rightSuppliers = rightSupRes.status === 'fulfilled' && Array.isArray(rightSupRes.value?.suppliers)
+ ? rightSupRes.value.suppliers
+ : [];
+ const leftHistory = leftHistRes.status === 'fulfilled' && Array.isArray(leftHistRes.value?.history)
+ ? leftHistRes.value.history
+ : [];
+ const rightHistory = rightHistRes.status === 'fulfilled' && Array.isArray(rightHistRes.value?.history)
+ ? rightHistRes.value.history
+ : [];
+
+ const allAttrKeys = new Set([
+ ...leftAttributes.map((a) => `attr-${a.attribute_name_id}-${a.id}`),
+ ...rightAttributes.map((a) => `attr-${a.attribute_name_id}-${a.id}`),
+ ]);
+ const allPropKeys = new Set([
+ ...leftProperties.map((p) => `prop-${p.property_name_id}-${p.id}`),
+ ...rightProperties.map((p) => `prop-${p.property_name_id}-${p.id}`),
+ ]);
+ const allSupplierKeys = new Set([
+ ...leftSuppliers.map((s) => String(s.id || `${s.entityId}-${s.sku}`)),
+ ...rightSuppliers.map((s) => String(s.id || `${s.entityId}-${s.sku}`)),
+ ]);
+
+ this.setState({
+ loadingDetails: false,
+ leftAttributes,
+ rightAttributes,
+ leftProperties,
+ rightProperties,
+ leftSuppliers,
+ rightSuppliers,
+ leftHistory,
+ rightHistory,
+ selectedAttributes: allAttrKeys,
+ selectedProperties: allPropKeys,
+ selectedSuppliers: allSupplierKeys,
+ });
+ } catch {
+ this.setState({ loadingDetails: false });
+ }
+ }
+
+ toggleSetItem = (stateKey, itemKey) => {
+ this.setState((prev) => {
+ const next = new Set(prev[stateKey]);
+ if (next.has(itemKey)) {
+ next.delete(itemKey);
+ } else {
+ next.add(itemKey);
+ }
+ return { [stateKey]: next };
+ });
+ };
+
+ toggleSideInSet = (stateKey, sideItems, isAdd) => {
+ this.setState((prev) => {
+ const next = new Set(prev[stateKey]);
+ for (const item of sideItems) {
+ if (isAdd) next.add(item);
+ else next.delete(item);
+ }
+ return { [stateKey]: next };
+ });
+ };
+
+ resolveBrandName(article) {
+ if (!article) return '–';
+ const brands = brandAutomat.state.brands || [];
+ if (article.brandId != null) {
+ const b = brands.find((x) => x.id === article.brandId);
+ if (b?.name) return b.name;
+ }
+ if (typeof article.brand === 'object' && article.brand?.name) return article.brand.name;
+ if (typeof article.brand === 'string') return article.brand;
+ return 'Keine Marke';
+ }
+
+ resolveCategoryName(catId) {
+ const categoriesMap = categoryAutomat.state.categories || {};
+ const cat = categoriesMap[catId];
+ return cat?.name || `Kategorie #${catId}`;
+ }
+
+ render() {
+ const { open, onClose, articleLeft, articleRight } = this.props;
+ if (!open || !articleLeft || !articleRight) return null;
+
+ const {
+ leftAttributes,
+ rightAttributes,
+ leftProperties,
+ rightProperties,
+ leftSuppliers,
+ rightSuppliers,
+ leftHistory,
+ rightHistory,
+ selectedName,
+ selectedBrand,
+ selectedSku,
+ selectedChildState,
+ selectedCategories,
+ selectedImages,
+ selectedEans,
+ selectedPrices,
+ selectedStock,
+ selectedAttributes,
+ selectedProperties,
+ selectedHistory,
+ selectedSuppliers,
+ selectedOptions,
+ } = this.state;
+
+ const left = articleLeft;
+ const right = articleRight;
+
+ const leftCategoryIds = (left.categoryIds || (Array.isArray(left.categories) ? left.categories.map((c) => c.id || c) : [])).map(String);
+ const rightCategoryIds = (right.categoryIds || (Array.isArray(right.categories) ? right.categories.map((c) => c.id || c) : [])).map(String);
+
+ const leftPhotos = Array.isArray(left.photos) ? left.photos : Array.isArray(left.articlepic) ? left.articlepic : [];
+ const rightPhotos = Array.isArray(right.photos) ? right.photos : Array.isArray(right.articlepic) ? right.articlepic : [];
+
+ const leftEans = (Array.isArray(left.eans) ? left.eans : left.eans ? [left.eans] : []).map((e) => (typeof e === 'object' ? e.ean : String(e))).filter(Boolean);
+ const rightEans = (Array.isArray(right.eans) ? right.eans : right.eans ? [right.eans] : []).map((e) => (typeof e === 'object' ? e.ean : String(e))).filter(Boolean);
+
+ const leftOptions = Array.isArray(left.options) ? left.options : [];
+ const rightOptions = Array.isArray(right.options) ? right.options : [];
+
+ const hasLeftAttrsProps = leftAttributes.length > 0 || leftProperties.length > 0;
+ const hasRightAttrsProps = rightAttributes.length > 0 || rightProperties.length > 0;
+
+ // Helper cell styling with strict overflow containment
+ const cellStyle = (isSelected, isActionable = true) => ({
+ p: 1.5,
+ cursor: isActionable ? 'pointer' : 'default',
+ bgcolor: isSelected && isActionable ? 'action.selected' : 'inherit',
+ transition: 'background-color 0.15s ease',
+ borderLeft: isSelected && isActionable ? '3px solid' : '1px solid',
+ borderLeftColor: isSelected && isActionable ? 'primary.main' : 'divider',
+ overflow: 'hidden',
+ wordBreak: 'break-word',
+ overflowWrap: 'break-word',
+ '&:hover': {
+ bgcolor: isActionable ? (isSelected ? 'action.selected' : 'action.hover') : 'inherit',
+ },
+ });
+
+ return (
+
+ {/* Title bar */}
+
+
+ Artikel vereinen
+
+
+
+
+
+
+ {/* Comparison & Selection Table with fixed layout */}
+
+
+
+
+
+
+
+
+ {/* 1. Name */}
+
+
+ Name
+
+ this.setState({ selectedName: 'left' })}
+ >
+
+
+
+ {left.name || '–'}
+
+
+
+ this.setState({ selectedName: 'right' })}
+ >
+
+
+
+ {right.name || '–'}
+
+
+
+
+
+ {/* 2. Brand */}
+
+
+ Marke
+
+ this.setState({ selectedBrand: 'left' })}
+ >
+
+
+
+ {this.resolveBrandName(left)}
+
+
+
+ this.setState({ selectedBrand: 'right' })}
+ >
+
+
+
+ {this.resolveBrandName(right)}
+
+
+
+
+
+ {/* 3. Categories */}
+
+
+ Kategorien
+
+ 0 && leftCategoryIds.some((id) => selectedCategories.has(id)), leftCategoryIds.length > 0)}>
+ {leftCategoryIds.length === 0 ? (
+ Keine Kategorien zugewiesen
+ ) : (
+
+ selectedCategories.has(id))}
+ indeterminate={leftCategoryIds.some((id) => selectedCategories.has(id)) && !leftCategoryIds.every((id) => selectedCategories.has(id))}
+ onChange={(e) => this.toggleSideInSet('selectedCategories', leftCategoryIds, e.target.checked)}
+ />
+
+
+ {leftCategoryIds.map((catId) => {
+ const isChecked = selectedCategories.has(catId);
+ return (
+ this.toggleSetItem('selectedCategories', catId)}
+ sx={{ maxWidth: '100%' }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+ 0 && rightCategoryIds.some((id) => selectedCategories.has(id)), rightCategoryIds.length > 0)}>
+ {rightCategoryIds.length === 0 ? (
+ Keine Kategorien zugewiesen
+ ) : (
+
+ selectedCategories.has(id))}
+ indeterminate={rightCategoryIds.some((id) => selectedCategories.has(id)) && !rightCategoryIds.every((id) => selectedCategories.has(id))}
+ onChange={(e) => this.toggleSideInSet('selectedCategories', rightCategoryIds, e.target.checked)}
+ />
+
+
+ {rightCategoryIds.map((catId) => {
+ const isChecked = selectedCategories.has(catId);
+ return (
+ this.toggleSetItem('selectedCategories', catId)}
+ sx={{ maxWidth: '100%' }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* 4. Images */}
+
+
+ Bilder
+
+ 0 && leftPhotos.some((p) => selectedImages.has(p.fullHash || p.hash)), leftPhotos.length > 0)}>
+ {leftPhotos.length === 0 ? (
+ Keine Bilder vorhanden
+ ) : (
+
+ selectedImages.has(p.fullHash || p.hash))}
+ indeterminate={leftPhotos.some((p) => selectedImages.has(p.fullHash || p.hash)) && !leftPhotos.every((p) => selectedImages.has(p.fullHash || p.hash))}
+ onChange={(e) =>
+ this.toggleSideInSet(
+ 'selectedImages',
+ leftPhotos.map((p) => p.fullHash || p.hash).filter(Boolean),
+ e.target.checked,
+ )
+ }
+ />
+
+
+ {leftPhotos.map((photo, i) => {
+ const hash = photo.fullHash || photo.hash || String(i);
+ const isChecked = selectedImages.has(hash);
+ return (
+ this.toggleSetItem('selectedImages', hash)}
+ sx={{
+ position: 'relative',
+ border: '2px solid',
+ borderColor: isChecked ? 'primary.main' : 'divider',
+ borderRadius: 1,
+ overflow: 'hidden',
+ cursor: 'pointer',
+ }}
+ >
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+ 0 && rightPhotos.some((p) => selectedImages.has(p.fullHash || p.hash)), rightPhotos.length > 0)}>
+ {rightPhotos.length === 0 ? (
+ Keine Bilder vorhanden
+ ) : (
+
+ selectedImages.has(p.fullHash || p.hash))}
+ indeterminate={rightPhotos.some((p) => selectedImages.has(p.fullHash || p.hash)) && !rightPhotos.every((p) => selectedImages.has(p.fullHash || p.hash))}
+ onChange={(e) =>
+ this.toggleSideInSet(
+ 'selectedImages',
+ rightPhotos.map((p) => p.fullHash || p.hash).filter(Boolean),
+ e.target.checked,
+ )
+ }
+ />
+
+
+ {rightPhotos.map((photo, i) => {
+ const hash = photo.fullHash || photo.hash || String(i);
+ const isChecked = selectedImages.has(hash);
+ return (
+ this.toggleSetItem('selectedImages', hash)}
+ sx={{
+ position: 'relative',
+ border: '2px solid',
+ borderColor: isChecked ? 'secondary.main' : 'divider',
+ borderRadius: 1,
+ overflow: 'hidden',
+ cursor: 'pointer',
+ }}
+ >
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* 5. ArtNR / SKU */}
+
+
+ Artikelnummer
+
+ this.setState({ selectedSku: 'left' })}
+ >
+
+
+
+
+
+ this.setState({ selectedSku: 'right' })}
+ >
+
+
+
+
+
+
+
+ {/* 6. EANs */}
+
+
+ EANs
+
+ 0 && leftEans.some((e) => selectedEans.has(e)), leftEans.length > 0)}>
+ {leftEans.length === 0 ? null : (
+
+ selectedEans.has(e))}
+ indeterminate={leftEans.some((e) => selectedEans.has(e)) && !leftEans.every((e) => selectedEans.has(e))}
+ onChange={(e) => this.toggleSideInSet('selectedEans', leftEans, e.target.checked)}
+ />
+
+
+ {leftEans.map((ean) => {
+ const isChecked = selectedEans.has(ean);
+ return (
+ }
+ label={ean}
+ size="small"
+ color={isChecked ? 'primary' : 'default'}
+ variant={isChecked ? 'filled' : 'outlined'}
+ onClick={() => this.toggleSetItem('selectedEans', ean)}
+ sx={{ maxWidth: '100%' }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+ 0 && rightEans.some((e) => selectedEans.has(e)), rightEans.length > 0)}>
+ {rightEans.length === 0 ? null : (
+
+ selectedEans.has(e))}
+ indeterminate={rightEans.some((e) => selectedEans.has(e)) && !rightEans.every((e) => selectedEans.has(e))}
+ onChange={(e) => this.toggleSideInSet('selectedEans', rightEans, e.target.checked)}
+ />
+
+
+ {rightEans.map((ean) => {
+ const isChecked = selectedEans.has(ean);
+ return (
+ }
+ label={ean}
+ size="small"
+ color={isChecked ? 'secondary' : 'default'}
+ variant={isChecked ? 'filled' : 'outlined'}
+ onClick={() => this.toggleSetItem('selectedEans', ean)}
+ sx={{ maxWidth: '100%' }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* 7. Prices */}
+
+
+ Preise
+
+
+
+ this.setState({ selectedPrices: { ...selectedPrices, left: e.target.checked } })}
+ />
+
+
+ {formatPrice(left.price ?? left.vknetto ?? left.net) || '0,00 €'} Netto
+
+ {Array.isArray(left.prices) && left.prices.length > 0 && (
+
+ {left.prices.length} Staffelpreis(e) definiert
+
+ )}
+
+
+
+
+
+ this.setState({ selectedPrices: { ...selectedPrices, right: e.target.checked } })}
+ />
+
+
+ {formatPrice(right.price ?? right.vknetto ?? right.net) || '0,00 €'} Netto
+
+ {Array.isArray(right.prices) && right.prices.length > 0 && (
+
+ {right.prices.length} Staffelpreis(e) definiert
+
+ )}
+
+
+
+
+
+ {/* 8. Stock Levels */}
+
+
+ Lagerbestände
+
+ this.setState({ selectedStock: selectedStock === 'right' ? 'both' : 'left' })}
+ >
+
+ {
+ if (e.target.checked) {
+ this.setState({ selectedStock: selectedStock === 'right' ? 'both' : 'left' });
+ } else {
+ this.setState({ selectedStock: selectedStock === 'both' ? 'right' : 'left' });
+ }
+ }}
+ />
+
+
+ {left.stock ?? 0} Stück
+
+ {Array.isArray(left.warehouses) && left.warehouses.length > 0 && (
+
+ {left.warehouses.map((w) => `${w.name || 'Lager'}: ${w.stock ?? 0}`).join(', ')}
+
+ )}
+
+
+
+ this.setState({ selectedStock: selectedStock === 'left' ? 'both' : 'right' })}
+ >
+
+ {
+ if (e.target.checked) {
+ this.setState({ selectedStock: selectedStock === 'left' ? 'both' : 'right' });
+ } else {
+ this.setState({ selectedStock: selectedStock === 'both' ? 'left' : 'right' });
+ }
+ }}
+ />
+
+
+ {right.stock ?? 0} Stück
+
+ {Array.isArray(right.warehouses) && right.warehouses.length > 0 && (
+
+ {right.warehouses.map((w) => `${w.name || 'Lager'}: ${w.stock ?? 0}`).join(', ')}
+
+ )}
+ {selectedStock === 'both' && (
+
+ )}
+
+
+
+
+
+ {/* 9. Attributes and Properties */}
+
+
+ Attribute & Merkmale
+
+ selectedAttributes.has(`attr-${a.attribute_name_id}-${a.id}`)) || leftProperties.some((p) => selectedProperties.has(`prop-${p.property_name_id}-${p.id}`))), hasLeftAttrsProps)}>
+ {!hasLeftAttrsProps ? null : (
+
+ selectedAttributes.has(`attr-${a.attribute_name_id}-${a.id}`)) &&
+ leftProperties.every((p) => selectedProperties.has(`prop-${p.property_name_id}-${p.id}`))
+ }
+ onChange={(e) => {
+ const checked = e.target.checked;
+ this.toggleSideInSet('selectedAttributes', leftAttributes.map((a) => `attr-${a.attribute_name_id}-${a.id}`), checked);
+ this.toggleSideInSet('selectedProperties', leftProperties.map((p) => `prop-${p.property_name_id}-${p.id}`), checked);
+ }}
+ />
+
+
+ {leftAttributes.map((attr) => {
+ const key = `attr-${attr.attribute_name_id}-${attr.id}`;
+ const isChecked = selectedAttributes.has(key);
+ return (
+ this.toggleSetItem('selectedAttributes', key)}
+ sx={{
+ maxWidth: '100%',
+ height: 'auto',
+ py: 0.25,
+ '& .MuiChip-label': { whiteSpace: 'normal', wordBreak: 'break-word' },
+ }}
+ />
+ );
+ })}
+ {leftProperties.map((prop) => {
+ const key = `prop-${prop.property_name_id}-${prop.id}`;
+ const isChecked = selectedProperties.has(key);
+ return (
+ this.toggleSetItem('selectedProperties', key)}
+ sx={{
+ maxWidth: '100%',
+ height: 'auto',
+ py: 0.25,
+ '& .MuiChip-label': { whiteSpace: 'normal', wordBreak: 'break-word' },
+ }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+ selectedAttributes.has(`attr-${a.attribute_name_id}-${a.id}`)) || rightProperties.some((p) => selectedProperties.has(`prop-${p.property_name_id}-${p.id}`))), hasRightAttrsProps)}>
+ {!hasRightAttrsProps ? null : (
+
+ selectedAttributes.has(`attr-${a.attribute_name_id}-${a.id}`)) &&
+ rightProperties.every((p) => selectedProperties.has(`prop-${p.property_name_id}-${p.id}`))
+ }
+ onChange={(e) => {
+ const checked = e.target.checked;
+ this.toggleSideInSet('selectedAttributes', rightAttributes.map((a) => `attr-${a.attribute_name_id}-${a.id}`), checked);
+ this.toggleSideInSet('selectedProperties', rightProperties.map((p) => `prop-${p.property_name_id}-${p.id}`), checked);
+ }}
+ />
+
+
+ {rightAttributes.map((attr) => {
+ const key = `attr-${attr.attribute_name_id}-${attr.id}`;
+ const isChecked = selectedAttributes.has(key);
+ return (
+ this.toggleSetItem('selectedAttributes', key)}
+ sx={{
+ maxWidth: '100%',
+ height: 'auto',
+ py: 0.25,
+ '& .MuiChip-label': { whiteSpace: 'normal', wordBreak: 'break-word' },
+ }}
+ />
+ );
+ })}
+ {rightProperties.map((prop) => {
+ const key = `prop-${prop.property_name_id}-${prop.id}`;
+ const isChecked = selectedProperties.has(key);
+ return (
+ this.toggleSetItem('selectedProperties', key)}
+ sx={{
+ maxWidth: '100%',
+ height: 'auto',
+ py: 0.25,
+ '& .MuiChip-label': { whiteSpace: 'normal', wordBreak: 'break-word' },
+ }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* 10. History entries */}
+
+
+ Historie-Einträge
+
+ 0 && selectedHistory.left, leftHistory.length > 0)}>
+ {leftHistory.length === 0 ? (
+ Keine Historie-Einträge
+ ) : (
+
+ this.setState({ selectedHistory: { ...selectedHistory, left: e.target.checked } })}
+ />
+
+
+
+
+ {leftHistory.length} Einträge vorhanden
+
+
+ {leftHistory.slice(0, 2).map((h, i) => (
+
+ • {h.created_at ? new Date(h.created_at).toLocaleDateString('de-DE') : ''} {h.text || h.action || h.message}
+
+ ))}
+
+
+ )}
+
+ 0 && selectedHistory.right, rightHistory.length > 0)}>
+ {rightHistory.length === 0 ? (
+ Keine Historie-Einträge
+ ) : (
+
+ this.setState({ selectedHistory: { ...selectedHistory, right: e.target.checked } })}
+ />
+
+
+
+
+ {rightHistory.length} Einträge vorhanden
+
+
+ {rightHistory.slice(0, 2).map((h, i) => (
+
+ • {h.created_at ? new Date(h.created_at).toLocaleDateString('de-DE') : ''} {h.text || h.action || h.message}
+
+ ))}
+
+
+ )}
+
+
+
+ {/* 11. Connected suppliers */}
+
+
+ Lieferanten
+
+ 0 && leftSuppliers.some((s) => selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`))), leftSuppliers.length > 0)}>
+ {leftSuppliers.length === 0 ? null : (
+
+ selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`)))}
+ indeterminate={leftSuppliers.some((s) => selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`))) && !leftSuppliers.every((s) => selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`)))}
+ onChange={(e) => this.toggleSideInSet('selectedSuppliers', leftSuppliers.map((s) => String(s.id || `${s.entityId}-${s.sku}`)), e.target.checked)}
+ />
+
+
+ {leftSuppliers.map((s) => {
+ const key = String(s.id || `${s.entityId}-${s.sku}`);
+ const isChecked = selectedSuppliers.has(key);
+ return (
+ }
+ label={`${s.company || s.supplierName || 'Lieferant'} (Lief.-ArtNr: ${s.sku || '–'}, EK: ${formatPrice(s.netPrice) || '–'})`}
+ size="small"
+ color={isChecked ? 'primary' : 'default'}
+ variant={isChecked ? 'filled' : 'outlined'}
+ onClick={() => this.toggleSetItem('selectedSuppliers', key)}
+ sx={{
+ maxWidth: '100%',
+ height: 'auto',
+ py: 0.25,
+ '& .MuiChip-label': { whiteSpace: 'normal', wordBreak: 'break-word' },
+ }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+ 0 && rightSuppliers.some((s) => selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`))), rightSuppliers.length > 0)}>
+ {rightSuppliers.length === 0 ? null : (
+
+ selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`)))}
+ indeterminate={rightSuppliers.some((s) => selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`))) && !rightSuppliers.every((s) => selectedSuppliers.has(String(s.id || `${s.entityId}-${s.sku}`)))}
+ onChange={(e) => this.toggleSideInSet('selectedSuppliers', rightSuppliers.map((s) => String(s.id || `${s.entityId}-${s.sku}`)), e.target.checked)}
+ />
+
+
+ {rightSuppliers.map((s) => {
+ const key = String(s.id || `${s.entityId}-${s.sku}`);
+ const isChecked = selectedSuppliers.has(key);
+ return (
+ }
+ label={`${s.company || s.supplierName || 'Lieferant'} (Lief.-ArtNr: ${s.sku || '–'}, EK: ${formatPrice(s.netPrice) || '–'})`}
+ size="small"
+ color={isChecked ? 'secondary' : 'default'}
+ variant={isChecked ? 'filled' : 'outlined'}
+ onClick={() => this.toggleSetItem('selectedSuppliers', key)}
+ sx={{
+ maxWidth: '100%',
+ height: 'auto',
+ py: 0.25,
+ '& .MuiChip-label': { whiteSpace: 'normal', wordBreak: 'break-word' },
+ }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* 12. Options */}
+
+
+ Optionen
+
+ 0 && leftOptions.some((o) => selectedOptions.has(o.sku || o.id)), leftOptions.length > 0)}>
+ {leftOptions.length === 0 ? null : (
+
+ selectedOptions.has(o.sku || o.id))}
+ indeterminate={leftOptions.some((o) => selectedOptions.has(o.sku || o.id)) && !leftOptions.every((o) => selectedOptions.has(o.sku || o.id))}
+ onChange={(e) => this.toggleSideInSet('selectedOptions', leftOptions.map((o) => o.sku || o.id), e.target.checked)}
+ />
+
+
+ {leftOptions.map((opt) => {
+ const key = opt.sku || opt.id;
+ const isChecked = selectedOptions.has(key);
+ return (
+ }
+ label={`${opt.name || opt.sku} (${opt.amount || 1}x)`}
+ size="small"
+ color={isChecked ? 'primary' : 'default'}
+ variant={isChecked ? 'filled' : 'outlined'}
+ onClick={() => this.toggleSetItem('selectedOptions', key)}
+ sx={{ maxWidth: '100%' }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+ 0 && rightOptions.some((o) => selectedOptions.has(o.sku || o.id)), rightOptions.length > 0)}>
+ {rightOptions.length === 0 ? null : (
+
+ selectedOptions.has(o.sku || o.id))}
+ indeterminate={rightOptions.some((o) => selectedOptions.has(o.sku || o.id)) && !rightOptions.every((o) => selectedOptions.has(o.sku || o.id))}
+ onChange={(e) => this.toggleSideInSet('selectedOptions', rightOptions.map((o) => o.sku || o.id), e.target.checked)}
+ />
+
+
+ {rightOptions.map((opt) => {
+ const key = opt.sku || opt.id;
+ const isChecked = selectedOptions.has(key);
+ return (
+ }
+ label={`${opt.name || opt.sku} (${opt.amount || 1}x)`}
+ size="small"
+ color={isChecked ? 'secondary' : 'default'}
+ variant={isChecked ? 'filled' : 'outlined'}
+ onClick={() => this.toggleSetItem('selectedOptions', key)}
+ sx={{ maxWidth: '100%' }}
+ />
+ );
+ })}
+
+
+
+ )}
+
+
+
+ {/* 13. Child state */}
+
+
+ Kind-Status
+
+ this.setState({ selectedChildState: 'left' }) : undefined}
+ >
+ {left.parentSku ? (
+
+
+
+ Kindartikel von „{left.parentSku}“
+
+
+ ) : null}
+
+ this.setState({ selectedChildState: 'right' }) : undefined}
+ >
+ {right.parentSku ? (
+
+
+
+ Kindartikel von „{right.parentSku}“
+
+
+ ) : null}
+
+
+
+
+
+
+ {/* Dialog Actions */}
+
+
+
+
+
+ }
+ disabled
+ >
+ Artikel vereinen
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/src/components/article/boxes/ArticleViewLists.jsx b/src/components/article/boxes/ArticleViewLists.jsx
index bc52a9a..daaa874 100644
--- a/src/components/article/boxes/ArticleViewLists.jsx
+++ b/src/components/article/boxes/ArticleViewLists.jsx
@@ -1485,7 +1485,7 @@ export default class ArticleViewLists extends PureComponent {
- Variantsvater für
+ Vater für Auswahl-Merkmal
0 ||
+ (Array.isArray(article.children) && article.children.length > 0) ||
+ article.parent_attribute_id != null ||
+ article.parentAttributeNameId != null
+ );
+}
+
+export function isArticleStueckliste(article) {
+ if (!article) return false;
+ return Boolean(
+ article.articlelistId != null ||
+ (article.articleListCount ?? 0) > 0 ||
+ (Array.isArray(article.articlelist) && article.articlelist.length > 0) ||
+ (Array.isArray(article.components) && article.components.length > 0) ||
+ (Array.isArray(article.stueckliste) && article.stueckliste.length > 0)
+ );
+}
export default class ArticleSettingsTab extends Component {
constructor(props) {
super(props);
- this.state = { busy: false };
+ this.state = {
+ busy: false,
+ searchOpen: false,
+ unifyOpen: false,
+ targetArticle: null,
+ };
this.handleDeleteArticle = this.handleDeleteArticle.bind(this);
+ this.handleOpenSearch = this.handleOpenSearch.bind(this);
+ this.handleSelectTargetArticle = this.handleSelectTargetArticle.bind(this);
}
async handleDeleteArticle() {
@@ -31,21 +68,131 @@ export default class ArticleSettingsTab extends Component {
}
}
+ handleOpenSearch() {
+ const { article: articleData } = this.props;
+ if (isArticleFather(articleData)) {
+ enqueueSnackbar('Ein Vaterartikel kann nicht vereint werden.', { variant: 'warning' });
+ return;
+ }
+ if (isArticleStueckliste(articleData)) {
+ enqueueSnackbar('Ein Stücklisten-Artikel kann nicht vereint werden.', { variant: 'warning' });
+ return;
+ }
+ this.setState({ searchOpen: true });
+ }
+
+ async handleSelectTargetArticle(selected) {
+ if (!selected?.sku) return;
+ this.setState({ searchOpen: false, busy: true });
+ try {
+ let fullTarget = getArticle(selected.sku);
+ if (!fullTarget || !fullTarget.warehouses || !fullTarget.prices) {
+ fullTarget = await artikelAutomat.actions.loadArticle(selected.sku);
+ }
+
+ if (isArticleFather(fullTarget)) {
+ enqueueSnackbar('Der ausgewählte Artikel ist ein Vaterartikel und kann nicht vereint werden.', {
+ variant: 'error',
+ });
+ this.setState({ busy: false });
+ return;
+ }
+
+ if (isArticleStueckliste(fullTarget)) {
+ enqueueSnackbar('Der ausgewählte Artikel ist ein Stücklisten-Artikel und kann nicht vereint werden.', {
+ variant: 'error',
+ });
+ this.setState({ busy: false });
+ return;
+ }
+
+ this.setState({
+ busy: false,
+ targetArticle: fullTarget,
+ unifyOpen: true,
+ });
+ } catch (err) {
+ this.setState({ busy: false });
+ notifyError(err);
+ }
+ }
+
render() {
- const { busy } = this.state;
+ const { busy, searchOpen, unifyOpen, targetArticle } = this.state;
+ const { article: articleData } = this.props;
+
+ const isFather = isArticleFather(articleData);
+ const isStueckliste = isArticleStueckliste(articleData);
+ const cannotUnify = isFather || isStueckliste;
+ const unifyDisabledReason = isFather
+ ? 'Ein Vaterartikel kann nicht vereint werden.'
+ : isStueckliste
+ ? 'Ein Stücklisten-Artikel kann nicht vereint werden.'
+ : null;
+
return (
- }
- disabled={busy}
- onClick={() => void this.handleDeleteArticle()}
- >
- Artikel löschen
-
+
+ }
+ disabled={busy}
+ onClick={() => void this.handleDeleteArticle()}
+ >
+ Artikel löschen
+
+
+ {unifyDisabledReason ? (
+
+
+ }
+ disabled
+ >
+ Artikel vereinen
+
+
+
+ ) : (
+ }
+ disabled={busy}
+ onClick={this.handleOpenSearch}
+ >
+ Artikel vereinen
+
+ )}
+
+
+ this.setState({ searchOpen: false })}
+ onSelect={this.handleSelectTargetArticle}
+ />
+
+ {unifyOpen && targetArticle && (
+ this.setState({ unifyOpen: false, targetArticle: null })}
+ />
+ )}
);
}
}
+
diff --git a/src/components/manifest/ManifestLines.jsx b/src/components/manifest/ManifestLines.jsx
index 261ed77..7321769 100644
--- a/src/components/manifest/ManifestLines.jsx
+++ b/src/components/manifest/ManifestLines.jsx
@@ -13,7 +13,6 @@ import DeleteSweepOutlinedIcon from '@mui/icons-material/DeleteSweepOutlined';
import { PanelTile } from 'dashgui/panels';
import { LINE_COLUMNS, hasArticleLines } from './manifestConfig';
import ManifestFormLine from './ManifestFormLine';
-import ArticleSearch from '../article/ArticleSearch';
class ManifestLines extends PureComponent {
render() {
@@ -23,7 +22,6 @@ class ManifestLines extends PureComponent {
linesColor,
onLineChange,
onAddLine,
- onAddArticleLine,
onRemoveLine,
onRemoveAllLines,
columns = LINE_COLUMNS,
@@ -120,24 +118,13 @@ class ManifestLines extends PureComponent {
- {onAddArticleLine && (
-
- )}
}
disabled={saving}
onClick={onAddLine}
>
- {onAddArticleLine ? 'Leere Zeile' : 'Zeile hinzufügen'}
+ Zeile