/** * Migration — add `collection_photo` (single file) to `order_items`. * * Why: * The driver app's Collect flow needs a place to attach the optional * proof-of-pickup photo (driver takes a snapshot of the goods at the * supplier, before signing off the row). Today there's no file field * on `order_items`, so drivers had no way to attach anything from * the field — operators had to take their word on quantity * discrepancies without visual context. * * Lifecycle: * - Optional on every collection. Driver can skip it. * - Single image per row (multi-photo would balloon PB storage * and isn't part of the current spec). * - Stamped alongside `qty_collected` / `drv_collected_at` so the * audit trail ties proof to numbers and time. * * Field spec: * - 10MB max — modern phone cameras run ~3–6MB JPEG; 10MB gives * headroom for HEIC and burst shots without throttling the UI. * - image/* mime allowlist. PDF and other docs go elsewhere. * - maxSelect: 1. A second photo would be a separate feature. * * Idempotent — skips if the field already exists. * * Run with (from WFDASH_NEXTJS so .env.local is found): * npx tsx scripts/migrations/2026-07-07-order-items-collection-photo.ts */ /* eslint-disable */ 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 { /* fine — rely on shell env */ } } loadEnvFile(".env"); loadEnvFile(".env.local"); const FIELD_NAME = "collection_photo"; 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) { throw new Error( "Set NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD (e.g. sourced from .env.local).", ); } const pb = new PocketBase(url); await pb.collection("_superusers").authWithPassword(email, password); console.log(`[order-items-collection-photo] Authenticated against ${url}`); const col = await pb.collections.getOne("order_items"); const fields = (col.fields as Array>).slice(); if (fields.some((f) => f.name === FIELD_NAME)) { console.log( `[order-items-collection-photo] ${FIELD_NAME} already exists — skip field add`, ); } else { fields.push({ name: FIELD_NAME, type: "file", required: false, presentable: false, options: { maxSelect: 1, maxSize: 10 * 1024 * 1024, // 10MB — phone cameras, room to breathe mimeTypes: ["image/jpeg", "image/png", "image/webp", "image/heic", "image/heif"], }, }); await pb.collections.update(col.id, { fields }); console.log( `[order-items-collection-photo] added \`${FIELD_NAME}\` (file, optional, max 10MB, image/* only) to order_items`, ); } const fresh = await pb.collections.getOne("order_items"); const verify = (fresh.fields as Array<{ name: string; type: string }>) .filter((f) => f.name === FIELD_NAME) .map((f) => `${f.name}:${f.type}`) .join(", "); console.log( `[order-items-collection-photo] Post-update → ${verify || "FIELD MISSING"}`, ); console.log("[order-items-collection-photo] Migration complete."); } main().catch((err) => { console.error("[order-items-collection-photo] failed:", err); process.exit(1); });