pricing update

This commit is contained in:
2026-08-06 10:58:34 +05:30
parent 13db63e219
commit ac6dbd7671
9 changed files with 304 additions and 107 deletions

View File

@@ -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<ProductMatrixItem | null>(null);
const [localSearch, setLocalSearch] = useState('');
const storeCat = useStoreCatalogue(tenantId, primaryLocationId);
const { priceEverywhere, isPending: isPricing } = usePriceEverywhere(tenantId);
const [importPrice, setImportPrice] = useState<string>('');
const [isSettingPrice, setIsSettingPrice] = useState(false);
const [addingPriceProdId, setAddingPriceProdId] = useState<string | null>(null);
@@ -619,7 +629,12 @@ export default function InventoryView({
</button>
) : addingPriceProdId === prod.id ? (
<div className="flex flex-col gap-1.5 p-2 bg-slate-50 border border-slate-200 rounded-none" onClick={(e) => e.stopPropagation()}>
<label className="text-[9px] font-extrabold text-slate-600 uppercase tracking-widest text-center">Set Selling Price (₹)</label>
<label className="text-[9px] font-extrabold text-slate-600 uppercase tracking-widest text-center">
Set Selling Price (₹)
</label>
<span className="text-[8px] font-bold text-slate-400 uppercase tracking-wider text-center -mt-1">
{allLocationIds.length === 1 ? 'Your store' : `All ${allLocationIds.length} stores`}
</span>
<input
type="number"
value={cardImportPrice}
@@ -631,15 +646,17 @@ export default function InventoryView({
<div className="flex gap-1">
<button onClick={(e) => { e.stopPropagation(); setAddingPriceProdId(null); }} className="flex-1 py-1 bg-white text-slate-500 border border-slate-200 text-[9px] font-bold">Cancel</button>
<button
disabled={!cardImportPrice || Number(cardImportPrice) <= 0}
onClick={(e) => {
e.stopPropagation();
// qty: 0 — publishing to the catalogue is a listing/pricing action,
// not a stock delivery. Real quantity only lands via the
// request → approve → Mark as Received flow.
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: Number(cardImportPrice), unit: prod.exposure, qty: 0, status: 'Active' });
setAddingPriceProdId(null);
}}
disabled={!cardImportPrice || Number(cardImportPrice) <= 0 || isPricing}
onClick={(e) => {
e.stopPropagation();
// Publishing to the catalogue is a listing/pricing action, not a
// stock delivery — priceEverywhere() writes quantity 0 at every
// outlet. Real quantity only lands via the request → approve →
// Mark as Received flow.
priceEverywhere(prod.id, Number(cardImportPrice), allLocationIds, {
onSuccess: () => setAddingPriceProdId(null),
});
}}
className="flex-1 py-1 bg-[#662582] text-white text-[9px] font-bold disabled:opacity-50"
>
Confirm
@@ -968,30 +985,26 @@ export default function InventoryView({
</div>
<button
disabled={!importPrice || Number(importPrice) <= 0}
disabled={!importPrice || Number(importPrice) <= 0 || isPricing}
onClick={() => {
// First-time publish gets qty 0 — it's a listing/pricing action, not a
// stock delivery; real quantity only lands via Mark as Received. If this
// product is already published, preserve its existing qty so a mere price
// edit here doesn't zero out stock that's since been received.
const existingQty = storeCat.items.find(i => i.productid === selectedAdminProduct.id)?.qty ?? 0;
storeCat.add({
productid: selectedAdminProduct.id,
name: selectedAdminProduct.name,
image: selectedAdminProduct.image,
category: selectedAdminProduct.category,
sku: selectedAdminProduct.sku,
price: Number(importPrice),
unit: selectedAdminProduct.exposure || 'Pc',
qty: existingQty,
status: 'Active'
});
// Publishing is a listing/pricing action, never a stock delivery — real
// quantity only lands via Mark as Received. priceEverywhere() sends
// quantity 0, and because the upsert doesn't update quantity, an outlet
// that already holds stock keeps it while a new outlet starts at zero.
priceEverywhere(selectedAdminProduct.id, Number(importPrice), allLocationIds);
}}
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl text-xs font-extrabold transition-all bg-[#662582] hover:bg-[#531e6a] text-white shadow-md disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
>
<CheckCircle size={16} strokeWidth={2.5} />
{storeCat.has(selectedAdminProduct.id) ? 'Save Price to Database' : 'Save Price & Publish to Store'}
{isPricing
? 'Saving…'
: storeCat.has(selectedAdminProduct.id)
? 'Save Price to All Stores'
: 'Save Price & Publish to All Stores'}
</button>
<p className="text-[10px] font-semibold text-slate-500 text-center -mt-1">
Applies to {allLocationIds.length === 1 ? 'your store' : `all ${allLocationIds.length} stores`}
</p>
{storeCat.has(selectedAdminProduct.id) && (
<button