WFDASH-WEBAPP/scripts/migrations/2026-07-07-order-items-collection-photo.ts

108 lines
3.8 KiB
TypeScript
Raw Normal View History

/**
* 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<Record<string, unknown>>).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);
});