diff --git a/src/features/store-admin/ProductsPanel.tsx b/src/features/store-admin/ProductsPanel.tsx index 56273e2..5b0b9fa 100644 --- a/src/features/store-admin/ProductsPanel.tsx +++ b/src/features/store-admin/ProductsPanel.tsx @@ -52,15 +52,23 @@ export function ProductsPanel({ const [publishing, setPublishing] = useState(null); /** - * One branch, always. + * The branch in scope, or all of them. * - * `getlocationproducts` takes a single `locationid` and there is no - * tenant-wide product read. Under "All branches" this reads the first. - * Publishing is tenant-wide regardless, so the choice affects what is - * LISTED, not what is changed. + * `current` is undefined under "All branches", and that now means what it + * says: `getlocationproducts` answers a tenant-wide read with one row per + * product and stock summed across outlets. + * + * It used to fall back to `scoped[0]` — the FIRST branch — so "All branches" + * listed one outlet's products and called them the catalogue. A merchant with + * five outlets saw four products instead of six, and picking a single branch + * showed MORE than picking all of them. + * + * Publishing is tenant-wide regardless, so this affects what is LISTED, not + * what is changed. */ - const branch = current ?? scoped[0]; - const products = useLocationProducts(tenantid || undefined, branch?.locationid, 0); + const products = useLocationProducts(tenantid || undefined, current?.locationid, 0, { + allBranches: true, + }); const categories = useTenantCategories(tenantid || undefined); /** @@ -123,7 +131,12 @@ export function ProductsPanel({ {isUploading ? ( ) : null} diff --git a/src/queries/hooks.ts b/src/queries/hooks.ts index 5fcae80..eb24e3c 100644 --- a/src/queries/hooks.ts +++ b/src/queries/hooks.ts @@ -166,20 +166,48 @@ export function useCatalogueProduct(brand: string | undefined, sku: string | und }); } +/** + * A tenant's products, at one branch or across all of them. + * + * `locationid` of 0 or undefined means EVERY branch, and the backend answers it + * — one row per product, stock summed across outlets. It did not always: the + * read required a single branch, so "All branches" listed the FIRST one and a + * merchant with five outlets saw four products at RS Puram instead of the six + * they stock. Picking one branch showed more than picking all of them. + * + * So the tenant alone is enough to run this query now. Gating on `locationid` + * is what turned a missing backend feature into a silently wrong list. + */ export function useLocationProducts( tenantid: number | undefined, locationid: number | undefined, page = 0, + options: { + /** + * Treat a missing branch as "every branch" rather than as "not ready yet". + * + * Off by default, and that default is the safe one. A Store user is pinned + * to a single outlet and their branch resolves asynchronously — so during + * that window an opt-out default would answer tenant-wide and show them + * every other branch's stock as if it were their own. + * + * The admin catalogue opts in, because there "All branches" is a real + * selection a merchant made, not a value that has not arrived. + */ + allBranches?: boolean; + } = {}, ) { + const wantsAll = options.allBranches === true; return useQuery({ queryKey: queryKeys.products.byLocation(tenantid ?? 0, locationid ?? 0, page), queryFn: () => productsApi.locationProducts({ tenantid: tenantid as number, - locationid: locationid as number, + // 0 is meaningful to the backend here, not a missing value. + locationid: locationid ?? 0, pageno: page, }), - enabled: Boolean(tenantid) && Boolean(locationid), + enabled: Boolean(tenantid) && (wantsAll || Boolean(locationid)), ...live, }); }