u
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Link as RouterLink, useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Autocomplete from '@mui/material/Autocomplete';
|
||||
import LayersOutlinedIcon from '@mui/icons-material/LayersOutlined';
|
||||
import StraightenOutlinedIcon from '@mui/icons-material/StraightenOutlined';
|
||||
import Box from '@mui/material/Box';
|
||||
@@ -23,10 +24,10 @@ import ArticleUnitEditorDialog from './ArticleUnitEditorDialog';
|
||||
import { article } from '../../api';
|
||||
import { EditablePanelTile, PanelTile, PanelTileGrid } from '../panels';
|
||||
import { requireDefined, requireObject } from '../../utils/apiShape';
|
||||
import { loadBrands, selectBrands, selectBrandsStatus, upsertBrand } from '../../store/brandSlice';
|
||||
import { loadUnits, selectUnits, selectUnitsStatus } from '../../store/unitSlice';
|
||||
import {
|
||||
articleEans,
|
||||
brandName,
|
||||
buildPosPricesPutBody,
|
||||
companyTaxRateOptions,
|
||||
priceBrutto,
|
||||
@@ -123,6 +124,32 @@ function articleHasPackUnit(article) {
|
||||
return !isNoPackUnit(si, amount);
|
||||
}
|
||||
|
||||
function brandDraftValue(article) {
|
||||
return { name: article?.brand?.name ?? '' };
|
||||
}
|
||||
|
||||
function brandDraftDirty(draft, saved) {
|
||||
return String(draft.name ?? '').trim() !== String(saved.name ?? '').trim();
|
||||
}
|
||||
|
||||
function brandDraftValid(draft) {
|
||||
return String(draft.name ?? '').trim().length > 0;
|
||||
}
|
||||
|
||||
function eansDraftValue(article) {
|
||||
return { eans: articleEans(article) };
|
||||
}
|
||||
|
||||
function eansDraftDirty(draft, saved) {
|
||||
return JSON.stringify(draft.eans ?? []) !== JSON.stringify(saved.eans ?? []);
|
||||
}
|
||||
|
||||
function eansDraftValid(draft) {
|
||||
return (draft.eans ?? []).every(
|
||||
(ean) => typeof ean === 'string' && ean.trim().length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
function priceDraftValue(article, company) {
|
||||
const basePrice = primaryPrice(article);
|
||||
const gross = priceBrutto(basePrice);
|
||||
@@ -567,8 +594,9 @@ export default function ArticleView({
|
||||
const company = useSelector(selectCurrentCompany);
|
||||
const units = useSelector(selectUnits);
|
||||
const unitsStatus = useSelector(selectUnitsStatus);
|
||||
const brands = useSelector(selectBrands);
|
||||
const brandsStatus = useSelector(selectBrandsStatus);
|
||||
const basePrice = primaryPrice(articleData);
|
||||
const eans = articleEans(articleData);
|
||||
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
|
||||
const priceValue = priceDraftValue(articleData, company);
|
||||
const unitOptions =
|
||||
@@ -581,6 +609,11 @@ export default function ArticleView({
|
||||
void dispatch(loadUnits()).unwrap().catch((err) => notifyError(err));
|
||||
}, [dispatch, unitsStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (brandsStatus !== 'idle') return;
|
||||
void dispatch(loadBrands()).unwrap().catch((err) => notifyError(err));
|
||||
}, [dispatch, brandsStatus]);
|
||||
|
||||
const syncCategoryListPhoto = (next) => {
|
||||
const photo = primaryPhoto(next);
|
||||
dispatch(
|
||||
@@ -618,6 +651,41 @@ export default function ArticleView({
|
||||
onArticleUpdated?.(res.article);
|
||||
};
|
||||
|
||||
const handleSaveBrand = async (draft) => {
|
||||
const name = String(draft.name ?? '').trim();
|
||||
if (!name) {
|
||||
throw new Error('Bitte eine Marke angeben.');
|
||||
}
|
||||
|
||||
const existing = brands.find(
|
||||
(row) => String(row.name ?? '').toLowerCase() === name.toLowerCase(),
|
||||
);
|
||||
|
||||
let brandId;
|
||||
if (existing) {
|
||||
brandId = existing.id;
|
||||
} else {
|
||||
const created = await dispatch(upsertBrand({ name })).unwrap();
|
||||
requireDefined(created.id, 'brand.id');
|
||||
brandId = created.id;
|
||||
}
|
||||
|
||||
const res = await article.setBrandIdBySku(articleData.sku, brandId);
|
||||
requireObject(res, 'PUT /api/article/:sku/brand');
|
||||
requireObject(res.article, 'article');
|
||||
onArticleUpdated?.(res.article);
|
||||
};
|
||||
|
||||
const handleSaveEans = async (draft) => {
|
||||
const eans = (draft.eans ?? [])
|
||||
.map((ean) => String(ean).trim())
|
||||
.filter((ean) => ean.length > 0);
|
||||
const res = await article.setEansBySku(articleData.sku, eans);
|
||||
requireObject(res, 'PUT /api/article/:sku/eans');
|
||||
requireObject(res.article, 'article');
|
||||
onArticleUpdated?.(res.article);
|
||||
};
|
||||
|
||||
const handleSavePrice = async (draft) => {
|
||||
const saved = priceDraftValue(articleData, company);
|
||||
let nextArticle = articleData;
|
||||
@@ -741,28 +809,85 @@ export default function ArticleView({
|
||||
</PanelValue>
|
||||
</PanelTile>
|
||||
|
||||
<PanelTile
|
||||
<EditablePanelTile
|
||||
wide={1}
|
||||
tileKey={PANEL_TILE_KEYS.brand}
|
||||
title="Marke"
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
>
|
||||
<PanelValue>{brandName(articleData)}</PanelValue>
|
||||
</PanelTile>
|
||||
value={brandDraftValue(articleData)}
|
||||
onSave={handleSaveBrand}
|
||||
isDirty={brandDraftDirty}
|
||||
canSave={brandDraftValid}
|
||||
renderDisplay={(saved) => (
|
||||
<PanelValue>{saved.name || null}</PanelValue>
|
||||
)}
|
||||
renderEdit={(draft, onChange, inputRef) => (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
freeSolo
|
||||
options={brands}
|
||||
value={draft.name ?? ''}
|
||||
inputValue={draft.name ?? ''}
|
||||
getOptionLabel={(option) =>
|
||||
typeof option === 'string' ? option : String(option.name ?? '')
|
||||
}
|
||||
onInputChange={(_event, value) => onChange({ name: value })}
|
||||
onChange={(_event, option) => {
|
||||
onChange({ name: option ? String(option.name ?? '') : '' });
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
inputRef={inputRef}
|
||||
label="Marke"
|
||||
size="small"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<PanelTile
|
||||
<EditablePanelTile
|
||||
wide={1}
|
||||
tileKey={PANEL_TILE_KEYS.identifiers}
|
||||
title="SKU / EAN"
|
||||
minHeight={SECOND_ROW_MIN_HEIGHT}
|
||||
>
|
||||
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
||||
{eans.map((ean) => (
|
||||
<PanelValue key={ean} label="EAN">
|
||||
{ean}
|
||||
</PanelValue>
|
||||
))}
|
||||
</PanelTile>
|
||||
contentAlignItems="flex-start"
|
||||
value={eansDraftValue(articleData)}
|
||||
onSave={handleSaveEans}
|
||||
isDirty={eansDraftDirty}
|
||||
canSave={eansDraftValid}
|
||||
renderDisplay={(saved) => (
|
||||
<>
|
||||
<PanelValue label="SKU">{articleData.sku}</PanelValue>
|
||||
{(saved.eans ?? []).map((ean) => (
|
||||
<PanelValue key={ean} label="EAN">
|
||||
{ean}
|
||||
</PanelValue>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
renderEdit={(draft, onChange, inputRef) => (
|
||||
<Autocomplete
|
||||
fullWidth
|
||||
multiple
|
||||
freeSolo
|
||||
options={[]}
|
||||
value={draft.eans ?? []}
|
||||
onChange={(_event, eans) => onChange({ eans })}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
inputRef={inputRef}
|
||||
label="EANs (Enter zum Hinzufügen)"
|
||||
size="small"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EditablePanelTile
|
||||
wide={1}
|
||||
|
||||
Reference in New Issue
Block a user