u
This commit is contained in:
@@ -3,10 +3,15 @@ import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
|||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import ButtonBase from '@mui/material/ButtonBase';
|
import ButtonBase from '@mui/material/ButtonBase';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Tile, tileBaseSx } from 'dashgui/tiles';
|
import { TILE_SIZE, Tile, tileBaseSx } from 'dashgui/tiles';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
import { articleSku } from '../article/articleRoute';
|
import { articleSku } from '../article/articleRoute';
|
||||||
import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage';
|
import {
|
||||||
|
getThumbHashDataUrl,
|
||||||
|
getCachedImage,
|
||||||
|
isImageLoaded,
|
||||||
|
markImageLoaded,
|
||||||
|
} from '../article/ThumbHashImage';
|
||||||
import { BrandLabel } from '../brand';
|
import { BrandLabel } from '../brand';
|
||||||
import { thumb } from '../../api';
|
import { thumb } from '../../api';
|
||||||
|
|
||||||
@@ -103,7 +108,26 @@ class ArticleTileBackdrop extends PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
|
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
|
||||||
this.img = getCachedImage(props.fullHash);
|
const fullHash = props.fullHash;
|
||||||
|
if (fullHash) {
|
||||||
|
const cached = getCachedImage(fullHash);
|
||||||
|
if (cached) {
|
||||||
|
this.img = cached;
|
||||||
|
} else {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
markImageLoaded(fullHash, img);
|
||||||
|
if (this.img === img) {
|
||||||
|
this.paint();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
img.src = thumb.url(fullHash);
|
||||||
|
this.img = img;
|
||||||
|
if (img.complete && img.naturalWidth) {
|
||||||
|
markImageLoaded(fullHash, img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -127,35 +151,37 @@ class ArticleTileBackdrop extends PureComponent {
|
|||||||
|
|
||||||
loadImage() {
|
loadImage() {
|
||||||
const { fullHash } = this.props;
|
const { fullHash } = this.props;
|
||||||
if (fullHash) {
|
if (!fullHash) return;
|
||||||
const cached = getCachedImage(fullHash);
|
const cached = getCachedImage(fullHash);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
this.img = cached;
|
this.img = cached;
|
||||||
|
this.paint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.img && this.img.src === thumb.url(fullHash)) {
|
||||||
|
if (this.img.complete && this.img.naturalWidth) {
|
||||||
|
markImageLoaded(fullHash, this.img);
|
||||||
this.paint();
|
this.paint();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const img = new Image();
|
return;
|
||||||
img.onload = () => {
|
}
|
||||||
imageCache.set(fullHash, img);
|
const img = new Image();
|
||||||
if (this.img === img) {
|
img.onload = () => {
|
||||||
this.paint();
|
markImageLoaded(fullHash, img);
|
||||||
}
|
if (this.img === img) {
|
||||||
};
|
|
||||||
img.src = thumb.url(fullHash);
|
|
||||||
this.img = img;
|
|
||||||
if (img.complete && img.naturalWidth) {
|
|
||||||
imageCache.set(fullHash, img);
|
|
||||||
this.paint();
|
this.paint();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
img.src = thumb.url(fullHash);
|
||||||
|
this.img = img;
|
||||||
|
if (img.complete && img.naturalWidth) {
|
||||||
|
markImageLoaded(fullHash, img);
|
||||||
|
this.paint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanvas = (canvas) => {
|
setCanvas = (canvas) => {
|
||||||
this.canvasRef = canvas;
|
this.canvasRef = canvas;
|
||||||
if (!this.img && this.props.fullHash) {
|
|
||||||
this.img = getCachedImage(this.props.fullHash);
|
|
||||||
}
|
|
||||||
if (canvas && typeof ResizeObserver !== 'undefined') {
|
if (canvas && typeof ResizeObserver !== 'undefined') {
|
||||||
this.observer?.disconnect();
|
this.observer?.disconnect();
|
||||||
this.observer = new ResizeObserver(() => this.paint());
|
this.observer = new ResizeObserver(() => this.paint());
|
||||||
@@ -168,16 +194,18 @@ class ArticleTileBackdrop extends PureComponent {
|
|||||||
const canvas = this.canvasRef;
|
const canvas = this.canvasRef;
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const dstW = Math.round(rect.width);
|
const dstW = Math.round(rect.width) || canvas.clientWidth || TILE_SIZE;
|
||||||
const dstH = Math.round(rect.height);
|
const dstH = Math.round(rect.height) || canvas.clientHeight || TILE_SIZE;
|
||||||
if (!dstW || !dstH) return;
|
if (!dstW || !dstH) return;
|
||||||
|
|
||||||
canvas.width = dstW;
|
if (canvas.width !== dstW) canvas.width = dstW;
|
||||||
canvas.height = dstH;
|
if (canvas.height !== dstH) canvas.height = dstH;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const img = this.img || getCachedImage(this.props.fullHash);
|
const img = (this.img && this.img.complete && this.img.naturalWidth)
|
||||||
|
? this.img
|
||||||
|
: getCachedImage(this.props.fullHash);
|
||||||
if (img && img.complete && img.naturalWidth && img.naturalHeight) {
|
if (img && img.complete && img.naturalWidth && img.naturalHeight) {
|
||||||
this.img = img;
|
this.img = img;
|
||||||
drawContainedWithStretchedEdges(
|
drawContainedWithStretchedEdges(
|
||||||
@@ -192,7 +220,8 @@ class ArticleTileBackdrop extends PureComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const hasCachedImage = Boolean(this.img && this.img.complete && this.img.naturalWidth);
|
const { fullHash } = this.props;
|
||||||
|
const showThumb = !isImageLoaded(fullHash) && Boolean(this.thumbUrl);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -203,7 +232,7 @@ class ArticleTileBackdrop extends PureComponent {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!hasCachedImage && this.thumbUrl ? (
|
{showThumb ? (
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={this.thumbUrl}
|
src={this.thumbUrl}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
|||||||
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
import PointOfSaleIcon from '@mui/icons-material/PointOfSale';
|
||||||
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
|
import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
|
||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import { getThumbHashDataUrl, getCachedImage, imageCache } from '../article/ThumbHashImage';
|
import { getThumbHashDataUrl, isImageLoaded, markImageLoaded } from '../article/ThumbHashImage';
|
||||||
import { thumb } from '../../api';
|
import { thumb } from '../../api';
|
||||||
import { tileBaseSx } from 'dashgui/tiles';
|
import { tileBaseSx } from 'dashgui/tiles';
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ class CategoryBackdrop extends PureComponent {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
|
this.thumbUrl = getThumbHashDataUrl(props.thumbHash);
|
||||||
this.isCached = Boolean(getCachedImage(props.fullHash));
|
this.isCached = isImageLoaded(props.fullHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@@ -206,21 +206,21 @@ class CategoryBackdrop extends PureComponent {
|
|||||||
this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash);
|
this.thumbUrl = getThumbHashDataUrl(this.props.thumbHash);
|
||||||
}
|
}
|
||||||
if (prevProps.fullHash !== this.props.fullHash) {
|
if (prevProps.fullHash !== this.props.fullHash) {
|
||||||
this.isCached = Boolean(getCachedImage(this.props.fullHash));
|
this.isCached = isImageLoaded(this.props.fullHash);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoad = (e) => {
|
handleLoad = (e) => {
|
||||||
if (this.props.fullHash && e.target) {
|
if (this.props.fullHash && e.target) {
|
||||||
imageCache.set(this.props.fullHash, e.target);
|
markImageLoaded(this.props.fullHash, e.target);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { fullHash } = this.props;
|
const { fullHash } = this.props;
|
||||||
if (!this.thumbUrl && !fullHash) return null;
|
if (!this.thumbUrl && !fullHash) return null;
|
||||||
const isCached = this.isCached || Boolean(getCachedImage(fullHash));
|
const isCached = this.isCached || isImageLoaded(fullHash);
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'absolute', inset: 0, zIndex: 0, overflow: 'hidden' }}>
|
<Box sx={{ position: 'absolute', inset: 0, zIndex: 0, overflow: 'hidden' }}>
|
||||||
{!isCached && this.thumbUrl ? (
|
{!isCached && this.thumbUrl ? (
|
||||||
|
|||||||
@@ -630,36 +630,44 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
<PanelTileGrid>
|
<PanelTileGrid>
|
||||||
{/* Tile 1: Merkmale (Freitext) */}
|
{/* Tile 1: Attribute (Auswahllisten) */}
|
||||||
<PanelTile wide={2} minHeight={280} color="#fed7aa">
|
<PanelTile wide={2} minHeight={280} color="#bfdbfe">
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
Merkmale
|
Auswahl-Merkmale
|
||||||
</Typography>
|
</Typography>
|
||||||
<Chip label={properties.length} size="small" sx={{ ml: 'auto', fontWeight: 600, height: 20 }} />
|
<Chip label={attributes.length} size="small" sx={{ ml: 'auto', fontWeight: 600, height: 20 }} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
<List dense disablePadding ref={this.attrListRef}>
|
||||||
Reihenfolge per Drag & Drop anpassen.
|
{attributes.map((a) => {
|
||||||
</Typography>
|
const isSelected = selectedAttr?.id === a.id;
|
||||||
|
return (
|
||||||
<List dense disablePadding ref={this.propListRef}>
|
|
||||||
{properties.length === 0 ? (
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
|
||||||
Noch keine Merkmale angelegt.
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
properties.map((p) => (
|
|
||||||
<ListItem
|
<ListItem
|
||||||
key={p.id}
|
key={a.id}
|
||||||
disableGutters
|
disableGutters
|
||||||
sx={{ gap: 1, py: 0.75, alignItems: 'center' }}
|
onClick={() => this.handleSelectAttribute(a.id)}
|
||||||
data-id={p.id}
|
sx={{
|
||||||
|
gap: 1,
|
||||||
|
py: 0.75,
|
||||||
|
px: 0.5,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
alignItems: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent',
|
||||||
|
border: isSelected ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid transparent',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.16)' : 'rgba(0, 0, 0, 0.04)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
data-id={a.id}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="drag-handle"
|
className="drag-handle"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -671,46 +679,57 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
>
|
>
|
||||||
<DragIndicatorIcon fontSize="small" />
|
<DragIndicatorIcon fontSize="small" />
|
||||||
</Box>
|
</Box>
|
||||||
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
|
||||||
<SingleLineTextEdit
|
<StyleIcon
|
||||||
size="small"
|
sx={{
|
||||||
label=""
|
color: isSelected ? 'primary.main' : 'text.secondary',
|
||||||
value={propDrafts[p.id] ?? ''}
|
fontSize: 20,
|
||||||
disabled={busy}
|
flexShrink: 0,
|
||||||
onChange={(next) =>
|
|
||||||
this.setState((prev) => ({
|
|
||||||
propDrafts: { ...prev.propDrafts, [p.id]: next },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onBlur={() => void this.handleRenameProperty(p)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.target.blur();
|
|
||||||
}
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.setState((prev) => ({
|
|
||||||
propDrafts: { ...prev.propDrafts, [p.id]: p.name },
|
|
||||||
}));
|
|
||||||
event.target.blur();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
sx={{ flex: 1 }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box onClick={(e) => e.stopPropagation()} sx={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<SingleLineTextEdit
|
||||||
|
size="small"
|
||||||
|
label=""
|
||||||
|
value={attrDrafts[a.id] ?? ''}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(next) =>
|
||||||
|
this.setState((prev) => ({
|
||||||
|
attrDrafts: { ...prev.attrDrafts, [a.id]: next },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onBlur={() => void this.handleRenameAttribute(a)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.target.blur();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState((prev) => ({
|
||||||
|
attrDrafts: { ...prev.attrDrafts, [a.id]: a.name },
|
||||||
|
}));
|
||||||
|
event.target.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
p.exclusive
|
a.exclusive
|
||||||
? 'Einzelauswahl (Artikel kann max. 1 Wert für dieses Merkmal haben)'
|
? 'Einzelauswahl (Artikel kann max. 1 Wert wählen)'
|
||||||
: 'Mehrfachauswahl (Artikel kann mehrere Werte für dieses Merkmal haben)'
|
: 'Mehrfachauswahl (Artikel kann mehrere Werte wählen)'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Chip
|
<Chip
|
||||||
label={p.exclusive ? '1x' : 'Multi'}
|
label={a.exclusive ? '1x' : 'Multi'}
|
||||||
size="small"
|
size="small"
|
||||||
color={p.exclusive ? 'primary' : 'default'}
|
color={a.exclusive ? 'primary' : 'default'}
|
||||||
variant={p.exclusive ? 'filled' : 'outlined'}
|
variant={a.exclusive ? 'filled' : 'outlined'}
|
||||||
onClick={(e) => void this.handleTogglePropertyExclusive(p, e)}
|
onClick={(e) => void this.handleToggleAttributeExclusive(a, e)}
|
||||||
sx={{
|
sx={{
|
||||||
height: 22,
|
height: 22,
|
||||||
fontSize: '0.72rem',
|
fontSize: '0.72rem',
|
||||||
@@ -720,206 +739,27 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
edge="end"
|
edge="end"
|
||||||
size="small"
|
size="small"
|
||||||
aria-label={`„${p.name}" löschen`}
|
aria-label={`„${a.name}" löschen`}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => void this.handleDeleteProperty(p)}
|
onClick={(e) => void this.handleDeleteAttribute(a, e)}
|
||||||
|
sx={{ flexShrink: 0 }}
|
||||||
>
|
>
|
||||||
<DeleteOutlinedIcon fontSize="small" />
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))
|
);
|
||||||
)}
|
})}
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<SingleLineTextEdit
|
<SingleLineTextEdit
|
||||||
size="small"
|
size="small"
|
||||||
label="Neues Merkmal"
|
label=""
|
||||||
value={newPropName}
|
|
||||||
disabled={busy}
|
|
||||||
onChange={(next) => this.setState({ newPropName: next })}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
void this.handleCreateProperty();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sx={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
size="small"
|
|
||||||
disabled={busy || !newPropName.trim()}
|
|
||||||
onClick={() => void this.handleCreateProperty()}
|
|
||||||
>
|
|
||||||
{busyKey === 'new-prop' ? '…' : 'Hinzufügen'}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Checkbox
|
|
||||||
size="small"
|
|
||||||
checked={newPropExclusive}
|
|
||||||
disabled={busy}
|
|
||||||
onChange={(e) => this.setState({ newPropExclusive: e.target.checked })}
|
|
||||||
sx={{ p: 0.5 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Einzelauswahl (max. 1 Wert pro Artikel)
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
sx={{ ml: 0.5, mt: -0.5 }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</PanelTile>
|
|
||||||
|
|
||||||
{/* Tile 2: Attribute (Auswahllisten) */}
|
|
||||||
<PanelTile wide={2} minHeight={280} color="#bfdbfe">
|
|
||||||
<Stack spacing={2}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<StyleIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
|
||||||
Attribute
|
|
||||||
</Typography>
|
|
||||||
<Chip label={attributes.length} size="small" sx={{ ml: 'auto', fontWeight: 600, height: 20 }} />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
Reihenfolge per Drag & Drop anpassen.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<List dense disablePadding ref={this.attrListRef}>
|
|
||||||
{attributes.length === 0 ? (
|
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
|
||||||
Noch keine Attribute angelegt.
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
attributes.map((a) => {
|
|
||||||
const isSelected = selectedAttr?.id === a.id;
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
key={a.id}
|
|
||||||
disableGutters
|
|
||||||
onClick={() => this.handleSelectAttribute(a.id)}
|
|
||||||
sx={{
|
|
||||||
gap: 1,
|
|
||||||
py: 0.75,
|
|
||||||
px: 0.5,
|
|
||||||
borderRadius: 1.5,
|
|
||||||
alignItems: 'center',
|
|
||||||
cursor: 'pointer',
|
|
||||||
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent',
|
|
||||||
border: isSelected ? '1px solid rgba(59, 130, 246, 0.3)' : '1px solid transparent',
|
|
||||||
transition: 'all 0.15s ease',
|
|
||||||
'&:hover': {
|
|
||||||
bgcolor: isSelected ? 'rgba(59, 130, 246, 0.16)' : 'rgba(0, 0, 0, 0.04)',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
data-id={a.id}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
className="drag-handle"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: 'text.disabled',
|
|
||||||
flexShrink: 0,
|
|
||||||
'&:hover': { color: 'text.secondary' },
|
|
||||||
transition: 'color 0.15s',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DragIndicatorIcon fontSize="small" />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<StyleIcon
|
|
||||||
sx={{
|
|
||||||
color: isSelected ? 'primary.main' : 'text.secondary',
|
|
||||||
fontSize: 20,
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box onClick={(e) => e.stopPropagation()} sx={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<SingleLineTextEdit
|
|
||||||
size="small"
|
|
||||||
label=""
|
|
||||||
value={attrDrafts[a.id] ?? ''}
|
|
||||||
disabled={busy}
|
|
||||||
onChange={(next) =>
|
|
||||||
this.setState((prev) => ({
|
|
||||||
attrDrafts: { ...prev.attrDrafts, [a.id]: next },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onBlur={() => void this.handleRenameAttribute(a)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.target.blur();
|
|
||||||
}
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.setState((prev) => ({
|
|
||||||
attrDrafts: { ...prev.attrDrafts, [a.id]: a.name },
|
|
||||||
}));
|
|
||||||
event.target.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
sx={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
a.exclusive
|
|
||||||
? 'Einzelauswahl (Artikel kann max. 1 Wert wählen)'
|
|
||||||
: 'Mehrfachauswahl (Artikel kann mehrere Werte wählen)'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Chip
|
|
||||||
label={a.exclusive ? '1x' : 'Multi'}
|
|
||||||
size="small"
|
|
||||||
color={a.exclusive ? 'primary' : 'default'}
|
|
||||||
variant={a.exclusive ? 'filled' : 'outlined'}
|
|
||||||
onClick={(e) => void this.handleToggleAttributeExclusive(a, e)}
|
|
||||||
sx={{
|
|
||||||
height: 22,
|
|
||||||
fontSize: '0.72rem',
|
|
||||||
fontWeight: 600,
|
|
||||||
cursor: 'pointer',
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
edge="end"
|
|
||||||
size="small"
|
|
||||||
aria-label={`„${a.name}" löschen`}
|
|
||||||
disabled={busy}
|
|
||||||
onClick={(e) => void this.handleDeleteAttribute(a, e)}
|
|
||||||
sx={{ flexShrink: 0 }}
|
|
||||||
>
|
|
||||||
<DeleteOutlinedIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<SingleLineTextEdit
|
|
||||||
size="small"
|
|
||||||
label="Neues Attribut"
|
|
||||||
value={newAttrName}
|
value={newAttrName}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onChange={(next) => this.setState({ newAttrName: next })}
|
onChange={(next) => this.setState({ newAttrName: next })}
|
||||||
@@ -961,7 +801,7 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
{/* Tile 3: Werte für das gewählte Attribut */}
|
{/* Tile 2: Werte für das gewählte Attribut */}
|
||||||
<PanelTile wide={2} minHeight={280} color="#ddd6fe">
|
<PanelTile wide={2} minHeight={280} color="#ddd6fe">
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
@@ -978,14 +818,8 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{selectedAttr
|
|
||||||
? 'Reihenfolge per Drag & Drop anpassen.'
|
|
||||||
: 'Wähle links ein Attribut aus, um Werte zu bearbeiten.'}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{!selectedAttr ? (
|
{!selectedAttr ? (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ py: 3, textAlign: 'center', fontStyle: 'italic' }}>
|
<Typography variant="body2" color="text.secondary" sx={{ py: 1 }}>
|
||||||
Kein Attribut ausgewählt.
|
Kein Attribut ausgewählt.
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
@@ -1060,7 +894,7 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<SingleLineTextEdit
|
<SingleLineTextEdit
|
||||||
size="small"
|
size="small"
|
||||||
label={`Neuer Wert für „${selectedAttr.name}“`}
|
label=""
|
||||||
value={newValText}
|
value={newValText}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onChange={(next) => this.setState({ newValText: next })}
|
onChange={(next) => this.setState({ newValText: next })}
|
||||||
@@ -1085,6 +919,146 @@ class ArtikelMerkmalePage extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
|
{/* Tile 3: Merkmale (Freitext) */}
|
||||||
|
<PanelTile wide={2} minHeight={280} color="#fed7aa">
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20 }} />
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
|
Text-Merkmale
|
||||||
|
</Typography>
|
||||||
|
<Chip label={properties.length} size="small" sx={{ ml: 'auto', fontWeight: 600, height: 20 }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<List dense disablePadding ref={this.propListRef}>
|
||||||
|
{properties.map((p) => (
|
||||||
|
<ListItem
|
||||||
|
key={p.id}
|
||||||
|
disableGutters
|
||||||
|
sx={{ gap: 1, py: 0.75, alignItems: 'center' }}
|
||||||
|
data-id={p.id}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
className="drag-handle"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: 'text.disabled',
|
||||||
|
flexShrink: 0,
|
||||||
|
'&:hover': { color: 'text.secondary' },
|
||||||
|
transition: 'color 0.15s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DragIndicatorIcon fontSize="small" />
|
||||||
|
</Box>
|
||||||
|
<TuneIcon sx={{ color: 'text.secondary', fontSize: 20, flexShrink: 0 }} />
|
||||||
|
<SingleLineTextEdit
|
||||||
|
size="small"
|
||||||
|
label=""
|
||||||
|
value={propDrafts[p.id] ?? ''}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(next) =>
|
||||||
|
this.setState((prev) => ({
|
||||||
|
propDrafts: { ...prev.propDrafts, [p.id]: next },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
onBlur={() => void this.handleRenameProperty(p)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.target.blur();
|
||||||
|
}
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.setState((prev) => ({
|
||||||
|
propDrafts: { ...prev.propDrafts, [p.id]: p.name },
|
||||||
|
}));
|
||||||
|
event.target.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
p.exclusive
|
||||||
|
? 'Einzelauswahl (Artikel kann max. 1 Wert für dieses Merkmal haben)'
|
||||||
|
: 'Mehrfachauswahl (Artikel kann mehrere Werte für dieses Merkmal haben)'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
label={p.exclusive ? '1x' : 'Multi'}
|
||||||
|
size="small"
|
||||||
|
color={p.exclusive ? 'primary' : 'default'}
|
||||||
|
variant={p.exclusive ? 'filled' : 'outlined'}
|
||||||
|
onClick={(e) => void this.handleTogglePropertyExclusive(p, e)}
|
||||||
|
sx={{
|
||||||
|
height: 22,
|
||||||
|
fontSize: '0.72rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
cursor: 'pointer',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
size="small"
|
||||||
|
aria-label={`„${p.name}" löschen`}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => void this.handleDeleteProperty(p)}
|
||||||
|
>
|
||||||
|
<DeleteOutlinedIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<SingleLineTextEdit
|
||||||
|
size="small"
|
||||||
|
label=""
|
||||||
|
value={newPropName}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(next) => this.setState({ newPropName: next })}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
void this.handleCreateProperty();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
disabled={busy || !newPropName.trim()}
|
||||||
|
onClick={() => void this.handleCreateProperty()}
|
||||||
|
>
|
||||||
|
{busyKey === 'new-prop' ? '…' : 'Hinzufügen'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={newPropExclusive}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => this.setState({ newPropExclusive: e.target.checked })}
|
||||||
|
sx={{ p: 0.5 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Einzelauswahl (max. 1 Wert pro Artikel)
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
sx={{ ml: 0.5, mt: -0.5 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</PanelTile>
|
||||||
</PanelTileGrid>
|
</PanelTileGrid>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user