diff --git a/src/components/AppShell.jsx b/src/components/AppShell.jsx index 82e1fe4..4236833 100644 --- a/src/components/AppShell.jsx +++ b/src/components/AppShell.jsx @@ -87,8 +87,8 @@ class AppSidebar extends PureComponent { disablePadding sx={{ flexGrow: 1, - display: 'flex', - flexDirection: { xs: 'row', lg: 'column' }, + display: { xs: 'flex', lg: 'block' }, + flexDirection: 'row', alignItems: 'center', height: { xs: '100%', lg: 'auto' }, overflowX: { xs: 'auto', lg: 'visible' }, @@ -104,6 +104,8 @@ class AppSidebar extends PureComponent { e.preventDefault()} onClick={() => { @@ -131,7 +133,7 @@ class AppSidebar extends PureComponent { borderRadius: { xs: 1, lg: 0 }, width: { xs: 'auto', lg: '100%' }, height: { xs: '100%', lg: 'auto' }, - px: { xs: 1.25, sm: 1.5 }, + px: { xs: active ? 1.25 : 1, sm: active ? 1.5 : 1.25, md: 1.5 }, py: { xs: 0.75, sm: 1, lg: 1.25 }, borderLeft: { xs: 'none', lg: '3px solid transparent' }, borderBottom: { xs: '3px solid transparent', lg: 'none' }, @@ -141,7 +143,8 @@ class AppSidebar extends PureComponent { '& .MuiListItemIcon-root': { color: 'inherit', minWidth: { xs: 'auto', lg: 36 }, - mr: { xs: 0.75, lg: 0 }, + mr: active ? { xs: 0.75, lg: 0 } : { xs: 0, md: 0.75, lg: 0 }, + justifyContent: 'center', }, '&:hover': { bgcolor: theme.nav.itemHover, @@ -172,7 +175,10 @@ class AppSidebar extends PureComponent { fontWeight: active ? 600 : 500, noWrap: true, }} - sx={{ m: 0 }} + sx={{ + m: { xs: 0, lg: 'initial' }, + display: active ? 'block' : { xs: 'none', md: 'block' }, + }} /> ); @@ -239,25 +245,34 @@ class AppSubNav extends PureComponent { ({ + width: { xs: '100%', sm: 72, md: SUB_NAV_WIDTH }, flexShrink: 0, display: 'flex', - flexDirection: 'column', + flexDirection: { xs: 'row', sm: 'column' }, alignItems: 'center', - gap: 1.5, + gap: { xs: 1, sm: 1.5 }, position: 'sticky', top: { xs: 52, sm: 56, lg: 0 }, + zIndex: { xs: 1090, sm: 'auto' }, alignSelf: 'flex-start', height: { - xs: 'calc(var(--df-vh, 100vh) - 52px)', + xs: 'auto', sm: 'calc(var(--df-vh, 100vh) - 56px)', lg: 'var(--df-vh, 100vh)', }, - py: { xs: 2, lg: 4 }, - ml: { xs: 1, sm: 2, lg: 3 }, - mr: { xs: 1, sm: 1.5, lg: 2 }, - }} + py: { xs: 0.75, sm: 2, lg: 4 }, + px: { xs: 1.5, sm: 0 }, + ml: { xs: 0, sm: 2, lg: 3 }, + mr: { xs: 0, sm: 1.5, lg: 2 }, + overflowX: { xs: 'auto', sm: 'visible' }, + scrollbarWidth: 'none', + '&::-webkit-scrollbar': { display: 'none' }, + bgcolor: { xs: theme.nav?.mainBackground || 'background.default', sm: 'transparent' }, + borderBottom: { xs: '1px solid', sm: 'none' }, + borderColor: { xs: 'divider', sm: 'transparent' }, + boxSizing: 'border-box', + })} > {subNav.map(({ segment, label, title, help, icon: Icon }) => { const active = segment === activeSub; @@ -266,6 +281,7 @@ class AppSubNav extends PureComponent { ({ - width: { xs: 60, sm: 72, md: SUB_NAV_WIDTH }, - py: 1, - px: 0.5, + width: { xs: 'auto', sm: 72, md: SUB_NAV_WIDTH }, + py: { xs: 0.5, sm: 1 }, + px: { xs: active ? 1.25 : 0.75, sm: 0.5 }, borderRadius: 2, display: 'flex', - flexDirection: 'column', + flexDirection: { xs: 'row', sm: 'column' }, alignItems: 'center', justifyContent: 'center', - gap: 0.5, + gap: { xs: active ? 0.75 : 0, sm: 0.5 }, + flexShrink: 0, + whiteSpace: 'nowrap', color: active ? theme.nav?.itemActiveAccent || NAV_ITEM_ACTIVE_ACCENT : theme.palette.text.secondary, @@ -314,18 +332,19 @@ class AppSubNav extends PureComponent { })} > - + {label ? ( {label} @@ -358,7 +377,7 @@ export default function AppShell() { ({ diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx index 9277da7..57fee87 100644 --- a/src/components/manifest/ManifestFormLine.jsx +++ b/src/components/manifest/ManifestFormLine.jsx @@ -19,11 +19,13 @@ import SearchIcon from '@mui/icons-material/Search'; import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined'; import { getFirstVal, parseNum, LINE_COLUMNS } from './manifestConfig'; import ArticlePhoto from '../article/ArticlePhoto'; -import { formatPrice } from '../../utils/price'; +import { formatPrice, grossFromNet } from '../../utils/price'; import { primaryPrice, priceBrutto } from '../../utils/articleDetail'; import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat'; import { artikelDetailPath } from '../article/articleRoute'; import { BrandLabel } from '../brand'; +import { articleSupplierAutomat } from '../../automats/articleDetailAutomat'; +import { selectCompanyTaxrate } from '../../automats/companyAutomat'; import { BORDER, } from '../../colors'; @@ -35,6 +37,7 @@ class ManifestFormLine extends PureComponent { this.state = { cachedArticle: sku ? getArticle(sku) : null, loadingArticle: false, + fallbackSuppliers: [], }; this.handleRemove = this.handleRemove.bind(this); this.handleBlur = this.handleBlur.bind(this); @@ -64,14 +67,18 @@ class ManifestFormLine extends PureComponent { componentWillUnmount() { this.unsubArticle?.(); + this.unsubSupplier?.(); } subscribeArticle(sku) { this.unsubArticle?.(); this.unsubArticle = null; + this.unsubSupplier?.(); + this.unsubSupplier = null; + if (!sku) { - if (this.state.cachedArticle || this.state.loadingArticle) { - this.setState({ cachedArticle: null, loadingArticle: false }); + if (this.state.cachedArticle || this.state.loadingArticle || this.state.fallbackSuppliers?.length) { + this.setState({ cachedArticle: null, loadingArticle: false, fallbackSuppliers: [] }); } return; } @@ -99,6 +106,15 @@ class ManifestFormLine extends PureComponent { ...(art ? { loadingArticle: false } : {}), }; }); + + // Fallback: subscribe to articleSupplierAutomat if cachedArticle doesn't have suppliers + if (!current?.suppliers?.length) { + const supKey = articleSupplierAutomat.key(sku); + this.unsubSupplier = supKey.subscribe(this, (item) => ({ + fallbackSuppliers: Array.isArray(item?.data) ? item.data : [], + })); + void articleSupplierAutomat.load(sku).catch(() => {}); + } } handleRemove() { @@ -235,8 +251,9 @@ class ManifestFormLine extends PureComponent { } else if (id === 'gross') { const netNum = parseNum(line.netprice ?? line.net); const taxNum = parseNum(line.taxrate ?? line.tax) ?? 0; + const rebateNum = parseNum(line.rebate) ?? 0; if (netNum != null) { - val = String(Math.round((netNum * (1 + taxNum / 100) + Number.EPSILON) * 100) / 100); + val = String(grossFromNet(netNum, taxNum, rebateNum)); } } } @@ -375,6 +392,41 @@ class ManifestFormLine extends PureComponent { : null) : null; + const suppliers = + Array.isArray(cachedArticle.suppliers) && cachedArticle.suppliers.length > 0 + ? cachedArticle.suppliers + : (this.state.fallbackSuppliers ?? []); + + const topSupplier = + suppliers.find((s) => s.sort === 0) ?? + (suppliers.length > 0 + ? [...suppliers].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0))[0] + : null); + + const ekNetto = + topSupplier?.price?.net != null + ? Number(topSupplier.price.net) + : topSupplier?.netPrice != null + ? Number(topSupplier.netPrice) + : topSupplier?.net != null + ? Number(topSupplier.net) + : null; + + const ekTaxRate = + topSupplier?.price?.taxRate != null + ? Number(topSupplier.price.taxRate) + : topSupplier?.taxRate != null + ? Number(topSupplier.taxRate) + : parseNum(this.props.line?.taxrate ?? this.props.line?.tax) ?? selectCompanyTaxrate() ?? 19; + + const ekBrutto = + ekNetto != null + ? Math.round((ekNetto * (1 + Number(ekTaxRate) / 100) + Number.EPSILON) * 100) / 100 + : null; + + const hasPriceInfo = + vkNetto != null || vkBrutto != null || ekNetto != null || ekBrutto != null || kind === 'quote'; + return ( )} - {/* VK Netto & VK Brutto: omit end on 2nd line for order */} - {!isOrder && (vkNetto != null || vkBrutto != null) && ( + {/* EK Netto, EK Brutto, VK Netto, VK Brutto */} + {!isOrder && hasPriceInfo && ( + {/* EK Netto */} + + + EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'} + + + + {/* EK Brutto */} + + + EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'} + + + + {/* VK Netto */} {vkNetto != null && ( )} + + {/* VK Brutto */} {vkBrutto != null && (