MEGAINFRA-WEBAPP/scripts/compare-dn-pipelines.mts

75 lines
4.2 KiB
TypeScript
Raw Normal View History

// .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.
import { pbAdmin } from "../src/lib/pb";
import { classifyUnit, poMatchCounts, verifyPurchaseOrderIds } from "../src/lib/dn-pipeline/compare";
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([
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" }),
]);
const poIds = [...new Set((notes as Row[]).map((note) => typeof note.purchase_order === "string" ? note.purchase_order : "").filter(Boolean))];
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[];
const itemById = new Map(poItems.map((item) => [item.id, item]));
let matchedItems = 0;
let itemCount = 0;
let unitCompatible = 0;
let unitIncompatible = 0;
let unitUnknown = 0;
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++;
const compatibility = classifyUnit(line.unit, poItem.unit);
if (compatibility === "compatible") unitCompatible++;
else if (compatibility === "incompatible") unitIncompatible++;
else unitUnknown++;
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])),
poMatch: poMatchCounts(notes as Row[], verifiedPoIds),
itemMatch: { matched: matchedItems, total: itemCount },
units: { compatible: unitCompatible, incompatible: unitIncompatible, unknown: unitUnknown, matchedItems },
quantity,
computedValue: Math.round(computedValue * 100) / 100,
writes: false,
});
}
console.log(JSON.stringify({ generatedAt: new Date().toISOString(), reports }, null, 2));