mega-plant/scripts/setup-job-cards-collection.mjs
David c269cd00b4 Add Job Cards (vehicle-QR start/finish) and retire Operators/Mechanics pages
- Plant Clock "Job cards" tab: scan a vehicle QR to start a job, scan again to finish
  with odometer (or no meter), kind of work and a note
- /running/job-cards table with period, work, vehicle and mechanic filters
- Remove /running/operators and /running/mechanics; mechanic badge PDF moves to Employees

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:54:59 +02:00

73 lines
3.5 KiB
JavaScript

// Create the `job_cards` PocketBase collection (idempotent).
//
// A job card is one stretch of work on one vehicle: a mechanic scans the vehicle's QR to
// START the job, scans it again to FINISH it and says what the odometer reads, what kind
// of work it was (service / breakdown / general repair) and what was done.
//
// Not wiped by the seed: job cards are history that exists nowhere else.
//
// Usage: node scripts/setup-job-cards-collection.mjs
import PocketBase from "pocketbase";
import { readFileSync, existsSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const envPath = resolve(ROOT, ".env.local");
if (!existsSync(envPath)) { console.error("✗ .env.local not found."); process.exit(1); }
for (const line of readFileSync(envPath, "utf8").split("\n")) {
const t = line.trim();
if (!t || t.startsWith("#")) continue;
const i = t.indexOf("=");
if (i > 0) process.env[t.slice(0, i).trim()] = t.slice(i + 1).trim();
}
const { NEXT_PUBLIC_PB_URL: PB_URL, PB_ADMIN, PB_PASS } = process.env;
if (!PB_URL || !PB_ADMIN || !PB_PASS) { console.error("✗ Missing NEXT_PUBLIC_PB_URL / PB_ADMIN / PB_PASS"); process.exit(1); }
const pb = new PocketBase(PB_URL);
pb.autoCancellation(false);
await pb.collection("_superusers").authWithPassword(PB_ADMIN, PB_PASS);
const ref = await pb.collections.getOne("monthly_income");
const assetsId = (await pb.collections.getOne("assets")).id;
const fields = [
{ name: "asset", type: "relation", collectionId: assetsId, required: true, minSelect: 1, maxSelect: 1, cascadeDelete: false },
{ name: "mechanic_name", type: "text" },
{ name: "mechanic_code", type: "text" },
{ name: "started_at", type: "date", required: true },
{ name: "ended_at", type: "date" },
// "open" while the job is running, "closed" once finished.
{ name: "status", type: "text", required: true },
// service | breakdown | repair — set when the job is finished.
{ name: "job_type", type: "text" },
{ name: "odometer", type: "number" },
{ name: "no_meter", type: "bool" },
{ name: "note", type: "text" },
{ name: "duration_minutes", type: "number" },
{ name: "started_by", type: "text" },
{ name: "ended_by", type: "text" },
];
// One OPEN job per vehicle, enforced by the database so two phones scanning at once cannot both start it.
const indexes = ["CREATE UNIQUE INDEX idx_job_cards_one_open ON job_cards (asset) WHERE status = 'open'"];
const existing = await pb.collections.getFullList({ filter: 'name = "job_cards"', perPage: 1 });
if (existing.length) {
const have = new Set(existing[0].fields.map((f) => f.name));
const absent = fields.filter((f) => !have.has(f.name));
if (absent.length) {
await pb.send(`/api/collections/job_cards`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fields: [...existing[0].fields, ...absent] }) });
console.log(` ✓ added: ${absent.map((f) => f.name).join(", ")}`);
} else console.log(" · job_cards already present");
} else {
const created = await pb.send("/api/collections", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "job_cards", type: "base", fields, indexes,
listRule: ref.listRule ?? "", viewRule: ref.viewRule ?? "",
createRule: ref.createRule ?? "", updateRule: ref.updateRule ?? "", deleteRule: ref.deleteRule ?? "",
}),
});
console.log(` ✓ created job_cards (${created.id})`);
}
process.exit(0);