WFDASH-WEBAPP/scripts/migrations/2026-07-06-quote-attachments-quote-item-and-kind.ts
SJCoetz 7f924ccc45 refactor: switch hardcoded UI colors to semantic theme tokens
Overhaul the app's color system by replacing all hardcoded Tailwind color classes with semantic theme variables to ensure consistent theming across all components, pages, and tests.

Additional changes:
- Add assigned date support for order item status updates: add `assignedDate` field to the order item status PATCH API, implement server-side normalization/validation, and add corresponding tests
- Refactor purchase order PDF workflows to use the consolidated `purchase_orders.documents` field instead of the legacy separate `purchase_order_pdfs` collection
- Fix quote number parsing and formatting to support both dashed (Q-YYYY-MM-NNN) and dashless (QYYYYMMNNN) formats
- Rename `driver_pool` item status to `collection_pool` across the codebase
- Add null handling for order item quantities and pricing to display "—" for missing values
- Improve filter tab styling: make mobile tabs scrollable, prevent text wrapping, hide scrollbar
- Add new utility scripts, API routes, migrations, and polish small UI/UX fixes including tsconfig updates and theme hook improvements
2026-07-07 17:22:03 +02:00

115 lines
No EOL
3.9 KiB
TypeScript

/**
* Migration: 2026-07-06 — quote_attachments: add quote_item relation + kind field
*
* What it does:
* 1. Adds `quote_item` relation to `quote_attachments` (nullable, single,
* collectionId = quote_items). Lets a per-line attachment live alongside
* per-quote attachments on the same collection instead of needing a
* separate `quote_item_attachments` table.
*
* 2. Adds `kind` select field (pdf / image / doc / other) to
* `quote_attachments`. The TypeScript interface in `src/lib/types.ts`
* already declares `kind` (and `QuoteAttachmentKind`) — the v5 attachment
* workflow writes it on upload. Live PB never got the migration that adds
* the field, so existing rows have `kind = null`. The migration is
* idempotent: it skips when the field already exists.
*
* Why both at once:
* The order process page surfaces matched quote + per-line attachments so
* the buyer can verify they're ordering the right SKU from the right
* supplier. Without `quote_item` we can't link an attachment to a specific
* line; without `kind` we can't pick the right thumbnail (PDF vs image vs
* doc icon). Both are needed for the inline preview UI to be usable.
*
* Idempotency:
* Both field adds are skipped if a field with that name already exists.
* Tested collection ids are discovered by name from PB on every run, so
* this migration works across environments.
*
* Run:
* set -a && source .env.local && set +a && \
* npx tsx scripts/migrations/2026-07-06-quote-attachments-quote-item-and-kind.ts
*/
import { getAdminPocketBase } from "@/lib/server/pocketbase-admin";
type PbField = {
id?: string;
name: string;
type: string;
collectionId?: string;
values?: string[];
required?: boolean;
maxSelect?: number;
minSelect?: number;
cascadeDelete?: boolean;
[k: string]: unknown;
};
async function main() {
const pb = await getAdminPocketBase();
// Resolve by name — works across PB instances even if the id shifts.
const coll = await pb.collections.getFirstListItem(`name="quote_attachments"`);
const quoteItemsColl = await pb.collections.getFirstListItem(`name="quote_items"`);
const fields = ((coll as unknown as { fields?: PbField[] }).fields ?? []) as PbField[];
const existingNames = new Set(fields.map((f) => f.name));
const planned: string[] = [];
if (existingNames.has("quote_item")) {
planned.push(`field "quote_item" already present — skip`);
} else {
fields.push({
hidden: false,
name: "quote_item",
presentable: false,
required: false,
system: false,
type: "relation",
collectionId: quoteItemsColl.id,
cascadeDelete: false,
minSelect: 0,
maxSelect: 1,
});
planned.push(`add "quote_item" relation field (→ quote_items, nullable)`);
}
if (existingNames.has("kind")) {
planned.push(`field "kind" already present — skip`);
} else {
fields.push({
hidden: false,
name: "kind",
presentable: false,
required: false,
system: false,
type: "select",
maxSelect: 1,
values: ["pdf", "image", "doc", "other"],
});
planned.push(`add "kind" select field (pdf | image | doc | other)`);
}
const willChange = planned.some((p) => !p.includes("skip"));
console.log("Planned schema changes:");
for (const p of planned) console.log(` • ${p}`);
if (willChange) {
await pb.collections.update(coll.id, { fields });
console.log("\n[done] schema updated.");
} else {
console.log("\n[done] schema already up to date — no changes written.");
}
// Read back to confirm what landed.
const after = (await pb.collections.getOne(coll.id)) as { fields?: PbField[] };
const afterNames = (after.fields ?? []).map((f) => f.name).join(", ");
console.log(`\nquote_attachments fields now: ${afterNames}`);
}
main().catch((err) => {
console.error("[fatal]", err);
process.exit(1);
});