financial page with bank data
All checks were successful
Deploy / build-deploy (push) Successful in 1m48s

This commit is contained in:
Jorijn van der Graaf 2026-08-14 04:14:13 +02:00
commit 33c68c2f44
11 changed files with 394 additions and 87 deletions

View file

@ -1473,32 +1473,22 @@ export RenderedPage RenderFinancials(std::int64_t salesCount,
std::vector<SafeHtml> incomeRows;
if (fin.Loaded()) {
incomeRows.push_back(MoneyRow(
fin.donationCount == 1 ? std::string("Donations (1)")
: std::format("Donations ({})", fin.donationCount),
std::format("Donations ({})", fin.donationCount),
fin.donationsMinor));
}
incomeRows.push_back(MoneyRow(
salesCount == 1 ? std::string("Sales (1 order)")
: std::format("Sales ({} orders)", salesCount),
std::format("Sales ({})", salesCount),
salesTotalMinor));
if (fin.Loaded()) {
incomeRows.push_back(totalRow("Income", fin.donationsMinor + salesTotalMinor));
}
// Expenses: one table, group-header rows for the recurring/one-off split,
// so the amounts stay in a single aligned column.
// Expenses: one flat table. No recurring/one-off grouping — see the note
// on Financials::expenses for why a lifetime total cannot carry a rate.
SafeHtml expenses;
if (fin.Loaded()) {
std::vector<SafeHtml> rows;
auto group = [&](std::string_view heading, std::span<const FinCategory> cats) {
if (cats.empty()) return;
rows.push_back(Format(
R"(<tr class="fin-group"><th colspan="2">{}</th></tr>)",
Escape(heading)));
for (const FinCategory& c : cats) rows.push_back(MoneyRow(c.label, c.totalMinor));
};
group("Recurring", fin.recurring);
group("One-off", fin.single);
for (const FinCategory& c : fin.expenses) rows.push_back(MoneyRow(c.label, c.totalMinor));
rows.push_back(totalRow("Expenses", fin.ExpensesMinor()));
expenses = Format(R"(<table class="spec-table"><tbody>{}</tbody></table>)",
Join(rows));
@ -1509,6 +1499,33 @@ export RenderedPage RenderFinancials(std::int64_t salesCount,
R"(figures above are already live.</p>)");
}
// Net: what is actually left. Shown only when the bank figures exist —
// income minus an unknown expense side is not a net of anything, and a
// figure equal to sales while expenses are unpublished would read as a
// company with no costs.
//
// Deliberately NOT called profit. On a cash basis this ignores stock
// still on the shelf, anything owed in either direction, and tax not yet
// paid, so calling it profit would be a claim the arithmetic cannot
// support. FormatEuro renders a negative as "€-12.34", which is the
// honest thing to show in a month that bought inventory.
const std::int64_t netMinor =
fin.donationsMinor + salesTotalMinor - fin.ExpensesMinor();
SafeHtml netBlock;
if (fin.Loaded()) {
netBlock = Format(
R"(<section class="section"><h2 class="section__title">Net</h2>)"
R"(<table class="spec-table"><tbody>{}</tbody></table>)"
R"(<p class="section__lede">On the same cash basis as everything above. )"
R"(This is not profit: it counts no stock still on the shelf, nothing )"
R"(owed in either direction, and no tax.</p>)"
R"(</section>)",
// The row says the arithmetic rather than repeating the heading —
// a section titled "Net" whose only row is also "Net" reads as a
// rendering fault, and the sum is worth spelling out anyway.
totalRow("Income expenses", netMinor));
}
// The freshness line keeps the page honest about its two cadences.
const SafeHtml freshness = fin.Loaded()
? Format(R"(<p class="legal__updated">Sales are live from the order ledger &middot; )"
@ -1539,10 +1556,11 @@ export RenderedPage RenderFinancials(std::int64_t salesCount,
R"(<p class="page-header__lede">{}</p>)"
R"({})"
R"(</header>)"
R"(<div class="fin"{}{}{}{}{}>)"
R"(<div class="fin"{}{}{}{}{}{}>)"
R"(<section class="section"><h2 class="section__title">Income</h2>)"
R"(<table class="spec-table"><tbody>{}</tbody></table></section>)"
R"(<section class="section"><h2 class="section__title">Expenses</h2>{}</section>)"
R"({})"
R"(</div>)"
R"(<div class="legal">{}</div>)",
Escape(notes.title), Escape(notes.lede), freshness,
@ -1554,7 +1572,8 @@ export RenderedPage RenderFinancials(std::int64_t salesCount,
: SafeHtml{},
fin.Loaded() ? Attr("data-fin-expenses-minor", std::to_string(fin.ExpensesMinor()))
: SafeHtml{},
Join(incomeRows), expenses, Join(sections));
fin.Loaded() ? Attr("data-fin-net-minor", std::to_string(netMinor)) : SafeHtml{},
Join(incomeRows), expenses, netBlock, Join(sections));
return page;
}