2026-09-20 21:49:58 +00:00
|
|
|
// .mts, not .ts: this script uses top-level await, and the package has no
|
|
|
|
|
// "type": "module", so tsx would otherwise transform it as CJS and fail.
|
2026-09-19 16:45:30 +00:00
|
|
|
import { pbAdmin } from "../src/lib/pb";
|
2026-09-19 16:58:07 +00:00
|
|
|
import { classifyUnit, poMatchCounts, verifyPurchaseOrderIds } from "../src/lib/dn-pipeline/compare";
|
2026-09-19 16:45:30 +00:00
|
|
|
|
|
|
|
|
type Row = Record<string, any>;
|
|
|
|
|
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
const batchId = args.find((arg) => !arg.startsWith("-"));
|
|
|
|
|
const all = args.includes("--all");
|
|
|
|
|
if (!batchId && !all) {
|
|
|
|
|
console.error("Usage: npx tsx scripts/compare-dn-pipelines.ts <batch-id> | --all");
|
|
|
|
|
process.exit(2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pb = await pbAdmin();
|
|
|
|
|
const batches = (all ? await pb.collection("dn_batches").getFullList({ sort: "created" }) : [await pb.collection("dn_batches").getOne(batchId!)]).map((row) => row as Row);
|
|
|
|
|
const reports: Row[] = [];
|
|
|
|
|
|
|
|
|
|
for (const batch of batches) {
|
|
|
|
|
const [pages, notes] = await Promise.all([
|
2026-09-19 16:53:25 +00:00
|
|
|
pb.collection("dn_batch_pages").getFullList({ filter: `batch = ${JSON.stringify(batch.id)}`, fields: "id,read_status" }),
|
|
|
|
|
pb.collection("dn_batch_notes").getFullList({ filter: `batch = ${JSON.stringify(batch.id)}`, sort: "note_index", fields: "id,stage,status,pages,purchase_order,items,checks,doc_number,delivery_note_number" }),
|
2026-09-19 16:45:30 +00:00
|
|
|
]);
|
|
|
|
|
const poIds = [...new Set((notes as Row[]).map((note) => typeof note.purchase_order === "string" ? note.purchase_order : "").filter(Boolean))];
|
2026-09-19 16:58:07 +00:00
|
|
|
const verifiedPoIds = await verifyPurchaseOrderIds(poIds, async (po) => {
|
|
|
|
|
try { const row = await pb.collection("purchase_orders").getOne(po, { fields: "id" }) as Row; return { id: String(row.id) }; }
|
|
|
|
|
catch (error) { throw new Error(`purchase order verification failed for ${po}: ${error instanceof Error ? error.message : String(error)}`, { cause: error }); }
|
|
|
|
|
});
|
|
|
|
|
const poItems = (await Promise.all([...verifiedPoIds].map((po) => pb.collection("purchase_order_items").getFullList({ filter: `purchase_order = ${JSON.stringify(po)}`, fields: "id,purchase_order,unit,qty,rate,description" })))).flat() as Row[];
|
2026-09-19 16:45:30 +00:00
|
|
|
const itemById = new Map(poItems.map((item) => [item.id, item]));
|
|
|
|
|
let matchedItems = 0;
|
|
|
|
|
let itemCount = 0;
|
2026-09-19 16:53:25 +00:00
|
|
|
let unitCompatible = 0;
|
|
|
|
|
let unitIncompatible = 0;
|
|
|
|
|
let unitUnknown = 0;
|
2026-09-19 16:45:30 +00:00
|
|
|
let quantity = 0;
|
|
|
|
|
let computedValue = 0;
|
|
|
|
|
for (const note of notes as Row[]) {
|
|
|
|
|
for (const line of Array.isArray(note.items) ? note.items : []) {
|
|
|
|
|
if (!line || typeof line !== "object") continue;
|
|
|
|
|
itemCount++;
|
|
|
|
|
const linked = typeof line.purchase_order_item === "string" ? line.purchase_order_item : "";
|
|
|
|
|
const poItem = itemById.get(linked);
|
|
|
|
|
const qty = typeof line.qty === "number" ? line.qty : Number(line.qty ?? 0);
|
|
|
|
|
quantity += Number.isFinite(qty) ? qty : 0;
|
|
|
|
|
if (poItem) {
|
|
|
|
|
matchedItems++;
|
2026-09-19 16:53:25 +00:00
|
|
|
const compatibility = classifyUnit(line.unit, poItem.unit);
|
|
|
|
|
if (compatibility === "compatible") unitCompatible++;
|
|
|
|
|
else if (compatibility === "incompatible") unitIncompatible++;
|
|
|
|
|
else unitUnknown++;
|
2026-09-19 16:45:30 +00:00
|
|
|
const rate = Number(poItem.rate);
|
|
|
|
|
if (Number.isFinite(rate) && Number.isFinite(qty)) computedValue += qty * rate;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const coveredPages = new Set((notes as Row[]).flatMap((note) => Array.isArray(note.pages) ? note.pages.filter((page: unknown) => Number.isInteger(page)) : []));
|
|
|
|
|
reports.push({
|
|
|
|
|
batchId: batch.id,
|
|
|
|
|
site: batch.site ?? null,
|
|
|
|
|
pageCoverage: { total: pages.length, assigned: coveredPages.size, open: Math.max(0, pages.length - coveredPages.size), read: (pages as Row[]).filter((page) => page.read_status === "read").length },
|
|
|
|
|
candidateCount: notes.length,
|
|
|
|
|
stages: Object.fromEntries([...new Set((notes as Row[]).map((note) => String(note.stage ?? note.status ?? "unknown")))].map((stage) => [stage, (notes as Row[]).filter((note) => String(note.stage ?? note.status ?? "unknown") === stage).length])),
|
2026-09-19 16:58:07 +00:00
|
|
|
poMatch: poMatchCounts(notes as Row[], verifiedPoIds),
|
2026-09-19 16:45:30 +00:00
|
|
|
itemMatch: { matched: matchedItems, total: itemCount },
|
2026-09-19 16:53:25 +00:00
|
|
|
units: { compatible: unitCompatible, incompatible: unitIncompatible, unknown: unitUnknown, matchedItems },
|
2026-09-19 16:45:30 +00:00
|
|
|
quantity,
|
|
|
|
|
computedValue: Math.round(computedValue * 100) / 100,
|
|
|
|
|
writes: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), reports }, null, 2));
|