diff --git a/src/api/index.js b/src/api/index.js index 418bd2a..1f32b79 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -90,7 +90,11 @@ export const article = { 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) { return http(`/api/article/${encodeURIComponent(sku)}`); }, diff --git a/src/components/article/ArticleView.jsx b/src/components/article/ArticleView.jsx index df11650..5a877a4 100644 --- a/src/components/article/ArticleView.jsx +++ b/src/components/article/ArticleView.jsx @@ -6,6 +6,12 @@ import Typography from '@mui/material/Typography'; import { article } from '../../api'; import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels'; import { requireObject } from '../../utils/apiShape'; +import { + articleEans, + brandName, + priceBrutto, + primaryPrice, +} from '../../utils/articleDetail'; import { formatPrice } from '../../utils/price'; 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) { return draft.name.trim() !== String(saved.name ?? '').trim(); } @@ -82,7 +83,9 @@ function nameDraftValid(draft) { } 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 name = draft.name.trim(); @@ -108,7 +111,7 @@ export default function ArticleView({ article: articleData, onArticleUpdated }) display: 'flex', alignItems: 'center', minHeight: { xs: 0, sm: 200 }, - pr: 4, + pr: 5, }} > @@ -153,20 +156,24 @@ export default function ArticleView({ article: articleData, onArticleUpdated }) - {articleData.brand} + {brandName(articleData)} {articleData.sku} - {articleData.ean} + {eans.map((ean) => ( + + {ean} + + ))} - {articleData.net != null ? formatPrice(articleData.net) : null} + {basePrice?.net != null ? formatPrice(basePrice.net) : null} - {articleData.taxRate != null ? `${articleData.taxRate} %` : null} + {basePrice?.taxRate != null ? `${basePrice.taxRate} %` : null} {brutto != null ? formatPrice(brutto) : null} diff --git a/src/components/panels/EditablePanelTile.jsx b/src/components/panels/EditablePanelTile.jsx index 7c8a872..865d7c1 100644 --- a/src/components/panels/EditablePanelTile.jsx +++ b/src/components/panels/EditablePanelTile.jsx @@ -70,7 +70,7 @@ export class EditablePanelTile extends Component { }; handleCancel = (event) => { - event.stopPropagation(); + event?.stopPropagation(); this.setState({ editing: false, draft: null, @@ -82,8 +82,25 @@ export class EditablePanelTile extends Component { 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) => { - event.stopPropagation(); + event?.stopPropagation(); if (!this.canSave()) return; const { onSave } = this.props; @@ -109,7 +126,7 @@ export class EditablePanelTile extends Component { {title} @@ -126,16 +143,16 @@ export class EditablePanelTile extends Component { top: 4, color: 'text.secondary', }; + const iconSx = { fontSize: 28 }; if (!editing) { return ( - + ); } @@ -143,16 +160,14 @@ export class EditablePanelTile extends Component { return ( <> - + {saving ? ( - + ) : ( - + )} @@ -188,7 +203,10 @@ export class EditablePanelTile extends Component { > {this.renderActions()} {this.renderTitle()} - + {editing && draft != null ? renderEdit(draft, this.handleDraftChange, this.inputRef) : renderDisplay(savedValue)} diff --git a/src/utils/articleDetail.js b/src/utils/articleDetail.js new file mode 100644 index 0000000..70b00e6 --- /dev/null +++ b/src/utils/articleDetail.js @@ -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); +}