This commit is contained in:
seb
2026-10-08 20:55:36 +02:00
parent fa6d910723
commit 90c502071a
5 changed files with 108 additions and 96 deletions

View File

@@ -8,7 +8,7 @@ import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
import unitAutomat from '../../../automats/unitAutomat';
import companyAutomat from '../../../automats/companyAutomat';
import artikelAutomat from '../../../automats/artikelAutomat';
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import {
buildPosPricesPutBody,
@@ -19,6 +19,7 @@ import {
} from '../../../utils/articleDetail';
import {
formatPriceAmount,
getArticleGross,
netFromGross,
parsePriceInput,
} from '../../../utils/price';
@@ -33,7 +34,6 @@ import TierPricePills from './TierPricePills';
import { PriceUnitRow, PriceUnitCorner } from './PriceUnitRow';
import {
BLUE_PANEL,
BORDER,
} from '../../../colors';
const SECOND_ROW_MIN_HEIGHT = 220;
@@ -387,6 +387,20 @@ export default class ArticleViewPrice extends PureComponent {
this.unsubCompany = companyAutomat.subscribe(this, (state) => ({
company: state.current ?? null,
}));
this.unsubArtikel = artikelAutomat.subscribe(this, () => ({}));
if (this.props.article?.isParent) {
this.ensureChildren(this.props.article.children);
}
}
componentDidUpdate(prevProps) {
if (
this.props.article?.isParent &&
(prevProps.article?.sku !== this.props.article?.sku ||
prevProps.article?.children !== this.props.article?.children)
) {
this.ensureChildren(this.props.article.children);
}
}
componentWillUnmount() {
@@ -395,6 +409,17 @@ export default class ArticleViewPrice extends PureComponent {
this.unsubUnits = null;
this.unsubCompany?.();
this.unsubCompany = null;
this.unsubArtikel?.();
this.unsubArtikel = null;
}
ensureChildren(children) {
if (!Array.isArray(children)) return;
for (const child of children) {
if (child?.sku && !getArticle(child.sku)) {
void artikelAutomat.actions.loadArticle(child.sku).catch(() => {});
}
}
}
handleSave = async (draft) => {
@@ -481,11 +506,33 @@ export default class ArticleViewPrice extends PureComponent {
const { article: articleData } = this.props;
if (articleData?.isParent) {
const { company } = this.state;
const defaultTaxRate = company?.taxrate ?? 19;
const children = Array.isArray(articleData.children) ? articleData.children : [];
const prices = [];
for (const child of children) {
const fullChild = getArticle(child.sku) || child;
const gross = getArticleGross(fullChild, defaultTaxRate);
if (gross != null && Number.isFinite(gross)) {
prices.push(gross);
}
}
let priceText = 'Kein Preis';
if (prices.length > 0) {
const min = Math.min(...prices);
const max = Math.max(...prices);
priceText =
min === max
? `€ ${formatPriceAmount(min)}`
: `€ ${formatPriceAmount(min)} bis € ${formatPriceAmount(max)}`;
}
return (
<PanelTile
wide={1}
minHeight={SECOND_ROW_MIN_HEIGHT}
color={BORDER}
color={BLUE_PANEL}
contentAlignItems="flex-start"
>
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', width: '100%' }}>
@@ -493,32 +540,25 @@ export default class ArticleViewPrice extends PureComponent {
variant="caption"
color="text.secondary"
sx={{
textTransform: 'uppercase',
letterSpacing: 0.6,
fontWeight: 600,
display: 'block',
}}
>
Preis
Preis auf Variantenebene pflegen
</Typography>
<Box sx={{ my: 0.75 }}>
<Typography
component="div"
sx={{
fontSize: '1.25rem',
fontSize: '1.5rem',
fontWeight: 700,
lineHeight: 1.2,
color: 'text.secondary',
letterSpacing: '-0.02em',
color: 'text.primary',
}}
>
Kein Preis
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 0.25 }}
>
Preise werden auf Variantenebene gepflegt
{priceText}
</Typography>
</Box>
</Box>

View File

@@ -7,8 +7,8 @@ import Tooltip from '@mui/material/Tooltip';
import { PanelTile } from 'dashgui/panels';
import { formatStock } from '../searchFormat';
import ArticleStockAdjustDialog from '../ArticleStockAdjustDialog';
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
import {
BORDER,
GREEN_PANEL,
PETROL_PANEL,
SORT_GHOST_BG,
@@ -28,6 +28,36 @@ export default class ArticleViewStock extends PureComponent {
this.handleCloseAdjust = this.handleCloseAdjust.bind(this);
}
componentDidMount() {
this.unsubArtikel = artikelAutomat.subscribe(this, () => ({}));
if (this.props.article?.isParent) {
this.ensureChildren(this.props.article.children);
}
}
componentDidUpdate(prevProps) {
if (
this.props.article?.isParent &&
(prevProps.article?.sku !== this.props.article?.sku ||
prevProps.article?.children !== this.props.article?.children)
) {
this.ensureChildren(this.props.article.children);
}
}
componentWillUnmount() {
this.unsubArtikel?.();
}
ensureChildren(children) {
if (!Array.isArray(children)) return;
for (const child of children) {
if (child?.sku && !getArticle(child.sku)) {
void artikelAutomat.actions.loadArticle(child.sku).catch(() => {});
}
}
}
handleOpenAdjust() {
this.setState({ adjustOpen: true });
}
@@ -54,6 +84,22 @@ export default class ArticleViewStock extends PureComponent {
const warehouses = rawWarehouses.filter((wh) => (Number(wh.stock) || 0) > 0);
const hasWarehouses = warehouses.length > 0;
let totalVariantsStock = 0;
if (isParent) {
const children = Array.isArray(articleData?.children) ? articleData.children : [];
for (const child of children) {
const fullChild = getArticle(child.sku) || child;
if (Array.isArray(fullChild.warehouses) && fullChild.warehouses.length > 0) {
totalVariantsStock += fullChild.warehouses.reduce(
(sum, wh) => sum + (Number(wh.stock) || 0),
0,
);
} else if (fullChild.stock != null) {
totalVariantsStock += Number(fullChild.stock) || 0;
}
}
}
const totalStock = rawWarehouses.length > 0
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: articleData?.stock != null
@@ -74,7 +120,7 @@ export default class ArticleViewStock extends PureComponent {
<PanelTile
wide={1}
minHeight={SECOND_ROW_MIN_HEIGHT}
color={isParent ? BORDER : isStueckliste ? PETROL_PANEL : GREEN_PANEL}
color={isStueckliste ? PETROL_PANEL : GREEN_PANEL}
action={
isParent || isStueckliste
? null
@@ -99,13 +145,13 @@ export default class ArticleViewStock extends PureComponent {
variant="caption"
color="text.secondary"
sx={{
textTransform: 'uppercase',
...(isParent ? {} : { textTransform: 'uppercase' }),
letterSpacing: 0.6,
fontWeight: 600,
display: 'block',
}}
>
Bestand
{isParent ? 'Bestand auf Variantenebene pflegen' : 'Bestand'}
</Typography>
{isParent ? (
@@ -116,17 +162,10 @@ export default class ArticleViewStock extends PureComponent {
fontSize: '1.25rem',
fontWeight: 700,
lineHeight: 1.2,
color: 'text.secondary',
color: 'text.primary',
}}
>
Kein Bestand
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block', mt: 0.25 }}
>
Bestand wird auf Variantenebene geführt
Alle Varianten zusammen: {formatStock(totalVariantsStock)}
</Typography>
</Box>
) : isStueckliste ? (

View File

@@ -319,27 +319,6 @@ export default class BrandImageSearchDialog extends PureComponent {
</Button>
</Box>
{/* User helper tip */}
{images.length > 0 && !error && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 1,
mb: 1.5,
px: 1,
py: 0.5,
borderRadius: 1,
bgcolor: 'action.hover',
}}
>
<AutoFixHighIcon color="primary" sx={{ fontSize: 17 }} />
<Typography variant="caption" color="text.secondary">
Klicken Sie auf ein Logo: Es wird heruntergeladen, automatisch freigestellt (Weiß- &amp; Transparenz-Ränder entfernt), in AVIF konvertiert und als Markenbild hinterlegt.
</Typography>
</Box>
)}
{/* Progress / Status banner */}
{status === 'loading' && <LinearProgress sx={{ mb: 2, borderRadius: 1 }} />}

View File

@@ -16,52 +16,6 @@ body {
touch-action: manipulation;
}
:root {
--df-zoom: 1;
--df-vh: 100vh;
}
/* ────────────────────────────────────────────────────────────────────────────
* RESPONSIVE VIEWPORT ZOOM
*
* Automatically scales the UI down on narrower viewports:
* - < 1700px: 80% zoom
* - < 1400px: 65% zoom
* - < 1000px: 50% zoom
*
* `--df-vh` scales inversely with zoom so that elements with full-screen
* height (like AppSidebar and full-height page wrappers) always reach the
* true bottom of the viewport instead of shrinking.
* ──────────────────────────────────────────────────────────────────────────── */
@media (max-width: 1699.98px) {
:root {
--df-zoom: 0.8;
--df-vh: calc(100vh / 0.8);
}
body {
zoom: 0.8;
}
}
@media (max-width: 1399.98px) {
:root {
--df-zoom: 0.65;
--df-vh: calc(100vh / 0.65);
}
body {
zoom: 0.65;
}
}
@media (max-width: 999.98px) {
:root {
--df-zoom: 0.5;
--df-vh: calc(100vh / 0.5);
}
body {
zoom: 0.5;
}
}
/* ────────────────────────────────────────────────────────────────────────────
* SYSTEM-WIDE SPELLCHECK OFF

View File

@@ -53,7 +53,7 @@ createRoot(document.getElementById('root')).render(
<AuthBootstrap>
{/* Flex row so the dev inspector docks beside the app instead of
overlaying it; the app content shrinks while the panel is open. */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', minHeight: 'var(--df-vh, 100vh)' }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', minHeight: '100vh' }}>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<App />
</Box>