This commit is contained in:
seb
2026-10-04 21:04:40 +02:00
parent 669954b496
commit f765b87336
59 changed files with 1491 additions and 2045 deletions

View File

@@ -24,8 +24,7 @@ import BrokenImageIcon from '@mui/icons-material/BrokenImage';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate';
import { enqueueSnackbar } from 'notistack';
import { serper } from '../../api';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { serper, article as articleApi } from '../../api';
import artikelAutomat from '../../automats/artikelAutomat';
import { articleWithUploadedPhoto } from '../../utils/articleDetail';
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
@@ -49,8 +48,6 @@ function computeDefaultQuery(article) {
* Dialog that searches Google Images via Serper.dev SSE stream for article photos.
*/
export default class ArticleImageSearchDialog extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -236,7 +233,7 @@ export default class ArticleImageSearchDialog extends PureComponent {
const file = avifBlob instanceof File
? avifBlob
: new File([avifBlob], 'article-photo.webp', { type: avifBlob.type || 'image/webp' });
const res = await this.context.article.addImage(articleData.sku, file);
const res = await articleApi.uploadImage(articleData.sku, file);
requireObject(res, 'POST /api/article/:sku/image');
requireObject(res.image, 'image');

View File

@@ -1,11 +1,9 @@
import { Component } from 'react';
import Box from '@mui/material/Box';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { thumb } from '../../api';
import ThumbHashImage from './ThumbHashImage';
class ArticlePhoto extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = { thumbLoaded: false };
@@ -80,7 +78,7 @@ class ArticlePhoto extends Component {
{fullHash ? (
<Box
component="img"
src={this.context.thumb.url(fullHash)}
src={thumb.url(fullHash)}
alt=""
onLoad={this.handleThumbLoad}
onError={this.handleThumbError}

View File

@@ -14,7 +14,7 @@ import DeleteOutlineOutlinedIcon from '@mui/icons-material/DeleteOutlineOutlined
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { enqueueSnackbar } from 'notistack';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { article as articleApi } from '../../api';
import artikelAutomat from '../../automats/artikelAutomat';
import { requireArray, requireObject } from '../../utils/apiShape';
import {
@@ -46,8 +46,6 @@ const overlayIconSx = {
};
class ArticlePhotosDialog extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.fileInputRef = createRef();
@@ -72,7 +70,7 @@ class ArticlePhotosDialog extends Component {
try {
const file = await prepareArticlePhoto(rawBlob);
const uploadStarted = performance.now();
const res = await this.context.article.addImage(articleData.sku, file);
const res = await articleApi.uploadImage(articleData.sku, file);
console.log('[article-photo] upload', {
uploadMs: Math.round(performance.now() - uploadStarted),
bytes: file.size,
@@ -130,7 +128,7 @@ class ArticlePhotosDialog extends Component {
const { article: articleData, onArticleUpdated } = this.props;
this.setState({ busy: true });
try {
await this.context.article.deleteImage(articleData.sku, fullHash);
await articleApi.deleteImage(articleData.sku, fullHash);
const updated = articleWithoutPhoto(articleData, fullHash);
artikelAutomat.actions.setArticle(updated);
onArticleUpdated?.(updated);
@@ -155,7 +153,7 @@ class ArticlePhotosDialog extends Component {
this.setState({ busy: true });
try {
const res = await this.context.article.orderImages(articleData.sku, { fullHashes });
const res = await articleApi.orderImages(articleData.sku, fullHashes);
requireObject(res, 'PUT /api/article/:sku/images/order');
requireArray(res.fullHashes, 'fullHashes');
const updated = articleWithPhotoOrder(articleData, res.fullHashes);

View File

@@ -27,8 +27,7 @@ import TrendingDownIcon from '@mui/icons-material/TrendingDown';
import TuneIcon from '@mui/icons-material/Tune';
import Switch from '@mui/material/Switch';
import { enqueueSnackbar } from 'notistack';
import { stock, article as articleApi } from '../../api';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { stock } from '../../api';
import { formatStock } from './searchFormat';
import { notifyError } from '../../utils/notify';
import artikelAutomat from '../../automats/artikelAutomat';
@@ -43,8 +42,6 @@ const REASON_PRESETS = [
];
export default class ArticleStockAdjustDialog extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
const initialWhId = this.resolveInitialWarehouseId(props);
@@ -214,9 +211,9 @@ export default class ArticleStockAdjustDialog extends PureComponent {
// 1. Update track_stock if changed
if (trackStockChanged) {
const store = this.context;
const setFn = store?.article?.setTrackStock ?? articleApi.setTrackStock;
const trackRes = await setFn(article.sku, { track_stock: trackStock });
const trackRes = await artikelAutomat.actions.setTrackStock(article.sku, {
track_stock: trackStock,
});
updatedArticle = trackRes?.article || null;
}

View File

@@ -15,7 +15,8 @@ import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import TextInput from '../TextInput';
import Typography from '@mui/material/Typography';
import { StorageManagerContext } from '../../api/storageManagerContext';
import unitAutomat from '../../automats/unitAutomat';
import { unit as unitApi } from '../../api';
import { requireObject } from '../../utils/apiShape';
import { notifyError } from '../../utils/notify';
import { parsePriceInput } from '../../utils/price';
@@ -63,8 +64,6 @@ function formValid(form) {
}
export default class ArticleUnitEditorDialog extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -86,7 +85,7 @@ export default class ArticleUnitEditorDialog extends Component {
async loadUnits() {
this.setState({ unitsLoading: true });
try {
const units = await this.context.unitPromise;
const units = await unitAutomat.actions.load();
this.setState({ units, unitsLoading: false });
} catch (err) {
this.setState({ unitsLoading: false });
@@ -117,7 +116,7 @@ export default class ArticleUnitEditorDialog extends Component {
if (!formValid(form)) return;
this.setState({ busy: true });
try {
const data = await this.context.unit.upsert({
const data = await unitApi.upsert({
si: String(form.si ?? '').trim(),
basesi: String(form.basesi ?? '').trim(),
basesifactor: parseFactor(form.basesifactor),
@@ -126,7 +125,7 @@ export default class ArticleUnitEditorDialog extends Component {
});
requireObject(data, 'PUT /api/unit');
requireObject(data.unit, 'unit');
const units = await this.context.updateUnit();
const units = await unitAutomat.actions.load();
this.setState({
busy: false,
units,
@@ -145,8 +144,8 @@ export default class ArticleUnitEditorDialog extends Component {
if (!selectedSi || selectedSi === DEFAULT_UNIT_SI) return;
this.setState({ busy: true });
try {
await this.context.unit.delete(selectedSi);
const units = await this.context.updateUnit();
await unitApi.delete(selectedSi);
const units = await unitAutomat.actions.load();
this.setState({ busy: false, units, form: emptyForm(), selectedSi: null });
this.props.onUnitDeleted?.(selectedSi);
} catch (err) {

View File

@@ -1,6 +1,6 @@
import { PureComponent } from 'react';
import { PanelTileGrid } from 'dashgui/panels';
import { StorageManagerContext } from '../../api/storageManagerContext';
import { getCategoryState } from '../../automats/categoryAutomat';
import withNavigate from '../withNavigate';
import {
buildCategoryPath,
@@ -21,8 +21,6 @@ import ArticleViewLists from './boxes/ArticleViewLists';
const PHOTO_TILE_SX = { minHeight: 0 };
class ArticleView extends PureComponent {
static contextType = StorageManagerContext;
constructor(props, context) {
super(props, context);
@@ -70,7 +68,7 @@ class ArticleView extends PureComponent {
handleArticleDeleted() {
const { navigate } = this.props;
const { article } = this.state;
const categoryState = this.context.getCategoryState();
const categoryState = getCategoryState();
const categories = categoryState?.categories ?? null;
const rootCategoryId = categoryState?.rootCategoryId ?? null;
const categoryId = article?.categoryIds?.[0];
@@ -83,7 +81,7 @@ class ArticleView extends PureComponent {
render() {
const { article } = this.state;
const categoryState = this.context.getCategoryState();
const categoryState = getCategoryState();
const categories = categoryState?.categories ?? null;
const rootCategoryId = categoryState?.rootCategoryId ?? null;

View File

@@ -4,8 +4,9 @@ import TextInput from '../../TextInput';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from 'dashgui/panels';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import brandAutomat from '../../../automats/brandAutomat';
import artikelAutomat from '../../../automats/artikelAutomat';
import { requireObject } from '../../../utils/apiShape';
import { notifyError } from '../../../utils/notify';
import InputAdornment from '@mui/material/InputAdornment';
import { BrandLabel, BrandThumb } from '../../brand';
@@ -111,37 +112,29 @@ class BrandEdit extends Component {
}
export default class ArticleViewBrand extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
const brands = this.context?.brands;
this.state = {
brands: brands ?? [],
brands: brandAutomat.state.brands ?? [],
};
this._cancelled = false;
this._brandsReady = brands != null;
}
componentDidMount() {
this._cancelled = false;
if (this._brandsReady) return;
this.context.brandPromise
.then((list) => {
if (!this._cancelled) this.setState({ brands: list });
})
.catch((err) => {
if (!this._cancelled) notifyError(err);
});
this.unsub = brandAutomat.subscribe(this, (state) => ({
brands: state.brands ?? [],
}));
}
componentWillUnmount() {
this._cancelled = true;
this.unsub?.();
this.unsub = null;
}
handleSave = async (draft) => {
const { article: articleData, onArticleUpdated } = this.props;
const store = this.context;
const { brands } = this.state;
const name = String(draft.name ?? '').trim();
if (!name) {
@@ -156,16 +149,16 @@ export default class ArticleViewBrand extends PureComponent {
if (existing) {
brandId = existing.id;
} else {
const createdRes = await store.brand.upsert({ name });
requireObject(createdRes, 'PUT /api/brand');
requireObject(createdRes.brand, 'brand');
requireDefined(createdRes.brand.id, 'brand.id');
brandId = createdRes.brand.id;
const nextBrands = await store.updateBrand();
await brandAutomat.actions.upsert({ name });
const nextBrands = await brandAutomat.actions.load();
this.setState({ brands: nextBrands });
brandId = nextBrands.find(
(row) => String(row.name ?? '').toLowerCase() === name.toLowerCase(),
)?.id;
if (brandId == null) throw new Error('Marke konnte nicht gespeichert werden.');
}
const res = await store.article.setBrand(articleData.sku, { brandId });
const res = await artikelAutomat.actions.setBrand(articleData.sku, { brandId });
requireObject(res, 'PUT /api/article/:sku/brand');
requireObject(res.article, 'article');
onArticleUpdated?.(res.article);

View File

@@ -4,7 +4,7 @@ import TextInput from '../../TextInput';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from 'dashgui/panels';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import artikelAutomat from '../../../automats/artikelAutomat';
import { requireObject } from '../../../utils/apiShape';
import { articleEans } from '../../../utils/articleDetail';
import { notifyError } from '../../../utils/notify';
@@ -131,14 +131,12 @@ class EansEdit extends Component {
}
export default class ArticleViewEANs extends PureComponent {
static contextType = StorageManagerContext;
handleSave = async (draft) => {
const { article: articleData, onArticleUpdated } = this.props;
const eans = (draft.eans ?? [])
.map((ean) => String(ean).trim())
.filter((ean) => ean.length > 0);
const res = await this.context.article.setEans(articleData.sku, { eans });
const res = await artikelAutomat.actions.setEans(articleData.sku, eans);
requireObject(res, 'PUT /api/article/:sku/eans');
requireObject(res.article, 'article');
onArticleUpdated?.(res.article);

View File

@@ -35,11 +35,9 @@ import { article as articleApi } from '../../../api';
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
import { notifyError } from '../../../utils/notify';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import articleIndexAutomat from '../../../automats/articleIndexAutomat';
export default class ArticleViewLists extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
const initialAttributes = selectAttributes() || [];
@@ -167,13 +165,12 @@ export default class ArticleViewLists extends PureComponent {
}
getApi() {
return this.context?.article || articleApi;
return articleApi;
}
async loadAllArticles() {
try {
const data = await this.context?.articleIndexPromise;
const articles = Array.isArray(data) ? data : (data?.articles || []);
const articles = await articleIndexAutomat.actions.ensureLoaded();
if (!this._cancelled) {
this.setState({ allArticles: articles });
}
@@ -1316,7 +1313,7 @@ export default class ArticleViewLists extends PureComponent {
}
renderStandardView(selectableAttributes, assignedAttributeIds) {
const { busy, activeCreatorMode, selectedAttributeId, attributes } = this.state;
const { busy, activeCreatorMode, selectedAttributeId } = this.state;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{activeCreatorMode === 'father' ? (
@@ -1422,7 +1419,6 @@ export default class ArticleViewLists extends PureComponent {
allArticles,
busy,
activeCreatorMode,
activeListTab,
selectedChildValueId,
newChildText,
isCustomChildValue,

View File

@@ -1,7 +1,8 @@
import { Component, PureComponent } from 'react';
import { EditablePanelTile } from 'dashgui/panels';
import SingleLineTextEdit from 'dashgui/SingleLineTextEdit';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import { getCategoryState } from '../../../automats/categoryAutomat';
import artikelAutomat from '../../../automats/artikelAutomat';
import { getCategoryBreadcrumbs, categoryDisplayName } from '../../../utils/categoryUtils';
import { Box, Chip, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
@@ -88,8 +89,6 @@ function categoryLabels(categoryIds, categories, rootCategoryId) {
}
class CategoryPills extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = { pickerOpen: false, targetPillId: null };
@@ -133,7 +132,7 @@ class CategoryPills extends Component {
render() {
const { categoryIds, editMode } = this.props;
const { pickerOpen, targetPillId } = this.state;
const { categories, rootCategoryId } = this.context.getCategoryState() ?? {};
const { categories, rootCategoryId } = getCategoryState() ?? {};
const labels = categoryLabels(categoryIds, categories, rootCategoryId);
const targetNode = categories && targetPillId != null ? categories[String(targetPillId)] : null;
@@ -273,8 +272,6 @@ class NameEdit extends Component {
}
export default class ArticleViewName extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -294,7 +291,6 @@ export default class ArticleViewName extends PureComponent {
handleSave = async () => {
const { article: articleData, onArticleUpdated } = this.props;
const { nameDraft, categoryIdsDraft } = this.state;
const store = this.context;
const name = nameDraft.trim();
const categoryIds = categoryIdsDraft;
const nameChanged = name !== String(articleData.name ?? '').trim();
@@ -303,14 +299,12 @@ export default class ArticleViewName extends PureComponent {
let article = null;
if (nameChanged) {
console.log('storing name update', name, articleData.name);
const nameRes = await store.article.update(articleData.sku, { name });
const nameRes = await artikelAutomat.actions.update(articleData.sku, { name });
article = nameRes.article;
}
if (categoriesChanged) {
console.log('storing categories update', categoryIds, articleData.categoryIds);
const catRes = await store.article.setCategories(articleData.sku, { categoryIds });
const catRes = await artikelAutomat.actions.setCategories(articleData.sku, categoryIds);
article = catRes.article;
}

View File

@@ -6,7 +6,9 @@ import Typography from '@mui/material/Typography';
import { EditablePanelTile, PanelTile } from 'dashgui/panels';
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import unitAutomat from '../../../automats/unitAutomat';
import companyAutomat from '../../../automats/companyAutomat';
import artikelAutomat from '../../../automats/artikelAutomat';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import {
buildPosPricesPutBody,
@@ -169,9 +171,8 @@ function priceDraftValid(draft) {
return true;
}
function companyFromStore(store) {
if (!store) return null;
return store.company ?? null;
function companyFromStore() {
return companyAutomat.state.current ?? null;
}
class PriceAmountField extends Component {
@@ -361,13 +362,10 @@ class PriceEdit extends Component {
}
export default class ArticleViewPrice extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
const store = this.context;
const company = companyFromStore(store);
const units = store?.units;
const company = companyFromStore();
const units = unitAutomat.state.units;
this.state = {
tiersDialogOpen: false,
unitEditorOpen: false,
@@ -375,37 +373,28 @@ export default class ArticleViewPrice extends PureComponent {
company,
};
this._cancelled = false;
this._ready = company != null && units != null;
}
componentDidMount() {
this._cancelled = false;
if (this._ready) return;
void this.load();
this.unsubUnits = unitAutomat.subscribe(this, (state) => ({
units: state.units ?? [],
}));
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
company: state.current ?? null,
}));
}
componentWillUnmount() {
this._cancelled = true;
}
async load() {
const store = this.context;
try {
const [unitList, company] = await Promise.all([
store.unitPromise,
store.companyPromise,
]);
if (!this._cancelled) {
this.setState({ units: unitList, company: company ?? null });
}
} catch (err) {
if (!this._cancelled) notifyError(err);
}
this.unsubUnits?.();
this.unsubUnits = null;
this.unsubCompany?.();
this.unsubCompany = null;
}
handleSave = async (draft) => {
const { article: articleData, onArticleUpdated } = this.props;
const store = this.context;
const { company } = this.state;
const saved = priceDraftValue(articleData, company);
let nextArticle = articleData;
@@ -426,12 +415,12 @@ export default class ArticleViewPrice extends PureComponent {
if (isNoPackUnit(unitSi, unitamount)) {
throw new Error('Einheit pc × 1 ist keine Packungseinheit');
}
const res = await store.article.update(articleData.sku, { unitamount, unitSi });
const res = await artikelAutomat.actions.update(articleData.sku, { unitamount, unitSi });
requireObject(res, 'PATCH /api/article/:sku');
requireObject(res.article, 'article');
nextArticle = res.article;
} else {
const res = await store.article.update(articleData.sku, {
const res = await artikelAutomat.actions.update(articleData.sku, {
unitamount: 1,
unitSi: 'pc',
});
@@ -456,7 +445,7 @@ export default class ArticleViewPrice extends PureComponent {
taxRate,
tiers: draft.tiers ?? [],
});
const res = await store.article.setPrices(articleData.sku, { prices });
const res = await artikelAutomat.actions.setPrices(articleData.sku, prices);
requireObject(res, 'PUT /api/article/:sku/prices');
requireObject(res.article, 'article');
nextArticle = res.article;
@@ -480,7 +469,8 @@ export default class ArticleViewPrice extends PureComponent {
}
refreshUnits = async () => {
this.setState({ units: await this.context.unitPromise });
const units = await unitAutomat.actions.load();
this.setState({ units });
};
render() {

View File

@@ -10,15 +10,12 @@ import Tooltip from '@mui/material/Tooltip';
import AddIcon from '@mui/icons-material/Add';
import { notifyError } from '../../../utils/notify';
import { enqueueSnackbar } from 'notistack';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
import propertyAutomat, { selectProperties } from '../../../automats/propertyAutomat';
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
import { article as articleApi } from '../../../api';
export default class ArticleAttributesTab extends PureComponent {
static contextType = StorageManagerContext;
getArticle() {
return this.props.article || getArticle(this.props.sku);
}
@@ -165,16 +162,15 @@ export default class ArticleAttributesTab extends PureComponent {
if (!sku) return;
this.setState({ loading: true });
try {
const api = this.context?.article || articleApi;
const [attrRes, propRes] = await Promise.all([
api.getAttributeValues(sku),
api.getProperties(sku),
const res = await Promise.all([
articleApi.getAttributeValues(sku),
articleApi.getProperties(sku),
]);
if (this._cancelled) return;
this.setState({
assignedAttributes: Array.isArray(attrRes?.attributevalues) ? attrRes.attributevalues : [],
assignedProperties: Array.isArray(propRes?.properties) ? propRes.properties : [],
assignedAttributes: Array.isArray(res[0]?.attributevalues) ? res[0].attributevalues : [],
assignedProperties: Array.isArray(res[1]?.properties) ? res[1].properties : [],
loading: false,
});
} catch (err) {
@@ -231,7 +227,6 @@ export default class ArticleAttributesTab extends PureComponent {
const effectiveKey = selectedOption?.key || selectedMerkmalKey;
if (saving || !sku || !effectiveKey) return;
const api = this.context?.article || articleApi;
if (effectiveKey.startsWith('attr-')) {
const attrId = Number(effectiveKey.replace('attr-', ''));
@@ -262,7 +257,7 @@ export default class ArticleAttributesTab extends PureComponent {
this.setState({ saving: true });
try {
const res = await api.setAttributeValues(sku, nextValues);
const res = await articleApi.setAttributeValues(sku, nextValues);
if (this._cancelled) return;
this.setState({
assignedAttributes: Array.isArray(res?.attributevalues) ? res.attributevalues : [],
@@ -304,7 +299,7 @@ export default class ArticleAttributesTab extends PureComponent {
this.setState({ saving: true });
try {
const res = await api.setProperties(sku, nextProps);
const res = await articleApi.setProperties(sku, nextProps);
if (this._cancelled) return;
this.setState({
assignedProperties: Array.isArray(res?.properties) ? res.properties : [],
@@ -331,8 +326,7 @@ export default class ArticleAttributesTab extends PureComponent {
this.setState({ saving: true });
try {
const api = this.context?.article || articleApi;
const res = await api.setAttributeValues(sku, nextValues);
const res = await articleApi.setAttributeValues(sku, nextValues);
if (this._cancelled) return;
this.setState({
assignedAttributes: Array.isArray(res?.attributevalues) ? res.attributevalues : [],
@@ -357,8 +351,7 @@ export default class ArticleAttributesTab extends PureComponent {
this.setState({ saving: true });
try {
const api = this.context?.article || articleApi;
const res = await api.setProperties(sku, nextProperties);
const res = await articleApi.setProperties(sku, nextProperties);
if (this._cancelled) return;
this.setState({
assignedProperties: Array.isArray(res?.properties) ? res.properties : [],

View File

@@ -1,10 +1,12 @@
import { Component } from 'react';
import { PureComponent } from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import { articleHistoryAutomat } from '../../../automats/articleDetailAutomat';
import sseAutomat from '../../../automats/sseAutomat';
import { notifyError } from '../../../utils/notify';
import ResultsPagination from '../ResultsPagination';
const PAGE_SIZE = 20;
@@ -13,46 +15,68 @@ function formatDate(iso) {
return new Date(iso).toLocaleString('de-DE');
}
export default class ArticleHistoryTab extends Component {
static contextType = StorageManagerContext;
export default class ArticleHistoryTab extends PureComponent {
constructor(props) {
super(props);
this.state = {
history: null,
total: null,
skip: 0,
};
this.unwatchHistory = null;
this.setSkipAndOffset = null;
this.state = { all: [], skip: 0 };
this.handlePageChange = this.handlePageChange.bind(this);
}
componentDidMount() {
const { unwatch, setSkipAndOffset } = this.context.article.historyWatch(
this.props.sku,
(history, total) => this.setState({ history, total }),
);
this.unwatchHistory = unwatch;
this.setSkipAndOffset = setSkipAndOffset;
setSkipAndOffset(0, PAGE_SIZE);
this.watch();
void articleHistoryAutomat.load(String(this.props.sku)).catch((err) => {
if (this.unsub) notifyError(err);
});
}
componentDidUpdate(prevProps) {
if (prevProps.sku !== this.props.sku) {
this.watch();
void articleHistoryAutomat.load(String(this.props.sku)).catch((err) => {
if (this.unsub) notifyError(err);
});
}
}
componentWillUnmount() {
this.unwatchHistory?.();
this.unwatchHistory = null;
this.setSkipAndOffset = null;
this.unsub?.();
this.unsub = null;
this.unwatchSignal?.();
this.unwatchSignal = null;
}
/**
* Subscribes to the keyed history cache and to the article SSE signal.
* The backend re-emits `article-<sku>` whenever the article changes, and
* `sseAutomat` reloads the history key for us.
*/
watch() {
this.unsub?.();
const handle = articleHistoryAutomat.key(String(this.props.sku));
this.unsub = handle.subscribe(this, (item) => {
const all = Array.isArray(item.data) ? item.data : [];
this.setState({ all });
});
this.unwatchSignal?.();
this.unwatchSignal = sseAutomat.actions.watchArticle(String(this.props.sku), () => {
void articleHistoryAutomat.reload(String(this.props.sku)).catch(() => {});
});
}
/** Client-side pagination over the full history list. */
pageOf(all, skip) {
return all.slice(skip, skip + PAGE_SIZE);
}
handlePageChange(_event, page) {
const skip = (page - 1) * PAGE_SIZE;
this.setSkipAndOffset?.(skip, PAGE_SIZE);
this.setState({ skip });
this.setState({ skip: (page - 1) * PAGE_SIZE });
}
render() {
const { history, total, skip } = this.state;
if (history == null || total == null) return null;
const { all, skip } = this.state;
const total = all.length;
const history = this.pageOf(all, skip);
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
const currentPage = Math.floor(skip / PAGE_SIZE) + 1;

View File

@@ -2,13 +2,11 @@ import { Component } from 'react';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import DeleteIcon from '@mui/icons-material/Delete';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import { article as articleApi } from '../../../api';
import { artikelAutomat } from '../../../automats';
import { notifyError } from '../../../utils/notify';
export default class ArticleSettingsTab extends Component {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = { busy: false };
@@ -24,7 +22,7 @@ export default class ArticleSettingsTab extends Component {
this.setState({ busy: true });
try {
await this.context.article.delete(articleData.sku);
await articleApi.delete(articleData.sku);
artikelAutomat.actions.clearArticle(articleData.sku);
onArticleDeleted?.(articleData);
} catch (err) {

View File

@@ -20,15 +20,16 @@ import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import { enqueueSnackbar } from 'notistack';
import { StorageManagerContext } from '../../../api/storageManagerContext';
import { article as articleApi } from '../../../api';
import { notifyError } from '../../../utils/notify';
import { formatPrice } from '../../../utils/price';
import supplierAutomat from '../../../automats/supplierAutomat';
import { articleSupplierAutomat } from '../../../automats/articleDetailAutomat';
import sseAutomat from '../../../automats/sseAutomat';
import companyAutomat from '../../../automats/companyAutomat';
import ArticleSupplierDialog from '../ArticleSupplierDialog';
export default class ArticleSupplierTab extends PureComponent {
static contextType = StorageManagerContext;
constructor(props) {
super(props);
this.state = {
@@ -40,10 +41,12 @@ export default class ArticleSupplierTab extends PureComponent {
editItem: null,
busyId: null,
};
this.unwatchSuppliers = null;
}
componentDidMount() {
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
company: state.current ?? null,
}));
this.initData();
}
@@ -54,46 +57,53 @@ export default class ArticleSupplierTab extends PureComponent {
}
componentWillUnmount() {
this.unwatchSuppliers?.();
this.unwatchSuppliers = null;
this.unsubSuppliers?.();
this.unsubSuppliers = null;
this.unwatchSignal?.();
this.unwatchSignal = null;
this.unsubCompany?.();
this.unsubCompany = null;
}
async initData() {
const sku = this.props.article?.sku;
if (!sku) return;
this.unwatchSuppliers?.();
this.setState({ loading: true });
// 1. Subscribe to SSE-backed supplier updates
const { unwatch, refresh } = this.context.article.supplierWatch(
sku,
(assignedSuppliers) => {
this.setState({
assignedSuppliers: assignedSuppliers ?? [],
loading: false,
});
},
);
this.unwatchSuppliers = unwatch;
this.refreshSuppliers = refresh;
// 2. Fetch available company suppliers and company metadata
try {
const [companySuppliers, company] = await Promise.all([
supplierAutomat.actions.load(),
this.context.company ?? this.context.companyPromise,
]);
// 1. Subscribe to the keyed supplier cache and the article SSE signal
this.unsubSuppliers?.();
const handle = articleSupplierAutomat.key(String(sku));
this.unsubSuppliers = handle.subscribe(this, (item) => {
this.setState({
companySuppliers: companySuppliers ?? [],
company: company ?? null,
assignedSuppliers: Array.isArray(item.data) ? item.data : [],
loading: false,
});
});
void articleSupplierAutomat.load(String(sku)).catch((err) => {
if (this.unsubSuppliers) notifyError(err);
});
this.unwatchSignal?.();
this.unwatchSignal = sseAutomat.actions.watchArticle(String(sku), () => {
void articleSupplierAutomat.reload(String(sku)).catch(() => {});
});
// 2. Fetch available company suppliers
try {
const companySuppliers = await supplierAutomat.actions.load();
this.setState({ companySuppliers: companySuppliers ?? [] });
} catch (err) {
notifyError(err);
this.setState({ loading: false });
}
}
/** Re-fetches the assigned suppliers for the current article. */
refreshSuppliers = async () => {
const sku = this.props.article?.sku;
if (!sku) return;
await articleSupplierAutomat.reload(String(sku));
};
handleOpenAdd = () => {
this.setState({ dialogOpen: true, editItem: null });
};
@@ -112,7 +122,7 @@ export default class ArticleSupplierTab extends PureComponent {
try {
if (payload.id) {
await this.context.article.updateSupplier(sku, payload.id, {
await articleApi.updateSupplier(sku, payload.id, {
sku: payload.sku,
pu: payload.pu,
stock: payload.stock,
@@ -121,7 +131,7 @@ export default class ArticleSupplierTab extends PureComponent {
});
enqueueSnackbar('Lieferantendaten aktualisiert', { variant: 'success' });
} else {
await this.context.article.addSupplier(sku, {
await articleApi.addSupplier(sku, {
entityId: payload.entityId,
sku: payload.sku,
pu: payload.pu,
@@ -132,7 +142,7 @@ export default class ArticleSupplierTab extends PureComponent {
enqueueSnackbar('Lieferant erfolgreich zugewiesen', { variant: 'success' });
}
this.handleCloseDialog();
await this.refreshSuppliers?.();
await this.refreshSuppliers();
} catch (err) {
notifyError(err);
throw err;
@@ -150,9 +160,9 @@ export default class ArticleSupplierTab extends PureComponent {
this.setState({ busyId: item.id });
try {
await this.context.article.deleteSupplier(sku, item.id);
await articleApi.deleteSupplier(sku, item.id);
enqueueSnackbar('Lieferant entfernt', { variant: 'success' });
await this.refreshSuppliers?.();
await this.refreshSuppliers();
} catch (err) {
notifyError(err);
} finally {
@@ -176,11 +186,11 @@ export default class ArticleSupplierTab extends PureComponent {
this.setState({ assignedSuppliers: nextList, busyId: moved.id });
try {
await this.context.article.orderSuppliers(sku, { ids });
await this.refreshSuppliers?.();
await articleApi.orderSuppliers(sku, ids);
await this.refreshSuppliers();
} catch (err) {
notifyError(err);
await this.refreshSuppliers?.();
await this.refreshSuppliers();
} finally {
this.setState({ busyId: null });
}