// Regenerate JSON extraction files from the source workbook. // Reads: ./data/source/original.xlsx // Writes: ./data/source/{assets,machines,mrc,overheads}.json // // Usage: node scripts/extract.mjs import XLSX from "xlsx"; import { writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const SRC = resolve(__dirname, "..", "data", "source", "original.xlsx"); console.log(`→ Reading ${SRC}`); const wb = XLSX.readFile(SRC, { cellFormula: false }); function find(ws, range) { const out = []; const r = XLSX.utils.decode_range(range); for (let R = r.s.r; R <= r.e.r; R++) { const row = []; for (let C = r.s.c; C <= r.e.c; C++) { const addr = XLSX.utils.encode_cell({ r: R, c: C }); row.push(ws[addr] ? ws[addr].v : null); } out.push(row); } return out; } // === ASSETS from Sheet1 === console.log("\n→ Extracting assets from Sheet1"); const sheet1 = wb.Sheets["Sheet1"]; const assets = []; // Plant Overheads (rows 6-42) for (let r = 5; r <= 41; r++) { const name = sheet1[XLSX.utils.encode_cell({ r, c: 1 })]?.v; if (!name) continue; const code = sheet1[XLSX.utils.encode_cell({ r, c: 5 })]?.v ?? ""; const excl = sheet1[XLSX.utils.encode_cell({ r, c: 6 })]?.v; const incl = sheet1[XLSX.utils.encode_cell({ r, c: 8 })]?.v; const pv = sheet1[XLSX.utils.encode_cell({ r, c: 9 })]?.v; const rate = sheet1[XLSX.utils.encode_cell({ r, c: 11 })]?.v; assets.push({ name: String(name).trim(), code: String(code).trim(), category: "Plant", monthly_excl: typeof excl === "number" ? excl : 0, monthly_incl: typeof incl === "number" ? incl : 0, purchase_value: typeof pv === "number" ? pv : 0, site_rate: typeof rate === "number" ? rate : 0, }); } // Vehicle Costs (rows 46-50) for (let r = 45; r <= 49; r++) { const name = sheet1[XLSX.utils.encode_cell({ r, c: 1 })]?.v; if (!name) continue; const code = sheet1[XLSX.utils.encode_cell({ r, c: 5 })]?.v ?? ""; const excl = sheet1[XLSX.utils.encode_cell({ r, c: 6 })]?.v; const incl = sheet1[XLSX.utils.encode_cell({ r, c: 8 })]?.v; const rate = sheet1[XLSX.utils.encode_cell({ r, c: 11 })]?.v; assets.push({ name: String(name).trim(), code: String(code).trim(), category: "Vehicle", monthly_excl: typeof excl === "number" ? excl : 0, monthly_incl: typeof incl === "number" ? incl : 0, purchase_value: 0, site_rate: typeof rate === "number" ? rate : 0, }); } // Bastion finance/insurance/tracking (rows 54-66) for (let r = 53; r <= 65; r++) { const name = sheet1[XLSX.utils.encode_cell({ r, c: 1 })]?.v; if (!name) continue; const code = sheet1[XLSX.utils.encode_cell({ r, c: 5 })]?.v ?? ""; const gl = sheet1[XLSX.utils.encode_cell({ r, c: 2 })]?.v; const sup = sheet1[XLSX.utils.encode_cell({ r, c: 3 })]?.v; const excl = sheet1[XLSX.utils.encode_cell({ r, c: 6 })]?.v; const incl = sheet1[XLSX.utils.encode_cell({ r, c: 8 })]?.v; assets.push({ name: String(name).trim(), code: String(code).trim(), category: (gl ? String(gl).trim() : "Finance"), gl_account: gl ? String(gl).trim() : "", supplier: sup ? String(sup).trim() : "", monthly_excl: typeof excl === "number" ? excl : 0, monthly_incl: typeof incl === "number" ? incl : 0, purchase_value: 0, site_rate: 0, }); } writeFileSync(resolve(__dirname, "..", "data", "source", "assets.json"), JSON.stringify(assets, null, 2)); console.log(` ✓ ${assets.length} assets`); // === MACHINES from Machine Graph Data === console.log("\n→ Extracting machines from Machine Graph Data"); const wsMGD = wb.Sheets["Machine Graph Data"]; const monthNames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; // Part C.3: assign sequential codes for uncoded duplicates in the workbook. // Maps "TYPE_" to a counter so "SITE CONTANER #1" → SC01, etc. // We also fix the spelling (CONTANER → CONTAINER) so it matches assets.json. // Each entry: { prefix, test(name) → bool, normalised(name) → string } const UNCODED_TYPES = [ { prefix: "SC", test: (n) => /^SITE CONTANER/i.test(n) || /^Site Container/i.test(n) }, { prefix: "OC", test: (n) => /^Office Container/i.test(n) }, { prefix: "PC", test: (n) => /^Porta camp/i.test(n) }, { prefix: "RV02", test: (n) => /^ELB 1TON RV/i.test(n) }, { prefix: "GN100", test: (n) => /^10 KVA GENERATOR/i.test(n) }, { prefix: "GN075", test: (n) => /^7,5 KVA GENERATOR/i.test(n) }, { prefix: "GN300", test: (n) => /^30KVA GENERATOR/i.test(n) }, { prefix: "TR01", test: (n) => /^TRAILOR PLANT/i.test(n) }, { prefix: "TR02", test: (n) => /^DIESEL TRAILOR/i.test(n) }, { prefix: "SO01", test: (n) => /^Solar office/i.test(n) }, ]; const uncodedCounters = {}; function codeForUncoded(rawName) { // Normalise the workbook spelling to match assets.json const name = rawName.replace(/^SITE CONTANER/, "Site Container").trim(); const match = UNCODED_TYPES.find((t) => t.test(name)); if (!match) return null; // Fixed code (single instance) vs sequential if (/^\D+\d+$/.test(match.prefix)) return match.prefix; uncodedCounters[match.prefix] = (uncodedCounters[match.prefix] || 0) + 1; return `${match.prefix}${String(uncodedCounters[match.prefix]).padStart(2, "0")}`; } const machineRows = []; let currentMachine = null; let currentData = []; const range = XLSX.utils.decode_range(wsMGD["!ref"]); for (let r = range.s.r; r <= range.e.r; r++) { const a = wsMGD[XLSX.utils.encode_cell({ r, c: 0 })]?.v; const b = wsMGD[XLSX.utils.encode_cell({ r, c: 1 })]?.v; if (a == null && b == null) { if (currentMachine && currentData.length) { machineRows.push({ machine: currentMachine, data: currentData }); currentMachine = null; currentData = []; } continue; } if (typeof a === "string" && a === "Month" && typeof b === "string" && b === "Income") continue; if (typeof a === "string" && monthNames.includes(a)) { if (!currentMachine) continue; const income = wsMGD[XLSX.utils.encode_cell({ r, c: 1 })]?.v; const fixed = wsMGD[XLSX.utils.encode_cell({ r, c: 2 })]?.v; const running = wsMGD[XLSX.utils.encode_cell({ r, c: 4 })]?.v; currentData.push({ month: monthNames.indexOf(a) + 1, income: typeof income === "number" ? income : 0, fixed_expenses: typeof fixed === "number" ? fixed : 0, running_cost: typeof running === "number" ? running : 0, }); continue; } if (typeof a === "string" && !monthNames.includes(a)) { if (currentMachine && currentData.length) machineRows.push({ machine: currentMachine, data: currentData }); currentMachine = a; currentData = []; } } if (currentMachine && currentData.length) machineRows.push({ machine: currentMachine, data: currentData }); // Strip code from "NAME (CODE)" pattern OR assign sequential code for uncoded types const machines = machineRows.map((m) => { const match = m.machine.match(/\(([^)]+)\)\s*$/); if (match) { return { machine: m.machine.replace(/\s*\([^)]+\)\s*$/, "").trim(), code: match[1].trim(), data: m.data, }; } // Try sequential-code assignment (Part C.3) const syntheticCode = codeForUncoded(m.machine); return { machine: m.machine.replace(/^SITE CONTANER/, "Site Container").trim(), code: syntheticCode || "", data: m.data, }; }); writeFileSync(resolve(__dirname, "..", "data", "source", "machines.json"), JSON.stringify(machines, null, 2)); console.log(` ✓ ${machines.length} machines (sequential codes: ${Object.keys(uncodedCounters).length} prefixes)`); // === MRC from Monthly Running Cost sheet === console.log("\n→ Extracting monthly running costs"); const wsMRC = wb.Sheets["Monthly Running Cost"]; const monthCols = { 1: 3, 2: 5, 3: 7, 4: 9, 5: 11, 6: 13, 7: 15, 8: 17 }; const mrc = []; const mrcRange = XLSX.utils.decode_range(wsMRC["!ref"]); for (let r = 3; r <= 46; r++) { const name = wsMRC[XLSX.utils.encode_cell({ r, c: 1 })]?.v; const code = wsMRC[XLSX.utils.encode_cell({ r, c: 2 })]?.v; if (!code) continue; for (const [month, col] of Object.entries(monthCols)) { const v = wsMRC[XLSX.utils.encode_cell({ r, c: col })]?.v; if (typeof v === "number" && v > 0) { mrc.push({ name: name ? String(name).trim() : String(code).trim(), code: String(code).trim(), month: Number(month), amount: v, }); } } } writeFileSync(resolve(__dirname, "..", "data", "source", "mrc.json"), JSON.stringify(mrc, null, 2)); console.log(` ✓ ${mrc.length} running-cost rows`); // === OVERHEADS from Cost sheet === console.log("\n→ Extracting overheads from Cost sheet"); const wsCost = wb.Sheets["Cost"]; const sections = [ { name: "Plant Overheads", rows: [5, 40] }, { name: "Vehicle Costs", rows: [45, 48] }, { name: "Bastion Finance", rows: [53, 65] }, { name: "Personnel", rows: [70, 74] }, { name: "Subscriptions", rows: [80, 82] }, { name: "Rent & Utilities", rows: [87, 88] }, ]; const overheads = []; for (const { name, rows } of sections) { for (let r = rows[0]; r <= rows[1]; r++) { const desc = wsCost[XLSX.utils.encode_cell({ r, c: 1 })]?.v; if (!desc) continue; const gl = wsCost[XLSX.utils.encode_cell({ r, c: 2 })]?.v; const sup = wsCost[XLSX.utils.encode_cell({ r, c: 3 })]?.v; const qty = wsCost[XLSX.utils.encode_cell({ r, c: 4 })]?.v; const pu = wsCost[XLSX.utils.encode_cell({ r, c: 5 })]?.v; const amt = wsCost[XLSX.utils.encode_cell({ r, c: 6 })]?.v; const vat = wsCost[XLSX.utils.encode_cell({ r, c: 7 })]?.v; const tot = wsCost[XLSX.utils.encode_cell({ r, c: 8 })]?.v; overheads.push({ section: name, description: String(desc).trim(), gl_account: gl ? String(gl).trim() : "", supplier: sup ? String(sup).trim() : "", qty: typeof qty === "number" ? qty : 0, unit_price: typeof pu === "number" ? pu : 0, amount_excl: typeof amt === "number" ? amt : 0, vat: typeof vat === "number" ? vat : 0, amount_incl: typeof tot === "number" ? tot : 0, }); } } writeFileSync(resolve(__dirname, "..", "data", "source", "overheads.json"), JSON.stringify(overheads, null, 2)); console.log(` ✓ ${overheads.length} overhead rows`); console.log("\n✅ Extraction complete → ./data/source/");