RegisterRow
NAME
App::Moneymoor::View::RegisterRow - the row models behind the accounts sidebar and the transaction register, including the running balance.
SYNOPSIS
use App::Moneymoor::View::RegisterRow;
# The sidebar: one string per row, plus the metadata the screen acts on.
my @side = sidebar-rows(
accounts => $ws.accounts.find-all(:include-closed),
view => $view,
icons => icons('unicode'),
);
@side[0]<label>; # 'All Accounts ยฃ1,240.00'
@side[1]<kind>; # 'header' โ BUDGET, not actionable
@side[2]<id>; # the first account's id
net-worth(accounts => @accounts, view => $view); # ฮฃ working, open only
# The register: rows straight into Selkie::Widget::Table.
my @rows = register-rows(
transactions => $ws.transactions.find-by-account($current.id),
payees => %payees-by-id,
categories => %categories-by-id,
accounts => %accounts-by-id,
splits => %splits-by-txn-id,
peer-accounts => %account-id-by-txn-id,
running-balance => True,
icons => $icons,
);
@rows[0]<payee>; # 'โ Savings' (a transfer leg)
@rows[1]<category>; # '(3 splits)'
@rows[1]<outflow>; # ' ยฃ42.10 '
@rows[1]<balance>; # ' ยฃ957.90 '
# The columns those rows fill, which depend on the view and the width:
register-columns(width => 120).map(*<name>);
# (date payee category cleared outflow inflow balance)
register-columns(width => 80).map(*<name>);
# (date payee category cleared outflow inflow) โ no room
register-columns(all-accounts => True, width => 120).map(*<name>);
# (date payee category cleared outflow inflow account)
next-cleared-state('cleared'); # 'reconciled'
DESCRIPTION
Pure functions. Models, lookup hashes and a BudgetView go in, row
hashes come out โ no widgets, no store, no database. The awkward parts
of a register (a running balance that only makes sense in one order, a
transfer leg that has to name the account at the other end, a split
set that renders three different ways) are all here, where they can be
tested without a terminal.
Rows come back newest first
register-rows returns rows in reverse chronological order โ the most
recent transaction first โ because a register grows at the recent end and
scrolling to the bottom of a long ledger to reach today is tedious.
This is a presentation order only. The gateway still returns (date, id)
ascending and must keep doing so: Service::Budget derives each period
from the running balance at the instant of each purchase, and the reports
depend on the same ordering. The reversal happens once, on the finished row
list, at the very end of register-rows.
Selection survives the flip: Screen::Accounts re-finds the selected row by
transaction id rather than by index, so the cursor follows its row.
The running balance is a scan, and only in one view
balance is the cumulative sum of every amount up to and including
the row, in the (date, id) order the gateway returns โ not in the
order the rows are displayed. The scan runs before the reversal above, so
each row's balance is the account's balance as of that transaction; running
it over reversed input would instead count backwards from zero, which yields
a column that looks plausible and is wrong.
That is a number about one account: in the All Accounts view it would be the
sum of unrelated ledgers interleaved by date, which is not a balance of
anything. So :running-balance is off by default, the column is only
added in the single-account view, and the All Accounts view spends the
same columns on an account column instead.
The column is also dropped below BALANCE-MIN-COLS (95) โ see
register-columns. Losing the running balance on a narrow terminal is
better than losing the pence off every figure in the row, which is what
Table truncation would do.
What a transfer leg says
A transfer is two rows in two registers, each with the peer's id in
transfer_peer_id and no payee. The payee cell therefore renders
'โ <the other account>', which needs a hop the row builder cannot do
on its own: peer transaction id โ account id โ account. That hop is the
:%peer-accounts parameter (transaction id โ account id); the screen
builds it once per repaint. A peer that is not in the map renders as a
bare 'โ Transfer' rather than a blank cell โ a leg with an unknown
peer is still legibly a transfer.
The category cell has three shapes
no splits โ
'โ'. An uncategorised transaction: every transaction on a tracking account, and both legs of a transfer that stays on one side of the budget.one split โ the category's name, or
'Inflow: Ready to Assign'when it is thertarow. The engine stores a single-category transaction as exactly one split, so this is the common case rather than a special one.more than one โ
'(3 splits)'. The figures are in the split editor; the register's job is to say that there are some.
Reconciling changes the title, not the rows
ยง4.5's reconcile mode is a state of the pane, not of the rows: the
register keeps showing the same transactions, c keeps cycling the
same three states, and the only thing that changes is the frame. So
this module's part in it is four small functions โ register-title's
:$statement variant, reconcile-diff, reconcile-style and
promotable-ids โ and no change at all to register-rows.
promotable-ids is the rule the whole flow turns on: finishing a
reconciliation promotes the transactions the user marked cleared,
and nothing else. An uncleared row is one the statement did not
mention, and sweeping it up would put a "this matched a statement"
mark on a transaction no statement has ever seen.
ยง2, and what a Table can colour
Selkie::Widget::Table resolves one style per row โ row-style
receives the whole row hash and returns a single Selkie::Style โ so
the register cannot paint the Outflow cell red and leave the rest of
the row alone. ยง2's "outflow amounts red / inflow green" is written
down here as the severity key on every row, and register-style
resolves it the only way a whole-row painter sensibly can:
| Row | severity | Style |
|---|---|---|
| reconciled | reconciled | fg-dim |
| money in | inflow | fg-green |
| money out | outflow | fg-base |
| zero, or no direction | normal | fg-base |
| sidebar section header | header | fg-bright bold on bg-surface |
Outflow is the default state of a register โ a month of shopping is mostly outflow โ and painting every one of those rows red would leave nothing for the colour to mean. Inflow keeps its green because it is the exception the eye is looking for. Reconciled outranks both: a locked row is finished business whichever way the money went.
The resolution itself is delegated to
App::Moneymoor::View::BudgetRow::severity-style, so the register and
the envelope grid cannot end up disagreeing about what the palette's
green is.
Column widths, and the two that are not ยง4.4's
ยง4.4 pins the register's columns. Two are one cell wider here, for the
same reason BudgetRow's money cells reserve a trailing gutter:
Table lays columns out edge to edge and pads every cell to the full
column width, so a cell whose content exactly fills its column touches
its neighbour.
date โ ยง4.4 says 10, which is exactly the width of
'YYYY-MM-DD'. At 10 the date would run into the payee; at 11 it keeps the one-cell gutter every other column has.cleared โ ยง4.4 says 1, which is exactly the width of the state glyph. At 2 (a leading space, then the glyph) the icon stops touching a category name long enough to fill the flex column.
Every other width is ยง4.4's.
EXPORTS
register-rows(:@transactions!, :%payees, :%categories, :%accounts, :%splits, :%peer-accounts, :$running-balance, :$icons --List)>register-columns(:$all-accounts, :$width, --List)> โ< { name, label, width } >,width0 meaning flex.register-title(Str $name, $balance, :$statement --Str)>reconcile-diff(Int $statement, $balance --Int)> /reconcile-style(Int $diff, :$theme! --Selkie::Style)> /promotable-ids(@transactions --List)>sidebar-rows(:@accounts!, :$view!, :$show-closed, :$width, :$icons --List)>net-worth(:@accounts!, :$view! --Int)>working-balance($view, $id --Int)>sidebar-label(Str $left, Str $right, Int $width --Str)>account-icon($account, :$icons --Str)>cleared-icon(Str $state, :$icons --Str)> /next-cleared-state(Str $state --Str)>register-style(Str $severity, :$theme! --Selkie::Style)>DATE-COLS/CLEARED-COLS/OUTFLOW-COLS/INFLOW-COLS/BALANCE-COLS/ACCOUNT-COLS/BALANCE-MIN-COLS/SIDEBAR-COLS/SIDEBAR-TEXT-COLS/UNCATEGORISED-CELL/RTA-LABEL/CLEARED-STATES
SEE ALSO
App::Moneymoor::Screen::Accounts โ the widget layer.
App::Moneymoor::View::BudgetRow โ
money-cell,money-headerand the severity palette these share.App::Moneymoor::Gateway::Transaction โ the
(date, id)order the running balance depends on.
allโ the All Accounts pseudo-row, id 0.accountโ a real account;idis its id.closed-toggleโ theCLOSED (n)fold.
balance โ single-account view only, and only at
BALANCE-MIN-COLScells or wider. See "The running balance".account โ All Accounts only, where "which ledger is this row in" is the question the balance column would otherwise be answering badly.
:%payees/:%categories/:%accountsโ id โ model.:%splitsโ transaction id โ its splits.:%peer-accountsโ transaction id โ the account it belongs to. Only transfer peers are ever looked up in it.:$running-balanceโ off by default; see the Pod. ) sub register-rows( :@transactions!, :%payees = %(), :%categories = %(), :%accounts = %(), :%splits = %(), :%peer-accounts = %(), Bool :$running-balance = False, :$icons = icons(), --> List ) is export { my Int $running = 0; my @rows;