This commit overhauls the order management system to use item-level status tracking instead of a single order-level status: - Remove deprecated `orders.status` field and add `invoiced` and quote match audit fields - Add `isOrderComplete` utility with tests to validate fully delivered orders - Add purchase order PDF rendering support and tooling - Add geo coordinate fields for companies/customers with map link UI - Add REST management APIs for customers, companies, and suppliers - Update customer/company services to use relation fields instead of free text - Add status pill and tab filter components for item status visualization - Add quote-to-order matching logic and unit tests - Update all pages, components, and tests to match the new schema - Include all required PocketBase migrations and schema updates
133 lines
4.7 KiB
TypeScript
133 lines
4.7 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* Migration: extend the order ↔ quote matching surface.
|
|
*
|
|
* Adds two fields to the `orders` collection:
|
|
*
|
|
* 1. `quote_reference_text` (text, optional)
|
|
* Free-text field where the operator pastes the reference they
|
|
* see on the customer's PO — typically the quote number we
|
|
* issued, e.g. "Q-2026-04-001", but it can also be a customer's
|
|
* own reference, an RFQ number, etc.
|
|
*
|
|
* 2. `quote_match_method` (text, optional)
|
|
* Records HOW the (potential_quote) match was made so the UI can
|
|
* show the operator why a suggestion exists. One of:
|
|
* - "exact_quote_number"
|
|
* - "exact_quote_reference"
|
|
* - "exact_rfq_number"
|
|
* - "exact_external_number"
|
|
* - "company_customer_desc_price" ← the heuristic fallback
|
|
* - "no_match"
|
|
*
|
|
* Why these exist:
|
|
* - Only ~55% of quotes have a real `quote_number` on them (per the
|
|
* operator). The remaining 45% land via email with only the
|
|
* customer's own reference / RFQ id / external id, OR with no
|
|
* reference at all and we have to fall back to fuzzy matching
|
|
* against company / customer / line item descriptions / pricing.
|
|
* - The schema already has `potential_quote` +
|
|
* `potential_quote_confidence` (someone started this work). This
|
|
* migration just adds the operator's input + the audit trail so
|
|
* the UI can explain itself.
|
|
*
|
|
* Idempotent — safe to re-run. Each step detects its own "already
|
|
* done" state.
|
|
*
|
|
* Run with:
|
|
* npx tsx scripts/migrations/2026-06-27-add-order-quote-match-fields.ts
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
function loadEnvFile(name: string) {
|
|
try {
|
|
const raw = readFileSync(resolve(process.cwd(), name), "utf8");
|
|
for (const line of raw.split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
const m = trimmed.match(/^([A-Z0-9_]+)\s*=\s*(.+)$/i);
|
|
if (m && !process.env[m[1]]) {
|
|
process.env[m[1]] = m[2].replace(/^["']|["']$/g, "").trim();
|
|
}
|
|
}
|
|
} catch {
|
|
// file not present — fine, rely on shell env
|
|
}
|
|
}
|
|
loadEnvFile(".env");
|
|
loadEnvFile(".env.local");
|
|
|
|
async function main() {
|
|
const url = process.env.NEXT_PUBLIC_POCKETBASE_URL;
|
|
const email = process.env.PB_ADMIN_EMAIL;
|
|
const password = process.env.PB_ADMIN_PASSWORD;
|
|
if (!url || !email || !password) {
|
|
throw new Error(
|
|
"Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD (e.g. sourced from .env.local)."
|
|
);
|
|
}
|
|
|
|
const pb = new PocketBase(url);
|
|
await pb.collection("_superusers").authWithPassword(email, password);
|
|
console.log(`[order-quote-match] Authenticated against ${url}`);
|
|
|
|
const orders = await pb.collections.getOne("orders");
|
|
|
|
// 1. quote_reference_text
|
|
if (!orders.fields.some((f) => f.name === "quote_reference_text")) {
|
|
await pb.collections.update(orders.id, {
|
|
fields: [
|
|
...orders.fields,
|
|
{
|
|
// Free-text reference the operator sees on the customer's
|
|
// PO. Searched at match time against quote_number /
|
|
// quote_reference / rfq_number / external_number; also
|
|
// drives the fuzzy fallback when no exact key matches.
|
|
name: "quote_reference_text",
|
|
type: "text",
|
|
required: false,
|
|
presentable: false,
|
|
options: { max: 200 },
|
|
},
|
|
],
|
|
});
|
|
console.log("[order-quote-match] Added orders.quote_reference_text (text, optional).");
|
|
} else {
|
|
console.log("[order-quote-match] orders.quote_reference_text already present — skipping.");
|
|
}
|
|
|
|
// Re-fetch so step 2 sees the post-add state.
|
|
const ordersFresh = await pb.collections.getOne("orders");
|
|
|
|
// 2. quote_match_method
|
|
if (!ordersFresh.fields.some((f) => f.name === "quote_match_method")) {
|
|
await pb.collections.update(ordersFresh.id, {
|
|
fields: [
|
|
...ordersFresh.fields,
|
|
{
|
|
// Audit trail for the potential_quote suggestion. Not a
|
|
// select enum on purpose — the set of methods may grow
|
|
// (LLM-based later) and a free text keeps the schema
|
|
// forward-compatible without a migration per addition.
|
|
name: "quote_match_method",
|
|
type: "text",
|
|
required: false,
|
|
presentable: false,
|
|
options: { max: 64 },
|
|
},
|
|
],
|
|
});
|
|
console.log("[order-quote-match] Added orders.quote_match_method (text, optional).");
|
|
} else {
|
|
console.log("[order-quote-match] orders.quote_match_method already present — skipping.");
|
|
}
|
|
|
|
console.log("[order-quote-match] Migration complete.");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("[order-quote-match] Migration failed:", err);
|
|
process.exit(1);
|
|
});
|