u
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { Component } from 'react';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import List from '@mui/material/List';
|
||||
@@ -11,6 +10,8 @@ import Typography from '@mui/material/Typography';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import { article } from '../../api';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { backendErrorFrom } from '../../utils/backendError';
|
||||
import { formatPrice } from '../../utils/price';
|
||||
import {
|
||||
buildSearchUrl,
|
||||
@@ -273,7 +274,7 @@ class ArtikelSearchPage extends Component {
|
||||
categoryFacets: null,
|
||||
totalCount: null,
|
||||
recordset: null,
|
||||
error: err.message || 'Suche fehlgeschlagen',
|
||||
error: backendErrorFrom(err),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -344,9 +345,11 @@ class ArtikelSearchPage extends Component {
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
<BackendError
|
||||
error={error}
|
||||
sx={{ mb: 2 }}
|
||||
onRetry={this.fetchResults}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchActive && (
|
||||
|
||||
@@ -1,7 +1,227 @@
|
||||
import { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import {
|
||||
ActionTile,
|
||||
ArticleTile,
|
||||
BackTile,
|
||||
CategoryTile,
|
||||
TILE_SIZE,
|
||||
} from '../../components/tiles';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import { selectCompanyId } from '../../store/companySlice';
|
||||
import {
|
||||
categoryDisplayName,
|
||||
loadCategoryArticles,
|
||||
loadCategoryTree,
|
||||
navigateBack,
|
||||
navigateToCategory,
|
||||
selectCanNavigateBack,
|
||||
selectCategoryArticles,
|
||||
selectCategoryArticlesStatus,
|
||||
selectCategoryError,
|
||||
selectCategoryTreeStatus,
|
||||
selectChildCategories,
|
||||
selectCurrentCategory,
|
||||
} from '../../store/categorySlice';
|
||||
|
||||
export default function ArtikelKategorienPage() {
|
||||
return (
|
||||
<Typography color="text.secondary">Kategorien — Inhalt folgt.</Typography>
|
||||
);
|
||||
const gridSx = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${TILE_SIZE}px, 1fr))`,
|
||||
gap: 2,
|
||||
};
|
||||
|
||||
class ArtikelKategorienPage extends Component {
|
||||
componentDidMount() {
|
||||
this.ensureTreeLoaded();
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.companyId !== prevProps.companyId) {
|
||||
this.ensureTreeLoaded();
|
||||
}
|
||||
if (this.props.currentCategoryId !== prevProps.currentCategoryId) {
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
if (
|
||||
this.props.treeStatus === 'succeeded' &&
|
||||
prevProps.treeStatus !== 'succeeded'
|
||||
) {
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
ensureTreeLoaded = () => {
|
||||
const { companyId, treeStatus } = this.props;
|
||||
if (!companyId || treeStatus !== 'idle') return;
|
||||
this.props.loadCategoryTree(companyId);
|
||||
};
|
||||
|
||||
ensureArticlesLoaded = () => {
|
||||
const { currentCategoryId, articlesStatus, rootCategoryId } = this.props;
|
||||
if (!currentCategoryId || articlesStatus !== 'idle') return;
|
||||
// Root articles come from tree response; only fetch for subcategories.
|
||||
if (String(currentCategoryId) === String(rootCategoryId)) return;
|
||||
this.props.loadCategoryArticles({ categoryId: currentCategoryId });
|
||||
};
|
||||
|
||||
handleRetryTree = () => {
|
||||
const { companyId } = this.props;
|
||||
if (companyId) this.props.loadCategoryTree(companyId);
|
||||
};
|
||||
|
||||
handleRetryArticles = () => {
|
||||
const { currentCategoryId } = this.props;
|
||||
if (currentCategoryId) {
|
||||
this.props.loadCategoryArticles({ categoryId: currentCategoryId });
|
||||
}
|
||||
};
|
||||
|
||||
handleCategoryClick = (categoryId) => {
|
||||
this.props.navigateToCategory(categoryId);
|
||||
};
|
||||
|
||||
handleArticleClick = (sku) => {
|
||||
this.props.navigate(`/artikel/${encodeURIComponent(sku)}`);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
companyId,
|
||||
rootCategoryId,
|
||||
treeStatus,
|
||||
articlesStatus,
|
||||
error,
|
||||
canGoBack,
|
||||
currentCategory,
|
||||
currentCategoryId,
|
||||
childCategories,
|
||||
articles,
|
||||
} = this.props;
|
||||
|
||||
if (!companyId) {
|
||||
return (
|
||||
<Typography color="text.secondary">
|
||||
Kein Unternehmen ausgewählt.
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
if (treeStatus === 'loading' || treeStatus === 'idle') {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (treeStatus === 'failed') {
|
||||
return (
|
||||
<BackendError
|
||||
error={error}
|
||||
onRetry={this.handleRetryTree}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!rootCategoryId) {
|
||||
return (
|
||||
<Typography color="text.secondary">
|
||||
Kategoriebaum ist leer.
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
const categoryName = categoryDisplayName(
|
||||
currentCategory?.name,
|
||||
currentCategoryId,
|
||||
rootCategoryId,
|
||||
);
|
||||
const showArticlesLoading = articlesStatus === 'loading';
|
||||
|
||||
return (
|
||||
<>
|
||||
{categoryName ? (
|
||||
<Typography variant="subtitle1" sx={{ mb: 2, fontWeight: 600 }}>
|
||||
{categoryName}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
{error && articlesStatus === 'failed' && (
|
||||
<BackendError
|
||||
error={error}
|
||||
sx={{ mb: 2 }}
|
||||
onRetry={this.handleRetryArticles}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Box sx={gridSx}>
|
||||
{canGoBack && (
|
||||
<BackTile onClick={this.props.navigateBack} />
|
||||
)}
|
||||
|
||||
{childCategories.map((item) => (
|
||||
<CategoryTile
|
||||
key={item.id}
|
||||
label={categoryDisplayName(item.name, item.id, rootCategoryId)}
|
||||
onClick={() => this.handleCategoryClick(item.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{articles.map((item) => (
|
||||
<ArticleTile
|
||||
key={item.id ?? item.sku}
|
||||
label={item.name?.trim() || undefined}
|
||||
article={item}
|
||||
onClick={() => this.handleArticleClick(item.sku)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ActionTile
|
||||
label="Neue Kategorie"
|
||||
disabled
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ActionTile
|
||||
label="Neuer Artikel"
|
||||
disabled
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{showArticlesLoading && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress size={28} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
companyId: selectCompanyId(state),
|
||||
rootCategoryId: state.category.rootCategoryId,
|
||||
treeStatus: selectCategoryTreeStatus(state),
|
||||
articlesStatus: selectCategoryArticlesStatus(state),
|
||||
error: selectCategoryError(state),
|
||||
currentCategoryId: state.category.currentCategoryId,
|
||||
currentCategory: selectCurrentCategory(state),
|
||||
childCategories: selectChildCategories(state),
|
||||
articles: selectCategoryArticles(state),
|
||||
canGoBack: selectCanNavigateBack(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadCategoryTree,
|
||||
loadCategoryArticles,
|
||||
navigateToCategory,
|
||||
navigateBack,
|
||||
};
|
||||
|
||||
export default withNavigate(connect(mapStateToProps, mapDispatchToProps)(ArtikelKategorienPage));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
@@ -13,6 +13,8 @@ import TableHead from '@mui/material/TableHead';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { article } from '../../api';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
|
||||
import { formatPrice, getBrutto } from '../../utils/price';
|
||||
import { formatStock } from './searchFormat';
|
||||
|
||||
@@ -272,6 +274,11 @@ export default function ArtikelSkuPage() {
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
|
||||
const retry = useCallback(() => {
|
||||
setReloadKey((key) => key + 1);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e) {
|
||||
@@ -306,7 +313,7 @@ export default function ArtikelSkuPage() {
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
setData(null);
|
||||
setError(err.message || 'Artikel konnte nicht geladen werden');
|
||||
setError(backendErrorFrom(err));
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
@@ -317,7 +324,7 @@ export default function ArtikelSkuPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [target]);
|
||||
}, [target, reloadKey]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -328,9 +335,17 @@ export default function ArtikelSkuPage() {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (isBackendFailure(error)) {
|
||||
return (
|
||||
<BackendError
|
||||
error={error}
|
||||
onRetry={retry}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert severity="error" sx={{ maxWidth: 640 }}>
|
||||
{error}
|
||||
{typeof error === 'string' ? error : error.message}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user