WFDASH-WEBAPP/scripts/migrations/2026-06-23-add-quote-email-composition-fields.ts
SJCoetz 588e9762c2 feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 22:57:01 +02:00

181 lines
5.9 KiB
TypeScript

/* 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<Record<string, unknown>> }).fields ?? []) as Array<
Record<string, unknown>
>;
const planned: string[] = [];
const newFields: Array<Record<string, unknown>> = [
{
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<Record<string, unknown>> }).fields ?? []) as Array<
Record<string, unknown>
>;
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);
});