mega-plant/scripts/setup-cash-float-collection.mjs
David 1ec52961ee Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
  code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:09:57 +02:00

56 lines
3 KiB
JavaScript

// Create the `cash_float_entries` PocketBase collection (idempotent).
//
// The cash-float CARD: one opening balance, then every slip subtracts from it. Stored as a
// ledger — the balance is the SUM of entries, never a number that is overwritten — so every
// rand on the card can be traced to a slip.
//
// kind "opening" the balance on the card when tracking began (set once)
// kind "topup" money loaded onto the card later
// kind "expense" a slip: supplier, cost code, comment, amount and the photo of the slip
//
// `amount` is always positive; the kind decides the sign. Not wiped by the seed.
//
// Usage: node scripts/setup-cash-float-collection.mjs
import PocketBase from "pocketbase";
import { readFileSync } from "node:fs";
for (const l of readFileSync(new URL("../.env.local", import.meta.url), "utf8").split("\n")) {
const i = l.indexOf("="); if (i > 0 && !l.trim().startsWith("#")) process.env[l.slice(0, i).trim()] = l.slice(i + 1).trim();
}
const pb = new PocketBase(process.env.NEXT_PUBLIC_PB_URL);
pb.autoCancellation(false);
await pb.collection("_superusers").authWithPassword(process.env.PB_ADMIN, process.env.PB_PASS);
const ref = await pb.collections.getOne("monthly_income");
const assetsId = (await pb.collections.getOne("assets")).id;
const fields = [
{ name: "kind", type: "text", required: true },
{ name: "amount", type: "number" },
// Full instant (ISO). `created` is not auto-filled on API-made collections.
{ name: "occurred_on", type: "text" },
{ name: "supplier", type: "text" },
{ name: "cost_code", type: "text" },
// Set when the cost code names a vehicle in the plant register; never guessed.
{ name: "asset", type: "relation", collectionId: assetsId, minSelect: 0, maxSelect: 1, cascadeDelete: false },
{ name: "comment", type: "text" },
{ name: "slip_photo", type: "file", maxSelect: 1, maxSize: 10485760, mimeTypes: ["image/jpeg", "image/png", "image/webp", "image/heic", "image/heif"], protected: true },
{ name: "recorded_by", type: "text" },
];
const existing = await pb.collections.getFullList({ filter: 'name = "cash_float_entries"', 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/cash_float_entries", { 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(" · cash_float_entries already present");
} else {
const c = await pb.send("/api/collections", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "cash_float_entries", type: "base", fields,
listRule: ref.listRule ?? "", viewRule: ref.viewRule ?? "",
createRule: ref.createRule ?? "", updateRule: ref.updateRule ?? "", deleteRule: ref.deleteRule ?? "",
}),
});
console.log(` ✓ created cash_float_entries (${c.id})`);
}