/* eslint-disable */ /** * Migration: add email-composition fields to the `quotes` collection. * * These power the new Approve & Send modal that replaces the previous * one-click "approve" button. The fields store the operator's * per-quote overrides for the outbound email; we save them when the * operator clicks Approve & Send so next time the modal opens they * see the values they last used (instead of starting from scratch). * * email_cc — comma-separated CC addresses * email_bcc — comma-separated BCC addresses * email_subject_outbound — operator-edited subject (overrides * the default `Re: {email_subject}`) * email_message — operator-edited body (overrides the * default "please find attached" template) * attach_auth_letters — bool toggle for "Attach supplier * authorization letters" in the modal; * defaults true so the operator doesn't * have to re-check it on every quote * * Idempotent — safe to re-run. * * Required env (in .env or shell): * NEXT_PUBLIC_POCKETBASE_URL * PB_ADMIN_EMAIL * PB_ADMIN_PASSWORD * * Usage: npx tsx scripts/migrations/2026-06-23-add-quote-email-composition-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 {} } 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) { console.error("Missing NEXT_PUBLIC_POCKETBASE_URL / PB_ADMIN_EMAIL / PB_ADMIN_PASSWORD"); process.exit(1); } const pb = new PocketBase(url); await pb.collection("_superusers").authWithPassword(email, password); const coll = await pb.collections.getOne("pbc_2527524235"); const fields = ((coll as { fields?: Array> }).fields ?? []) as Array< Record >; const planned: string[] = []; const newFields: Array> = [ { help: "Comma-separated CC addresses for the outbound quote email. Persisted so the operator's last choice becomes the default next time.", hidden: false, id: "text_email_cc", max: 2000, min: 0, name: "email_cc", pattern: "", presentable: false, required: false, system: false, type: "text", }, { help: "Comma-separated BCC addresses for the outbound quote email. Defaults to the customer's sales rep.", hidden: false, id: "text_email_bcc", max: 2000, min: 0, name: "email_bcc", pattern: "", presentable: false, required: false, system: false, type: "text", }, { help: "Operator-edited subject for the outbound quote email. Empty falls back to `Re: {email_subject}` at send time.", hidden: false, id: "text_email_subject_outbound", max: 500, min: 0, name: "email_subject_outbound", pattern: "", presentable: false, required: false, system: false, type: "text", }, { help: "Operator-edited body for the outbound quote email. Empty falls back to a default `please find attached quote {quote_number}` template at send time.", hidden: false, id: "text_email_message", max: 10000, min: 0, name: "email_message", pattern: "", presentable: false, required: false, system: false, type: "text", }, { help: "When true (default), the outbound email includes the per-letterhead supplier authorization letter (CNL or ISHERWOOD as applicable). Requires the letterhead_settings.auth_letter field to be uploaded.", hidden: false, id: "bool_attach_auth_letters", name: "attach_auth_letters", presentable: false, required: false, system: false, type: "bool", }, ]; for (const def of newFields) { const name = def.name as string; if (fields.some((f) => f.name === name)) { planned.push(`field "${name}" already present — skip`); } else { fields.push(def); planned.push(`add field "${name}"`); } } const willChange = planned.some((p) => !p.includes("skip")); if (!willChange) { console.log("Schema already up to date."); console.log("Verified fields:", fields.map((f) => f.name).join(", ")); return; } console.log("Planned changes:"); for (const p of planned) console.log(` • ${p}`); await pb.collections.update(coll.id, { fields }); const after = await pb.collections.getOne(coll.id); const afterFields = ((after as { fields?: Array> }).fields ?? []) as Array< Record >; const afterNames = afterFields.map((f) => f.name); const needed = ["email_cc", "email_bcc", "email_subject_outbound", "email_message", "attach_auth_letters"]; console.log("\nMigration complete."); for (const n of needed) { console.log(` ${n} present:`, afterNames.includes(n)); } if (needed.some((n) => !afterNames.includes(n))) { console.error("\n!!! Migration verification failed. Please inspect manually."); process.exit(1); } console.log("\nAll good. The Approve & Send modal will now persist its fields."); } main().catch((err) => { console.error("Migration failed:", err); process.exit(1); });