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 the rta row. 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 } >, width 0 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

  • all โ€” the All Accounts pseudo-row, id 0.

  • account โ€” a real account; id is its id.

  • closed-toggle โ€” the CLOSED (n) fold.

  • balance โ€” single-account view only, and only at BALANCE-MIN-COLS cells 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;

App::Moneymoor v0.4.2

YNAB-style envelope budgeting: a derivation engine

Authors

  • Matt Doughty

License

Artistic-2.0

Dependencies

DBIish:ver<0.6.7>:auth<zef:raku-community-modules>Notcurses::Native:ver<0.6.5+>:auth<zef:apogee>Selkie:ver<0.16.0+>:auth<zef:apogee>JSON::Fast:ver<0.19>:auth<cpan:TIMOTIMO>MacOS::NativeLib:ver<0.0.6>:auth<zef:lizmat>

Test Dependencies

Provides

  • App::Moneymoor
  • App::Moneymoor::Config
  • App::Moneymoor::DB
  • App::Moneymoor::Gateway::Account
  • App::Moneymoor::Gateway::Assignment
  • App::Moneymoor::Gateway::Category
  • App::Moneymoor::Gateway::Payee
  • App::Moneymoor::Gateway::Transaction
  • App::Moneymoor::Handlers::Boot
  • App::Moneymoor::Model::Account
  • App::Moneymoor::Model::Assignment
  • App::Moneymoor::Model::Category
  • App::Moneymoor::Model::CategoryGroup
  • App::Moneymoor::Model::Payee
  • App::Moneymoor::Model::Split
  • App::Moneymoor::Model::Transaction
  • App::Moneymoor::Screen::Accounts
  • App::Moneymoor::Screen::Budget
  • App::Moneymoor::Screen::Login
  • App::Moneymoor::Screen::Main
  • App::Moneymoor::Screen::Main::Keybinds
  • App::Moneymoor::Screen::Main::Modals
  • App::Moneymoor::Screen::Main::Subscriptions
  • App::Moneymoor::Screen::Reports
  • App::Moneymoor::Service::Budget
  • App::Moneymoor::Service::Icons
  • App::Moneymoor::Service::Target
  • App::Moneymoor::Service::Workspace
  • App::Moneymoor::StoreHandlers
  • App::Moneymoor::Theme
  • App::Moneymoor::Theme::Catppuccin
  • App::Moneymoor::Theme::Dracula
  • App::Moneymoor::Theme::Everforest
  • App::Moneymoor::Theme::Gruvbox
  • App::Moneymoor::Theme::Kanagawa
  • App::Moneymoor::Theme::Monokai
  • App::Moneymoor::Theme::Nord
  • App::Moneymoor::Theme::OneDark
  • App::Moneymoor::Theme::RosePine
  • App::Moneymoor::Theme::Solarized
  • App::Moneymoor::Theme::TokyoNight
  • App::Moneymoor::Themes
  • App::Moneymoor::UI
  • App::Moneymoor::Util::Money
  • App::Moneymoor::Util::Period
  • App::Moneymoor::View::BudgetRow
  • App::Moneymoor::View::EmptyState
  • App::Moneymoor::View::HintBar
  • App::Moneymoor::View::InspectorPane
  • App::Moneymoor::View::ModalChrome
  • App::Moneymoor::View::RegisterRow
  • App::Moneymoor::View::ReportRow
  • App::Moneymoor::Widget::BannerBar
  • App::Moneymoor::Widget::BootProgressModal

The Camelia image is copyright 2009 by Larry Wall. "Raku" is a trademark of the Yet Another Society. All rights reserved.

Built with Podlite โ€” the markup and publishing tools behind this site.