108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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);
|
|||
|
|
});
|