// Quarterly budget ledger: a two-page accounting book. The left page sets sector
// budgets, the right page sets tax rates and shows the Treasury summary and a
// four-quarter projection. Nothing is applied until the player signs.
import { t, fmt, getLang } from "../i18n/i18n.js";
import {
previewBudget, setBudget, confirmBudget, qualityFromRatio, taxRevenue, TAX_IDS,
} from "../engine/index.js";
import { $ } from "./desk.js";
const ALLOC_STEP = 0.05;
const TAX_STEP = 0.5;
// Projection rows: label key, field on the summary, digits, suffix
const PROJ_ROWS = [
["econ.debt", (p) => p.debtRatio, 1, ""],
["econ.deficit", (p) => p.deficit, 1, ""],
["econ.growth", (p) => p.growth, 1, ""],
["econ.unemployment", (p) => p.unemployment, 1, ""],
["ind.opinion", (p) => p.indicators.opinion, 0, ""],
["ind.stability", (p) => p.indicators.stability, 0, ""],
["ind.economy", (p) => p.indicators.economy, 0, ""],
];
export function createLedger(root, scenario, { onConfirm }) {
let openKey = null;
let ctx = null; // { state, bal }
let draft = null;
let base = null;
let queued = false;
const sectorIds = (state) => Object.keys(state.sectors);
function open(state, bal) {
ctx = { state, bal };
base = {
alloc: Object.fromEntries(Object.entries(state.sectors).map(([id, s]) => [id, s.alloc])),
taxes: { ...state.economy.taxes },
};
draft = structuredClone(base);
const allocMax = Math.min(bal.limits.allocMaxRatio, bal.limits.sliderMaxRatio);
const sectorRows = sectorIds(state).map((id) => {
const s = state.sectors[id];
const max = Math.max(s.need * allocMax, s.alloc);
return `
`;
}).join("");
const taxRows = TAX_IDS.map((id) => {
const max = bal.tax[id].peak * bal.limits.taxMaxFactorOfPeak;
return `
`;
}).join("");
root.innerHTML = `
${t("ledger.title")}
${state.risk.emergency ? `${t("ledger.emergency")}
` : ""}
${t("ledger.spending")}. ${t("ledger.spendingHint")}
${sectorRows}
${t("ledger.revenue")}
${t("ledger.revenueHint")}
${taxRows}
${t("ledger.summary")}
${t("ledger.projection")}
${t("ledger.error")}
`;
root.hidden = false;
refresh();
}
function close() {
root.hidden = true;
root.innerHTML = "";
openKey = null;
ctx = null;
}
// Cheap parts (values next to sliders) run immediately; the preview is coalesced per frame.
function refresh() {
const { state, bal } = ctx;
let total = 0;
for (const id of sectorIds(state)) {
const s = state.sectors[id];
const v = draft.alloc[id];
total += v;
const row = $(root, `[data-sector="${id}"]`);
$(row, ".lrow__val").textContent = fmt(v, 2);
const target = qualityFromRatio(v / s.need, bal);
const diff = target - s.quality;
const q = $(row, ".lrow__q");
q.textContent = diff > 1 ? "▲" : diff < -1 ? "▼" : "=";
q.className = `lrow__q ${diff > 1 ? "up" : diff < -1 ? "down" : "flat"}`;
q.title = t("ledger.quality", { now: Math.round(s.quality), target: Math.round(target) });
row.classList.toggle("is-changed", Math.abs(v - base.alloc[id]) > 1e-9);
}
$(root, "[data-total]").textContent = `${fmt(total, 2)} %`;
for (const id of TAX_IDS) {
const row = $(root, `[data-tax="${id}"]`);
$(row, ".lrow__val").textContent = `${fmt(draft.taxes[id], 1)} %`;
$(row, ".lrow__yield").textContent = t("ledger.yield", { n: fmt(taxRevenue(id, draft.taxes[id], bal), 1) });
row.classList.toggle("is-changed", Math.abs(draft.taxes[id] - base.taxes[id]) > 1e-9);
}
if (!queued) {
queued = true;
requestAnimationFrame(() => {
queued = false;
if (ctx) renderPreview();
});
}
}
function renderPreview() {
const { state, bal } = ctx;
const pv = previewBudget(state, bal, draft);
if (!pv.ok) return;
const f = pv.fiscal;
const gdp = state.economy.gdp;
const bn = (pct) => fmt(Math.abs((pct / 100) * gdp), 1);
const balance = -f.deficit; // positive = surplus
$(root, "[data-summary]").innerHTML = `
${t("ledger.sumRevenue")}${fmt(f.revenue, 1)} %
${t("ledger.sumSectors")}${fmt(f.allocTotal, 1)} %
${t("ledger.sumInterest")}${fmt(f.interest, 1)} %
${t("ledger.sumSpending")}${fmt(f.spending, 1)} %
${t(balance >= 0 ? "ledger.sumSurplus" : "ledger.sumDeficit")}
${fmt(Math.abs(balance), 1)} %
${t("ledger.perYear", { n: bn(balance), currency: scenario.currency })}`;
const months = (i) => (i === 0 ? t("ledger.now") : t("ledger.months", { n: i * 3 }));
$(root, ".proj thead").innerHTML = ` | ${pv.points.map((_, i) => `${months(i)} | `).join("")}
`;
$(root, ".proj tbody").innerHTML = PROJ_ROWS.map(([label, get, digits, suffix]) =>
`| ${t(label)} | ${pv.points.map((p) => `${fmt(get(p), digits)}${suffix} | `).join("")}
`).join("");
}
function syncSliders() {
for (const id of sectorIds(ctx.state)) $(root, `#alloc-${id}`).value = draft.alloc[id];
for (const id of TAX_IDS) $(root, `#tax-${id}`).value = draft.taxes[id];
}
root.addEventListener("input", (e) => {
const input = e.target;
if (!ctx || input.type !== "range") return;
const v = Number(input.value);
if (input.id.startsWith("alloc-")) draft.alloc[input.id.slice(6)] = v;
else if (input.id.startsWith("tax-")) draft.taxes[input.id.slice(4)] = v;
refresh();
});
root.addEventListener("click", (e) => {
const b = e.target.closest("button[data-act]");
if (!b || !ctx) return;
if (b.dataset.act === "reset") {
draft = structuredClone(base);
syncSliders();
refresh();
} else {
const { state, bal } = ctx;
const res = setBudget(state, bal, draft);
if (!res.ok) {
$(root, "[data-error]").hidden = false;
return;
}
confirmBudget(state);
close();
onConfirm();
}
});
function update(state, bal) {
if (!(state.pending.review && !state.pending.profile && !state.over)) {
if (openKey !== null) close();
return;
}
const key = `${state.day}:${getLang()}`;
if (key !== openKey) {
openKey = key;
open(state, bal);
}
}
return { update };
}