Compare commits
3 Commits
8b680b5a1a
...
6ca7597c59
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ca7597c59 | ||
|
|
05f1311892 | ||
|
|
dba9ae5cf0 |
@@ -12,4 +12,10 @@ export const thumb = {
|
|||||||
return `/api/thumb/${encodeURIComponent(fullHash)}`;
|
return `/api/thumb/${encodeURIComponent(fullHash)}`;
|
||||||
},
|
},
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
|
// #region GET /api/smallthumb/:fullHash — smallUrl
|
||||||
|
smallUrl(fullHash) {
|
||||||
|
return `/api/smallthumb/${encodeURIComponent(fullHash)}`;
|
||||||
|
},
|
||||||
|
// #endregion
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class ArticleNameDialog extends Component {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
mode: SINGLE_MODE,
|
mode: SINGLE_MODE,
|
||||||
name: '',
|
name: props.initialName ?? '',
|
||||||
bulkText: '',
|
bulkText: '',
|
||||||
busy: false,
|
busy: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -46,7 +46,7 @@ class ArticleNameDialog extends Component {
|
|||||||
if (this.props.open && !prevProps.open) {
|
if (this.props.open && !prevProps.open) {
|
||||||
this.setState({
|
this.setState({
|
||||||
mode: SINGLE_MODE,
|
mode: SINGLE_MODE,
|
||||||
name: '',
|
name: this.props.initialName ?? '',
|
||||||
bulkText: '',
|
bulkText: '',
|
||||||
busy: false,
|
busy: false,
|
||||||
error: null,
|
error: null,
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ import {
|
|||||||
class ArticlePhoto extends Component {
|
class ArticlePhoto extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { thumbLoaded: false };
|
this.state = { thumbLoaded: false, smallThumbError: false };
|
||||||
this.handleThumbLoad = this.handleThumbLoad.bind(this);
|
this.handleThumbLoad = this.handleThumbLoad.bind(this);
|
||||||
this.handleThumbError = this.handleThumbError.bind(this);
|
this.handleThumbError = this.handleThumbError.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.photo?.fullHash !== this.props.photo?.fullHash) {
|
if (
|
||||||
this.setState({ thumbLoaded: false });
|
prevProps.photo?.fullHash !== this.props.photo?.fullHash ||
|
||||||
|
prevProps.useSmallThumb !== this.props.useSmallThumb
|
||||||
|
) {
|
||||||
|
this.setState({ thumbLoaded: false, smallThumbError: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,8 +28,12 @@ class ArticlePhoto extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleThumbError() {
|
handleThumbError() {
|
||||||
|
if (this.props.useSmallThumb && !this.state.smallThumbError) {
|
||||||
|
this.setState({ smallThumbError: true, thumbLoaded: false });
|
||||||
|
} else {
|
||||||
this.setState({ thumbLoaded: false });
|
this.setState({ thumbLoaded: false });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
@@ -34,9 +41,11 @@ class ArticlePhoto extends Component {
|
|||||||
size = 200,
|
size = 200,
|
||||||
fill = false,
|
fill = false,
|
||||||
objectFit = fill ? 'cover' : 'contain',
|
objectFit = fill ? 'cover' : 'contain',
|
||||||
|
useSmallThumb = false,
|
||||||
sx,
|
sx,
|
||||||
|
...rest
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { thumbLoaded } = this.state;
|
const { thumbLoaded, smallThumbError } = this.state;
|
||||||
const fullHash = photo?.fullHash;
|
const fullHash = photo?.fullHash;
|
||||||
const hasPreview = photo?.thumbHash != null;
|
const hasPreview = photo?.thumbHash != null;
|
||||||
const aspectRatio =
|
const aspectRatio =
|
||||||
@@ -44,8 +53,15 @@ class ArticlePhoto extends Component {
|
|||||||
? `${photo.thumbW} / ${photo.thumbH}`
|
? `${photo.thumbW} / ${photo.thumbH}`
|
||||||
: '1 / 1';
|
: '1 / 1';
|
||||||
|
|
||||||
|
const imgSrc = fullHash
|
||||||
|
? useSmallThumb && !smallThumbError
|
||||||
|
? thumb.smallUrl(fullHash)
|
||||||
|
: thumb.url(fullHash)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
{...rest}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
...(fill
|
...(fill
|
||||||
@@ -78,10 +94,10 @@ class ArticlePhoto extends Component {
|
|||||||
<ThumbHashImage thumbHash={photo.thumbHash} />
|
<ThumbHashImage thumbHash={photo.thumbHash} />
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
{fullHash ? (
|
{imgSrc ? (
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={thumb.url(fullHash)}
|
src={imgSrc}
|
||||||
alt=""
|
alt=""
|
||||||
onLoad={this.handleThumbLoad}
|
onLoad={this.handleThumbLoad}
|
||||||
onError={this.handleThumbError}
|
onError={this.handleThumbError}
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
import { PanelTile } from 'dashgui/panels';
|
import { PanelTile } from 'dashgui/panels';
|
||||||
import { primaryPhoto } from '../../utils/articleDetail';
|
import { sortedPhotos, primaryPhoto } from '../../utils/articleDetail';
|
||||||
import ArticlePhoto from './ArticlePhoto';
|
import ArticlePhoto from './ArticlePhoto';
|
||||||
import ArticlePhotosDialog from './ArticlePhotosDialog';
|
import ArticlePhotosDialog from './ArticlePhotosDialog';
|
||||||
|
|
||||||
class ArticlePhotoTile extends PureComponent {
|
class ArticlePhotoTile extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { editOpen: false };
|
this.state = {
|
||||||
|
editOpen: false,
|
||||||
|
selectedHash: null,
|
||||||
|
};
|
||||||
this.handleEditOpen = this.handleEditOpen.bind(this);
|
this.handleEditOpen = this.handleEditOpen.bind(this);
|
||||||
this.handleEditClose = this.handleEditClose.bind(this);
|
this.handleEditClose = this.handleEditClose.bind(this);
|
||||||
|
this.handleSelectPhoto = this.handleSelectPhoto.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.article?.sku !== this.props.article?.sku) {
|
||||||
|
this.setState({ selectedHash: null });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEditOpen() {
|
handleEditOpen() {
|
||||||
@@ -21,6 +32,10 @@ class ArticlePhotoTile extends PureComponent {
|
|||||||
this.setState({ editOpen: false });
|
this.setState({ editOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleSelectPhoto(fullHash) {
|
||||||
|
this.setState({ selectedHash: fullHash });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
article: articleData,
|
article: articleData,
|
||||||
@@ -30,8 +45,14 @@ class ArticlePhotoTile extends PureComponent {
|
|||||||
sx,
|
sx,
|
||||||
color,
|
color,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { editOpen } = this.state;
|
const { editOpen, selectedHash } = this.state;
|
||||||
const photo = primaryPhoto(articleData);
|
|
||||||
|
const photos = sortedPhotos(articleData);
|
||||||
|
const primary = primaryPhoto(articleData);
|
||||||
|
const activePhoto =
|
||||||
|
(selectedHash && photos.find((p) => p.fullHash === selectedHash)) ||
|
||||||
|
primary;
|
||||||
|
const otherPhotos = photos.filter((p) => p.fullHash !== activePhoto?.fullHash);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -47,7 +68,86 @@ class ArticlePhotoTile extends PureComponent {
|
|||||||
busy: false,
|
busy: false,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ArticlePhoto photo={photo} />
|
{otherPhotos.length === 0 ? (
|
||||||
|
<ArticlePhoto photo={activePhoto} />
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArticlePhoto photo={activePhoto} />
|
||||||
|
|
||||||
|
{/* Smallthumbs of all other images beside the main image */}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 0.75,
|
||||||
|
maxHeight: 200,
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
mt: '32px',
|
||||||
|
py: 0.25,
|
||||||
|
px: 0.25,
|
||||||
|
flexShrink: 0,
|
||||||
|
scrollbarWidth: 'thin',
|
||||||
|
'&::-webkit-scrollbar': { width: 4 },
|
||||||
|
'&::-webkit-scrollbar-thumb': {
|
||||||
|
bgcolor: 'action.hover',
|
||||||
|
borderRadius: 2,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{otherPhotos.map((p) => (
|
||||||
|
<Box
|
||||||
|
key={p.fullHash}
|
||||||
|
component="button"
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.handleSelectPhoto(p.fullHash);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
p: 0,
|
||||||
|
m: 0,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
flexShrink: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '2px solid transparent',
|
||||||
|
bgcolor: 'transparent',
|
||||||
|
outline: 'none',
|
||||||
|
display: 'block',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'scale(1.08)',
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
boxShadow: 2,
|
||||||
|
},
|
||||||
|
'&:focus-visible': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArticlePhoto
|
||||||
|
photo={p}
|
||||||
|
size={40}
|
||||||
|
useSmallThumb
|
||||||
|
objectFit="cover"
|
||||||
|
sx={{ width: '100%', height: '100%', borderRadius: 1 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
<ArticlePhotosDialog
|
<ArticlePhotosDialog
|
||||||
open={editOpen}
|
open={editOpen}
|
||||||
|
|||||||
@@ -43,14 +43,12 @@ export default class CategoryPickerDialog extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
|
|
||||||
this._unsub = categoryAutomat.subscribe((s) => {
|
this._unsub = categoryAutomat.subscribe((s) => {
|
||||||
this.setState(() => ({
|
this.setState({
|
||||||
categories: this.props.categories ?? s.categories,
|
categories: s.categories,
|
||||||
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
|
rootCategoryId: s.rootCategoryId ?? this.props.rootCategoryId,
|
||||||
}));
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@@ -64,10 +62,12 @@ export default class CategoryPickerDialog extends PureComponent {
|
|||||||
} else if (this.state.rootCategoryId != null) {
|
} else if (this.state.rootCategoryId != null) {
|
||||||
nextSelectedId = this.state.rootCategoryId;
|
nextSelectedId = this.state.rootCategoryId;
|
||||||
}
|
}
|
||||||
this.setState({ selectedId: nextSelectedId });
|
this.setState({
|
||||||
}
|
selectedId: nextSelectedId,
|
||||||
|
categories: categoryAutomat.state.categories ?? this.props.categories,
|
||||||
if (
|
rootCategoryId: categoryAutomat.state.rootCategoryId ?? this.props.rootCategoryId,
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
this.props.categories !== prevProps.categories ||
|
this.props.categories !== prevProps.categories ||
|
||||||
this.props.rootCategoryId !== prevProps.rootCategoryId
|
this.props.rootCategoryId !== prevProps.rootCategoryId
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
alpha,
|
alpha,
|
||||||
} from '../../../colors';
|
} from '../../../colors';
|
||||||
|
|
||||||
function getVariantDetails(child, allArticles, defaultTaxRate) {
|
function getVariantDetails(child, allArticles, defaultTaxRate, parentArticle) {
|
||||||
const cached = child?.sku ? getArticle(child.sku) : null;
|
const cached = child?.sku ? getArticle(child.sku) : null;
|
||||||
const fromIndex = !cached && child?.sku
|
const fromIndex = !cached && child?.sku
|
||||||
? (allArticles || []).find((a) => a.sku === child.sku)
|
? (allArticles || []).find((a) => a.sku === child.sku)
|
||||||
@@ -56,8 +56,23 @@ function getVariantDetails(child, allArticles, defaultTaxRate) {
|
|||||||
const gross = getArticleGross(merged, defaultTaxRate);
|
const gross = getArticleGross(merged, defaultTaxRate);
|
||||||
const priceDisplay = gross != null && Number.isFinite(gross) ? formatPrice(gross) : null;
|
const priceDisplay = gross != null && Number.isFinite(gross) ? formatPrice(gross) : null;
|
||||||
|
|
||||||
// 2. Stock & Unit
|
// 2. Stock tracking
|
||||||
|
const rawTrack =
|
||||||
|
merged.track_stock ??
|
||||||
|
merged.trackStock ??
|
||||||
|
child?.trackStock ??
|
||||||
|
child?.track_stock ??
|
||||||
|
parentArticle?.track_stock ??
|
||||||
|
parentArticle?.trackStock;
|
||||||
|
|
||||||
|
const trackStock =
|
||||||
|
rawTrack !== undefined
|
||||||
|
? rawTrack !== false && rawTrack !== 0 && rawTrack !== '0'
|
||||||
|
: true;
|
||||||
|
|
||||||
|
// 3. Stock & Unit
|
||||||
let stockVal = null;
|
let stockVal = null;
|
||||||
|
if (trackStock) {
|
||||||
if (Array.isArray(merged.warehouses) && merged.warehouses.length > 0) {
|
if (Array.isArray(merged.warehouses) && merged.warehouses.length > 0) {
|
||||||
stockVal = merged.warehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0);
|
stockVal = merged.warehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0);
|
||||||
} else if (merged.stock != null && !Number.isNaN(Number(merged.stock))) {
|
} else if (merged.stock != null && !Number.isNaN(Number(merged.stock))) {
|
||||||
@@ -65,6 +80,7 @@ function getVariantDetails(child, allArticles, defaultTaxRate) {
|
|||||||
} else if (child?.stock != null && !Number.isNaN(Number(child.stock))) {
|
} else if (child?.stock != null && !Number.isNaN(Number(child.stock))) {
|
||||||
stockVal = Number(child.stock);
|
stockVal = Number(child.stock);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const rawUnit =
|
const rawUnit =
|
||||||
merged.unit?.symbol ||
|
merged.unit?.symbol ||
|
||||||
@@ -76,9 +92,9 @@ function getVariantDetails(child, allArticles, defaultTaxRate) {
|
|||||||
rawUnit && !['stk', 'stück', 'pc', 'stk.'].includes(rawUnit.trim().toLowerCase())
|
rawUnit && !['stk', 'stück', 'pc', 'stk.'].includes(rawUnit.trim().toLowerCase())
|
||||||
? ` ${rawUnit}`
|
? ` ${rawUnit}`
|
||||||
: '';
|
: '';
|
||||||
const stockDisplay = stockVal != null ? `${formatStock(stockVal)}${unit}` : null;
|
const stockDisplay = trackStock && stockVal != null ? `${formatStock(stockVal)}${unit}` : null;
|
||||||
|
|
||||||
// 3. EAN(s)
|
// 4. EAN(s)
|
||||||
const eanCandidates = [];
|
const eanCandidates = [];
|
||||||
const rawEans = merged.eans ?? child?.eans;
|
const rawEans = merged.eans ?? child?.eans;
|
||||||
if (Array.isArray(rawEans)) {
|
if (Array.isArray(rawEans)) {
|
||||||
@@ -103,6 +119,7 @@ function getVariantDetails(child, allArticles, defaultTaxRate) {
|
|||||||
stockDisplay,
|
stockDisplay,
|
||||||
primaryEan,
|
primaryEan,
|
||||||
allEans: eanCandidates,
|
allEans: eanCandidates,
|
||||||
|
trackStock,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,6 +140,7 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
attributes: initialAttributes,
|
attributes: initialAttributes,
|
||||||
|
attributesStatus: companyAutomat.state.attributesStatus || 'idle',
|
||||||
assignedAttributes: [],
|
assignedAttributes: [],
|
||||||
allArticles: [],
|
allArticles: [],
|
||||||
busy: false,
|
busy: false,
|
||||||
@@ -170,10 +188,11 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
return {
|
return {
|
||||||
company: state.current,
|
company: state.current,
|
||||||
attributes: state.attributes,
|
attributes: state.attributes,
|
||||||
|
attributesStatus: state.attributesStatus,
|
||||||
selectedAttributeId: nextSelectedId,
|
selectedAttributeId: nextSelectedId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, ['current', 'attributes']);
|
}, ['current', 'attributes', 'attributesStatus']);
|
||||||
|
|
||||||
this.unsubArtikel = artikelAutomat.subscribe(() => {
|
this.unsubArtikel = artikelAutomat.subscribe(() => {
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
@@ -391,30 +410,41 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
const { selectedChildValueId, newChildText, isCustomChildValue, busy } = this.state;
|
const { selectedChildValueId, newChildText, isCustomChildValue, busy } = this.state;
|
||||||
if (busy || !article?.sku) return;
|
if (busy || !article?.sku) return;
|
||||||
|
|
||||||
const payload = {};
|
|
||||||
if (isCustomChildValue) {
|
|
||||||
const val = newChildText.trim();
|
const val = newChildText.trim();
|
||||||
if (!val) return;
|
const payload = {};
|
||||||
|
|
||||||
|
if (val && (isCustomChildValue || !selectedChildValueId)) {
|
||||||
|
payload.value = val;
|
||||||
|
} else if (selectedChildValueId) {
|
||||||
|
payload.attributeValueId = Number(selectedChildValueId);
|
||||||
|
} else if (val) {
|
||||||
payload.value = val;
|
payload.value = val;
|
||||||
} else {
|
} else {
|
||||||
if (!selectedChildValueId) return;
|
return;
|
||||||
payload.attributeValueId = Number(selectedChildValueId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
try {
|
try {
|
||||||
const api = this.getApi();
|
const api = this.getApi();
|
||||||
await api.addChild(article.sku, payload);
|
const res = await api.addChild(article.sku, payload);
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
|
|
||||||
|
if (res?.sku) {
|
||||||
|
void artikelAutomat.actions.loadArticle(res.sku).catch(() => { });
|
||||||
|
}
|
||||||
|
|
||||||
await artikelAutomat.actions.reloadArticle(article.sku);
|
await artikelAutomat.actions.reloadArticle(article.sku);
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
|
|
||||||
|
void articleIndexAutomat.actions.reload().catch(() => { });
|
||||||
|
void companyAutomat.actions.loadAttributes?.().catch?.(() => { });
|
||||||
|
|
||||||
enqueueSnackbar('Variante erfolgreich angelegt.', { variant: 'success' });
|
enqueueSnackbar('Variante erfolgreich angelegt.', { variant: 'success' });
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
selectedChildValueId: '',
|
selectedChildValueId: '',
|
||||||
newChildText: '',
|
newChildText: '',
|
||||||
|
isCustomChildValue: false,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (this._cancelled) return;
|
if (this._cancelled) return;
|
||||||
@@ -1346,7 +1376,8 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
stockDisplay,
|
stockDisplay,
|
||||||
primaryEan,
|
primaryEan,
|
||||||
allEans,
|
allEans,
|
||||||
} = getVariantDetails(sibling, allArticles, defaultTaxRate);
|
trackStock,
|
||||||
|
} = getVariantDetails(sibling, allArticles, defaultTaxRate, parentArticle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -1463,7 +1494,7 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
sx={{
|
sx={{
|
||||||
fontSize: '0.74rem',
|
fontSize: '0.74rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: stockVal == null ? 'text.disabled' : stockVal > 0 ? 'success.dark' : 'error.main',
|
color: !trackStock || stockVal == null ? 'text.disabled' : stockVal > 0 ? 'success.dark' : 'error.main',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1582,7 +1613,7 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
sx={{
|
sx={{
|
||||||
fontSize: '0.74rem',
|
fontSize: '0.74rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: stockVal == null ? 'text.disabled' : stockVal > 0 ? 'success.dark' : 'error.main',
|
color: !trackStock || stockVal == null ? 'text.disabled' : stockVal > 0 ? 'success.dark' : 'error.main',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1602,7 +1633,7 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderStandardView(selectableAttributes, assignedAttributeIds) {
|
renderStandardView(selectableAttributes, assignedAttributeIds) {
|
||||||
const { busy, activeCreatorMode, selectedAttributeId } = this.state;
|
const { busy, activeCreatorMode, selectedAttributeId, attributes, attributesStatus } = this.state;
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
{activeCreatorMode === 'parent' ? (
|
{activeCreatorMode === 'parent' ? (
|
||||||
@@ -1624,7 +1655,36 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{selectableAttributes.length === 0 ? (
|
{attributesStatus === 'loading' ? (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||||||
|
<CircularProgress size={14} />
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Merkmale werden geladen…
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : !attributes || attributes.length === 0 ? (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, py: 0.5 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Im System sind noch keine Merkmale angelegt.
|
||||||
|
</Typography>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to="/artikel/merkmale"
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
fontWeight: 600,
|
||||||
|
textDecoration: 'none',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 0.5,
|
||||||
|
'&:hover': { textDecoration: 'underline' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Zu den Merkmalen →
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
) : selectableAttributes.length === 0 ? (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
|
Alle vorhandenen Merkmale sind diesem Artikel bereits zugewiesen.
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -1877,11 +1937,7 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
<Typography variant="caption" sx={{ fontWeight: 600, color: 'text.secondary' }}>
|
||||||
Varianten:
|
Varianten:
|
||||||
</Typography>
|
</Typography>
|
||||||
{children.length === 0 ? (
|
{children.length === 0 ? null : (
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ py: 1, fontSize: '0.8rem' }}>
|
|
||||||
Noch keine Varianten angelegt.
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||||
{children.map((child) => {
|
{children.map((child) => {
|
||||||
const defaultTaxRate = this.state.company?.taxrate ?? companyAutomat.state.current?.taxrate ?? 19;
|
const defaultTaxRate = this.state.company?.taxrate ?? companyAutomat.state.current?.taxrate ?? 19;
|
||||||
@@ -1891,7 +1947,8 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
stockDisplay,
|
stockDisplay,
|
||||||
primaryEan,
|
primaryEan,
|
||||||
allEans,
|
allEans,
|
||||||
} = getVariantDetails(child, allArticles, defaultTaxRate);
|
trackStock,
|
||||||
|
} = getVariantDetails(child, allArticles, defaultTaxRate, articleData);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -2033,7 +2090,7 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
sx={{
|
sx={{
|
||||||
fontSize: '0.74rem',
|
fontSize: '0.74rem',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: stockVal == null ? 'text.disabled' : stockVal > 0 ? 'success.dark' : 'error.main',
|
color: !trackStock || stockVal == null ? 'text.disabled' : stockVal > 0 ? 'success.dark' : 'error.main',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2046,7 +2103,6 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => this.handleOpenRemoveDialog(child)}
|
onClick={() => this.handleOpenRemoveDialog(child)}
|
||||||
title="Variante entfernen"
|
|
||||||
sx={{
|
sx={{
|
||||||
p: 0.35,
|
p: 0.35,
|
||||||
color: 'text.secondary',
|
color: 'text.secondary',
|
||||||
@@ -2067,7 +2123,6 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
{isCustomChildValue || availableParentValues.length === 0 ? (
|
{isCustomChildValue || availableParentValues.length === 0 ? (
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Neuer Wert"
|
label="Neuer Wert"
|
||||||
placeholder="Neuer Wert..."
|
|
||||||
value={newChildText}
|
value={newChildText}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onChange={(e) => this.setState({ newChildText: e.target.value })}
|
onChange={(e) => this.setState({ newChildText: e.target.value })}
|
||||||
@@ -2112,11 +2167,10 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
</TextInput>
|
</TextInput>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip title="Variante hinzufügen">
|
|
||||||
<span>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
aria-label="Variante hinzufügen"
|
||||||
disabled={
|
disabled={
|
||||||
busy ||
|
busy ||
|
||||||
(isCustomChildValue || availableParentValues.length === 0
|
(isCustomChildValue || availableParentValues.length === 0
|
||||||
@@ -2138,8 +2192,6 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
>
|
>
|
||||||
{busy ? <CircularProgress size={16} color="inherit" /> : <AddIcon fontSize="small" />}
|
{busy ? <CircularProgress size={16} color="inherit" /> : <AddIcon fontSize="small" />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Toggle custom value / predefined value if values exist */}
|
{/* Toggle custom value / predefined value if values exist */}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, PureComponent } from 'react';
|
import { Component, PureComponent } from 'react';
|
||||||
import { EditablePanelTile } from 'dashgui/panels';
|
import { EditablePanelTile } from 'dashgui/panels';
|
||||||
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
|
||||||
import { getCategoryState } from '../../../automats/categoryAutomat';
|
import categoryAutomat, { getCategoryState } from '../../../automats/categoryAutomat';
|
||||||
import artikelAutomat from '../../../automats/artikelAutomat';
|
import artikelAutomat from '../../../automats/artikelAutomat';
|
||||||
import { getCategoryBreadcrumbs, categoryDisplayName } from '../../../utils/categoryUtils';
|
import { getCategoryBreadcrumbs, categoryDisplayName } from '../../../utils/categoryUtils';
|
||||||
import { Box, Chip, IconButton, Typography } from '@mui/material';
|
import { Box, Chip, IconButton, Typography } from '@mui/material';
|
||||||
@@ -92,10 +92,29 @@ function categoryLabels(categoryIds, categories, rootCategoryId) {
|
|||||||
.filter((c) => c.label);
|
.filter((c) => c.label);
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryPills extends Component {
|
class CategoryPills extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { pickerOpen: false, targetPillId: null };
|
const catState = getCategoryState();
|
||||||
|
this.state = {
|
||||||
|
pickerOpen: false,
|
||||||
|
targetPillId: null,
|
||||||
|
categories: catState?.categories ?? categoryAutomat.state.categories,
|
||||||
|
rootCategoryId: catState?.rootCategoryId ?? categoryAutomat.state.rootCategoryId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._unsubCat = categoryAutomat.subscribe((s) => {
|
||||||
|
this.setState({
|
||||||
|
categories: s.categories,
|
||||||
|
rootCategoryId: s.rootCategoryId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unsubCat?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveCategory = (removeId) => {
|
handleRemoveCategory = (removeId) => {
|
||||||
@@ -135,8 +154,7 @@ class CategoryPills extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { categoryIds, editMode } = this.props;
|
const { categoryIds, editMode } = this.props;
|
||||||
const { pickerOpen, targetPillId } = this.state;
|
const { pickerOpen, targetPillId, categories, rootCategoryId } = this.state;
|
||||||
const { categories, rootCategoryId } = getCategoryState() ?? {};
|
|
||||||
const labels = categoryLabels(categoryIds, categories, rootCategoryId);
|
const labels = categoryLabels(categoryIds, categories, rootCategoryId);
|
||||||
|
|
||||||
const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null;
|
const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import Link from '@mui/material/Link';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
@@ -562,6 +564,27 @@ export default class ArticleAttributesTab extends PureComponent {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{merkmalOptions.length === 0 && (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.5 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Im System sind noch keine Merkmale angelegt.
|
||||||
|
</Typography>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
to="/artikel/merkmale"
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
fontWeight: 600,
|
||||||
|
textDecoration: 'none',
|
||||||
|
'&:hover': { textDecoration: 'underline' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Zu den Merkmalen →
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Unified Assigned Merkmale List */}
|
{/* Unified Assigned Merkmale List */}
|
||||||
{assignedGroups.length > 0 && (
|
{assignedGroups.length > 0 && (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||||
|
|||||||
@@ -416,15 +416,13 @@ export default class CategoryTree extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.categories === undefined || this.props.rootCategoryId === undefined) {
|
|
||||||
this._unsub = categoryAutomat.subscribe((s) => {
|
this._unsub = categoryAutomat.subscribe((s) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
categories: this.props.categories ?? s.categories,
|
categories: s.categories,
|
||||||
rootCategoryId: this.props.rootCategoryId ?? s.rootCategoryId,
|
rootCategoryId: s.rootCategoryId ?? this.props.rootCategoryId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (
|
if (
|
||||||
@@ -463,7 +461,14 @@ export default class CategoryTree extends PureComponent {
|
|||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
enqueueSnackbar(`Kategorie „${name}“ angelegt.`, { variant: 'success' });
|
enqueueSnackbar(`Kategorie „${name}“ angelegt.`, { variant: 'success' });
|
||||||
this.setState({ addDialogOpen: false });
|
const nextCategories = categoryAutomat.state.categories ?? {
|
||||||
|
...(this.state.categories || {}),
|
||||||
|
[String(created.id)]: created,
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
addDialogOpen: false,
|
||||||
|
categories: nextCategories,
|
||||||
|
});
|
||||||
this.props.onSelect?.(created.id, created);
|
this.props.onSelect?.(created.id, created);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
@@ -489,12 +494,19 @@ export default class CategoryTree extends PureComponent {
|
|||||||
if (!name || !renameNode) return;
|
if (!name || !renameNode) return;
|
||||||
this.setState({ isSubmitting: true });
|
this.setState({ isSubmitting: true });
|
||||||
try {
|
try {
|
||||||
await categoryAutomat.actions.renameCategory({
|
const updated = await categoryAutomat.actions.renameCategory({
|
||||||
categoryId: renameNode.id,
|
categoryId: renameNode.id,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
enqueueSnackbar(`Kategorie in „${name}“ umbenannt.`, { variant: 'success' });
|
enqueueSnackbar(`Kategorie in „${name}“ umbenannt.`, { variant: 'success' });
|
||||||
this.setState({ renameDialogOpen: false });
|
const nextCategories = categoryAutomat.state.categories ?? {
|
||||||
|
...(this.state.categories || {}),
|
||||||
|
[String(updated.id)]: updated,
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
renameDialogOpen: false,
|
||||||
|
categories: nextCategories,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -524,7 +536,10 @@ export default class CategoryTree extends PureComponent {
|
|||||||
if (this.props.selectedId != null && String(this.props.selectedId) === String(deleteNode.id)) {
|
if (this.props.selectedId != null && String(this.props.selectedId) === String(deleteNode.id)) {
|
||||||
this.props.onSelect?.(deleteNode.parentId ?? rootCategoryId);
|
this.props.onSelect?.(deleteNode.parentId ?? rootCategoryId);
|
||||||
}
|
}
|
||||||
this.setState({ deleteDialogOpen: false });
|
this.setState({
|
||||||
|
deleteDialogOpen: false,
|
||||||
|
categories: categoryAutomat.state.categories,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -19,17 +19,18 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
this.state = {
|
this.state = {
|
||||||
assigningSku: null,
|
assigningSku: null,
|
||||||
error: null,
|
error: null,
|
||||||
|
showAllArticles: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!prevProps.open && this.props.open) {
|
if (!prevProps.open && this.props.open) {
|
||||||
this.setState({ assigningSku: null, error: null });
|
this.setState({ assigningSku: null, error: null, showAllArticles: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
this.setState({ assigningSku: null, error: null });
|
this.setState({ assigningSku: null, error: null, showAllArticles: false });
|
||||||
this.props.onClose?.();
|
this.props.onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -61,9 +62,9 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose, line, kind = 'delivery', supplierArticles: propSupplierArticles } = this.props;
|
const { open, onClose, line, kind = 'delivery', supplierArticles: propSupplierArticles } = this.props;
|
||||||
const { assigningSku, error } = this.state;
|
const { assigningSku, error, showAllArticles } = this.state;
|
||||||
const isDelivery = kind === 'delivery';
|
const isDelivery = kind === 'delivery';
|
||||||
const onlySupplierArticles = isDelivery && !line;
|
const onlySupplierArticles = isDelivery && !line && !showAllArticles;
|
||||||
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
|
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
|
||||||
|
|
||||||
const supplierArticlesList = onlySupplierArticles
|
const supplierArticlesList = onlySupplierArticles
|
||||||
@@ -122,7 +123,7 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h6" component="div" fontWeight={600}>
|
<Typography variant="h6" component="div" fontWeight={600}>
|
||||||
{line
|
{line
|
||||||
? 'Existierenden Artikel zuordnen'
|
? (isDelivery ? 'Artikel neu zuordnen' : 'Existierenden Artikel zuordnen')
|
||||||
: isDelivery
|
: isDelivery
|
||||||
? 'Lieferanten-Artikel auswählen'
|
? 'Lieferanten-Artikel auswählen'
|
||||||
: 'Artikel auswählen'}
|
: 'Artikel auswählen'}
|
||||||
@@ -134,7 +135,21 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
{line?.name ? ` · ${line.name}` : ''}
|
{line?.name ? ` · ${line.name}` : ''}
|
||||||
</>
|
</>
|
||||||
) : isDelivery ? (
|
) : isDelivery ? (
|
||||||
'Nur mit dem ausgewählten Lieferanten verknüpfte Artikel'
|
<Box component="span" sx={{ display: 'inline-flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<span>
|
||||||
|
{showAllArticles
|
||||||
|
? 'Alle Artikel im Katalog durchsuchen'
|
||||||
|
: 'Nur mit dem ausgewählten Lieferanten verknüpfte Artikel'}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => this.setState({ showAllArticles: !showAllArticles })}
|
||||||
|
sx={{ fontSize: '0.72rem', py: 0, px: 0.75, textTransform: 'none', minWidth: 'auto', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
{showAllArticles ? 'Nur Lieferanten-Artikel' : 'Alle Artikel durchsuchen'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
'Artikel für Beleg auswählen'
|
'Artikel für Beleg auswählen'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -766,6 +766,30 @@ class ManifestFormLine extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{this.props.onSearchArticle && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
||||||
|
disabled={this.props.saving || this.props.supplierArticleBusy || this.isFullyLedgered()}
|
||||||
|
onClick={this.handleSearchArticle}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
minWidth: 'auto',
|
||||||
|
py: 0.25,
|
||||||
|
px: 1,
|
||||||
|
color: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'primary.50',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Artikel neu zuordnen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
{cachedArticle.brand?.name && (
|
{cachedArticle.brand?.name && (
|
||||||
<BrandLabel
|
<BrandLabel
|
||||||
@@ -1124,6 +1148,30 @@ class ManifestFormLine extends PureComponent {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{this.props.onSearchArticle && (
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
||||||
|
disabled={this.props.saving || this.props.supplierArticleBusy || this.isFullyLedgered()}
|
||||||
|
onClick={this.handleSearchArticle}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
minWidth: 'auto',
|
||||||
|
py: 0.25,
|
||||||
|
px: 1,
|
||||||
|
color: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'primary.50',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Artikel neu zuordnen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
{(fullArticle.brand?.name || article.brand?.name) && (
|
{(fullArticle.brand?.name || article.brand?.name) && (
|
||||||
<BrandLabel
|
<BrandLabel
|
||||||
@@ -1307,6 +1355,34 @@ class ManifestFormLine extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.onSearchArticle) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
||||||
|
disabled={this.isFullyLedgered()}
|
||||||
|
onClick={this.handleSearchArticle}
|
||||||
|
sx={{
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
fontWeight: 600,
|
||||||
|
textTransform: 'none',
|
||||||
|
minWidth: 'auto',
|
||||||
|
py: 0.25,
|
||||||
|
px: 1,
|
||||||
|
color: 'primary.main',
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: 'primary.50',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Artikel suchen
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
|||||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||||
import articleSearchAutomat from '../automats/articleSearchAutomat';
|
import articleSearchAutomat from '../automats/articleSearchAutomat';
|
||||||
|
import articleIndexAutomat from '../automats/articleIndexAutomat';
|
||||||
import BackendError from '../components/BackendError';
|
import BackendError from '../components/BackendError';
|
||||||
import { formatPrice, getArticleGross } from '../utils/price';
|
import { formatPrice, getArticleGross } from '../utils/price';
|
||||||
import { thumb } from '../api';
|
import { thumb } from '../api';
|
||||||
@@ -50,6 +51,7 @@ import navigationAutomat, {
|
|||||||
import categoryAutomat from '../automats/categoryAutomat';
|
import categoryAutomat from '../automats/categoryAutomat';
|
||||||
import companyAutomat from '../automats/companyAutomat';
|
import companyAutomat from '../automats/companyAutomat';
|
||||||
import { BrandLabel, BrandThumb } from '../components/brand';
|
import { BrandLabel, BrandThumb } from '../components/brand';
|
||||||
|
import { ArticleNameDialog } from '../components/article';
|
||||||
import { SCRIM_LABEL } from '../colors';
|
import { SCRIM_LABEL } from '../colors';
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
@@ -282,6 +284,7 @@ function initialSearchState(searchParams) {
|
|||||||
categoryFilterOpen: false,
|
categoryFilterOpen: false,
|
||||||
brandFilterOpen: false,
|
brandFilterOpen: false,
|
||||||
supplierFilterOpen: false,
|
supplierFilterOpen: false,
|
||||||
|
newArticleOpen: false,
|
||||||
error: automatResult.error,
|
error: automatResult.error,
|
||||||
categoryFacets: automatResult.categoryFacets,
|
categoryFacets: automatResult.categoryFacets,
|
||||||
brandFacets: automatResult.brandFacets,
|
brandFacets: automatResult.brandFacets,
|
||||||
@@ -354,6 +357,42 @@ class ArtikelSearchPage extends Component {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleOpenNewArticle = () => {
|
||||||
|
this.setState({ newArticleOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCloseNewArticle = () => {
|
||||||
|
this.setState({ newArticleOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleCreateArticle = async ({ names }) => {
|
||||||
|
const filterParam = this.props.searchParams.get('filter') ?? '';
|
||||||
|
const categories = parseFilter(filterParam);
|
||||||
|
const parentId =
|
||||||
|
categories[0] ||
|
||||||
|
categoryAutomat.state.rootCategoryId ||
|
||||||
|
companyAutomat.state.current?.rootCategoryId ||
|
||||||
|
companyAutomat.state.current?.rootcategory_id ||
|
||||||
|
1;
|
||||||
|
const list = (names ?? []).filter(Boolean);
|
||||||
|
if (list.length <= 1) {
|
||||||
|
const created = await categoryAutomat.actions.createArticle({
|
||||||
|
parentId,
|
||||||
|
name: list[0] ?? '',
|
||||||
|
});
|
||||||
|
void articleIndexAutomat.actions.reload().catch(() => {});
|
||||||
|
if (created) {
|
||||||
|
this.props.navigate(artikelDetailPath(created));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const createdList = await categoryAutomat.actions.createArticles({ parentId, names: list });
|
||||||
|
void articleIndexAutomat.actions.reload().catch(() => {});
|
||||||
|
if (createdList?.[0]) {
|
||||||
|
this.props.navigate(artikelDetailPath(createdList[0]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleOpenArticle = (row) => {
|
handleOpenArticle = (row) => {
|
||||||
this.persistQuery();
|
this.persistQuery();
|
||||||
this.props.navigate(artikelDetailPath(row));
|
this.props.navigate(artikelDetailPath(row));
|
||||||
@@ -548,6 +587,7 @@ class ArtikelSearchPage extends Component {
|
|||||||
categoryFilterOpen,
|
categoryFilterOpen,
|
||||||
brandFilterOpen,
|
brandFilterOpen,
|
||||||
supplierFilterOpen,
|
supplierFilterOpen,
|
||||||
|
newArticleOpen,
|
||||||
error,
|
error,
|
||||||
totalCount,
|
totalCount,
|
||||||
categoryFacets,
|
categoryFacets,
|
||||||
@@ -696,6 +736,13 @@ class ArtikelSearchPage extends Component {
|
|||||||
>
|
>
|
||||||
Lieferanten
|
Lieferanten
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={this.handleOpenNewArticle}
|
||||||
|
sx={{ height: 40, whiteSpace: 'nowrap', flexShrink: 0 }}
|
||||||
|
>
|
||||||
|
Neuer Artikel
|
||||||
|
</Button>
|
||||||
|
|
||||||
{categories.map((catId) => {
|
{categories.map((catId) => {
|
||||||
const label = getCategoryBreadcrumbLabel(
|
const label = getCategoryBreadcrumbLabel(
|
||||||
@@ -1047,6 +1094,12 @@ class ArtikelSearchPage extends Component {
|
|||||||
</PanelTile>
|
</PanelTile>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<ArticleNameDialog
|
||||||
|
open={newArticleOpen}
|
||||||
|
initialName={input}
|
||||||
|
onClose={this.handleCloseNewArticle}
|
||||||
|
onSubmit={this.handleCreateArticle}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,6 +576,20 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// If this line was previously mapped to a different article, unlink the old supplier article
|
||||||
|
const existingSupplierArticle =
|
||||||
|
(line?.key != null ? this.state.supplierArticlesByLineKey?.[line.key] : null) ||
|
||||||
|
(supplierSku ? (this.state.supplierArticlesMap?.[supplierSku.toLowerCase()] || this.state.supplierArticlesMap?.[supplierSku]) : null);
|
||||||
|
const oldInternalSku = existingSupplierArticle?.article?.sku;
|
||||||
|
const oldAssignmentId = existingSupplierArticle?.id;
|
||||||
|
if (oldInternalSku && oldAssignmentId != null && String(oldInternalSku).toLowerCase() !== String(internalSku).toLowerCase()) {
|
||||||
|
try {
|
||||||
|
await this.automat.actions.unlinkSupplierArticle(oldInternalSku, oldAssignmentId, line?.key);
|
||||||
|
} catch (unlinkErr) {
|
||||||
|
console.warn('Vorherige Lieferanten-Zuordnung konnte nicht gelöst werden:', unlinkErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await articleApi.addSupplier(internalSku, {
|
await articleApi.addSupplier(internalSku, {
|
||||||
entityId: partyId,
|
entityId: partyId,
|
||||||
sku: supplierSku || undefined,
|
sku: supplierSku || undefined,
|
||||||
@@ -587,13 +601,16 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
|
|
||||||
if (supplierSku) {
|
if (supplierSku) {
|
||||||
await this.automat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
await this.automat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
||||||
|
} else {
|
||||||
|
await this.automat.actions.fetchSupplierArticleForSku(internalSku, line.key);
|
||||||
}
|
}
|
||||||
await this.automat.actions.fetchSupplierArticles(partyId);
|
await this.automat.actions.fetchSupplierArticles(partyId);
|
||||||
|
|
||||||
if (!line.name && article.name) {
|
if (!line.name || (existingSupplierArticle?.article?.name && line.name === existingSupplierArticle.article.name)) {
|
||||||
this.automat.actions.updateLineitem({
|
this.automat.actions.updateLineitem({
|
||||||
...line,
|
...line,
|
||||||
name: article.name,
|
name: article.name || line.name,
|
||||||
|
...(!line.sku ? { sku: internalSku } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user