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
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
/**
|
|
* Migration — add `ready_for_collection` (bool) to `order_items`.
|
|
*
|
|
* Why:
|
|
* The collection-pool flow needs a single toggle that says "the
|
|
* supplier has confirmed this is ready to be collected" so the
|
|
* dashboard can filter the pool into "ready to dispatch" vs
|
|
* "still waiting on supplier" and the driver app can hide rows
|
|
* the operator hasn't yet approved. This sits on top of the
|
|
* existing `item_status` enum (which tracks the *who*, not the
|
|
* *when-is-it-physically-ready*).
|
|
*
|
|
* Lifecycle:
|
|
* - On the SMTP outcome callback that flips items into the
|
|
* collection pool, `ready_for_collection` lands as `false`
|
|
* (the supplier hasn't confirmed yet).
|
|
* - Operator manually flips to `true` from the dashboard's
|
|
* collection-pool page or the collection-map popover.
|
|
* - Driver app's `getMyAssignedItems` and `getPoolItems`
|
|
* filter on `ready_for_collection = true` so unready rows
|
|
* never reach the driver.
|
|
*
|
|
* Backfill — every existing `collection_pool` row gets
|
|
* `ready_for_collection = true` so today's rows keep flowing to
|
|
* the driver app without operator intervention. New rows from the
|
|
* route land `false` and require a manual flip.
|
|
*
|
|
* Idempotent — skips if `ready_for_collection` already exists.
|
|
*
|
|
* Run with:
|
|
* npx tsx scripts/migrations/2026-07-07-order-items-ready-for-collection.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");
|
|
|
|
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-ready-for-collection] Authenticated against ${url}`);
|
|
|
|
const col = await pb.collections.getOne("order_items");
|
|
const fields = (col.fields as Array<{
|
|
name: string;
|
|
type: string;
|
|
required?: boolean;
|
|
options?: Record<string, unknown>;
|
|
}>).slice();
|
|
|
|
if (fields.some((f) => f.name === "ready_for_collection")) {
|
|
console.log(
|
|
"[order-items-ready-for-collection] ready_for_collection already exists — skip field add",
|
|
);
|
|
} else {
|
|
fields.push({
|
|
name: "ready_for_collection",
|
|
type: "bool",
|
|
required: false,
|
|
options: {},
|
|
});
|
|
await pb.collections.update(col.id, { fields });
|
|
console.log(
|
|
"[order-items-ready-for-collection] added `ready_for_collection` (bool, optional) to order_items",
|
|
);
|
|
}
|
|
|
|
// Backfill — flip every existing collection_pool row to ready=true so
|
|
// drivers keep seeing today's items without operator intervention.
|
|
// New rows from recordPurchaseOrderSendOutcome land ready=false.
|
|
const pool = await pb.collection("order_items").getFullList({
|
|
filter: 'item_status = "collection_pool"',
|
|
fields: "id,item_status,ready_for_collection",
|
|
});
|
|
|
|
let backfilled = 0;
|
|
let skipped = 0;
|
|
for (const row of pool) {
|
|
if (row.ready_for_collection === true) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
await pb.collection("order_items").update(row.id, {
|
|
ready_for_collection: true,
|
|
});
|
|
backfilled++;
|
|
}
|
|
console.log(
|
|
`[order-items-ready-for-collection] backfill — ${backfilled} rows set to ready=true, ${skipped} already true`,
|
|
);
|
|
|
|
const fresh = await pb.collections.getOne("order_items");
|
|
const verify = (fresh.fields as Array<{ name: string; type: string }>)
|
|
.filter((f) => f.name === "ready_for_collection")
|
|
.map((f) => `${f.name}:${f.type}`)
|
|
.join(", ");
|
|
console.log(
|
|
`[order-items-ready-for-collection] Post-update → ${verify || "FIELD MISSING"}`,
|
|
);
|
|
console.log("[order-items-ready-for-collection] Migration complete.");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("[order-items-ready-for-collection] Migration failed:", err);
|
|
process.exit(1);
|
|
});
|