Remove redundant Assign Drivers top-level navigation link since driver assignment is now embedded directly in the collection pool page per-row, and add an Other nav section for leftover navigation items Extract collection pool view logic into a dedicated view-model module with full Vitest test coverage for assignment state classification, row sorting, pool age labeling, and proximity marker grouping Flatten the collection pool page into a single unified list replacing the old grouped assigned/unassigned layout, with updated row UI showing all required operational details and context-aware actions Add database migration to add optional `collection_photo` field to order_items for driver pickup proof photos Add complete design documentation, planning artifacts, and brainstorming session files for the collection pool redesign project
108 lines
No EOL
3.8 KiB
TypeScript
108 lines
No EOL
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);
|
||
}); |