Add planned vs actual turnover dashboards, monthly actual costs, editable retention forecasts and actuals, starting retention, payouts and running balances. Preserve manual values across workbook imports and include retention backfill tooling. Add Cedar Gate model extraction and reports, measured service routes and ground surfaces, plus site-map section totals and quantity refinements.
39 lines
2.8 KiB
TypeScript
39 lines
2.8 KiB
TypeScript
/** Run with npx tsx --env-file=.env.local scripts/backfill-retention-forecasts.ts [--apply]. Defaults to a read-only preview. */
|
|
import { pbAdmin } from "../src/lib/pb";
|
|
import { buildManagementBoard } from "../src/lib/management/model";
|
|
import { planRowFromRecord, projectPlanRowFromRecord, projectRowFromRecord, rowsToSnapshot } from "../src/lib/management/store";
|
|
import { matchRetentionForecasts } from "../src/lib/management/match-retention-forecasts";
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
if (args.some((arg) => arg !== "--apply")) throw new Error("Only --apply is supported");
|
|
const apply = args.includes("--apply");
|
|
const pb = await pbAdmin();
|
|
const [plans, projectRecords] = await Promise.all([pb.collection("management_plans").getFullList({ sort: "plan_year" }), pb.collection("management_projects").getFullList()]);
|
|
const projects = projectRecords.map((p) => projectRowFromRecord(p));
|
|
let changed = 0;
|
|
for (const record of plans) {
|
|
const plan = planRowFromRecord(record);
|
|
const records = await pb.collection("management_project_plans").getFullList({ filter: `plan = "${plan.id}"` });
|
|
const board = buildManagementBoard(rowsToSnapshot({ plan, projects, projectPlans: records.map((r) => projectPlanRowFromRecord(r)), retentions: [] }));
|
|
const result = matchRetentionForecasts(board, new Date().toISOString(), "Retention forecast backfill requested by user");
|
|
console.log(JSON.stringify({ year: plan.plan_year, retentionCells: result.audit.length, mode: apply ? "apply" : "preview" }));
|
|
if (!result.audit.length) continue;
|
|
changed += result.audit.length;
|
|
if (!apply) continue;
|
|
const fresh = await pb.collection("management_plans").getOne(plan.id);
|
|
if ((Number(fresh.revision) || 1) !== plan.revision) throw new Error(`Plan ${plan.plan_year} changed; no update made to that plan`);
|
|
const history = fresh.history && typeof fresh.history === "object" ? fresh.history : {};
|
|
await pb.collection("management_plans").update(plan.id, {
|
|
history: { ...history, retentionEdits: result.retentionEdits, retentionAudit: [...(plan.history.retentionAudit ?? []), ...result.audit] },
|
|
revision: plan.revision + 1,
|
|
});
|
|
const saved = planRowFromRecord(await pb.collection("management_plans").getOne(plan.id));
|
|
for (const entry of result.audit) {
|
|
const value = saved.history.retentionEdits?.[entry.project]?.[String(entry.month)];
|
|
if (!value || value.forecast !== entry.value.actual || value.actual !== entry.value.actual) throw new Error(`Could not verify ${plan.plan_year} retention edit`);
|
|
}
|
|
}
|
|
console.log(JSON.stringify({ totalRetentionCells: changed, applied: apply }));
|
|
}
|
|
main().catch(() => { console.error("Retention backfill failed. No income or cost records were changed."); process.exitCode = 1; });
|