ledgr 0.1.0

A TUI personal finance app: import bank/pension statements, build a local database, and analyze your finances.
use crate::app::{App, Screen};
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use ratatui::Frame;

pub fn draw(frame: &mut Frame, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(3), Constraint::Length(1)])
        .split(frame.area());

    match app.screen {
        Screen::Accounts => draw_accounts(frame, app, chunks[0]),
        Screen::Transactions => draw_transactions(frame, app, chunks[0]),
    }
    draw_status(frame, app, chunks[1]);
}

fn draw_accounts(frame: &mut Frame, app: &App, area: Rect) {
    let items: Vec<ListItem> = if app.accounts.is_empty() {
        vec![ListItem::new(
            "No accounts yet. Import a statement to get started.",
        )]
    } else {
        app.accounts
            .iter()
            .enumerate()
            .map(|(i, account)| {
                let label = format!(
                    "{}  ({}{})",
                    account.name,
                    account.account_type.as_str(),
                    account
                        .institution
                        .as_ref()
                        .map(|inst| format!(", {inst}"))
                        .unwrap_or_default()
                );
                style_item(label, i == app.selected_account)
            })
            .collect()
    };

    let list = List::new(items).block(Block::default().title("Accounts").borders(Borders::ALL));
    frame.render_widget(list, area);
}

fn draw_transactions(frame: &mut Frame, app: &App, area: Rect) {
    let items: Vec<ListItem> = if app.transactions.is_empty() {
        vec![ListItem::new("No transactions for this account yet.")]
    } else {
        app.transactions
            .iter()
            .enumerate()
            .map(|(i, tx)| {
                let amount = tx.amount_minor as f64 / 100.0;
                let label = format!(
                    "{}  {:>10.2} {}  {}",
                    tx.posted_at, amount, tx.currency, tx.description
                );
                style_item(label, i == app.selected_transaction)
            })
            .collect()
    };

    let account_name = app
        .accounts
        .get(app.selected_account)
        .map(|a| a.name.as_str())
        .unwrap_or("Transactions");
    let list = List::new(items).block(
        Block::default()
            .title(format!("Transactions \u{2014} {account_name}"))
            .borders(Borders::ALL),
    );
    frame.render_widget(list, area);
}

fn style_item(label: String, selected: bool) -> ListItem<'static> {
    if selected {
        ListItem::new(Line::from(Span::styled(
            label,
            Style::default()
                .fg(Color::Black)
                .bg(Color::White)
                .add_modifier(Modifier::BOLD),
        )))
    } else {
        ListItem::new(label)
    }
}

fn draw_status(frame: &mut Frame, app: &App, area: Rect) {
    let paragraph = Paragraph::new(app.status.as_str()).style(Style::default().fg(Color::DarkGray));
    frame.render_widget(paragraph, area);
}