This commit is contained in:
seb
2026-08-05 08:40:12 +02:00
parent a9e942df7a
commit 66f88e395b
12 changed files with 176 additions and 140 deletions

View File

@@ -1,13 +1,11 @@
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import { loadBrands, selectBrands, selectBrandsStatus, upsertBrand } from '../../../store/brandSlice';
import { notifyError } from '../../../utils/notify';
import { PANEL_TILE_KEYS } from '../../../theme';
@@ -42,14 +40,22 @@ function brandDraftValid(draft) {
const SECOND_ROW_MIN_HEIGHT = 220;
export default function ArticleViewBrand({ article: articleData, onArticleUpdated }) {
const dispatch = useDispatch();
const brands = useSelector(selectBrands);
const brandsStatus = useSelector(selectBrandsStatus);
const store = useStorageManager();
const [brands, setBrands] = useState([]);
useEffect(() => {
if (brandsStatus !== 'idle') return;
void dispatch(loadBrands()).unwrap().catch((err) => notifyError(err));
}, [dispatch, brandsStatus]);
let cancelled = false;
store.brandPromise
.then((list) => {
if (!cancelled) setBrands(list);
})
.catch((err) => {
if (!cancelled) notifyError(err);
});
return () => {
cancelled = true;
};
}, [store]);
const handleSave = async (draft) => {
const name = String(draft.name ?? '').trim();
@@ -65,12 +71,16 @@ export default function ArticleViewBrand({ article: articleData, onArticleUpdate
if (existing) {
brandId = existing.id;
} else {
const created = await dispatch(upsertBrand({ name })).unwrap();
requireDefined(created.id, 'brand.id');
brandId = created.id;
const createdRes = await store.brand.upsert({ name });
requireObject(createdRes, 'PUT /api/brand');
requireObject(createdRes.brand, 'brand');
requireDefined(createdRes.brand.id, 'brand.id');
brandId = createdRes.brand.id;
const nextBrands = await store.updateBrand();
setBrands(nextBrands);
}
const res = await article.setBrandIdBySku(articleData.sku, brandId);
const res = await store.article.setBrand(articleData.sku, { brandId });
requireObject(res, 'PUT /api/article/:sku/brand');
requireObject(res.article, 'article');
onArticleUpdated?.(res.article);

View File

@@ -3,7 +3,7 @@ import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import { EditablePanelTile } from '../../panels';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireObject } from '../../../utils/apiShape';
import { articleEans } from '../../../utils/articleDetail';
import { PANEL_TILE_KEYS } from '../../../theme';
@@ -41,11 +41,13 @@ function eansDraftValid(draft) {
const SECOND_ROW_MIN_HEIGHT = 220;
export default function ArticleViewEANs({ article: articleData, onArticleUpdated }) {
const store = useStorageManager();
const handleSave = async (draft) => {
const eans = (draft.eans ?? [])
.map((ean) => String(ean).trim())
.filter((ean) => ean.length > 0);
const res = await article.setEansBySku(articleData.sku, eans);
const res = await store.article.setEans(articleData.sku, { eans });
requireObject(res, 'PUT /api/article/:sku/eans');
requireObject(res.article, 'article');
onArticleUpdated?.(res.article);

View File

@@ -1,7 +1,7 @@
import { useState } from 'react';
import { EditablePanelTile } from '../../panels';
import SingleLineTextEdit from '../../SingleLineTextEdit';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireObject } from '../../../utils/apiShape';
import { PANEL_TILE_KEYS } from '../../../theme';
import { getCategoryBreadcrumbs } from '../../../store/categorySlice';
@@ -122,13 +122,15 @@ function CategoryPills({
}
export default function ArticleViewName({ article: articleData, categories, rootCategoryId, onArticleUpdated }) {
const store = useStorageManager();
const handleSave = async (draft) => {
const name = draft.name.trim();
const categoryIds = draft.categoryIds ?? [];
const [nameRes] = await Promise.all([
article.update(articleData.sku, { name }),
article.setCategories(articleData.sku, categoryIds),
store.article.update(articleData.sku, { name }),
store.article.setCategories(articleData.sku, { categoryIds }),
]);
requireObject(nameRes, 'PATCH /api/article/:sku');

View File

@@ -1,4 +1,3 @@
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import InputAdornment from '@mui/material/InputAdornment';
@@ -7,11 +6,8 @@ import Typography from '@mui/material/Typography';
import { EditablePanelTile } from '../../panels';
import ArticlePriceTiersDialog from '../ArticlePriceTiersDialog';
import ArticleUnitEditorDialog from '../ArticleUnitEditorDialog';
import { article } from '../../../api';
import { useStorageManager } from '../../../api/storageManager';
import { requireDefined, requireObject } from '../../../utils/apiShape';
import { loadUnits, selectUnits, selectUnitsStatus } from '../../../store/unitSlice';
import { selectCurrentCompany } from '../../../store/companySlice';
import { patchArticleInLists } from '../../../store/categorySlice';
import {
buildPosPricesPutBody,
companyTaxRateOptions,
@@ -219,12 +215,11 @@ function PriceAmountField({
/* ── main component ────────────────────────────────────────────────── */
export default function ArticleViewPrice({ article: articleData, onArticleUpdated }) {
const dispatch = useDispatch();
const store = useStorageManager();
const [tiersDialogOpen, setTiersDialogOpen] = useState(false);
const [unitEditorOpen, setUnitEditorOpen] = useState(false);
const units = useSelector(selectUnits);
const unitsStatus = useSelector(selectUnitsStatus);
const company = useSelector(selectCurrentCompany);
const [units, setUnits] = useState([]);
const [company, setCompany] = useState(null);
const basePrice = primaryPrice(articleData);
const taxRates = companyTaxRateOptions(company, basePrice?.taxRate);
const priceValue = priceDraftValue(articleData, company);
@@ -234,20 +229,32 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
: units;
useEffect(() => {
if (unitsStatus !== 'idle') return;
void dispatch(loadUnits()).unwrap().catch((err) => notifyError(err));
}, [dispatch, unitsStatus]);
const syncCategoryListPrice = (next) => {
const price = primaryPrice(next);
dispatch(
patchArticleInLists({
sku: next.sku,
net: price?.net ?? null,
taxRate: price?.taxRate ?? null,
}),
);
};
let cancelled = false;
async function load() {
try {
const [unitList, companies, session] = await Promise.all([
store.unitPromise,
store.companyPromise,
store.authMePromise,
]);
const companyId = requireDefined(session.user?.selectedCompanyId, 'selectedCompanyId');
const current = requireDefined(
companies.find((c) => c.id === companyId),
'company',
);
if (!cancelled) {
setUnits(unitList);
setCompany(current);
}
} catch (err) {
if (!cancelled) notifyError(err);
}
}
void load();
return () => {
cancelled = true;
};
}, [store]);
const handleSave = async (draft) => {
const saved = priceDraftValue(articleData, company);
@@ -269,12 +276,12 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
if (isNoPackUnit(unitSi, unitamount)) {
throw new Error('Einheit pc × 1 ist keine Packungseinheit');
}
const res = await article.update(articleData.sku, { unitamount, unitSi });
const res = await store.article.update(articleData.sku, { unitamount, unitSi });
requireObject(res, 'PATCH /api/article/:sku');
requireObject(res.article, 'article');
nextArticle = res.article;
} else {
const res = await article.update(articleData.sku, {
const res = await store.article.update(articleData.sku, {
unitamount: 1,
unitSi: 'pc',
});
@@ -299,11 +306,10 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
taxRate,
tiers: draft.tiers ?? [],
});
const res = await article.setPrices(articleData.sku, prices);
const res = await store.article.setPrices(articleData.sku, { prices });
requireObject(res, 'PUT /api/article/:sku/prices');
requireObject(res.article, 'article');
nextArticle = res.article;
syncCategoryListPrice(res.article);
}
onArticleUpdated?.(nextArticle);
@@ -421,12 +427,14 @@ export default function ArticleViewPrice({ article: articleData, onArticleUpdate
<ArticleUnitEditorDialog
open={unitEditorOpen}
onClose={() => setUnitEditorOpen(false)}
onUnitUpserted={(row) => {
onUnitUpserted={async (row) => {
setUnits(await store.unitPromise);
if (!draft.unitSi) {
onChange({ ...draft, unitSi: row.si });
}
}}
onUnitDeleted={(si) => {
onUnitDeleted={async (si) => {
setUnits(await store.unitPromise);
if (draft.unitSi === si) {
onChange({ ...draft, unitSi: '' });
}