category changes

This commit is contained in:
2026-09-08 12:03:23 +05:30
parent 1a37949bbe
commit d54fadef20
22 changed files with 776 additions and 540 deletions

View File

@@ -29,6 +29,8 @@ import type {
ProductStockRequest,
ProductSubCategory,
} from './types';
import { APP_BROWSE_CATEGORY } from '@/features/catalogue/tenantCategories';
import { aisleIdForCategory, aisleIdsFrom } from '@/features/store-admin/appAisle';
export interface LocationProductQuery {
tenantid: number;
@@ -153,13 +155,20 @@ export const productsApi = {
api.post<Record<string, number>>(`${WEB}/products/resolvecategories`, { tenantid, names }),
/**
* Re-files products into different categories, in bulk.
* Re-files products into different aisles, in bulk.
*
* `categoryid` should be 2 on every row — the customer app FILTERS on it and
* anything else removes the product from its browse. `subcategoryid` is the
* one that decides the heading a shopper reads; 0 leaves whatever the product
* already has, so a caller that does not know the aisle cannot erase one.
*
* Scoped by tenant on the server as well as here — a productid is global, so
* a wrong id in the list would otherwise move another merchant's product.
*/
recategorise: (tenantid: number, updates: { productid: number; categoryid: number }[]) =>
api.put<{ moved: number }>(`${WEB}/products/recategorise`, { tenantid, updates }),
recategorise: (
tenantid: number,
updates: { productid: number; categoryid: number; subcategoryid?: number }[],
) => api.put<{ moved: number }>(`${WEB}/products/recategorise`, { tenantid, updates }),
/** Unlinks from the store. The product row and its order history survive. */
removeFromStore: (body: { tenantid: number; locationid: number; productid: number }) =>
@@ -237,30 +246,26 @@ export async function importSheetProducts(
const createdSkus: string[] = [];
/*
Categories first, in one call, before a single product is created.
The aisles first, in one call, before a single product is created.
Every row carries a category NAME worked out by the ladder in
`productCategory.ts`, and the customer app browses by categoryid and rejects
0 — so a product created before its aisle exists is a product no shopper can
find. Resolving up front also means one round trip for the whole sheet rather
than one per row.
`productCategory.ts`. What the customer app groups by is not that category
but `products.subcategoryid` — one of ten platform rows under category 2 —
so the name is folded into an aisle and the aisle looked up by name here. See
`appAisle.ts` for the endpoint that is measured against.
A failure here is not fatal. The rows still carry whatever categoryid the
operator picked for the upload, which is exactly the behaviour this had
before, so a category service that is down costs the new filing and not the
import.
A failure here is not fatal: the lookup falls back to the ids last read from
the platform, and a row that still cannot be placed is created with
subcategoryid 0, which the app lists under "Uncategorized".
*/
const wanted = [...new Set(rows.map((row) => row.category ?? '').filter(Boolean))];
let resolved: Record<string, number> = {};
if (wanted.length > 0) {
try {
resolved = (await productsApi.resolveCategories(tenantid, wanted)) ?? {};
} catch {
resolved = {};
}
let aisleIds: ReadonlyMap<string, number>;
try {
aisleIds = aisleIdsFrom(await productsApi.subCategories(tenantid, APP_BROWSE_CATEGORY));
} catch {
aisleIds = aisleIdsFrom(undefined);
}
const categoryIdFor = (row: SheetProductRow): number =>
resolved[(row.category ?? '').trim().toLowerCase()] ?? row.categoryid;
const subcategoryIdFor = (row: SheetProductRow): number =>
aisleIdForCategory(row.category, aisleIds) || Number(row.subcategoryid) || 0;
for (const [index, row] of rows.entries()) {
try {
@@ -268,8 +273,10 @@ export async function importSheetProducts(
tenantid,
productname: row.productname,
productsku: row.productsku,
categoryid: categoryIdFor(row),
subcategoryid: row.subcategoryid,
// ALWAYS 2 — the app filters on it and would drop anything else. The
// aisle a shopper reads is the subcategory.
categoryid: APP_BROWSE_CATEGORY,
subcategoryid: subcategoryIdFor(row),
retailprice: row.retailprice,
productcost: row.productcost,
taxpercent: row.taxpercent,