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
181 lines
5.9 KiB
TypeScript
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);
|
|
});
|