main stocks update
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user