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 */} + + + + + + + + + + +
+ ); + } +} 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 ( - + + + + {unifyDisabledReason ? ( + + + + + + ) : ( + + )} + + + 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 && ( - - )}