main stocks update

This commit is contained in:
2026-07-27 19:00:09 +05:30
parent 8885c55817
commit eb6e750b6a
6 changed files with 200 additions and 117 deletions

View File

@@ -800,7 +800,13 @@ export interface CreateProductLocationInput {
/** POST /products/createproductlocation — Add a product to a store catalogue / inventory. (Expects array payload) */
export async function createProductLocation(input: CreateProductLocationInput): Promise<Row> {
const payload = {
// The selling price field is `price` — it binds to models.Productlocations
// .Price, and CreateProductLocation's upsert lists "price" in its
// DoUpdates, so this both inserts and updates the per-store price.
// (`retailprice` is the MASTER price on the products table; that struct
// ignores it, so sending it here persisted nothing.) Only send when a price
// was supplied, so a quantity-only write can't blank an existing one.
const payload: Record<string, unknown> = {
tenantid: input.tenantid,
locationid: input.locationid,
productid: input.productid,
@@ -808,6 +814,7 @@ export async function createProductLocation(input: CreateProductLocationInput):
stocktype: input.stocktype || 'in',
status: input.status || 'Active',
};
if (num(input.price) > 0) payload.price = input.price;
return fiestaSend<Row>('products/createproductlocation', 'POST', [payload]);
}

View File

@@ -757,7 +757,12 @@ export function useFiestaGetStockRequests(opts: {
return useQuery({
queryKey: fiestaKeys.stockRequests(opts),
queryFn: () => getStockRequests(opts),
enabled: Boolean(opts.tenantid),
// Omitting locationid is a deliberate tenant-wide query (the admin console
// needs every outlet's requests). But passing an EMPTY one is not: the
// backend treats locationid=0 as "no filter" and returns every outlet, so a
// store user whose outlet hasn't resolved yet would see other stores'
// requests. Wait for a real id in that case.
enabled: Boolean(opts.tenantid) && (opts.locationid === undefined || Boolean(opts.locationid)),
});
}

View File

@@ -9,12 +9,16 @@
* users** see and pick from. It's the design-stage bridge between the admin
* catalogue page (InventoryView) and the user catalogue page (StoreCatalogView).
*
* Persisted in localStorage so the flow is fully demonstrable on one device; it
* syncs live across tabs/pages via a storage event. The backend equivalents
* (once built) are:
* • admin curates → POST /products/createproductlocation (productid, qty, …)
* • user reads → GET /products/getlocationproducts
* Swap `read`/`write` for those calls when the API is ready; the hook API stays.
* Backed entirely by the database — there is no localStorage here (an earlier
* revision of this comment described a localStorage bridge that no longer
* exists). Every read and write goes to Fiesta, scoped by tenant + outlet:
* • read → GET /products/getlocationproducts (useFiestaProductLocations)
* • add / update qty / update price
* → POST /products/createproductlocation
* • remove → DELETE /products/deleteproductlocation
*
* Mutations invalidate the React Query cache, so admin edits show up on the
* store user's catalogue on its next fetch.
*/
import { useFiestaProductLocations, useFiestaCreateProductLocation, useFiestaDeleteProductLocation } from './fiestaQueries';
@@ -30,9 +34,41 @@ export interface StoreCatalogueItem {
unit: string;
/** Quantity the admin intends to stock for this product. */
qty: number;
/**
* The row's status as the backend reports it — NOT a publish/unpublish flag.
*
* This field is overloaded. We write 'Active' when publishing, but the
* backend later overwrites it with a stock-availability value ('available',
* 'outofstock'), so a published product does not stay 'Active'. Membership of
* this list IS publication; treat status as display metadata only and never
* as a visibility gate. Use `isPublishedItem` / `isRemovedStatus` instead.
*/
status: string;
}
/**
* Statuses that mean the row is no longer part of the store catalogue.
*
* Deliberately a small deny-list rather than an 'Active' allow-list: the
* backend mixes lifecycle values ('Active') with availability values
* ('available', 'outofstock') in the same field, so anything not explicitly
* removed is still catalogued — an out-of-stock product is still on the menu.
*/
const REMOVED_STATUSES = new Set(['inactive', 'deleted', 'removed', 'archived']);
export function isRemovedStatus(status: unknown): boolean {
return REMOVED_STATUSES.has(String(status ?? '').trim().toLowerCase());
}
/**
* Is this product published to the store catalogue? A product-location row
* exists for it and has not been removed — that is the whole test. It stays
* published, whatever its stock level, until the admin deletes it.
*/
export function isPublishedItem(item: StoreCatalogueItem | undefined | null): boolean {
return Boolean(item) && !isRemovedStatus(item!.status);
}
/**
* Live view of the store catalogue + curation helpers. Re-renders whenever the
* catalogue changes via React Query invalidation.
@@ -44,14 +80,20 @@ export function useStoreCatalogue(tenantid: number = FIESTA_TENANT_ID, locationi
pagesize: 500,
});
const items: StoreCatalogueItem[] = (q.data || []).filter((r: any) => r.status !== 'Inactive').map((r: any) => ({
const items: StoreCatalogueItem[] = (q.data || []).filter((r: any) => !isRemovedStatus(r.status)).map((r: any) => ({
productid: String(r.productid),
name: String(r.name || r.productname || ''),
image: String(r.image || r.productimage || ''),
category: String(r.category || r.categoryname || 'General'),
sku: String(r.sku || ''),
price: Number(r.price || 0),
unit: String(r.unit || ''),
// The row's columns are `productsku` / `productunit` — the bare `sku` /
// `unit` names don't exist on it, so a real SKU "PEPSIC-CHE-100-002"
// rendered as "SKU-7075" and a 100g unit rendered as "Pc".
sku: String(r.productsku || r.sku || ''),
// Per-store price (productlocations.price) wins; `retailprice` is the
// master price on the products row and is the fallback for an outlet that
// hasn't set its own.
price: Number(r.price || r.retailprice || 0),
unit: String(r.productunit || r.unit || ''),
qty: Number(r.quantity ?? r.qty ?? 0),
status: String(r.status || 'Draft').charAt(0).toUpperCase() + String(r.status || 'Draft').slice(1).toLowerCase(),
}));