From 5304d9aeb65812008609bc3d4cd9bac82d506a77 Mon Sep 17 00:00:00 2001 From: abhishek Date: Mon, 31 Aug 2026 16:26:58 +0530 Subject: [PATCH] updated on shelfon store catalogue --- src/features/store-admin/PublishDialog.tsx | 96 +++++++++++++++----- src/features/store-admin/publishPlan.test.ts | 62 +++++++++++++ src/features/store-admin/publishPlan.ts | 41 +++++++++ 3 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 src/features/store-admin/publishPlan.test.ts create mode 100644 src/features/store-admin/publishPlan.ts diff --git a/src/features/store-admin/PublishDialog.tsx b/src/features/store-admin/PublishDialog.tsx index bc3a891..35d8871 100644 --- a/src/features/store-admin/PublishDialog.tsx +++ b/src/features/store-admin/PublishDialog.tsx @@ -10,6 +10,7 @@ import { productsApi } from '@/api/products'; import type { Product } from '@/api/types'; import { queryKeys } from '@/queries/keys'; import { effectivePrice } from './productState'; +import { needingPrice, planPublish } from './publishPlan'; /** * Set a price and release to the shops. @@ -58,6 +59,17 @@ export function PublishDialog({ products, tenantid, branchCount, onClose }: Publ return () => window.removeEventListener('keydown', onKey); }, [onClose, running]); + /** + * The products that still need a price typed in. + * + * Usually none. A spreadsheet import carries a price on every row, so asking + * for one again is asking a question that has already been answered — and it + * was doing more than annoy: the typed figure was applied to every product in + * the selection. + */ + const needPrice = needingPrice(products); + const askForPrice = needPrice.length > 0; + const priceValue = Number(price); // The backend refuses `price <= 0` with "set a price before publishing — a // product with no price cannot be sold at a till". Refusing here too means @@ -72,7 +84,11 @@ export function PublishDialog({ products, tenantid, branchCount, onClose }: Publ const taxError = !Number.isFinite(taxValue) || taxValue < 0 ? 'Tax cannot be negative.' : null; const canSubmit = - !running && price.trim() !== '' && priceError === null && taxError === null; + !running && + // A price is required only when something in the selection lacks one. + (!askForPrice || price.trim() !== '') && + priceError === null && + taxError === null; async function run() { setRunning(true); @@ -83,13 +99,25 @@ export function PublishDialog({ products, tenantid, branchCount, onClose }: Publ // its own transaction with a per-outlet loop inside it; firing hundreds at // a single Go instance to save a few seconds risks a half-published // catalogue, which is worse than a slow one. - for (const product of products) { + const plan = planPublish(products, { price: priceValue, taxpercent: taxValue }); + + for (const [index, product] of products.entries()) { + // Each product is released AT ITS OWN PRICE. The field above only fills + // in for one that has none. + // + // Sending the typed figure for every row was a quiet way to destroy a + // catalogue: `publishproduct` writes `products.retailprice` as well as + // the per-outlet price, so releasing seventeen imported products together + // — ₹395 rice, ₹30 crisps, ₹105 cleaner — would have set all seventeen to + // whichever price happened to prefill from the first one, at every + // branch, with no way back to the originals. + const line = plan[index]!; try { await productsApi.publish({ tenantid, - productid: product.productid, - price: priceValue, - taxpercent: taxValue, + productid: line.productid, + price: line.price, + taxpercent: line.taxpercent, }); outcomes.push({ product, ok: true }); } catch (error) { @@ -140,30 +168,54 @@ export function PublishDialog({ products, tenantid, branchCount, onClose }: Publ {isBulk ? `Price ${products.length} products` : 'Price and release'} + {/* This line used to read "The same price is set on every product + selected" — which was true, and was the bug. It is now each + product's own, and saying so is what stops someone reaching + for the field to "correct" a price that is already right. */} {isBulk - ? 'The same price is set on every product selected.' + ? 'Each keeps its own price. Only products without one use the figure below.' : (first?.productname ?? '')} {results === null ? ( <> - - + {/* Only asked when something in the selection actually lacks a + price. A sheet import carries one on every row, and asking + again reads as though the price were missing when it is on + screen two lines above. */} + {askForPrice ? ( + <> + + + {isBulk ? ( + + {needPrice.length} of {products.length} have no price yet and will use this + one. The rest keep their own. + + ) : null} + + ) : ( + + {isBulk + ? `All ${products.length} already have a price — each is released at its own. Nothing is overwritten.` + : `Released at its own price of ₹${effectivePrice(first as Product)}.`} + + )} {/* The blast radius, stated before the button is pressed. */} diff --git a/src/features/store-admin/publishPlan.test.ts b/src/features/store-admin/publishPlan.test.ts new file mode 100644 index 0000000..b5d4285 --- /dev/null +++ b/src/features/store-admin/publishPlan.test.ts @@ -0,0 +1,62 @@ +/** + * Releasing several products at once must not give them all one price. + * + * `publishproduct` writes `products.retailprice` as well as the per-outlet + * price, at every branch. So publishing the seventeen products R mart imported + * on 2026-08-31 — ₹395 rice, ₹30 crisps, ₹105 cleaner — with the single figure + * the dialog prefilled from the first row would have set all seventeen to ₹395 + * and left nothing to restore the real prices from. + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { needingPrice, planPublish } from './publishPlan'; +import type { Product } from '@/api/types'; + +const product = (id: number, price: number, retail = 0, tax?: number) => + ({ productid: id, price, retailprice: retail, ...(tax === undefined ? {} : { taxpercent: tax }) }) as Product; + +test('each product is released at its own price', () => { + const plan = planPublish( + [product(7089, 395), product(7103, 30), product(7104, 105)], + { price: 395, taxpercent: 0 }, + ); + assert.deepEqual( + plan.map((line) => line.price), + [395, 30, 105], + ); +}); + +// The whole point of the fallback: a product with no price of its own is the +// only one the typed figure may touch. +test('only a product with no price uses the typed figure', () => { + const plan = planPublish([product(1, 0), product(2, 250)], { price: 99, taxpercent: 5 }); + assert.equal(plan[0]?.price, 99); + assert.equal(plan[1]?.price, 250); +}); + +/* +The per-outlet price and the tenant master are two columns and either can be the +one that holds the figure — an imported product carries both, a hand-priced one +may carry only the master. `effectivePrice` already resolves that, and publish +has to use the same resolution or it would "fill in" a price that exists. +*/ +test('the master price counts as having a price', () => { + const plan = planPublish([product(1, 0, 175)], { price: 99, taxpercent: 0 }); + assert.equal(plan[0]?.price, 175); +}); + +test('tax follows the same rule as price', () => { + const plan = planPublish([product(1, 100, 0, 12), product(2, 100)], { price: 0, taxpercent: 5 }); + assert.equal(plan[0]?.taxpercent, 12); + assert.equal(plan[1]?.taxpercent, 5); +}); + +// Zero is a real tax rate and must survive, not fall through to the fallback. +test('a tax rate of zero is kept, not treated as missing', () => { + const plan = planPublish([product(1, 100, 0, 0)], { price: 0, taxpercent: 18 }); + assert.equal(plan[0]?.taxpercent, 0); +}); + +test('only the unpriced products are counted as needing one', () => { + assert.equal(needingPrice([product(1, 395), product(2, 0), product(3, 0, 88)]).length, 1); +}); diff --git a/src/features/store-admin/publishPlan.ts b/src/features/store-admin/publishPlan.ts new file mode 100644 index 0000000..bdd0980 --- /dev/null +++ b/src/features/store-admin/publishPlan.ts @@ -0,0 +1,41 @@ +import type { Product } from '@/api/types'; +import { effectivePrice } from './productState'; + +/** + * What price and tax each product is released at. + * + * Its own function because getting it wrong destroys data rather than merely + * failing. `publishproduct` writes `products.retailprice` as well as the + * per-outlet price, at every branch — so releasing seventeen imported products + * with one typed figure sets all seventeen to it and loses the originals, with + * nothing to restore them from. + * + * The rule: a product that HAS a price is released at its own. The typed figure + * fills in only for one that has none, which after a spreadsheet import is + * usually nothing at all. + */ +export interface PublishLine { + productid: number; + price: number; + taxpercent: number; +} + +export function planPublish( + products: readonly Product[], + fallback: { price: number; taxpercent: number }, +): PublishLine[] { + return products.map((product) => { + const own = effectivePrice(product); + const ownTax = Number(product.taxpercent ?? NaN); + return { + productid: product.productid, + price: own > 0 ? own : fallback.price, + taxpercent: Number.isFinite(ownTax) ? ownTax : fallback.taxpercent, + }; + }); +} + +/** The products that still need a price typed in — usually none. */ +export function needingPrice(products: readonly Product[]): Product[] { + return products.filter((product) => effectivePrice(product) <= 0); +}