updated on shelf
This commit is contained in:
@@ -26,15 +26,14 @@ import { errorMessage } from '@/api/client';
|
||||
import { buildSender, uploadsApi } from '@/api/uploads';
|
||||
import { useAuth } from '@/auth/AuthContext';
|
||||
import { useTenantLocations, useTenants } from '@/queries/hooks';
|
||||
import { catalogueApi } from '@/api/catalogue';
|
||||
import { productsApi } from '@/api/products';
|
||||
import {
|
||||
ImportScope,
|
||||
describeMissingTarget,
|
||||
isTargetComplete,
|
||||
type ImportTarget,
|
||||
} from './ImportScope';
|
||||
import { buildImportRequests, planOpeningStock, type StockPlan } from './openingStock';
|
||||
import { type StockPlan } from './openingStock';
|
||||
import { shelveBatch } from './shelve';
|
||||
import { SectionHeader } from '@/components/SectionHeader';
|
||||
import { SheetDropzone } from '@/components/SheetDropzone';
|
||||
import { downloadTemplate, parseProductSheet, type ParsedSheet } from './parseProductSheet';
|
||||
@@ -217,6 +216,11 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
uploadedby: user?.userid ?? 0,
|
||||
uploadedname: user?.name ?? '',
|
||||
rowcount: parsed?.rows.length ?? 0,
|
||||
// The sheet itself, so the shelving step can be run later from the
|
||||
// Uploads page. The prices and opening stock exist nowhere else —
|
||||
// the ingest service holds a catalogue every merchant shares, which
|
||||
// carries neither — and this browser is otherwise their only copy.
|
||||
sheetrows: JSON.stringify(parsed?.rows ?? []),
|
||||
laststatus: submitted.status,
|
||||
});
|
||||
} catch (cause) {
|
||||
@@ -253,33 +257,13 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
setShelving(null);
|
||||
setIsWorking(true);
|
||||
try {
|
||||
// Narrowed to OUR file. A run can be assembled from several drops, so
|
||||
// its manifest may carry other senders' products — and matching those
|
||||
// against our sheet would price and stock someone else's goods into this
|
||||
// merchant's branch.
|
||||
const products = productsOf(batch, file ? [file.name] : undefined);
|
||||
const nextPlan = planOpeningStock(products, parsed.rows);
|
||||
setPlan(nextPlan);
|
||||
|
||||
// One catalogue read per BRAND rather than per product. A 500-row sheet
|
||||
// would otherwise open 500 requests from a shop's browser.
|
||||
const brands = [...new Set(nextPlan.matched.map((entry) => entry.product.brand))];
|
||||
const catalogueIds = new Map<string, number>();
|
||||
for (const brand of brands) {
|
||||
for (const [imageId, id] of await catalogueApi.idsByImageId(brand)) {
|
||||
catalogueIds.set(imageId, id);
|
||||
}
|
||||
}
|
||||
|
||||
const { requests, unresolved, unpriced } = buildImportRequests(nextPlan, {
|
||||
tenantid: target.tenantid,
|
||||
locationid: target.locationid,
|
||||
fallbackCategoryId: target.categoryid,
|
||||
catalogueIds,
|
||||
});
|
||||
|
||||
if (requests.length > 0) await productsApi.importFromCatalogue(requests);
|
||||
setShelved({ count: requests.length, skipped: unresolved.length + unpriced.length });
|
||||
// One implementation, shared with the Uploads page — see `shelve.ts`.
|
||||
// This step used to live only here, in a closure over this page's state,
|
||||
// which meant it could not be run once the tab was gone. It normally is
|
||||
// gone: the drop waits for their admin, and the run then takes minutes.
|
||||
const result = await shelveBatch(batch, parsed.rows, target, file?.name);
|
||||
setPlan(result.plan);
|
||||
setShelved({ count: result.shelved, skipped: result.skipped });
|
||||
|
||||
// The other half of the confirmation, onto the receipt.
|
||||
//
|
||||
@@ -301,8 +285,8 @@ export function SheetImportPanel({ tenantid, locationid }: SheetImportPanelProps
|
||||
.markShelved({
|
||||
// The DROP id, never `batch.batch_id` — see the note on `dropId`.
|
||||
batchid: dropId,
|
||||
shelved: requests.length,
|
||||
skipped: unresolved.length + unpriced.length,
|
||||
shelved: result.shelved,
|
||||
skipped: result.skipped,
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
81
src/features/nearle-admin/import/shelve.ts
Normal file
81
src/features/nearle-admin/import/shelve.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Putting an ingested batch on a branch's shelf.
|
||||
*
|
||||
* The half the catalogue ingest service cannot do, extracted so it has exactly
|
||||
* one implementation. Their pipeline writes the GLOBAL catalogue, which every
|
||||
* merchant shares and which therefore holds no price and no stock; the sheet
|
||||
* carries both. Joining them is what turns "the products exist" into "this shop
|
||||
* can sell them", and until it runs the products are invisible to shoppers.
|
||||
*
|
||||
* It lived inside the import panel, in a closure over that page's state. That
|
||||
* was wrong in the ordinary case rather than a rare one: a drop waits for the
|
||||
* ingest service's admin to release it and the run then takes minutes, so by the
|
||||
* time there is anything to shelve the tab that uploaded the file is usually
|
||||
* closed — and the step had nowhere else to run from. Now the Uploads page can
|
||||
* call it days later, from a receipt.
|
||||
*/
|
||||
|
||||
import { catalogueApi } from '@/api/catalogue';
|
||||
import { productsApi, type SheetProductRow } from '@/api/products';
|
||||
import { productsOf, type IngestBatch } from '@/api/ingest';
|
||||
import { buildImportRequests, planOpeningStock, type StockPlan } from './openingStock';
|
||||
|
||||
export interface ShelveTarget {
|
||||
tenantid: number;
|
||||
locationid: number;
|
||||
/** Applied to every row; no sheet column carries one any more. */
|
||||
categoryid: number;
|
||||
}
|
||||
|
||||
export interface ShelveResult {
|
||||
/** Products priced, shelved and given their opening stock. */
|
||||
shelved: number;
|
||||
/** Products the run produced that the sheet could not price. */
|
||||
skipped: number;
|
||||
/** Kept so the caller can say which ones, and why. */
|
||||
plan: StockPlan;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a finished run's manifest to the sheet, and writes the result.
|
||||
*
|
||||
* `filename` is not optional in spirit. A run an admin assembles from several
|
||||
* drops lists every file in it, so its manifest can carry other senders'
|
||||
* products — and the sheet's price and opening stock are applied to whatever the
|
||||
* manifest is matched against. Without narrowing, another merchant's product
|
||||
* sharing a name with one of our rows would be priced and stocked into THIS
|
||||
* merchant's branch.
|
||||
*/
|
||||
export async function shelveBatch(
|
||||
batch: IngestBatch,
|
||||
rows: readonly SheetProductRow[],
|
||||
target: ShelveTarget,
|
||||
filename?: string,
|
||||
): Promise<ShelveResult> {
|
||||
const products = productsOf(batch, filename ? [filename] : undefined);
|
||||
const plan = planOpeningStock(products, rows);
|
||||
|
||||
// One catalogue read per BRAND rather than per product. A 500-row sheet would
|
||||
// otherwise open 500 requests from a shop's connection.
|
||||
const brands = [...new Set(plan.matched.map((entry) => entry.product.brand))];
|
||||
const catalogueIds = new Map<string, number>();
|
||||
for (const brand of brands) {
|
||||
for (const [imageId, id] of await catalogueApi.idsByImageId(brand)) {
|
||||
catalogueIds.set(imageId, id);
|
||||
}
|
||||
}
|
||||
|
||||
const { requests, unresolved, unpriced } = buildImportRequests(plan, {
|
||||
tenantid: target.tenantid,
|
||||
locationid: target.locationid,
|
||||
fallbackCategoryId: target.categoryid,
|
||||
catalogueIds,
|
||||
});
|
||||
|
||||
// One `importcatalogueproduct` call for the batch, not one per product: it
|
||||
// takes an array, and each element writes the product row, the outlet row and
|
||||
// the stock ledger entry together.
|
||||
if (requests.length > 0) await productsApi.importFromCatalogue(requests);
|
||||
|
||||
return { shelved: requests.length, skipped: unresolved.length + unpriced.length, plan };
|
||||
}
|
||||
Reference in New Issue
Block a user