u
This commit is contained in:
@@ -90,7 +90,11 @@ export const article = {
|
|||||||
return http(`/api/article/search?${params}`);
|
return http(`/api/article/search?${params}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** GET /api/article/:sku → { article: { id, sku, name, slug, net, taxRate, fatherArticleId, fatherSku, … } } */
|
/**
|
||||||
|
* GET /api/article/:sku → { article }
|
||||||
|
* Detail: prices[], photos[], eans[], brand, unit, categoryIds, …
|
||||||
|
* (Flat net/taxRate/thumb* only on category tree / getCategoryArticles.)
|
||||||
|
*/
|
||||||
getBySku(sku) {
|
getBySku(sku) {
|
||||||
return http(`/api/article/${encodeURIComponent(sku)}`);
|
return http(`/api/article/${encodeURIComponent(sku)}`);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ import Typography from '@mui/material/Typography';
|
|||||||
import { article } from '../../api';
|
import { article } from '../../api';
|
||||||
import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels';
|
import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels';
|
||||||
import { requireObject } from '../../utils/apiShape';
|
import { requireObject } from '../../utils/apiShape';
|
||||||
|
import {
|
||||||
|
articleEans,
|
||||||
|
brandName,
|
||||||
|
priceBrutto,
|
||||||
|
primaryPrice,
|
||||||
|
} from '../../utils/articleDetail';
|
||||||
import { formatPrice } from '../../utils/price';
|
import { formatPrice } from '../../utils/price';
|
||||||
import { formatStock } from '../../pages/artikel/searchFormat';
|
import { formatStock } from '../../pages/artikel/searchFormat';
|
||||||
|
|
||||||
@@ -68,11 +74,6 @@ function RelationLine({ label, active, children }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function articleBrutto(article) {
|
|
||||||
if (article.net == null || article.taxRate == null) return null;
|
|
||||||
return article.net * (1 + article.taxRate / 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nameDraftDirty(draft, saved) {
|
function nameDraftDirty(draft, saved) {
|
||||||
return draft.name.trim() !== String(saved.name ?? '').trim();
|
return draft.name.trim() !== String(saved.name ?? '').trim();
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,9 @@ function nameDraftValid(draft) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ArticleView({ article: articleData, onArticleUpdated }) {
|
export default function ArticleView({ article: articleData, onArticleUpdated }) {
|
||||||
const brutto = articleBrutto(articleData);
|
const basePrice = primaryPrice(articleData);
|
||||||
|
const brutto = priceBrutto(basePrice);
|
||||||
|
const eans = articleEans(articleData);
|
||||||
|
|
||||||
const handleSaveName = async (draft) => {
|
const handleSaveName = async (draft) => {
|
||||||
const name = draft.name.trim();
|
const name = draft.name.trim();
|
||||||
@@ -108,7 +111,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
minHeight: { xs: 0, sm: 200 },
|
minHeight: { xs: 0, sm: 200 },
|
||||||
pr: 4,
|
pr: 5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" sx={{ fontWeight: 700, lineHeight: 1.25 }}>
|
<Typography variant="h6" sx={{ fontWeight: 700, lineHeight: 1.25 }}>
|
||||||
@@ -153,20 +156,24 @@ export default function ArticleView({ article: articleData, onArticleUpdated })
|
|||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="Marke">
|
<PanelTile wide={1} title="Marke">
|
||||||
<PanelValue>{articleData.brand}</PanelValue>
|
<PanelValue>{brandName(articleData)}</PanelValue>
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="SKU / EAN">
|
<PanelTile wide={1} title="SKU / EAN">
|
||||||
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
||||||
<PanelValue label="EAN">{articleData.ean}</PanelValue>
|
{eans.map((ean) => (
|
||||||
|
<PanelValue key={ean} label="EAN">
|
||||||
|
{ean}
|
||||||
|
</PanelValue>
|
||||||
|
))}
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
|
|
||||||
<PanelTile wide={1} title="Preis">
|
<PanelTile wide={1} title="Preis">
|
||||||
<PanelValue label="Netto">
|
<PanelValue label="Netto">
|
||||||
{articleData.net != null ? formatPrice(articleData.net) : null}
|
{basePrice?.net != null ? formatPrice(basePrice.net) : null}
|
||||||
</PanelValue>
|
</PanelValue>
|
||||||
<PanelValue label="MwSt.">
|
<PanelValue label="MwSt.">
|
||||||
{articleData.taxRate != null ? `${articleData.taxRate} %` : null}
|
{basePrice?.taxRate != null ? `${basePrice.taxRate} %` : null}
|
||||||
</PanelValue>
|
</PanelValue>
|
||||||
<PanelValue label="Brutto">
|
<PanelValue label="Brutto">
|
||||||
{brutto != null ? formatPrice(brutto) : null}
|
{brutto != null ? formatPrice(brutto) : null}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export class EditablePanelTile extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
handleCancel = (event) => {
|
handleCancel = (event) => {
|
||||||
event.stopPropagation();
|
event?.stopPropagation();
|
||||||
this.setState({
|
this.setState({
|
||||||
editing: false,
|
editing: false,
|
||||||
draft: null,
|
draft: null,
|
||||||
@@ -82,8 +82,25 @@ export class EditablePanelTile extends Component {
|
|||||||
this.setState({ draft: nextDraft, error: null });
|
this.setState({ draft: nextDraft, error: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleEditKeyDown = (event) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.handleCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (this.canSave()) {
|
||||||
|
void this.handleSave(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleSave = async (event) => {
|
handleSave = async (event) => {
|
||||||
event.stopPropagation();
|
event?.stopPropagation();
|
||||||
if (!this.canSave()) return;
|
if (!this.canSave()) return;
|
||||||
|
|
||||||
const { onSave } = this.props;
|
const { onSave } = this.props;
|
||||||
@@ -109,7 +126,7 @@ export class EditablePanelTile extends Component {
|
|||||||
<Typography
|
<Typography
|
||||||
variant="overline"
|
variant="overline"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{ display: 'block', mb: 1, pr: editing ? 7 : 4 }}
|
sx={{ display: 'block', mb: 1, pr: editing ? 9 : 5 }}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -126,16 +143,16 @@ export class EditablePanelTile extends Component {
|
|||||||
top: 4,
|
top: 4,
|
||||||
color: 'text.secondary',
|
color: 'text.secondary',
|
||||||
};
|
};
|
||||||
|
const iconSx = { fontSize: 28 };
|
||||||
|
|
||||||
if (!editing) {
|
if (!editing) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
|
||||||
onClick={this.handleStartEdit}
|
onClick={this.handleStartEdit}
|
||||||
aria-label="Bearbeiten"
|
aria-label="Bearbeiten"
|
||||||
sx={{ ...actionSx, right: 4 }}
|
sx={{ ...actionSx, right: 4 }}
|
||||||
>
|
>
|
||||||
<EditOutlinedIcon fontSize="small" />
|
<EditOutlinedIcon sx={iconSx} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -143,16 +160,14 @@ export class EditablePanelTile extends Component {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
|
||||||
onClick={this.handleCancel}
|
onClick={this.handleCancel}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
aria-label="Abbrechen"
|
aria-label="Abbrechen"
|
||||||
sx={{ ...actionSx, right: 40 }}
|
sx={{ ...actionSx, right: 48 }}
|
||||||
>
|
>
|
||||||
<CloseOutlinedIcon fontSize="small" />
|
<CloseOutlinedIcon sx={iconSx} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
|
||||||
onClick={this.handleSave}
|
onClick={this.handleSave}
|
||||||
disabled={!this.canSave()}
|
disabled={!this.canSave()}
|
||||||
aria-label="Speichern"
|
aria-label="Speichern"
|
||||||
@@ -160,9 +175,9 @@ export class EditablePanelTile extends Component {
|
|||||||
sx={{ ...actionSx, right: 4 }}
|
sx={{ ...actionSx, right: 4 }}
|
||||||
>
|
>
|
||||||
{saving ? (
|
{saving ? (
|
||||||
<CircularProgress size={18} color="inherit" />
|
<CircularProgress size={22} color="inherit" />
|
||||||
) : (
|
) : (
|
||||||
<SaveOutlinedIcon fontSize="small" />
|
<SaveOutlinedIcon sx={iconSx} />
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</>
|
</>
|
||||||
@@ -188,7 +203,10 @@ export class EditablePanelTile extends Component {
|
|||||||
>
|
>
|
||||||
{this.renderActions()}
|
{this.renderActions()}
|
||||||
{this.renderTitle()}
|
{this.renderTitle()}
|
||||||
<Box sx={{ flex: 1, minWidth: 0, pr: editing ? 1 : 0 }}>
|
<Box
|
||||||
|
sx={{ flex: 1, minWidth: 0, pr: editing ? 1 : 0 }}
|
||||||
|
onKeyDown={editing ? this.handleEditKeyDown : undefined}
|
||||||
|
>
|
||||||
{editing && draft != null
|
{editing && draft != null
|
||||||
? renderEdit(draft, this.handleDraftChange, this.inputRef)
|
? renderEdit(draft, this.handleDraftChange, this.inputRef)
|
||||||
: renderDisplay(savedValue)}
|
: renderDisplay(savedValue)}
|
||||||
|
|||||||
30
src/utils/articleDetail.js
Normal file
30
src/utils/articleDetail.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/** Helpers for GET/PATCH /api/article/:sku detail shape. */
|
||||||
|
|
||||||
|
export function sortedPhotos(article) {
|
||||||
|
if (!Array.isArray(article?.photos)) return [];
|
||||||
|
return [...article.photos].sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function primaryPhoto(article) {
|
||||||
|
const photos = sortedPhotos(article);
|
||||||
|
return photos[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function primaryPrice(article) {
|
||||||
|
if (!Array.isArray(article?.prices) || article.prices.length === 0) return null;
|
||||||
|
return article.prices.find((row) => row.minmultiplier === 1) ?? article.prices[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function priceBrutto(price) {
|
||||||
|
if (price?.net == null || price?.taxRate == null) return null;
|
||||||
|
return price.net * (1 + price.taxRate / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function brandName(article) {
|
||||||
|
return article?.brand?.name ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function articleEans(article) {
|
||||||
|
if (!Array.isArray(article?.eans)) return [];
|
||||||
|
return article.eans.filter(Boolean);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user