Add full PIN-protected TV dashboard stack including auth, redirects, map view, APIs, and server services. Also include these critical refinements: - Overhaul PO number generation to use per-company monthly sequences - Rework quote rejection to bounce quotes back to operator queues instead of marking them terminal - Add post_processing order tab group for clearer workflow visibility - Fix collection pool tooltip positioning, overflow clipping, and default collapsed state - Refactor customer list API, clean up unused form props, and fix item intel popover target ID handling - Add comprehensive tests for new components and update existing test expectations
149 lines
No EOL
5.2 KiB
TypeScript
149 lines
No EOL
5.2 KiB
TypeScript
/**
|
|
* Backfill: heal broken `ai_item_insights.item.id` fields.
|
|
*
|
|
* Context
|
|
* ────────
|
|
* The `ItemIntelPopover` apply button uses
|
|
* `intel.item.id || itemId`
|
|
* to decide which PB record to PATCH. The user-side `itemId` prop is
|
|
* the authoritative target, but the stored blobl was historically
|
|
* written without that field (or written for the wrong collection).
|
|
*
|
|
* Live data:
|
|
* - 246 items have `intel.item.id = ""` (empty string)
|
|
* - 32 items have `intel.item` missing entirely
|
|
* - Of those, many also have `intel.item.collection = "quote_items"`
|
|
* on an order_item record (the blob was carried over from a quote
|
|
* without the field being re-stamped)
|
|
*
|
|
* The `??` operator in the popover only catches null/undefined, so an
|
|
* empty string ids bypasses the fallback and the apply fails. The
|
|
* popover now uses `||` which catches empty strings too — this script
|
|
* heals the existing data so the blob is correct end-to-end.
|
|
*
|
|
* What it does
|
|
* ────────────
|
|
* For every `order_items` and `quote_items` row that has a non-null
|
|
* `ai_item_insights` blob:
|
|
* - If `intel.item` is missing OR `intel.item.id` is empty OR
|
|
* `intel.item.collection` doesn't match the source collection:
|
|
* rebuild `intel.item` from the item's own id/description/sku/etc.
|
|
* - Writes the corrected blob back to the row.
|
|
* - Leaves the rest of the blob (stock_list, supplier_history,
|
|
* price_list, recent_quote, recent_order, summary, generated_at,
|
|
* verified) untouched.
|
|
*
|
|
* Idempotent: re-running produces no further changes once everything
|
|
* is healed. Add `--dry-run` to preview without writing.
|
|
*
|
|
* Run with:
|
|
* npx tsx --env-file=.env.local scripts/2026-08-13-heal-item-intel.ts
|
|
*
|
|
* Requires .env.local with NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL,
|
|
* PB_ADMIN_PASSWORD.
|
|
*/
|
|
|
|
import "./_env"; // side-effect import: loads .env.local before anything else
|
|
import { getAdminPocketBase } from "../src/lib/server/pocketbase-admin";
|
|
|
|
const DRY_RUN = process.argv.includes("--dry-run");
|
|
|
|
async function healCollection(
|
|
collection: "order_items" | "quote_items",
|
|
pb: Awaited<ReturnType<typeof getAdminPocketBase>>,
|
|
): Promise<{ scanned: number; repaired: number; skipped: number }> {
|
|
let scanned = 0;
|
|
let repaired = 0;
|
|
let skipped = 0;
|
|
|
|
// Pull every row with a non-null blob. We deliberately keep all
|
|
// fields on the blobs we read so we can write them back unchanged.
|
|
const rows = await pb.collection(collection).getFullList<{
|
|
id: string;
|
|
description: string;
|
|
sku: string;
|
|
quantity: number;
|
|
ai_item_insights: Record<string, unknown> | null;
|
|
}>({
|
|
filter: `ai_item_insights != "" && ai_item_insights != null`,
|
|
fields: "id,description,sku,quantity,ai_item_insights",
|
|
batch: 500,
|
|
});
|
|
|
|
for (const row of rows) {
|
|
scanned++;
|
|
const blob = row.ai_item_insights;
|
|
if (!blob || typeof blob !== "object") {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
const item = (blob as { item?: unknown }).item;
|
|
const itemObj = item && typeof item === "object"
|
|
? (item as Record<string, unknown>)
|
|
: null;
|
|
const itemId = itemObj?.id;
|
|
const itemCollection = itemObj?.collection;
|
|
|
|
const needsFix =
|
|
!itemObj ||
|
|
typeof itemId !== "string" ||
|
|
itemId === "" ||
|
|
itemCollection !== collection;
|
|
|
|
if (!needsFix) {
|
|
continue;
|
|
}
|
|
|
|
const fixedItem = {
|
|
id: row.id,
|
|
collection,
|
|
description: (itemObj?.description as string) || row.description || "",
|
|
sku: (itemObj?.sku as string) || row.sku || "",
|
|
quantity: (itemObj?.quantity as number) || row.quantity || 0,
|
|
};
|
|
|
|
if (DRY_RUN) {
|
|
console.log(` [DRY] ${collection} ${row.id}: would fix item.id "" → "${row.id}"`);
|
|
repaired++;
|
|
continue;
|
|
}
|
|
|
|
await pb.collection(collection).update(row.id, {
|
|
ai_item_insights: { ...blob, item: fixedItem },
|
|
});
|
|
repaired++;
|
|
if (repaired % 25 === 0) {
|
|
console.log(` ... ${repaired} repaired so far`);
|
|
}
|
|
}
|
|
|
|
return { scanned, repaired, skipped };
|
|
}
|
|
|
|
async function main() {
|
|
const startedAt = Date.now();
|
|
console.log(`=== Heal broken item-intel.item.id fields ===\n`);
|
|
if (DRY_RUN) console.log(" (DRY RUN — no writes)\n");
|
|
|
|
const pb = await getAdminPocketBase();
|
|
|
|
const orderResult = await healCollection("order_items", pb);
|
|
console.log(`order_items: scanned=${orderResult.scanned} repaired=${orderResult.repaired} skipped=${orderResult.skipped}`);
|
|
|
|
const quoteResult = await healCollection("quote_items", pb);
|
|
console.log(`quote_items: scanned=${quoteResult.scanned} repaired=${quoteResult.repaired} skipped=${quoteResult.skipped}`);
|
|
|
|
const totalScanned = orderResult.scanned + quoteResult.scanned;
|
|
const totalRepaired = orderResult.repaired + quoteResult.repaired;
|
|
console.log(`\n=== Done ===`);
|
|
console.log(` Total scanned: ${totalScanned}`);
|
|
console.log(` Total repaired: ${totalRepaired}`);
|
|
console.log(` Total skipped: ${totalScanned - totalRepaired}`);
|
|
console.log(` Duration: ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);
|
|
if (DRY_RUN) console.log(` (Run without --dry-run to write)`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("FATAL:", err);
|
|
process.exit(1);
|
|
}); |