u
This commit is contained in:
@@ -215,18 +215,22 @@ class StorageManager {
|
|||||||
Object.defineProperties(this, {
|
Object.defineProperties(this, {
|
||||||
authMePromise: { get: () => authMeCache.promise },
|
authMePromise: { get: () => authMeCache.promise },
|
||||||
updateAuthMe: { value: authMeCache.update },
|
updateAuthMe: { value: authMeCache.update },
|
||||||
|
|
||||||
articleIndexPromise: { get: () => articleIndexCache.promise },
|
articleIndexPromise: { get: () => articleIndexCache.promise },
|
||||||
updateArticleIndex: { value: articleIndexCache.update },
|
updateArticleIndex: { value: articleIndexCache.update },
|
||||||
/** @deprecated use updateArticleIndex */
|
|
||||||
update: { value: articleIndexCache.update },
|
|
||||||
brandPromise: { get: () => brandCache.promise },
|
brandPromise: { get: () => brandCache.promise },
|
||||||
updateBrand: { value: brandCache.update },
|
updateBrand: { value: brandCache.update },
|
||||||
|
|
||||||
unitPromise: { get: () => unitCache.promise },
|
unitPromise: { get: () => unitCache.promise },
|
||||||
updateUnit: { value: unitCache.update },
|
updateUnit: { value: unitCache.update },
|
||||||
|
|
||||||
companyPromise: { get: () => companyCache.promise },
|
companyPromise: { get: () => companyCache.promise },
|
||||||
updateCompany: { value: companyCache.update },
|
updateCompany: { value: companyCache.update },
|
||||||
|
|
||||||
categoryTreePromise: { get: () => categoryTreeCache.promise },
|
categoryTreePromise: { get: () => categoryTreeCache.promise },
|
||||||
updateCategoryTree: { value: categoryTreeCache.update },
|
updateCategoryTree: { value: categoryTreeCache.update },
|
||||||
|
|
||||||
posterminalPromise: { get: () => posterminalCache.promise },
|
posterminalPromise: { get: () => posterminalCache.promise },
|
||||||
updatePosterminal: { value: posterminalCache.update },
|
updatePosterminal: { value: posterminalCache.update },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { thumb } from '../../api';
|
import { StorageManagerContext } from '../../api/storageManager';
|
||||||
import ThumbHashImage from './ThumbHashImage';
|
import ThumbHashImage from './ThumbHashImage';
|
||||||
|
|
||||||
class ArticlePhoto extends Component {
|
class ArticlePhoto extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { thumbLoaded: false };
|
this.state = { thumbLoaded: false };
|
||||||
@@ -78,7 +80,7 @@ class ArticlePhoto extends Component {
|
|||||||
{fullHash ? (
|
{fullHash ? (
|
||||||
<Box
|
<Box
|
||||||
component="img"
|
component="img"
|
||||||
src={thumb.url(fullHash)}
|
src={this.context.thumb.url(fullHash)}
|
||||||
alt=""
|
alt=""
|
||||||
onLoad={this.handleThumbLoad}
|
onLoad={this.handleThumbLoad}
|
||||||
onError={this.handleThumbError}
|
onError={this.handleThumbError}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, createRef } from 'react';
|
import { Component, createRef } from 'react';
|
||||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||||
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
|
||||||
import { article } from '../../api';
|
import { StorageManagerContext } from '../../api/storageManager';
|
||||||
import { PanelTile } from '../panels';
|
import { PanelTile } from '../panels';
|
||||||
import { requireObject } from '../../utils/apiShape';
|
import { requireObject } from '../../utils/apiShape';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
@@ -12,6 +12,8 @@ import ArticlePhoto from './ArticlePhoto';
|
|||||||
import ArticlePhotosDialog from './ArticlePhotosDialog';
|
import ArticlePhotosDialog from './ArticlePhotosDialog';
|
||||||
|
|
||||||
class ArticlePhotoTile extends Component {
|
class ArticlePhotoTile extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.fileInputRef = createRef();
|
this.fileInputRef = createRef();
|
||||||
@@ -29,7 +31,7 @@ class ArticlePhotoTile extends Component {
|
|||||||
try {
|
try {
|
||||||
const file = await prepareArticlePhoto(rawBlob);
|
const file = await prepareArticlePhoto(rawBlob);
|
||||||
const uploadStarted = performance.now();
|
const uploadStarted = performance.now();
|
||||||
const res = await article.uploadImage(articleData.sku, file);
|
const res = await this.context.article.addImage(articleData.sku, file);
|
||||||
console.log('[article-photo] upload', {
|
console.log('[article-photo] upload', {
|
||||||
uploadMs: Math.round(performance.now() - uploadStarted),
|
uploadMs: Math.round(performance.now() - uploadStarted),
|
||||||
bytes: file.size,
|
bytes: file.size,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
|||||||
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
||||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||||
import { article } from '../../api';
|
import { StorageManagerContext } from '../../api/storageManager';
|
||||||
import { requireArray, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireObject } from '../../utils/apiShape';
|
||||||
import {
|
import {
|
||||||
articleWithPhotoOrder,
|
articleWithPhotoOrder,
|
||||||
@@ -41,6 +41,8 @@ const overlayIconSx = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class ArticlePhotosDialog extends Component {
|
class ArticlePhotosDialog extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.fileInputRef = createRef();
|
this.fileInputRef = createRef();
|
||||||
@@ -62,7 +64,7 @@ class ArticlePhotosDialog extends Component {
|
|||||||
try {
|
try {
|
||||||
const file = await prepareArticlePhoto(rawBlob);
|
const file = await prepareArticlePhoto(rawBlob);
|
||||||
const uploadStarted = performance.now();
|
const uploadStarted = performance.now();
|
||||||
const res = await article.uploadImage(articleData.sku, file);
|
const res = await this.context.article.addImage(articleData.sku, file);
|
||||||
console.log('[article-photo] upload', {
|
console.log('[article-photo] upload', {
|
||||||
uploadMs: Math.round(performance.now() - uploadStarted),
|
uploadMs: Math.round(performance.now() - uploadStarted),
|
||||||
bytes: file.size,
|
bytes: file.size,
|
||||||
@@ -98,7 +100,7 @@ class ArticlePhotosDialog extends Component {
|
|||||||
const { article: articleData, onArticleUpdated } = this.props;
|
const { article: articleData, onArticleUpdated } = this.props;
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
try {
|
try {
|
||||||
await article.deleteImage(articleData.sku, fullHash);
|
await this.context.article.deleteImage(articleData.sku, fullHash);
|
||||||
onArticleUpdated?.(articleWithoutPhoto(articleData, fullHash));
|
onArticleUpdated?.(articleWithoutPhoto(articleData, fullHash));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
@@ -121,7 +123,7 @@ class ArticlePhotosDialog extends Component {
|
|||||||
|
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
try {
|
try {
|
||||||
const res = await article.orderImages(articleData.sku, fullHashes);
|
const res = await this.context.article.orderImages(articleData.sku, { fullHashes });
|
||||||
requireObject(res, 'PUT /api/article/:sku/images/order');
|
requireObject(res, 'PUT /api/article/:sku/images/order');
|
||||||
requireArray(res.fullHashes, 'fullHashes');
|
requireArray(res.fullHashes, 'fullHashes');
|
||||||
onArticleUpdated?.(articleWithPhotoOrder(articleData, res.fullHashes));
|
onArticleUpdated?.(articleWithPhotoOrder(articleData, res.fullHashes));
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import LocalShippingIcon from '@mui/icons-material/LocalShipping';
|
|||||||
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
import ManageSearchIcon from '@mui/icons-material/ManageSearch';
|
||||||
import PushPinIcon from '@mui/icons-material/PushPin';
|
import PushPinIcon from '@mui/icons-material/PushPin';
|
||||||
import SettingsIcon from '@mui/icons-material/Settings';
|
import SettingsIcon from '@mui/icons-material/Settings';
|
||||||
import { article } from '../../api';
|
import { StorageManagerContext } from '../../api/storageManager';
|
||||||
import {
|
import {
|
||||||
ARTICLE_TAB,
|
ARTICLE_TAB,
|
||||||
DEFAULT_ARTICLE_TAB,
|
DEFAULT_ARTICLE_TAB,
|
||||||
@@ -92,6 +92,8 @@ function Placeholder({ label }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ArticleTileTab extends Component {
|
class ArticleTileTab extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -123,7 +125,7 @@ class ArticleTileTab extends Component {
|
|||||||
|
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
try {
|
try {
|
||||||
await article.delete(articleData.sku);
|
await this.context.article.delete(articleData.sku);
|
||||||
onArticleDeleted?.(articleData);
|
onArticleDeleted?.(articleData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
@@ -16,16 +15,11 @@ import ListItemButton from '@mui/material/ListItemButton';
|
|||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { StorageManagerContext } from '../../api/storageManager';
|
||||||
|
import { requireObject } from '../../utils/apiShape';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { parsePriceInput } from '../../utils/price';
|
import { parsePriceInput } from '../../utils/price';
|
||||||
import { COMMON_UNITS, DEFAULT_UNIT_SI, unitLabel } from '../../utils/units';
|
import { COMMON_UNITS, DEFAULT_UNIT_SI, unitLabel } from '../../utils/units';
|
||||||
import {
|
|
||||||
deleteUnit,
|
|
||||||
loadUnits,
|
|
||||||
selectUnits,
|
|
||||||
selectUnitsStatus,
|
|
||||||
upsertUnit,
|
|
||||||
} from '../../store/unitSlice';
|
|
||||||
|
|
||||||
function emptyForm() {
|
function emptyForm() {
|
||||||
return {
|
return {
|
||||||
@@ -68,11 +62,15 @@ function formValid(form) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ArticleUnitEditorDialog extends Component {
|
export default class ArticleUnitEditorDialog extends Component {
|
||||||
|
static contextType = StorageManagerContext;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
|
units: [],
|
||||||
|
unitsLoading: false,
|
||||||
form: emptyForm(),
|
form: emptyForm(),
|
||||||
selectedSi: null,
|
selectedSi: null,
|
||||||
};
|
};
|
||||||
@@ -81,10 +79,19 @@ class ArticleUnitEditorDialog extends Component {
|
|||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.open && !prevProps.open) {
|
if (this.props.open && !prevProps.open) {
|
||||||
this.setState({ form: emptyForm(), selectedSi: null });
|
this.setState({ form: emptyForm(), selectedSi: null });
|
||||||
if (this.props.status !== 'succeeded') {
|
void this.loadUnits();
|
||||||
void this.props.loadUnits().catch((err) => notifyError(err));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadUnits() {
|
||||||
|
this.setState({ unitsLoading: true });
|
||||||
|
try {
|
||||||
|
const units = await this.context.unitPromise;
|
||||||
|
this.setState({ units, unitsLoading: false });
|
||||||
|
} catch (err) {
|
||||||
|
this.setState({ unitsLoading: false });
|
||||||
|
notifyError(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectUnit = (row) => {
|
handleSelectUnit = (row) => {
|
||||||
@@ -110,19 +117,23 @@ class ArticleUnitEditorDialog extends Component {
|
|||||||
if (!formValid(form)) return;
|
if (!formValid(form)) return;
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
try {
|
try {
|
||||||
const unit = await this.props.upsertUnit({
|
const data = await this.context.unit.upsert({
|
||||||
si: String(form.si ?? '').trim(),
|
si: String(form.si ?? '').trim(),
|
||||||
basesi: String(form.basesi ?? '').trim(),
|
basesi: String(form.basesi ?? '').trim(),
|
||||||
basesifactor: parseFactor(form.basesifactor),
|
basesifactor: parseFactor(form.basesifactor),
|
||||||
symbol: String(form.symbol ?? '').trim(),
|
symbol: String(form.symbol ?? '').trim(),
|
||||||
name: String(form.name ?? '').trim(),
|
name: String(form.name ?? '').trim(),
|
||||||
}).unwrap();
|
});
|
||||||
|
requireObject(data, 'PUT /api/unit');
|
||||||
|
requireObject(data.unit, 'unit');
|
||||||
|
const units = await this.context.updateUnit();
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
form: formFromUnit(unit),
|
units,
|
||||||
selectedSi: unit.si,
|
form: formFromUnit(data.unit),
|
||||||
|
selectedSi: data.unit.si,
|
||||||
});
|
});
|
||||||
this.props.onUnitUpserted?.(unit);
|
this.props.onUnitUpserted?.(data.unit);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ busy: false });
|
this.setState({ busy: false });
|
||||||
notifyError(err);
|
notifyError(err);
|
||||||
@@ -134,8 +145,9 @@ class ArticleUnitEditorDialog extends Component {
|
|||||||
if (!selectedSi || selectedSi === DEFAULT_UNIT_SI) return;
|
if (!selectedSi || selectedSi === DEFAULT_UNIT_SI) return;
|
||||||
this.setState({ busy: true });
|
this.setState({ busy: true });
|
||||||
try {
|
try {
|
||||||
await this.props.deleteUnit(selectedSi).unwrap();
|
await this.context.unit.delete(selectedSi);
|
||||||
this.setState({ busy: false, form: emptyForm(), selectedSi: null });
|
const units = await this.context.updateUnit();
|
||||||
|
this.setState({ busy: false, units, form: emptyForm(), selectedSi: null });
|
||||||
this.props.onUnitDeleted?.(selectedSi);
|
this.props.onUnitDeleted?.(selectedSi);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ busy: false });
|
this.setState({ busy: false });
|
||||||
@@ -148,10 +160,10 @@ class ArticleUnitEditorDialog extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose, units, status } = this.props;
|
const { open, onClose } = this.props;
|
||||||
const { busy, form, selectedSi } = this.state;
|
const { busy, form, selectedSi, units, unitsLoading } = this.state;
|
||||||
const canSave = formValid(form) && !busy;
|
const canSave = formValid(form) && !busy;
|
||||||
const loading = status === 'loading' && units.length === 0;
|
const loading = unitsLoading && units.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={busy ? undefined : onClose} maxWidth="md" fullWidth>
|
<Dialog open={open} onClose={busy ? undefined : onClose} maxWidth="md" fullWidth>
|
||||||
@@ -302,16 +314,3 @@ class ArticleUnitEditorDialog extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
|
||||||
units: selectUnits(state),
|
|
||||||
status: selectUnitsStatus(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
loadUnits: () => dispatch(loadUnits()),
|
|
||||||
upsertUnit: (body) => dispatch(upsertUnit(body)),
|
|
||||||
deleteUnit: (si) => dispatch(deleteUnit(si)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ArticleUnitEditorDialog);
|
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { PanelTileGrid } from '../panels';
|
import { PanelTileGrid } from '../panels';
|
||||||
import { requireDefined } from '../../utils/apiShape';
|
import { requireDefined } from '../../utils/apiShape';
|
||||||
import { primaryPhoto } from '../../utils/articleDetail';
|
import { useStorageManager } from '../../api/storageManager';
|
||||||
|
import { parseCategoryTreeResponse } from '../../store/categorySlice';
|
||||||
import { PANEL_TILE_KEYS } from '../../theme';
|
import { PANEL_TILE_KEYS } from '../../theme';
|
||||||
|
import { notifyError } from '../../utils/notify';
|
||||||
import {
|
import {
|
||||||
buildCategoryPath,
|
buildCategoryPath,
|
||||||
defaultCategoryPath,
|
defaultCategoryPath,
|
||||||
} from '../../pages/artikel/categoryRoute';
|
} from '../../pages/artikel/categoryRoute';
|
||||||
import {
|
|
||||||
patchArticleInLists,
|
|
||||||
removeArticleBySku,
|
|
||||||
selectCategoryRootId,
|
|
||||||
loadCategoryArticles,
|
|
||||||
selectCurrentCategoryId,
|
|
||||||
} from '../../store/categorySlice';
|
|
||||||
|
|
||||||
import ArticleTileTab from './ArticleTileTab';
|
import ArticleTileTab from './ArticleTileTab';
|
||||||
import ArticlePhotoTile from './ArticlePhotoTile';
|
import ArticlePhotoTile from './ArticlePhotoTile';
|
||||||
@@ -32,48 +27,55 @@ export default function ArticleView({
|
|||||||
onArticleUpdated,
|
onArticleUpdated,
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const store = useStorageManager();
|
||||||
const categories = useSelector((state) => state.category.categories);
|
const [categories, setCategories] = useState(null);
|
||||||
const rootCategoryId = useSelector(selectCategoryRootId);
|
const [rootCategoryId, setRootCategoryId] = useState(null);
|
||||||
const currentCategoryId = useSelector(selectCurrentCategoryId);
|
|
||||||
|
|
||||||
const syncCategoryListPhoto = (next) => {
|
useEffect(() => {
|
||||||
const photo = primaryPhoto(next);
|
let cancelled = false;
|
||||||
dispatch(
|
async function load() {
|
||||||
patchArticleInLists({
|
try {
|
||||||
sku: next.sku,
|
const [treeData, companies, session] = await Promise.all([
|
||||||
fullHash: photo?.fullHash ?? null,
|
store.categoryTreePromise,
|
||||||
thumbW: photo?.thumbW ?? null,
|
store.companyPromise,
|
||||||
thumbH: photo?.thumbH ?? null,
|
store.authMePromise,
|
||||||
thumbHash: photo?.thumbHash ?? null,
|
]);
|
||||||
}),
|
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
|
||||||
|
const company = requireDefined(
|
||||||
|
companies.find((c) => c.id === companyId),
|
||||||
|
'company',
|
||||||
);
|
);
|
||||||
|
const parsed = parseCategoryTreeResponse(treeData, company.rootCategoryId);
|
||||||
|
if (!cancelled) {
|
||||||
|
setCategories(parsed.categories);
|
||||||
|
setRootCategoryId(parsed.rootCategoryId);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) notifyError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void load();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [store]);
|
||||||
|
|
||||||
|
const refreshSearchCaches = () => {
|
||||||
|
void store.updateArticleIndex();
|
||||||
|
void store.updateCategoryTree();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArticleUpdated = (next) => {
|
const handleArticleUpdated = (next) => {
|
||||||
onArticleUpdated?.(next);
|
onArticleUpdated?.(next);
|
||||||
syncCategoryListPhoto(next);
|
refreshSearchCaches();
|
||||||
dispatch(
|
|
||||||
patchArticleInLists({
|
|
||||||
sku: next.sku,
|
|
||||||
name: next.name,
|
|
||||||
categoryIds: next.categoryIds,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reload articles to reflect the update
|
|
||||||
// The reducer handles removing from lists where article no longer belongs
|
|
||||||
if (currentCategoryId != null && String(currentCategoryId) !== String(rootCategoryId)) {
|
|
||||||
dispatch(loadCategoryArticles(currentCategoryId));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArticleDeleted = () => {
|
const handleArticleDeleted = () => {
|
||||||
|
refreshSearchCaches();
|
||||||
const categoryId = requireDefined(
|
const categoryId = requireDefined(
|
||||||
articleData.categoryIds?.[0],
|
articleData.categoryIds?.[0],
|
||||||
'article.categoryIds[0]',
|
'article.categoryIds[0]',
|
||||||
);
|
);
|
||||||
dispatch(removeArticleBySku(articleData.sku));
|
|
||||||
if (categories && rootCategoryId != null) {
|
if (categories && rootCategoryId != null) {
|
||||||
navigate(buildCategoryPath(categories, rootCategoryId, categoryId));
|
navigate(buildCategoryPath(categories, rootCategoryId, categoryId));
|
||||||
return;
|
return;
|
||||||
@@ -102,17 +104,17 @@ export default function ArticleView({
|
|||||||
|
|
||||||
<ArticleViewBrand
|
<ArticleViewBrand
|
||||||
article={articleData}
|
article={articleData}
|
||||||
onArticleUpdated={onArticleUpdated}
|
onArticleUpdated={handleArticleUpdated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArticleViewEANs
|
<ArticleViewEANs
|
||||||
article={articleData}
|
article={articleData}
|
||||||
onArticleUpdated={onArticleUpdated}
|
onArticleUpdated={handleArticleUpdated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArticleViewPrice
|
<ArticleViewPrice
|
||||||
article={articleData}
|
article={articleData}
|
||||||
onArticleUpdated={onArticleUpdated}
|
onArticleUpdated={handleArticleUpdated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ArticleTileTab
|
<ArticleTileTab
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useEffect, useState } from 'react';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import Autocomplete from '@mui/material/Autocomplete';
|
import Autocomplete from '@mui/material/Autocomplete';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from '../../panels';
|
||||||
import { article } from '../../../api';
|
import { useStorageManager } from '../../../api/storageManager';
|
||||||
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
||||||
import { loadBrands, selectBrands, selectBrandsStatus, upsertBrand } from '../../../store/brandSlice';
|
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
import { PANEL_TILE_KEYS } from '../../../theme';
|
import { PANEL_TILE_KEYS } from '../../../theme';
|
||||||
|
|
||||||
@@ -42,14 +40,22 @@ function brandDraftValid(draft) {
|
|||||||
const SECOND_ROW_MIN_HEIGHT = 220;
|
const SECOND_ROW_MIN_HEIGHT = 220;
|
||||||
|
|
||||||
export default function ArticleViewBrand({ article: articleData, onArticleUpdated }) {
|
export default function ArticleViewBrand({ article: articleData, onArticleUpdated }) {
|
||||||
const dispatch = useDispatch();
|
const store = useStorageManager();
|
||||||
const brands = useSelector(selectBrands);
|
const [brands, setBrands] = useState([]);
|
||||||
const brandsStatus = useSelector(selectBrandsStatus);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (brandsStatus !== 'idle') return;
|
let cancelled = false;
|
||||||
void dispatch(loadBrands()).unwrap().catch((err) => notifyError(err));
|
store.brandPromise
|
||||||
}, [dispatch, brandsStatus]);
|
.then((list) => {
|
||||||
|
if (!cancelled) setBrands(list);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!cancelled) notifyError(err);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [store]);
|
||||||
|
|
||||||
const handleSave = async (draft) => {
|
const handleSave = async (draft) => {
|
||||||
const name = String(draft.name ?? '').trim();
|
const name = String(draft.name ?? '').trim();
|
||||||
@@ -65,12 +71,16 @@ export default function ArticleViewBrand({ article: articleData, onArticleUpdate
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
brandId = existing.id;
|
brandId = existing.id;
|
||||||
} else {
|
} else {
|
||||||
const created = await dispatch(upsertBrand({ name })).unwrap();
|
const createdRes = await store.brand.upsert({ name });
|
||||||
requireDefined(created.id, 'brand.id');
|
requireObject(createdRes, 'PUT /api/brand');
|
||||||
brandId = created.id;
|
requireObject(createdRes.brand, 'brand');
|
||||||
|
requireDefined(createdRes.brand.id, 'brand.id');
|
||||||
|
brandId = createdRes.brand.id;
|
||||||
|
const nextBrands = await store.updateBrand();
|
||||||
|
setBrands(nextBrands);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await article.setBrandIdBySku(articleData.sku, brandId);
|
const res = await store.article.setBrand(articleData.sku, { brandId });
|
||||||
requireObject(res, 'PUT /api/article/:sku/brand');
|
requireObject(res, 'PUT /api/article/:sku/brand');
|
||||||
requireObject(res.article, 'article');
|
requireObject(res.article, 'article');
|
||||||
onArticleUpdated?.(res.article);
|
onArticleUpdated?.(res.article);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import TextField from '@mui/material/TextField';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from '../../panels';
|
||||||
import { article } from '../../../api';
|
import { useStorageManager } from '../../../api/storageManager';
|
||||||
import { requireObject } from '../../../utils/apiShape';
|
import { requireObject } from '../../../utils/apiShape';
|
||||||
import { articleEans } from '../../../utils/articleDetail';
|
import { articleEans } from '../../../utils/articleDetail';
|
||||||
import { PANEL_TILE_KEYS } from '../../../theme';
|
import { PANEL_TILE_KEYS } from '../../../theme';
|
||||||
@@ -41,11 +41,13 @@ function eansDraftValid(draft) {
|
|||||||
const SECOND_ROW_MIN_HEIGHT = 220;
|
const SECOND_ROW_MIN_HEIGHT = 220;
|
||||||
|
|
||||||
export default function ArticleViewEANs({ article: articleData, onArticleUpdated }) {
|
export default function ArticleViewEANs({ article: articleData, onArticleUpdated }) {
|
||||||
|
const store = useStorageManager();
|
||||||
|
|
||||||
const handleSave = async (draft) => {
|
const handleSave = async (draft) => {
|
||||||
const eans = (draft.eans ?? [])
|
const eans = (draft.eans ?? [])
|
||||||
.map((ean) => String(ean).trim())
|
.map((ean) => String(ean).trim())
|
||||||
.filter((ean) => ean.length > 0);
|
.filter((ean) => ean.length > 0);
|
||||||
const res = await article.setEansBySku(articleData.sku, eans);
|
const res = await store.article.setEans(articleData.sku, { eans });
|
||||||
requireObject(res, 'PUT /api/article/:sku/eans');
|
requireObject(res, 'PUT /api/article/:sku/eans');
|
||||||
requireObject(res.article, 'article');
|
requireObject(res.article, 'article');
|
||||||
onArticleUpdated?.(res.article);
|
onArticleUpdated?.(res.article);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from '../../panels';
|
||||||
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
import SingleLineTextEdit from '../../SingleLineTextEdit';
|
||||||
import { article } from '../../../api';
|
import { useStorageManager } from '../../../api/storageManager';
|
||||||
import { requireObject } from '../../../utils/apiShape';
|
import { requireObject } from '../../../utils/apiShape';
|
||||||
import { PANEL_TILE_KEYS } from '../../../theme';
|
import { PANEL_TILE_KEYS } from '../../../theme';
|
||||||
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
|
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
|
||||||
@@ -122,13 +122,15 @@ function CategoryPills({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleViewName({ article: articleData, categories, rootCategoryId, onArticleUpdated }) {
|
export default function ArticleViewName({ article: articleData, categories, rootCategoryId, onArticleUpdated }) {
|
||||||
|
const store = useStorageManager();
|
||||||
|
|
||||||
const handleSave = async (draft) => {
|
const handleSave = async (draft) => {
|
||||||
const name = draft.name.trim();
|
const name = draft.name.trim();
|
||||||
const categoryIds = draft.categoryIds ?? [];
|
const categoryIds = draft.categoryIds ?? [];
|
||||||
|
|
||||||
const [nameRes] = await Promise.all([
|
const [nameRes] = await Promise.all([
|
||||||
article.update(articleData.sku, { name }),
|
store.article.update(articleData.sku, { name }),
|
||||||
article.setCategories(articleData.sku, categoryIds),
|
store.article.setCategories(articleData.sku, { categoryIds }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
requireObject(nameRes, 'PATCH /api/article/:sku');
|
requireObject(nameRes, 'PATCH /api/article/:sku');
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
@@ -7,11 +6,8 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { EditablePanelTile } from '../../panels';
|
import { EditablePanelTile } from '../../panels';
|
||||||
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
|
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
|
||||||
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
|
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
|
||||||
import { article } from '../../../api';
|
import { useStorageManager } from '../../../api/storageManager';
|
||||||
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
import { requireDefined, requireObject } from '../../../utils/apiShape';
|
||||||
import { loadUnits, selectUnits, selectUnitsStatus } from '../../../store/unitSlice';
|
|
||||||
import { selectCurrentCompany } from '../../../store/companySlice';
|
|
||||||
import { patchArticleInLists } from '../../../store/categorySlice';
|
|
||||||
import {
|
import {
|
||||||
buildPosPricesPutBody,
|
buildPosPricesPutBody,
|
||||||
companyTaxRateOptions,
|
companyTaxRateOptions,
|
||||||
@@ -219,12 +215,11 @@ function PriceAmountField({
|
|||||||
/* ── main component ────────────────────────────────────────────────── */
|
/* ── main component ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
export default function ArticleViewPrice({ article: articleData, onArticleUpdated }) {
|
export default function ArticleViewPrice({ article: articleData, onArticleUpdated }) {
|
||||||
const dispatch = useDispatch();
|
const store = useStorageManager();
|
||||||
const [tiersDialogOpen, setTiersDialogOpen] = useState(false);
|
const [tiersDialogOpen, setTiersDialogOpen] = useState(false);
|
||||||
const [unitEditorOpen, setUnitEditorOpen] = useState(false);
|
const [unitEditorOpen, setUnitEditorOpen] = useState(false);
|
||||||
const units = useSelector(selectUnits);
|
const [units, setUnits] = useState([]);
|
||||||
const unitsStatus = useSelector(selectUnitsStatus);
|
const [company, setCompany] = useState(null);
|
||||||
const company = useSelector(selectCurrentCompany);
|
|
||||||
const basePrice = primaryPrice(articleData);
|
const basePrice = primaryPrice(articleData);
|
||||||
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
|
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
|
||||||
const priceValue = priceDraftValue(articleData, company);
|
const priceValue = priceDraftValue(articleData, company);
|
||||||
@@ -234,20 +229,32 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
|
|||||||
: units;
|
: units;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (unitsStatus !== 'idle') return;
|
let cancelled = false;
|
||||||
void dispatch(loadUnits()).unwrap().catch((err) => notifyError(err));
|
async function load() {
|
||||||
}, [dispatch, unitsStatus]);
|
try {
|
||||||
|
const [unitList, companies, session] = await Promise.all([
|
||||||
const syncCategoryListPrice = (next) => {
|
store.unitPromise,
|
||||||
const price = primaryPrice(next);
|
store.companyPromise,
|
||||||
dispatch(
|
store.authMePromise,
|
||||||
patchArticleInLists({
|
]);
|
||||||
sku: next.sku,
|
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
|
||||||
net: price?.net ?? null,
|
const current = requireDefined(
|
||||||
taxRate: price?.taxRate ?? null,
|
companies.find((c) => c.id === companyId),
|
||||||
}),
|
'company',
|
||||||
);
|
);
|
||||||
|
if (!cancelled) {
|
||||||
|
setUnits(unitList);
|
||||||
|
setCompany(current);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!cancelled) notifyError(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void load();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
};
|
};
|
||||||
|
}, [store]);
|
||||||
|
|
||||||
const handleSave = async (draft) => {
|
const handleSave = async (draft) => {
|
||||||
const saved = priceDraftValue(articleData, company);
|
const saved = priceDraftValue(articleData, company);
|
||||||
@@ -269,12 +276,12 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
|
|||||||
if (isNoPackUnit(unitSi, unitamount)) {
|
if (isNoPackUnit(unitSi, unitamount)) {
|
||||||
throw new Error('Einheit pc × 1 ist keine Packungseinheit');
|
throw new Error('Einheit pc × 1 ist keine Packungseinheit');
|
||||||
}
|
}
|
||||||
const res = await article.update(articleData.sku, { unitamount, unitSi });
|
const res = await store.article.update(articleData.sku, { unitamount, unitSi });
|
||||||
requireObject(res, 'PATCH /api/article/:sku');
|
requireObject(res, 'PATCH /api/article/:sku');
|
||||||
requireObject(res.article, 'article');
|
requireObject(res.article, 'article');
|
||||||
nextArticle = res.article;
|
nextArticle = res.article;
|
||||||
} else {
|
} else {
|
||||||
const res = await article.update(articleData.sku, {
|
const res = await store.article.update(articleData.sku, {
|
||||||
unitamount: 1,
|
unitamount: 1,
|
||||||
unitSi: 'pc',
|
unitSi: 'pc',
|
||||||
});
|
});
|
||||||
@@ -299,11 +306,10 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
|
|||||||
taxRate,
|
taxRate,
|
||||||
tiers: draft.tiers ?? [],
|
tiers: draft.tiers ?? [],
|
||||||
});
|
});
|
||||||
const res = await article.setPrices(articleData.sku, prices);
|
const res = await store.article.setPrices(articleData.sku, { prices });
|
||||||
requireObject(res, 'PUT /api/article/:sku/prices');
|
requireObject(res, 'PUT /api/article/:sku/prices');
|
||||||
requireObject(res.article, 'article');
|
requireObject(res.article, 'article');
|
||||||
nextArticle = res.article;
|
nextArticle = res.article;
|
||||||
syncCategoryListPrice(res.article);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onArticleUpdated?.(nextArticle);
|
onArticleUpdated?.(nextArticle);
|
||||||
@@ -421,12 +427,14 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
|
|||||||
<ArticleUnitEditorDialog
|
<ArticleUnitEditorDialog
|
||||||
open={unitEditorOpen}
|
open={unitEditorOpen}
|
||||||
onClose={() => setUnitEditorOpen(false)}
|
onClose={() => setUnitEditorOpen(false)}
|
||||||
onUnitUpserted={(row) => {
|
onUnitUpserted={async (row) => {
|
||||||
|
setUnits(await store.unitPromise);
|
||||||
if (!draft.unitSi) {
|
if (!draft.unitSi) {
|
||||||
onChange({ ...draft, unitSi: row.si });
|
onChange({ ...draft, unitSi: row.si });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onUnitDeleted={(si) => {
|
onUnitDeleted={async (si) => {
|
||||||
|
setUnits(await store.unitPromise);
|
||||||
if (draft.unitSi === si) {
|
if (draft.unitSi === si) {
|
||||||
onChange({ ...draft, unitSi: '' });
|
onChange({ ...draft, unitSi: '' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import Alert from '@mui/material/Alert';
|
import Alert from '@mui/material/Alert';
|
||||||
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 { article } from '../../api';
|
import { useStorageManager } from '../../api/storageManager';
|
||||||
import { ArticleView } from '../../components/article';
|
import { ArticleView } from '../../components/article';
|
||||||
import BackendError from '../../components/BackendError';
|
import BackendError from '../../components/BackendError';
|
||||||
import { requireObject } from '../../utils/apiShape';
|
import { requireObject } from '../../utils/apiShape';
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
export default function ArtikelSkuPage() {
|
export default function ArtikelSkuPage() {
|
||||||
const { sku: skuParam, tab: tabParam } = useParams();
|
const { sku: skuParam, tab: tabParam } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const store = useStorageManager();
|
||||||
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
@@ -51,7 +52,7 @@ export default function ArtikelSkuPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const res = await article.getBySku(sku);
|
const res = await store.article.get(sku);
|
||||||
requireObject(res, 'GET /api/article/:sku');
|
requireObject(res, 'GET /api/article/:sku');
|
||||||
requireObject(res.article, 'article');
|
requireObject(res.article, 'article');
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -71,7 +72,7 @@ export default function ArtikelSkuPage() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [sku, reloadKey]);
|
}, [sku, reloadKey, store]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user