u
This commit is contained in:
8
src/pages/artikel/RedirectArtikelKategorien.jsx
Normal file
8
src/pages/artikel/RedirectArtikelKategorien.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Navigate, useParams } from 'react-router-dom';
|
||||
import { KATEGORIEN_BASE } from './categoryRoute';
|
||||
|
||||
export default function RedirectArtikelKategorien() {
|
||||
const { '*': rest = '' } = useParams();
|
||||
const suffix = rest ? `/${rest}` : '';
|
||||
return <Navigate to={`${KATEGORIEN_BASE}${suffix}`} replace />;
|
||||
}
|
||||
11
src/pages/artikel/articleRoute.js
Normal file
11
src/pages/artikel/articleRoute.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function articleSku(article) {
|
||||
if (!article) return null;
|
||||
const sku = article.sku?.trim();
|
||||
return sku || null;
|
||||
}
|
||||
|
||||
export function artikelDetailPath(article) {
|
||||
const sku = articleSku(article);
|
||||
if (!sku) return null;
|
||||
return `/artikel/${encodeURIComponent(sku)}`;
|
||||
}
|
||||
67
src/pages/artikel/categoryRoute.js
Normal file
67
src/pages/artikel/categoryRoute.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
ROOT_CATEGORY_NAME,
|
||||
categorySlug,
|
||||
getCategoryBreadcrumbs,
|
||||
getCategoryChildren,
|
||||
} from '../../store/categorySlice';
|
||||
|
||||
export { ROOT_CATEGORY_NAME, categorySlug };
|
||||
|
||||
export const KATEGORIEN_BASE = '/kategorien';
|
||||
|
||||
export function isKategorienPath(pathname) {
|
||||
return pathname === KATEGORIEN_BASE || pathname.startsWith(`${KATEGORIEN_BASE}/`);
|
||||
}
|
||||
|
||||
function isRootSlug(slug) {
|
||||
return slug === ROOT_CATEGORY_NAME;
|
||||
}
|
||||
|
||||
export function categoryPathFromCrumbs(crumbs) {
|
||||
const segments = (crumbs ?? [])
|
||||
.filter((crumb) => crumb.slug && !isRootSlug(crumb.slug))
|
||||
.map((crumb) => encodeURIComponent(crumb.slug));
|
||||
if (segments.length === 0) {
|
||||
return KATEGORIEN_BASE;
|
||||
}
|
||||
return `${KATEGORIEN_BASE}/${segments.join('/')}`;
|
||||
}
|
||||
|
||||
export function buildCategoryPath(categories, rootCategoryId, categoryId) {
|
||||
const crumbs = getCategoryBreadcrumbs(categories, categoryId, rootCategoryId);
|
||||
return categoryPathFromCrumbs(crumbs);
|
||||
}
|
||||
|
||||
export function resolveCategoryIdFromSlugPath(categories, rootCategoryId, slugPath) {
|
||||
if (!categories || rootCategoryId == null) return null;
|
||||
|
||||
let segments = (slugPath ?? '')
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((segment) => decodeURIComponent(segment));
|
||||
|
||||
// Legacy slug URLs: .../Home/...
|
||||
if (segments[0] === ROOT_CATEGORY_NAME) {
|
||||
segments = segments.slice(1);
|
||||
}
|
||||
|
||||
if (segments.length === 0) {
|
||||
return rootCategoryId;
|
||||
}
|
||||
|
||||
let currentId = rootCategoryId;
|
||||
for (const slug of segments) {
|
||||
const children = getCategoryChildren(categories, currentId);
|
||||
const match = children.find(
|
||||
(child) => categorySlug(child.name, child.id, rootCategoryId) === slug,
|
||||
);
|
||||
if (!match) return null;
|
||||
currentId = match.id;
|
||||
}
|
||||
|
||||
return currentId;
|
||||
}
|
||||
|
||||
export function defaultCategoryPath() {
|
||||
return KATEGORIEN_BASE;
|
||||
}
|
||||
@@ -4,28 +4,36 @@ import Box from '@mui/material/Box';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { NewCategoryTile, CategoryBreadcrumbs } from '../../components/category';
|
||||
import { NewArticleTile } from '../../components/article';
|
||||
import {
|
||||
ActionTile,
|
||||
ArticleTile,
|
||||
BackTile,
|
||||
CategoryTile,
|
||||
TILE_SIZE,
|
||||
} from '../../components/tiles';
|
||||
import withNavigate from '../../components/withNavigate';
|
||||
import withCategoryRoute from './withCategoryRoute';
|
||||
import { artikelDetailPath, articleSku } from './articleRoute';
|
||||
import { selectCompanyId } from '../../store/companySlice';
|
||||
import {
|
||||
buildCategoryPath,
|
||||
defaultCategoryPath,
|
||||
resolveCategoryIdFromSlugPath,
|
||||
ROOT_CATEGORY_NAME,
|
||||
} from './categoryRoute';
|
||||
import {
|
||||
categoryDisplayName,
|
||||
loadCategoryArticles,
|
||||
loadCategoryTree,
|
||||
navigateBack,
|
||||
navigateToCategory,
|
||||
selectCanNavigateBack,
|
||||
selectCategoryArticles,
|
||||
selectCategoryArticlesStatus,
|
||||
selectCategoryBreadcrumbs,
|
||||
selectCategoryMap,
|
||||
selectCategoryError,
|
||||
selectCategoryTreeStatus,
|
||||
selectChildCategories,
|
||||
selectCurrentCategory,
|
||||
} from '../../store/categorySlice';
|
||||
|
||||
const gridSx = {
|
||||
@@ -37,6 +45,7 @@ const gridSx = {
|
||||
class ArtikelKategorienPage extends Component {
|
||||
componentDidMount() {
|
||||
this.ensureTreeLoaded();
|
||||
this.syncCategoryFromUrl();
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
|
||||
@@ -44,13 +53,23 @@ class ArtikelKategorienPage extends Component {
|
||||
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.syncCategoryFromUrl();
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.treeStatus === 'succeeded' &&
|
||||
this.props.categorySlugPath !== prevProps.categorySlugPath
|
||||
) {
|
||||
this.syncCategoryFromUrl();
|
||||
}
|
||||
|
||||
if (this.props.currentCategoryId !== prevProps.currentCategoryId) {
|
||||
this.ensureArticlesLoaded();
|
||||
}
|
||||
}
|
||||
@@ -58,35 +77,76 @@ class ArtikelKategorienPage extends Component {
|
||||
ensureTreeLoaded = () => {
|
||||
const { companyId, treeStatus } = this.props;
|
||||
if (!companyId || treeStatus !== 'idle') return;
|
||||
this.props.loadCategoryTree(companyId);
|
||||
this.props.loadCategoryTree();
|
||||
};
|
||||
|
||||
ensureArticlesLoaded = () => {
|
||||
const { currentCategoryId, articlesStatus, rootCategoryId } = this.props;
|
||||
if (!currentCategoryId || articlesStatus !== 'idle') return;
|
||||
// Root articles come from tree response; only fetch for subcategories.
|
||||
const { currentCategoryId, articlesStatus, rootCategoryId, treeStatus } = this.props;
|
||||
if (treeStatus !== 'succeeded' || !currentCategoryId || articlesStatus !== 'idle') return;
|
||||
if (String(currentCategoryId) === String(rootCategoryId)) return;
|
||||
this.props.loadCategoryArticles({ categoryId: currentCategoryId });
|
||||
this.props.loadCategoryArticles(currentCategoryId);
|
||||
};
|
||||
|
||||
syncCategoryFromUrl = () => {
|
||||
const {
|
||||
categories,
|
||||
rootCategoryId,
|
||||
categorySlugPath,
|
||||
currentCategoryId,
|
||||
treeStatus,
|
||||
navigate,
|
||||
} = this.props;
|
||||
|
||||
if (treeStatus !== 'succeeded' || !categories || rootCategoryId == null) return;
|
||||
|
||||
const segments = (categorySlugPath ?? '').split('/').filter(Boolean);
|
||||
const hasLegacyHomePrefix = segments[0] === ROOT_CATEGORY_NAME;
|
||||
|
||||
const resolvedId = resolveCategoryIdFromSlugPath(
|
||||
categories,
|
||||
rootCategoryId,
|
||||
categorySlugPath,
|
||||
);
|
||||
|
||||
if (resolvedId == null) {
|
||||
navigate(defaultCategoryPath(), { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasLegacyHomePrefix) {
|
||||
navigate(buildCategoryPath(categories, rootCategoryId, resolvedId), { replace: true });
|
||||
}
|
||||
|
||||
if (resolvedId !== currentCategoryId) {
|
||||
this.props.navigateToCategory(resolvedId);
|
||||
}
|
||||
};
|
||||
|
||||
navigateToCategoryById = (categoryId) => {
|
||||
const { categories, rootCategoryId, navigate } = this.props;
|
||||
navigate(buildCategoryPath(categories, rootCategoryId, categoryId));
|
||||
};
|
||||
|
||||
handleRetryTree = () => {
|
||||
const { companyId } = this.props;
|
||||
if (companyId) this.props.loadCategoryTree(companyId);
|
||||
if (this.props.companyId) this.props.loadCategoryTree();
|
||||
};
|
||||
|
||||
handleRetryArticles = () => {
|
||||
const { currentCategoryId } = this.props;
|
||||
if (currentCategoryId) {
|
||||
this.props.loadCategoryArticles({ categoryId: currentCategoryId });
|
||||
this.props.loadCategoryArticles(currentCategoryId);
|
||||
}
|
||||
};
|
||||
|
||||
handleCategoryClick = (categoryId) => {
|
||||
this.props.navigateToCategory(categoryId);
|
||||
handleNavigateBack = () => {
|
||||
const { breadcrumbs } = this.props;
|
||||
if (breadcrumbs.length < 2) return;
|
||||
this.navigateToCategoryById(breadcrumbs[breadcrumbs.length - 2].id);
|
||||
};
|
||||
|
||||
handleArticleClick = (sku) => {
|
||||
this.props.navigate(`/artikel/${encodeURIComponent(sku)}`);
|
||||
handleArticleClick = (article) => {
|
||||
const path = artikelDetailPath(article);
|
||||
if (path) this.props.navigate(path);
|
||||
};
|
||||
|
||||
render() {
|
||||
@@ -97,7 +157,7 @@ class ArtikelKategorienPage extends Component {
|
||||
articlesStatus,
|
||||
error,
|
||||
canGoBack,
|
||||
currentCategory,
|
||||
breadcrumbs,
|
||||
currentCategoryId,
|
||||
childCategories,
|
||||
articles,
|
||||
@@ -128,28 +188,22 @@ class ArtikelKategorienPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (!rootCategoryId) {
|
||||
if (rootCategoryId == null) {
|
||||
return (
|
||||
<Typography color="text.secondary">
|
||||
Kategoriebaum ist leer.
|
||||
<Typography color="error">
|
||||
rootCategoryId fehlt.
|
||||
</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}
|
||||
<CategoryBreadcrumbs
|
||||
crumbs={breadcrumbs}
|
||||
onNavigate={this.navigateToCategoryById}
|
||||
/>
|
||||
|
||||
{error && articlesStatus === 'failed' && (
|
||||
<BackendError
|
||||
@@ -161,35 +215,31 @@ class ArtikelKategorienPage extends Component {
|
||||
|
||||
<Box sx={gridSx}>
|
||||
{canGoBack && (
|
||||
<BackTile onClick={this.props.navigateBack} />
|
||||
<BackTile onClick={this.handleNavigateBack} />
|
||||
)}
|
||||
|
||||
{childCategories.map((item) => (
|
||||
<CategoryTile
|
||||
key={item.id}
|
||||
label={categoryDisplayName(item.name, item.id, rootCategoryId)}
|
||||
onClick={() => this.handleCategoryClick(item.id)}
|
||||
onClick={() => this.navigateToCategoryById(item.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{articles.map((item) => (
|
||||
<ArticleTile
|
||||
key={item.id ?? item.sku}
|
||||
label={item.name?.trim() || undefined}
|
||||
key={item.id}
|
||||
label={item.name != null ? item.name.trim() : undefined}
|
||||
article={item}
|
||||
onClick={() => this.handleArticleClick(item.sku)}
|
||||
disabled={!articleSku(item)}
|
||||
onClick={() => this.handleArticleClick(item)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<ActionTile
|
||||
label="Neue Kategorie"
|
||||
disabled
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ActionTile
|
||||
label="Neuer Artikel"
|
||||
disabled
|
||||
onClick={() => {}}
|
||||
<NewCategoryTile parentId={currentCategoryId} />
|
||||
<NewArticleTile
|
||||
parentId={currentCategoryId}
|
||||
onOpenArticle={this.handleArticleClick}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -198,7 +248,6 @@ class ArtikelKategorienPage extends Component {
|
||||
<CircularProgress size={28} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -206,12 +255,13 @@ class ArtikelKategorienPage extends Component {
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
companyId: selectCompanyId(state),
|
||||
categories: selectCategoryMap(state),
|
||||
rootCategoryId: state.category.rootCategoryId,
|
||||
treeStatus: selectCategoryTreeStatus(state),
|
||||
articlesStatus: selectCategoryArticlesStatus(state),
|
||||
error: selectCategoryError(state),
|
||||
currentCategoryId: state.category.currentCategoryId,
|
||||
currentCategory: selectCurrentCategory(state),
|
||||
breadcrumbs: selectCategoryBreadcrumbs(state),
|
||||
childCategories: selectChildCategories(state),
|
||||
articles: selectCategoryArticles(state),
|
||||
canGoBack: selectCanNavigateBack(state),
|
||||
@@ -221,7 +271,6 @@ const mapDispatchToProps = {
|
||||
loadCategoryTree,
|
||||
loadCategoryArticles,
|
||||
navigateToCategory,
|
||||
navigateBack,
|
||||
};
|
||||
|
||||
export default withNavigate(connect(mapStateToProps, mapDispatchToProps)(ArtikelKategorienPage));
|
||||
export default withCategoryRoute(connect(mapStateToProps, mapDispatchToProps)(ArtikelKategorienPage));
|
||||
|
||||
@@ -1,276 +1,18 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Link from '@mui/material/Link';
|
||||
import Paper from '@mui/material/Paper';
|
||||
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 Typography from '@mui/material/Typography';
|
||||
import { article } from '../../api';
|
||||
import { ArticleView } from '../../components/article';
|
||||
import BackendError from '../../components/BackendError';
|
||||
import { requireObject } from '../../utils/apiShape';
|
||||
import { backendErrorFrom, isBackendFailure } from '../../utils/backendError';
|
||||
import { formatPrice, getBrutto } from '../../utils/price';
|
||||
import { formatStock } from './searchFormat';
|
||||
|
||||
function DetailRow({ label, children }) {
|
||||
if (children == null || children === '') return null;
|
||||
return (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '160px 1fr', gap: 1, py: 0.75 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body2" component="div">
|
||||
{children}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ title, children }) {
|
||||
if (!children) return null;
|
||||
return (
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArticleView({ article: a }) {
|
||||
const brutto =
|
||||
a.fVKNEtto != null ? getBrutto(a.fVKNEtto, a.kSteuerklasse) : null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mr: 1 }}>
|
||||
{a.cName}
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={a.cAktiv === 'Y' ? 'Aktiv' : 'Inaktiv'}
|
||||
color={a.cAktiv === 'Y' ? 'success' : 'default'}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
<DetailRow label="Artikelnr.">{a.cArtNr}</DetailRow>
|
||||
<DetailRow label="Barcode">{a.cBarcode}</DetailRow>
|
||||
<DetailRow label="Hersteller">{a.cHersteller}</DetailRow>
|
||||
<DetailRow label="VK Netto">
|
||||
{a.fVKNEtto != null ? formatPrice(a.fVKNEtto) : null}
|
||||
</DetailRow>
|
||||
<DetailRow label="VK Brutto">
|
||||
{brutto != null ? formatPrice(brutto) : null}
|
||||
</DetailRow>
|
||||
<DetailRow label="UVP">{a.fUVP != null ? formatPrice(a.fUVP) : null}</DetailRow>
|
||||
<DetailRow label="Bestand">{formatStock(a.fBestand)}</DetailRow>
|
||||
<DetailRow label="Garage">{a.garage != null ? formatStock(a.garage) : null}</DetailRow>
|
||||
<DetailRow label="Keller">{a.keller != null ? formatStock(a.keller) : null}</DetailRow>
|
||||
<DetailRow label="Versandklasse">{a.tVersandklassecName}</DetailRow>
|
||||
<DetailRow label="Gewicht">
|
||||
{a.fGewicht != null
|
||||
? `${new Intl.NumberFormat('de-DE').format(a.fGewicht)} kg`
|
||||
: null}
|
||||
</DetailRow>
|
||||
{a.vater?.cArtNr && (
|
||||
<DetailRow label="Vaterartikel">
|
||||
<Link component={RouterLink} to={`/artikel/${encodeURIComponent(a.vater.cArtNr)}`}>
|
||||
{a.vater.cArtNr}
|
||||
</Link>
|
||||
</DetailRow>
|
||||
)}
|
||||
{(a.cEigenschaftName || a.cEigenschaftWertName) && (
|
||||
<DetailRow label="Variante">
|
||||
{[a.cEigenschaftName, a.cEigenschaftWertName].filter(Boolean).join(': ')}
|
||||
</DetailRow>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{a.cKurzBeschreibung && (
|
||||
<Section title="Kurzbeschreibung">
|
||||
<Typography variant="body2">{a.cKurzBeschreibung}</Typography>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.cBeschreibung && (
|
||||
<Section title="Beschreibung">
|
||||
<Box
|
||||
sx={{ typography: 'body2', '& p': { mt: 0 } }}
|
||||
dangerouslySetInnerHTML={{ __html: a.cBeschreibung }}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.EANs?.length > 0 && (
|
||||
<Section title="EANs">
|
||||
<Typography variant="body2">
|
||||
{a.EANs.map((row) => row.cEAN).filter(Boolean).join(', ')}
|
||||
</Typography>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.KATs?.length > 0 && (
|
||||
<Section title="Kategorien">
|
||||
<Typography variant="body2">
|
||||
{a.KATs.map((row) => row.kKategorie).join(', ')}
|
||||
</Typography>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.staffel?.length > 0 && (
|
||||
<Section title="Staffelpreise">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Menge</TableCell>
|
||||
<TableCell align="right">VK Brutto</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{a.staffel.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>{row.menge}</TableCell>
|
||||
<TableCell align="right">{row.fVKBrutto}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.lief?.length > 0 && (
|
||||
<Section title="Lieferanten">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Firma</TableCell>
|
||||
<TableCell>Lief.-Artikelnr.</TableCell>
|
||||
<TableCell align="right">EK Netto</TableCell>
|
||||
<TableCell align="right">Lager</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{a.lief.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
{row.cFirma}
|
||||
{row.nStandard ? ' ★' : ''}
|
||||
</TableCell>
|
||||
<TableCell>{row.cLiefArtNr || '—'}</TableCell>
|
||||
<TableCell align="right">
|
||||
{row.fEKNetto != null ? formatPrice(row.fEKNetto) : '—'}
|
||||
</TableCell>
|
||||
<TableCell align="right">{formatStock(row.fLagerbestand)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.merkmale?.length > 0 && (
|
||||
<Section title="Merkmale">
|
||||
<Paper variant="outlined" sx={{ p: 2 }}>
|
||||
{a.merkmale.map((row, i) => (
|
||||
<DetailRow key={i} label={row.cName}>
|
||||
{row.cWert}
|
||||
</DetailRow>
|
||||
))}
|
||||
</Paper>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.children?.length > 0 && (
|
||||
<Section title="Varianten">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Artikelnr.</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{a.children.map((row) => (
|
||||
<TableRow key={row.cArtNr || row.kArtikel} hover>
|
||||
<TableCell>
|
||||
{row.cArtNr ? (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/artikel/${encodeURIComponent(row.cArtNr)}`}
|
||||
>
|
||||
{row.cArtNr}
|
||||
</Link>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{row.cName || '—'}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.liste?.length > 0 && (
|
||||
<Section title="Stückliste">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Artikelnr.</TableCell>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell align="right">Menge</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{a.liste.map((row, i) => (
|
||||
<TableRow key={i}>
|
||||
<TableCell>
|
||||
{row.cArtNr ? (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={`/artikel/${encodeURIComponent(row.cArtNr)}`}
|
||||
>
|
||||
{row.cArtNr}
|
||||
</Link>
|
||||
) : (
|
||||
'—'
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{row.cName || '—'}</TableCell>
|
||||
<TableCell align="right">{formatStock(row.fAnzahl ?? row.menge)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{a.cAnmerkung && (
|
||||
<Section title="Interne Anmerkung">
|
||||
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{a.cAnmerkung}
|
||||
</Typography>
|
||||
</Section>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ArtikelSkuPage() {
|
||||
const { sku } = useParams();
|
||||
const { sku: skuParam } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const target = sku ? decodeURIComponent(sku) : '';
|
||||
const sku = skuParam ? decodeURIComponent(skuParam) : '';
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -295,7 +37,7 @@ export default function ArtikelSkuPage() {
|
||||
let cancelled = false;
|
||||
|
||||
async function load() {
|
||||
if (!target) {
|
||||
if (!sku) {
|
||||
setError('Keine Artikelnr. angegeben');
|
||||
setData(null);
|
||||
setLoading(false);
|
||||
@@ -305,10 +47,11 @@ export default function ArtikelSkuPage() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await article.get({ target });
|
||||
const res = await article.getBySku(sku);
|
||||
requireObject(res, 'GET /api/article/:sku');
|
||||
requireObject(res.article, 'article');
|
||||
if (!cancelled) {
|
||||
setData(res.article ?? null);
|
||||
if (!res.article) setError('Artikel nicht gefunden');
|
||||
setData(res.article);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
@@ -324,7 +67,7 @@ export default function ArtikelSkuPage() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [target, reloadKey]);
|
||||
}, [sku, reloadKey]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -352,5 +95,5 @@ export default function ArtikelSkuPage() {
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
return <ArticleView article={data} />;
|
||||
return <ArticleView article={data} onArticleUpdated={setData} />;
|
||||
}
|
||||
|
||||
19
src/pages/artikel/withCategoryRoute.jsx
Normal file
19
src/pages/artikel/withCategoryRoute.jsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
export default function withCategoryRoute(Wrapped) {
|
||||
function CategoryRouteWrapper(props) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { '*': categorySlugPath = '' } = useParams();
|
||||
|
||||
return (
|
||||
<Wrapped
|
||||
{...props}
|
||||
navigate={navigate}
|
||||
location={location}
|
||||
categorySlugPath={categorySlugPath}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return CategoryRouteWrapper;
|
||||
}
|
||||
Reference in New Issue
Block a user