210 lines
7.6 KiB
JavaScript
210 lines
7.6 KiB
JavaScript
// Seed PocketBase with data extracted from data/source/*.json
|
|
// Regenerate those files with `node scripts/extract.mjs`
|
|
//
|
|
// Usage: PB_ADMIN=... PB_PASS=... node scripts/seed.mjs
|
|
import PocketBase from "pocketbase";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, resolve } from "node:path";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DATA_DIR = resolve(__dirname, "..", "data", "source");
|
|
|
|
// Read required env — fail loudly if missing (per Part 5.2)
|
|
const PB = process.env.NEXT_PUBLIC_PB_URL;
|
|
const EMAIL = process.env.PB_ADMIN;
|
|
const PASS = process.env.PB_PASS;
|
|
const errors = [];
|
|
if (!PB) errors.push("NEXT_PUBLIC_PB_URL");
|
|
if (!EMAIL) errors.push("PB_ADMIN");
|
|
if (!PASS) errors.push("PB_PASS");
|
|
if (errors.length) {
|
|
console.error(`✗ Missing required env: ${errors.join(", ")}`);
|
|
console.error(" Copy .env.example to .env.local and fill in.");
|
|
process.exit(1);
|
|
}
|
|
|
|
const ASSETS_COLL = "assets";
|
|
const INCOME_COLL = "monthly_income";
|
|
const RUN_COLL = "monthly_running_costs";
|
|
const OVERHEADS_COLL = "overheads";
|
|
|
|
const assets = JSON.parse(readFileSync(resolve(DATA_DIR, "assets.json"), "utf8"));
|
|
const machines = JSON.parse(readFileSync(resolve(DATA_DIR, "machines.json"), "utf8"));
|
|
const mrc = JSON.parse(readFileSync(resolve(DATA_DIR, "mrc.json"), "utf8"));
|
|
const overheads = JSON.parse(readFileSync(resolve(DATA_DIR, "overheads.json"), "utf8"));
|
|
|
|
const pb = new PocketBase(PB);
|
|
pb.autoCancellation(false);
|
|
|
|
console.log(`→ Connecting to ${PB} as ${EMAIL}`);
|
|
await pb.collection("_superusers").authWithPassword(EMAIL, PASS); // Part 5.3: _superusers, not admins
|
|
console.log(" ✓ authenticated");
|
|
|
|
// Wipe existing rows so we can re-seed cleanly
|
|
for (const c of [ASSETS_COLL, INCOME_COLL, RUN_COLL, OVERHEADS_COLL]) {
|
|
const items = await pb.collection(c).getFullList();
|
|
for (const r of items) await pb.collection(c).delete(r.id);
|
|
}
|
|
console.log(" ✓ wiped existing rows");
|
|
|
|
// === Build alias map for code mismatches between graph sheet and asset register ===
|
|
// Per Part 1.4: derived from the workbook, not invented.
|
|
const codeAliases = {
|
|
"428E": "TLB02", // CAT 425F (428E) → TLB02
|
|
"430": "SS01", // CASE420 (430) → first SS01 (handled below)
|
|
};
|
|
function resolveCode(graphCode, graphName) {
|
|
if (!graphCode) return null;
|
|
if (codeAliases[graphCode]) {
|
|
// Special-case 430 — first match wins unless name indicates second
|
|
if (graphCode === "430" && /SS02/i.test(graphName)) return "SS02";
|
|
return codeAliases[graphCode];
|
|
}
|
|
return graphCode;
|
|
}
|
|
|
|
// === Synthesize stable codes for uncoded items (containers, generators, camps) ===
|
|
// Per Part 1.4: deterministic by normalised name.
|
|
function normalizeName(n) {
|
|
return String(n).toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 12);
|
|
}
|
|
function syntheticCode(name) {
|
|
return "Z" + normalizeName(name).toUpperCase().padEnd(6, "0").slice(0, 6);
|
|
}
|
|
|
|
// === Insert assets ===
|
|
console.log(`\n→ Seeding ${assets.length} assets`);
|
|
const codeToId = {};
|
|
const nameToId = {};
|
|
let ok = 0;
|
|
const seedErrors = [];
|
|
for (const a of assets) {
|
|
try {
|
|
const rec = await pb.collection(ASSETS_COLL).create({
|
|
code: a.code,
|
|
name: a.name,
|
|
category: a.category || "Plant",
|
|
gl_account: a.gl_account || "",
|
|
supplier: a.supplier || "",
|
|
monthly_excl: a.monthly_excl || 0,
|
|
monthly_incl: a.monthly_incl || 0,
|
|
purchase_value: a.purchase_value || 0,
|
|
// Part 1.6: site_rate dropped — it was always a copy of monthly_incl
|
|
});
|
|
if (a.code) codeToId[a.code] = rec.id;
|
|
nameToId[normalizeName(a.name)] = rec.id;
|
|
ok++;
|
|
} catch (e) {
|
|
seedErrors.push(`asset "${a.name}": ${e.response?.data?.message || e.message}`);
|
|
}
|
|
}
|
|
console.log(` ✓ ${ok}/${assets.length} assets inserted`);
|
|
|
|
// === Insert monthly_income from Machine Graph Data ===
|
|
// Part 1.5: monthly_income no longer carries running_cost — we derive it from monthly_running_costs on read.
|
|
// Part 1.4: alias map + synthetic codes for uncoded items, fail loudly on unmatched rows.
|
|
console.log("\n→ Seeding monthly_income from Machine Graph Data");
|
|
let incomeRows = 0;
|
|
const incomeErrors = [];
|
|
const lastActualMonth = (() => {
|
|
// Last month with any recorded income across the whole workbook
|
|
let m = 0;
|
|
for (const x of machines) for (const d of x.data) if (d.income > 0 && d.month > m) m = d.month;
|
|
return m;
|
|
})();
|
|
for (const m of machines) {
|
|
const resolvedCode = resolveCode(m.code, m.machine);
|
|
let assetId = resolvedCode ? codeToId[resolvedCode] : null;
|
|
if (!assetId && m.machine) assetId = nameToId[normalizeName(m.machine)];
|
|
if (!assetId) {
|
|
// Last-resort: synthesize a stable code
|
|
const syn = syntheticCode(m.machine);
|
|
assetId = codeToId[syn];
|
|
if (!assetId) {
|
|
incomeErrors.push(`machine "${m.machine}" (code=${m.code || "—"}) — no asset matched; need to add to codeAliases or seed the asset first`);
|
|
continue;
|
|
}
|
|
}
|
|
for (const d of m.data) {
|
|
const isForecast = d.month > lastActualMonth;
|
|
try {
|
|
await pb.collection(INCOME_COLL).create({
|
|
asset: assetId,
|
|
year: 2026,
|
|
month: d.month,
|
|
income: d.income,
|
|
fixed_expenses: d.fixed_expenses,
|
|
is_forecast: isForecast, // Part 1.2
|
|
// running_cost removed (Part 1.5) — derived from monthly_running_costs at read time
|
|
});
|
|
incomeRows++;
|
|
} catch (e) {
|
|
incomeErrors.push(`income ${m.code || m.machine} m=${d.month}: ${e.response?.data?.message || e.message}`);
|
|
}
|
|
}
|
|
}
|
|
console.log(` ✓ ${incomeRows} income rows inserted`);
|
|
|
|
// === Insert monthly_running_costs ===
|
|
console.log("\n→ Seeding monthly_running_costs");
|
|
let runRows = 0;
|
|
const runErrors = [];
|
|
for (const r of mrc) {
|
|
let assetId = codeToId[r.code];
|
|
if (!assetId) assetId = nameToId[normalizeName(r.name)];
|
|
if (!assetId) { runErrors.push(`running ${r.code || r.name} m=${r.month}: no asset matched`); continue; }
|
|
if (!r.amount) continue;
|
|
try {
|
|
await pb.collection(RUN_COLL).create({
|
|
asset: assetId,
|
|
year: 2026,
|
|
month: r.month,
|
|
amount: r.amount,
|
|
});
|
|
runRows++;
|
|
} catch (e) {
|
|
runErrors.push(`running ${r.code} m=${r.month}: ${e.response?.data?.message || e.message}`);
|
|
}
|
|
}
|
|
console.log(` ✓ ${runRows} running-cost rows inserted`);
|
|
|
|
// === Insert overheads ===
|
|
// Part 1.3: skip "Plant Overheads" — those rows are machines already in assets.
|
|
console.log("\n→ Seeding overheads (excluding Plant Overheads per Part 1.3)");
|
|
let ohRows = 0;
|
|
const ohErrors = [];
|
|
for (const o of overheads) {
|
|
if (o.section === "Plant Overheads") continue; // Part 1.3
|
|
try {
|
|
await pb.collection(OVERHEADS_COLL).create({
|
|
section: o.section,
|
|
description: o.description,
|
|
gl_account: o.gl_account || "",
|
|
supplier: o.supplier || "",
|
|
qty: o.qty || 0,
|
|
unit_price: o.unit_price || 0,
|
|
amount_excl: o.amount_excl || 0,
|
|
vat: o.vat || 0,
|
|
amount_incl: o.amount_incl || 0,
|
|
});
|
|
ohRows++;
|
|
} catch (e) {
|
|
ohErrors.push(`overhead "${o.description}": ${e.response?.data?.message || e.message}`);
|
|
}
|
|
}
|
|
console.log(` ✓ ${ohRows} overhead rows inserted`);
|
|
|
|
console.log("\n=== Summary ===");
|
|
console.log(`Assets: ${ok}/${assets.length}`);
|
|
console.log(`Income: ${incomeRows}`);
|
|
console.log(`Running: ${runRows}`);
|
|
console.log(`Overheads: ${ohRows}`);
|
|
|
|
const totalErrors = seedErrors.length + incomeErrors.length + runErrors.length + ohErrors.length;
|
|
if (totalErrors > 0) {
|
|
console.error(`\n✗ ${totalErrors} errors — seed FAILED loudly:`);
|
|
for (const e of [...seedErrors, ...incomeErrors, ...runErrors, ...ohErrors]) console.error(` - ${e}`);
|
|
process.exit(2);
|
|
}
|
|
console.log("\n✅ Seed complete");
|