WFDASH-WEBAPP/scripts/migrations/2026-06-30-add-price-list-source-file.ts
SJCoetz b75873a51b feat(pricelist): Catalogue tab with search + category/brand filters
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.
2026-06-30 11:49:48 +02:00

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);
});