Adds a second tab on /pricelist that shows the structured `price_list` collection (AI-extracted line items) instead of the raw PDF uploads. Operators can now: - Search across SKU, description, and brand - Filter by category (twist_drill, hss_cutter, countersink, ...) - Filter by brand (JEI, TurboSteel, TurboTough, ...) - See cost vs selling price side-by-side in ZAR - Click a row's 'Source' link to jump back to the raw PDF The distinct filter values are pulled from a one-shot getList(1, 500) on mount, with hardcoded fallbacks (FALLBACK_CATEGORIES / FALLBACK_BRANDS) so the dropdowns are useful even before any extractions have run. Layout: Tabs (Raw Uploads | Catalogue) wrap both views; the header + page chrome stay shared. The DeleteDialog stays outside the Tabs since it's a modal. Also ships an idempotent migration to add `source_file` (text) to `price_list` — used by the n8n workflow to track which raw PDF each extracted row came from, and by the verify-after-POST step the workflow uses to decide whether to flip `raw_price_list.status` to Completed vs Failed. Price type is in PriceListRow (shared subset) and PriceList (full record). Supplier names are resolved client-side via getAllSuppliers() since the live schema has supplier as a text field rather than a relation. Tests: 229/230 (same pre-existing failure, unrelated). TypeScript clean.
63 lines
1.9 KiB
TypeScript
63 lines
1.9 KiB
TypeScript
/**
|
|
* Migration — add `source_file` (text) to `price_list`.
|
|
*
|
|
* Stores the `raw_price_list.id` that an extracted line item came
|
|
* from. Used by the n8n workflow's verification step:
|
|
*
|
|
* // After the POST loop
|
|
* const actual = await pb.collection('price_list').getList(1, expected, {
|
|
* filter: `source_file = "${rawId}"`,
|
|
* });
|
|
* if (actual.items.length === expected) {
|
|
* // safe to PATCH raw_price_list.status = "Completed"
|
|
* } else {
|
|
* // PATCH Failed with "X/Y items created"
|
|
* }
|
|
*
|
|
* Also useful for the agent at quote time: "give me the full price
|
|
* list for the file this row was sourced from."
|
|
*
|
|
* Idempotent: re-runs are safe.
|
|
*
|
|
* Run with:
|
|
* npm run pb:migrate:price-list-source-file
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
|
|
const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090";
|
|
const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL;
|
|
const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD;
|
|
|
|
if (!ADMIN_EMAIL || !ADMIN_PASSWORD) {
|
|
throw new Error("PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD are required.");
|
|
}
|
|
|
|
async function main() {
|
|
const pb = new PocketBase(PB_URL);
|
|
await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
|
|
|
|
const cols = await pb.collections.getFullList();
|
|
const priceList = cols.find((c) => c.name === "price_list");
|
|
if (!priceList) {
|
|
throw new Error("price_list collection not found. Run the setup script first.");
|
|
}
|
|
|
|
if (priceList.fields.some((f) => f.name === "source_file")) {
|
|
console.log("price_list.source_file already exists, skipping.");
|
|
return;
|
|
}
|
|
|
|
await pb.collections.update(priceList.id, {
|
|
fields: [
|
|
...priceList.fields,
|
|
{ name: "source_file", type: "text", required: false, options: { max: 0 } },
|
|
],
|
|
});
|
|
console.log("Added price_list.source_file");
|
|
console.log("source_file migration complete.");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|