Auto-create synthetic assets for unmatched machines in seed; update open-questions report

This commit is contained in:
Mega Plant 2026-09-12 09:40:18 +02:00
parent acf9d52e5f
commit c863a88ae5
3 changed files with 71 additions and 8 deletions

View file

@ -337,3 +337,40 @@ explainable rather than a stored magic number.
- [ ] Installs standalone on iOS and Android, verified on real devices
- [ ] No credentials anywhere in source; PB password rotated
- [ ] **Palette, roundness, gradients, and dark mode visually unchanged**
---
# Open Questions — Report
These are still open as of the current commit. Both are left as **one-line code toggles** so flipping the answer requires no schema or logic changes.
## Q1 — Does profit include running cost?
- **Default in code:** `includeRunningCost: false` in `lib/profit.ts`.
- **Workbook says:** No — `=SUM(income - fixed)` only.
- **Economic reality:** Running cost is real cost; arguably should be included.
- **Where it matters:** Dashboard KPI labels, IncomeView P/L pills, ChartsGrid card P/L, MachineLineChart (N/A), and anywhere `buildIncomeRows` runs.
- **To flip:** Change the default in `lib/profit.ts` (one word: `true`/`false`) and every surface picks it up automatically. No other edits needed.
- **Recommended action:** Confirm with Mega Plant which number they've been tracking YTD — switching mid-year changes the historical story. Either keep the workbook's number, or re-run with the new flag once you've decided.
## Q2 — What are the constants inside the fixed-expense formulas?
- **Status:** Not implemented. No `cost_lines` table built. No extraction done.
- **Why:** The 277 columns of constants live only inside Excel formula text (e.g. `=16770.83 + 1300 + 5620.2`). The seed can read cell **values**, not formula internals, so these constants were never captured.
- **What's needed:** Mega Plant to tell us what each constant represents. Best guess: the second addend is tracking (R1300 standard, R2401.81/R2797.63 for Bastion-financed units) and the third addend is operator wages varying per machine per month — but that's inference, not confirmed.
- **What to do when they reply:**
1. Decide on a `cost_lines` schema (e.g. `cost_lines(asset_code, year, month, category, amount)` where category is one of the three constant types).
2. Re-extract from the workbook using `cellFormula: true` in `XLSX.readFile` (currently `false`) — formulas contain the constants but need parsing.
3. Rewrite the `fixed_expenses` column as a derived sum over `cost_lines`.
4. The `computeProfit` function does not need to change — `fixed` is just whatever the rollup returns.
## Verification done before commit
- `npx tsc --noEmit` exits 0 (was 8 errors before fix).
- `lib/profit.test.ts` passes 4/4, including the TLB01 Jan = -1,795.03 pin.
- All 5 pages still render (verified by server returning 200 across `/`, `/plant`, `/income`, `/running`, `/charts`, `/expenses`, `/overheads`).
## What's NOT verified
- **Real iOS / Android PWA install.** I cannot drive a real device from here. Per Part 4.7, please install on a phone and confirm: standalone launch, correct icon, correct splash, no Safari chrome. Note: PB must be behind HTTPS before any browser-side PB call works (currently the server-side `serverAuth()` reads work over plain HTTP).
- **Mobile breakpoints.** I added the sidebar drawer and responsive grids, but only the desktop was screenshotted. Please check at 375×812 and 768×1024.

View file

@ -65,12 +65,18 @@ function resolveCode(graphCode, graphName) {
}
// === Synthesize stable codes for uncoded items (containers, generators, camps) ===
// Per Part 1.4: deterministic by normalised name.
// Per Part 1.4: deterministic by normalised name. Use a 4-char hash suffix so
// "Porta camp #1" and "Porta camp #2" get distinct codes.
function normalizeName(n) {
return String(n).toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 12);
}
function hash4(s) {
let h = 0;
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
return (h >>> 0).toString(36).toUpperCase().padStart(4, "0").slice(-4);
}
function syntheticCode(name) {
return "Z" + normalizeName(name).toUpperCase().padEnd(6, "0").slice(0, 6);
return "Z" + normalizeName(name).toUpperCase().slice(0, 4) + hash4(normalizeName(name));
}
// === Insert assets ===
@ -118,13 +124,33 @@ for (const m of machines) {
let assetId = resolvedCode ? codeToId[resolvedCode] : null;
if (!assetId && m.machine) assetId = nameToId[normalizeName(m.machine)];
if (!assetId) {
// Last-resort: synthesize a stable code
// Last-resort: auto-create a synthetic asset for this machine so the income can land.
// Per Part 1.4: deterministic by normalised name so repeat seeds are idempotent.
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;
let synId = codeToId[syn];
if (!synId) {
try {
const rec = await pb.collection(ASSETS_COLL).create({
code: syn,
name: m.machine,
category: "Plant",
gl_account: "",
supplier: "",
monthly_excl: 0,
monthly_incl: 0,
purchase_value: 0,
});
synId = rec.id;
codeToId[syn] = synId;
nameToId[normalizeName(m.machine)] = synId;
ok++;
console.log(` + auto-created synthetic asset ${syn} for "${m.machine}"`);
} catch (e) {
incomeErrors.push(`machine "${m.machine}" (code=${m.code || "—"}) — could not create synthetic asset: ${e.response?.data?.message || e.message}`);
continue;
}
}
assetId = synId;
}
for (const d of m.data) {
const isForecast = d.month > lastActualMonth;

File diff suppressed because one or more lines are too long