Files
wanderbooks/crates/ledger-gui/src/nav.rs
T
swaitsandClaude Opus 5.5 b7b7f17163
check / check (push) Canceled after 0s
feat(viewer): Invoices & Bills, a document's page, overview tiles (P4b S2)
One sidebar item, Invoices & Bills (the owner's choice, 2026-10-24),
with Money in (invoices) and Money out (bills) tabs: what is owed and
overdue, and the list tied to its receivables or payables account
(Agrees, or what to look at); filters Open, Overdue, Paid, Void, All;
rows by due date, an overdue one saying how many days late, each named
in full for assistive tech. A document's page: total, received or paid,
still owed, status; its lines with their projects; its timeline, a
payment later reversed marked so, each row opening its entry. The
overview adds Owed to you and You owe on bills once the books have
documents. Every figure is the document's derived state (rules.md B-38).
The sidebar's rows (and the rail's) give way to 22 points, not 24, so
13 links still fit the owner's 1479x519 window unscrolled; the
sidebar's tests compute heights from the link count; the 1440x920
golden is re-blessed for the new link.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
2026-10-24 15:33:04 -06:00

259 lines
7.6 KiB
Rust

//! Where you are and how you got there: a browser-style history.
use ledger_core::{AccountId, AssetId, EntryId, PartyId};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ReportKind {
ProfitAndLoss,
BalanceSheet,
CashFlow,
TrialBalance,
GeneralLedger,
MembersEquity,
TaxSummary,
ArAging,
ApAging,
Vendor1099,
FixedAssetRegister,
}
impl ReportKind {
pub const ALL: [ReportKind; 11] = [
ReportKind::ProfitAndLoss,
ReportKind::BalanceSheet,
ReportKind::CashFlow,
ReportKind::MembersEquity,
ReportKind::TrialBalance,
ReportKind::GeneralLedger,
ReportKind::FixedAssetRegister,
ReportKind::TaxSummary,
ReportKind::Vendor1099,
ReportKind::ArAging,
ReportKind::ApAging,
];
pub fn title(self) -> &'static str {
match self {
ReportKind::ProfitAndLoss => "Profit and loss",
ReportKind::BalanceSheet => "Balance sheet",
ReportKind::CashFlow => "Cash flow",
ReportKind::TrialBalance => "Trial balance",
ReportKind::GeneralLedger => "General ledger",
ReportKind::MembersEquity => "Owners' equity",
ReportKind::TaxSummary => "Tax summary",
ReportKind::ArAging => "Receivables aging",
ReportKind::ApAging => "Payables aging",
ReportKind::Vendor1099 => "1099 worksheet",
ReportKind::FixedAssetRegister => "Fixed asset register",
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Route {
Overview,
Cash,
Profit,
Transactions,
Entry(EntryId),
Reports(ReportKind),
Accounts,
Account(AccountId),
/// One reconciliation: its statement, balance, and the lines it
/// cleared.
Reconciliation(u64),
/// The fixed asset register.
Assets,
/// One fixed asset.
Asset(AssetId),
/// The party directory.
Parties,
/// One party.
Party(PartyId),
/// The projects (classes with their facts).
Projects,
/// One project.
Project(ledger_core::ClassId),
/// Invoices: what customers owe the company (money in).
Receivables,
/// Bills: what the company owes vendors (money out).
Payables,
/// One invoice.
Invoice(ledger_core::InvoiceId),
/// One bill.
Bill(ledger_core::BillId),
Documents,
Document(u64),
Company,
Journal,
JournalEvent(u64),
}
impl Route {
/// The sidebar section this route belongs to.
pub fn section(&self) -> Section {
match self {
Route::Overview => Section::Overview,
Route::Cash => Section::Cash,
Route::Profit => Section::Profit,
Route::Transactions | Route::Entry(_) => Section::Transactions,
Route::Reports(_) => Section::Reports,
Route::Accounts | Route::Account(_) | Route::Reconciliation(_) => Section::Accounts,
Route::Assets | Route::Asset(_) => Section::Assets,
Route::Parties | Route::Party(_) => Section::Parties,
Route::Projects | Route::Project(_) => Section::Projects,
Route::Receivables | Route::Payables | Route::Invoice(_) | Route::Bill(_) => {
Section::Invoices
}
Route::Documents | Route::Document(_) => Section::Documents,
Route::Company => Section::Company,
Route::Journal | Route::JournalEvent(_) => Section::Journal,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Section {
Overview,
Cash,
Profit,
Transactions,
Reports,
Accounts,
Assets,
Parties,
Projects,
Invoices,
Documents,
Company,
Journal,
}
impl Section {
pub const ALL: [Section; 13] = [
Section::Overview,
Section::Cash,
Section::Profit,
Section::Transactions,
Section::Reports,
Section::Accounts,
Section::Assets,
Section::Parties,
Section::Projects,
Section::Invoices,
Section::Documents,
Section::Company,
Section::Journal,
];
pub fn title(self) -> &'static str {
match self {
Section::Overview => "Overview",
Section::Cash => "Cash",
Section::Profit => "Profit",
Section::Transactions => "Transactions",
Section::Reports => "Reports",
Section::Accounts => "Accounts",
Section::Assets => "Assets",
Section::Parties => "Parties",
Section::Projects => "Projects",
Section::Invoices => "Invoices & Bills",
Section::Documents => "Documents",
Section::Company => "Company",
Section::Journal => "Journal",
}
}
pub fn icon(self) -> &'static str {
use egui_phosphor::regular as i;
match self {
Section::Overview => i::SQUARES_FOUR,
Section::Cash => i::BANK,
Section::Profit => i::TREND_UP,
Section::Transactions => i::LIST_BULLETS,
Section::Reports => i::FILE_TEXT,
Section::Accounts => i::TREE_STRUCTURE,
Section::Assets => i::PACKAGE,
Section::Parties => i::ADDRESS_BOOK,
Section::Projects => i::BRIEFCASE,
Section::Invoices => i::RECEIPT,
Section::Documents => i::FILES,
Section::Company => i::BUILDINGS,
Section::Journal => i::SCROLL,
}
}
pub fn home(self) -> Route {
match self {
Section::Overview => Route::Overview,
Section::Cash => Route::Cash,
Section::Profit => Route::Profit,
Section::Transactions => Route::Transactions,
Section::Reports => Route::Reports(ReportKind::ProfitAndLoss),
Section::Accounts => Route::Accounts,
Section::Assets => Route::Assets,
Section::Parties => Route::Parties,
Section::Projects => Route::Projects,
Section::Invoices => Route::Receivables,
Section::Documents => Route::Documents,
Section::Company => Route::Company,
Section::Journal => Route::Journal,
}
}
}
/// ```text
/// GOAL: back and forward behave like a web browser's.
/// SOURCE: CONVENTION (browser session history: going somewhere new
/// drops the forward list).
/// INVARIANTS:
/// 1. go(r) to a different route makes it current, pushes the old one
/// on back, and clears forward; go to the current route does nothing.
/// 2. back() then forward() returns to the same route (and vice versa);
/// either does nothing when its list is empty.
/// 3. can_back/can_forward say whether back/forward would move.
/// ```
/// Back/forward history.
pub struct Nav {
back: Vec<Route>,
forward: Vec<Route>,
pub current: Route,
}
impl Nav {
pub fn new(start: Route) -> Self {
Self {
back: Vec::new(),
forward: Vec::new(),
current: start,
}
}
pub fn go(&mut self, to: Route) {
if to != self.current {
self.back.push(std::mem::replace(&mut self.current, to));
self.forward.clear();
}
}
pub fn can_back(&self) -> bool {
!self.back.is_empty()
}
pub fn can_forward(&self) -> bool {
!self.forward.is_empty()
}
pub fn back(&mut self) {
if let Some(r) = self.back.pop() {
self.forward.push(std::mem::replace(&mut self.current, r));
}
}
pub fn forward(&mut self) {
if let Some(r) = self.forward.pop() {
self.back.push(std::mem::replace(&mut self.current, r));
}
}
}