From ac6dbd76710e12537023d0b3b8348233e545cea0 Mon Sep 17 00:00:00 2001 From: abhishek Date: Thu, 6 Aug 2026 10:58:34 +0530 Subject: [PATCH] pricing update --- .env | 1 + src/components/CatalogueBrowser.tsx | 37 ++++++- src/components/CustomerDetailPanel.tsx | 7 +- src/components/ImportProductModal.tsx | 136 +++++++++++++++++++------ src/components/InventoryView.tsx | 73 +++++++------ src/services/catalogueApi.ts | 61 ++++------- src/services/fiestaApi.ts | 30 ++++++ src/services/fiestaQueries.ts | 20 ++++ src/services/storeCatalogue.ts | 46 ++++++++- 9 files changed, 304 insertions(+), 107 deletions(-) diff --git a/.env b/.env index 94eebbd..29b3dc3 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ + # Local secrets — gitignored, never committed. # Used ONLY by the Vite dev-server proxy (vite.config.ts) to inject the # x-hasura-admin-secret header server-side. NOT prefixed with VITE_, so it diff --git a/src/components/CatalogueBrowser.tsx b/src/components/CatalogueBrowser.tsx index b3a3a8c..6916655 100644 --- a/src/components/CatalogueBrowser.tsx +++ b/src/components/CatalogueBrowser.tsx @@ -8,7 +8,9 @@ import { useTenantCategories } from '../hooks/useCatalogueImport'; import { CatalogueProduct, ImportCatalogueProductRequest } from '../services/catalogueApi'; -import { useStoreCatalogue } from '../services/storeCatalogue'; +import { useStoreCatalogue, usePriceEverywhere } from '../services/storeCatalogue'; +import { useFiestaTenantLocations, useFiestaProductLocations } from '../services/fiestaQueries'; +import { num as fnum } from '../services/fiestaApi'; import ImportProductModal from './ImportProductModal'; interface CatalogueBrowserProps { @@ -46,10 +48,41 @@ export default function CatalogueBrowser({ tenantid, locationid, onClose }: Cata const importProductMutation = useImportCatalogueProduct(tenantid, locationid); + // Every outlet the tenant runs — the price entered on import has to reach all + // of them, not just the one this browser happens to be scoped to. + const locationsQ = useFiestaTenantLocations(tenantid); + const allLocationIds = React.useMemo(() => { + const ids = (locationsQ.data ?? []).map((l) => fnum(l.locationid)).filter((id) => id > 0); + return ids.length ? ids : [locationid]; + }, [locationsQ.data, locationid]); + + const { priceEverywhere } = usePriceEverywhere(tenantid); + // Used only to look the freshly-created tenant productid back up: the import + // response returns just {status, message}, and the price has to be attached to + // the tenant's own productid, not the global catalogueid. + const importedRowsQ = useFiestaProductLocations({ tenantid, locationid, pagesize: 500 }); + const handleImportSubmit = (item: ImportCatalogueProductRequest) => { importProductMutation.mutate([item], { - onSuccess: () => { + onSuccess: async () => { setImportingProduct(null); + if (!(item.retailprice > 0)) return; + // The import writes the price to products.retailprice, but the per-store + // price on productlocations is what the staff catalogue, the customer + // app and the order all read — so publish it across the tenant here. + // Status stays 'Draft' to match what the import itself wrote; this step + // prices the product, it doesn't change its lifecycle. + try { + const { data } = await importedRowsQ.refetch(); + const row = (data ?? []).find((r) => fnum(r.catalogueid) === Number(item.catalogueid)); + const productid = fnum(row?.productid); + if (productid) { + priceEverywhere(productid, item.retailprice, allLocationIds, { status: 'Draft' }); + } + } catch { + // Import succeeded; only the price broadcast failed. The admin can + // still set it from the Admin Catalogue, so don't fail the import. + } }, onError: (err: any) => { alert(err.message || 'Failed to import product.'); diff --git a/src/components/CustomerDetailPanel.tsx b/src/components/CustomerDetailPanel.tsx index cb176a8..a51427e 100644 --- a/src/components/CustomerDetailPanel.tsx +++ b/src/components/CustomerDetailPanel.tsx @@ -49,7 +49,10 @@ export default function CustomerDetailPanel({ customer, onClose }: CustomerDetai const groups = new Map(); for (const order of orders) { - const total = fnum(order.totalamount) || fnum(order.payableamount) || 0; + // `getorders` returns `orderamount` — it has no `totalamount` or + // `payableamount` column, so reading only those left Lifetime Value, + // Highest Bill and Avg Value at ₹0 for every customer. + const total = fnum(order.orderamount) || fnum(order.totalamount) || fnum(order.payableamount) || 0; spend += total; if (total > highest) highest = total; @@ -194,7 +197,7 @@ export default function CustomerDetailPanel({ customer, onClose }: CustomerDetai {monthOrders.map((order, orderIdx) => { const orderDate = fstr(order.createddate) || fstr(order.orderdate) || ''; const orderId = fstr(order.orderid) || String(fnum(order.orderheaderid)); - const total = fnum(order.totalamount) || fnum(order.payableamount) || 0; + const total = fnum(order.orderamount) || fnum(order.totalamount) || fnum(order.payableamount) || 0; const statusStr = fstr(order.orderstatus) || 'CREATED'; const statusBadgeClass = getStatusClass(statusStr); return ( diff --git a/src/components/ImportProductModal.tsx b/src/components/ImportProductModal.tsx index 7a5b247..67d0bf1 100644 --- a/src/components/ImportProductModal.tsx +++ b/src/components/ImportProductModal.tsx @@ -27,7 +27,6 @@ export default function ImportProductModal({ const [retailPrice, setRetailPrice] = useState(''); const [productCost, setProductCost] = useState(''); const [taxPercent, setTaxPercent] = useState('0'); - const [quantity, setQuantity] = useState('1'); // Categories this tenant's own products actually use const { data: categories = [], isLoading: isLoadingCategories } = useTenantCategories(tenantid); @@ -38,12 +37,15 @@ export default function ImportProductModal({ categoryId ? Number(categoryId) : undefined, ); + // A price is mandatory. Importing at ₹0 is what left every catalogue product + // on the platform priced at nothing: the staff catalogue showed "—", the + // customer app charged nothing, and each order booked orderamount 0. + const priceValue = Number(retailPrice); + const canImport = Boolean(categoryId) && priceValue > 0; + const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (!categoryId || !retailPrice || !productCost) { - alert("Please fill in all required fields."); - return; - } + if (!canImport) return; onImport({ tenantid, @@ -52,14 +54,16 @@ export default function ImportProductModal({ catalogueid: product.id, categoryid: Number(categoryId), subcategoryid: subcategoryId ? Number(subcategoryId) : 0, - quantity: Number(quantity), + // Importing lists the product; it does not deliver stock. Quantity lands + // via the request → approve → Mark as Received flow. + quantity: 0, stocktype: "in", // Draft: lands in the Admin Catalogue only, published to the store // catalogue separately via the explicit "Add to Store Catalogue" step. status: "Draft", - retailprice: Number(retailPrice), - productcost: Number(productCost), - taxpercent: Number(taxPercent), + retailprice: priceValue, + productcost: Number(productCost) || 0, + taxpercent: Number(taxPercent) || 0, }); }; @@ -123,7 +127,7 @@ export default function ImportProductModal({ ) : ( -
+

Import Product @@ -131,32 +135,102 @@ export default function ImportProductModal({ Global FMCG

- Import this product to your Admin Catalogue. Selling price and outlet parameters will be managed directly in your Admin Catalogue. + The global catalogue only carries an indicative price range, so set your own + selling price here. It applies to every store under your tenant.

- -
+ {!canImport && ( +

+ + A selling price and category are required. +

+ )} + )} diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx index d2b2952..3583b9c 100644 --- a/src/components/InventoryView.tsx +++ b/src/components/InventoryView.tsx @@ -49,7 +49,7 @@ import { } from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, str as fstr } from '../services/fiestaApi'; import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers'; -import { useStoreCatalogue, isPublishedItem } from '../services/storeCatalogue'; +import { useStoreCatalogue, usePriceEverywhere, isPublishedItem } from '../services/storeCatalogue'; import BulkCartDrawer from './BulkCartDrawer'; import AwaitingApi from './AwaitingApi'; import { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi'; @@ -99,10 +99,19 @@ export default function InventoryView({ [locationsQ.data], ); - // The admin catalogue (imports, curation) is scoped to this tenant's own + // The admin catalogue (imports, curation) reads through this tenant's own // primary/hub outlet — not the hardcoded Fiesta defaults, so onboarded // tenants other than Fiesta see their own imports reflected here. + // + // READS are scoped here; PRICE WRITES are not. A price the admin sets is a + // business-level decision that has to reach every branch, so it goes out to + // `allLocationIds` via priceEverywhere() below. Writing only to this outlet + // left every other branch at ₹0. const primaryLocationId = locations[0]?.locationid ?? FIESTA_PRIMARY_LOCATION_ID; + const allLocationIds = useMemo( + () => (locations.length ? locations.map((l) => l.locationid) : [primaryLocationId]), + [locations, primaryLocationId], + ); const storesStock = useFiestaStoresStock( tenantId, @@ -125,6 +134,7 @@ export default function InventoryView({ const [hoveredAdminProduct, setHoveredAdminProduct] = useState(null); const [localSearch, setLocalSearch] = useState(''); const storeCat = useStoreCatalogue(tenantId, primaryLocationId); + const { priceEverywhere, isPending: isPricing } = usePriceEverywhere(tenantId); const [importPrice, setImportPrice] = useState(''); const [isSettingPrice, setIsSettingPrice] = useState(false); const [addingPriceProdId, setAddingPriceProdId] = useState(null); @@ -619,7 +629,12 @@ export default function InventoryView({ ) : addingPriceProdId === prod.id ? (
e.stopPropagation()}> - + + + {allLocationIds.length === 1 ? 'Your store' : `All ${allLocationIds.length} stores`} +
+

+ Applies to {allLocationIds.length === 1 ? 'your store' : `all ${allLocationIds.length} stores`} +

{storeCat.has(selectedAdminProduct.id) && (