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
134 lines
5 KiB
TypeScript
134 lines
5 KiB
TypeScript
/* eslint-disable */
|
|
/**
|
|
* Migration — make `order_items.quantity` and `order_items.selling_price`
|
|
* nullable.
|
|
*
|
|
* Why:
|
|
* The /orders/new page and the /orders/[id]/process page bind these
|
|
* fields to <Input type="number"> elements. Number inputs have two
|
|
* operator-hostile behaviours:
|
|
*
|
|
* 1. They can't be left empty by the user — clearing them snaps
|
|
* the value back to the previous number, or 0.
|
|
* 2. They commit a value on focus→blur even when the operator
|
|
* didn't type anything.
|
|
*
|
|
* The fix is to bind the fields to <input type="text" inputMode="decimal">
|
|
* elements (so the field is genuinely empty until the operator types),
|
|
* parse to number|null on blur, and ship null to PB when the field is
|
|
* blank. For null to round-trip, the PB columns must be nullable.
|
|
*
|
|
* Scope:
|
|
* - `order_items.quantity` → number, not required, no min.
|
|
* - `order_items.selling_price` → number, not required, no min.
|
|
*
|
|
* `cost_price` is intentionally left NON-nullable. It has a different
|
|
* default (0) and is the operator's authoritative number — leaving it
|
|
* nullable introduces a "what does a null cost mean?" ambiguity that
|
|
* the matcher / PO generator cannot resolve without an extra decision.
|
|
* If that ever changes, get a sign-off first.
|
|
*
|
|
* Idempotent — re-running after a partial success is safe (each field
|
|
* is rewritten with the target shape; PB's collections.update is
|
|
* idempotent when the resulting array matches the target).
|
|
*
|
|
* Run with:
|
|
* npx tsx scripts/migrations/2026-07-06-order-items-nullable-numbers.ts
|
|
*/
|
|
import PocketBase from "pocketbase";
|
|
import { readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
|
|
// ─── env loading (matches sibling migrations) ───────────────────────────
|
|
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 TARGET_NULLABLE_FIELDS = ["quantity", "selling_price"] as const;
|
|
|
|
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-nullable] 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>;
|
|
}>;
|
|
|
|
// Walk every field we want to be nullable, rewrite its `required`
|
|
// flag, and reassemble. We also tighten `options.min` if any leak
|
|
// remains (PB will keep prior min from earlier schema changes).
|
|
let touched = 0;
|
|
const updatedFields = fields.map((f) => {
|
|
if (
|
|
f.type === "number" &&
|
|
TARGET_NULLABLE_FIELDS.includes(f.name as (typeof TARGET_NULLABLE_FIELDS)[number])
|
|
) {
|
|
const prevRequired = f.required !== false;
|
|
if (!prevRequired) {
|
|
console.log(`[order-items-nullable] ${f.name} already nullable — skip`);
|
|
return f;
|
|
}
|
|
touched++;
|
|
const opts = { ...(f.options ?? {}) };
|
|
// Drop any `min` constraint — null must round-trip cleanly. The
|
|
// canSubmit gate on the form keeps zero-blank invariants honest.
|
|
delete (opts as Record<string, unknown>).min;
|
|
return { ...f, required: false, options: opts };
|
|
}
|
|
return f;
|
|
});
|
|
|
|
if (touched === 0) {
|
|
console.log(
|
|
"[order-items-nullable] No fields needed updating — schema is already nullable.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
await pb.collections.update(col.id, { fields: updatedFields });
|
|
console.log(
|
|
`[order-items-nullable] Made nullable: ${TARGET_NULLABLE_FIELDS.join(", ")}. Verify in UI by saving an order item with quantity = null.`,
|
|
);
|
|
|
|
// Re-fetch so we can print the post-update shape for the operator.
|
|
const fresh = await pb.collections.getOne("order_items");
|
|
const verify = (fresh.fields as Array<{ name: string; required?: boolean }>)
|
|
.filter((f) => TARGET_NULLABLE_FIELDS.includes(f.name as (typeof TARGET_NULLABLE_FIELDS)[number]))
|
|
.map((f) => `${f.name}:required=${f.required !== false ? "true" : "false"}`)
|
|
.join(", ");
|
|
console.log(`[order-items-nullable] Post-update → ${verify}`);
|
|
console.log("[order-items-nullable] Migration complete.");
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("[order-items-nullable] Migration failed:", err);
|
|
process.exit(1);
|
|
});
|