got working history tab

This commit is contained in:
Nickiel12 2023-08-11 22:46:43 -07:00
parent b3ab5a5865
commit 20700e8a38
9 changed files with 215 additions and 225 deletions

View file

@ -1,69 +1,18 @@
use std::sync::Mutex;
use std::sync::Arc; use std::sync::Arc;
use crossterm::event::{Event, self, KeyCode}; use crossterm::event::{Event, self, KeyCode};
use tokio; use tokio;
use ratatui::widgets::ListState;
use crossbeam_channel::Receiver;
use log::warn; use log::warn;
use crate::db::DB; use crate::db::DB;
use crate::db::data_cache::DataCache;
use crate::uis::history::HistoryState; use crate::uis::history::HistoryState;
use crate::uis::new_transaction::NewTransactionTabState; use crate::uis::new_transaction::NewTransactionTabState;
use crate::uis::navigation_frame::NavigationState; use crate::uis::navigation_frame::NavigationState;
use crate::db::transaction::TransactionRecord;
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>; pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub struct StatefulList<T> {
pub state: ListState,
pub items: Vec<T>,
}
impl<T> StatefulList<T> {
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
StatefulList {
state: ListState::default(),
items,
}
}
pub fn next(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i >= self.items.len() - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn previous(&mut self) {
let i = match self.state.selected() {
Some(i) => {
if i == 0 {
self.items.len() - 1
} else {
i - 1
}
}
None => 0,
};
self.state.select(Some(i));
}
pub fn unselect(&mut self) {
self.state.select(None);
}
}
pub enum InputMode { pub enum InputMode {
Insert, Insert,
Normal, Normal,
@ -77,17 +26,17 @@ pub struct App<'a> {
pub input_mode: InputMode, pub input_mode: InputMode,
pub db: Arc<tokio::sync::Mutex<DB>>, pub db: Arc<tokio::sync::Mutex<DB>>,
pub records: Arc<Mutex<Vec<TransactionRecord>>>, pub data_cache: DataCache,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
pub fn new(db: DB, records: Arc<Mutex<Vec<TransactionRecord>>>, r: Receiver<bool>) -> App<'a> { pub fn new(db: DB, data_cache: DataCache) -> App<'a> {
App { App {
running: true, running: true,
states: States::new(r), states: States::new(),
input_mode: InputMode::Normal, input_mode: InputMode::Normal,
db: Arc::new(tokio::sync::Mutex::new(db)), db: Arc::new(tokio::sync::Mutex::new(db)),
records, data_cache,
} }
} }
@ -160,20 +109,16 @@ pub struct States<'a> {
pub transactions: NewTransactionTabState<'a>, pub transactions: NewTransactionTabState<'a>,
pub history: HistoryState, pub history: HistoryState,
pub new_data: Receiver<bool>,
pub active_frame: ActiveFrame, pub active_frame: ActiveFrame,
} }
impl<'a> States<'a> { impl<'a> States<'a> {
pub fn new(r: Receiver<bool>) -> States<'a> { pub fn new() -> States<'a> {
States { States {
nav_state: NavigationState::new(), nav_state: NavigationState::new(),
transactions: NewTransactionTabState::new(), transactions: NewTransactionTabState::new(),
history: HistoryState::new(), history: HistoryState::new(),
new_data: r,
active_frame: ActiveFrame::Navigation, active_frame: ActiveFrame::Navigation,
} }
} }

View file

@ -1,60 +1,46 @@
use std::sync::Mutex;
use std::sync::Arc;
use crossbeam_channel::Sender;
use futures::TryStreamExt; use futures::TryStreamExt;
use sqlx::Error; use sqlx::Error;
use sqlx::PgPool; use sqlx::PgPool;
use sqlx::Row;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use sqlx::postgres::types::PgMoney;
use time::Date;
use crate::db::transaction::TransactionRecord; use super::data_cache::DataCache;
use super::tables::Transaction;
pub struct DB { pub struct DB {
conn_pool: PgPool, conn_pool: PgPool,
new_data_notify: Sender<bool>, data_cache: DataCache,
records: Arc<Mutex<Vec<TransactionRecord>>>,
} }
impl DB { impl DB {
pub async fn new(records: Arc<Mutex<Vec<TransactionRecord>>>, s: Sender<bool>) -> Result<DB, sqlx::Error> { pub async fn new(data_cache: DataCache) -> Result<DB, sqlx::Error> {
let connection_pool = PgPoolOptions::new() let connection_pool = PgPoolOptions::new()
.max_connections(3) .max_connections(3)
.connect("postgres://rcntuser:Devel@pmentPa$$w0rd@10.0.0.183/Borealis").await?; .connect("postgres://rcntuser:Devel@pmentPa$$w0rd@10.0.0.183/Borealis").await?;
Ok(DB { Ok(DB {
conn_pool: connection_pool, conn_pool: connection_pool,
new_data_notify: s, data_cache,
records,
}) })
} }
pub async fn get_all_records(&mut self) -> Result<(), Error> { pub async fn get_all_records(&mut self) -> Result<(), Error> {
let mut rows = sqlx::query("SELECT trns_id, trns_amount, trns_description, trns_account, trns_bucket, trns_date FROM rcnt.transactions") let mut rows = sqlx::query_as::<_, Transaction>("SELECT trns_id, trns_amount, trns_description, trns_account, trns_bucket, trns_date FROM rcnt.transactions ORDER BY trns_id DESC")
.fetch(&self.conn_pool); .fetch(&self.conn_pool);
while let Some(row) = rows.try_next().await? { let mut temp_transactions: Vec<Transaction> = Vec::new();
let id: i32 = row.try_get("trns_id")?;
let amount: PgMoney = row.try_get("trns_amount")?;
let date: Date = row.try_get("trns_date")?;
self.records.lock().unwrap().clear();
self.records.lock().unwrap().push( while let Some(row) = rows.try_next().await? {
TransactionRecord { temp_transactions.push(row);
id: id.into(),
amount: amount.to_decimal(2).to_string(),
date: date.to_string()
}
);
} }
self.new_data_notify.try_send(true).unwrap();
{
let mut transactions = self.data_cache.transactions.lock().unwrap();
transactions.clear();
transactions.append(&mut temp_transactions);
}
return Ok(()); return Ok(());

32
src/db/data_cache.rs Normal file
View file

@ -0,0 +1,32 @@
use std::sync::{Arc, Mutex};
use super::tables;
#[derive(Clone)]
pub struct DataCache {
pub accounts: Arc<Mutex<Vec<tables::Account>>>,
pub buckets: Arc<Mutex<Vec<tables::Buckets>>>,
pub transaction_catagories: Arc<Mutex<Vec<tables::TransactionCatagories>>>,
pub transaction_breakdowns: Arc<Mutex<Vec<tables::TransactionBreakdown>>>,
pub transactions: Arc<Mutex<Vec<tables::Transaction>>>,
}
impl DataCache {
pub fn new() -> DataCache {
DataCache {
accounts: Arc::new(Mutex::new(Vec::new())),
buckets: Arc::new(Mutex::new(Vec::new())),
transaction_catagories: Arc::new(Mutex::new(Vec::new())),
transaction_breakdowns: Arc::new(Mutex::new(Vec::new())),
transactions: Arc::new(Mutex::new(Vec::new())),
}
}
}

View file

@ -1,8 +1,6 @@
pub mod transaction;
pub use self::transaction::TransactionRecord;
pub mod connection; pub mod connection;
pub use self::connection::DB; pub use self::connection::DB;
pub mod tables; pub mod tables;
pub mod data_cache;

65
src/db/tables.rs Normal file
View file

@ -0,0 +1,65 @@
use std::borrow::Cow;
use sqlx::{FromRow, postgres::types::PgMoney};
use time::Date;
#[derive(Clone, FromRow)]
pub struct Account {
pub acnt_id: i32,
pub acnt_dsply_name: String,
pub acnt_description: String,
}
#[derive(Clone, FromRow)]
pub struct Buckets {
pub bkt_id: i32,
pub bkt_dsply_code: String,
pub bkt_dsply_name: String,
pub bkt_description: String,
}
#[derive(Clone, FromRow)]
pub struct TransactionCatagories {
pub trns_ctgry_id: i32,
pub trns_ctgry_dsply_code: String,
pub trns_ctgry_dsply_name: String,
pub trns_ctgry_description: String,
}
#[derive(Clone, FromRow)]
pub struct TransactionBreakdown {
pub trns_brkdwn_id: i32,
pub trns_brkdwn_amount: PgMoney,
pub trns_brkdwn_parent_transaction: i32,
pub trns_brkdwn_catagory: i32,
pub trns_brkdwn_bucket: i32,
}
#[derive(Clone, FromRow)]
pub struct Transaction{
pub trns_id: i32,
pub trns_amount: PgMoney,
pub trns_description: String,
pub trns_bucket: i32,
pub trns_date: Date
}
impl Transaction {
pub fn get_header() -> String {
return format!(" {:<7} | {:<9} | {:<10}", "Id", "Amount", "Date")
}
pub fn to_string(&self) -> String {
return format!(" T{:0>6} | {:>9} | {:>10}", self.trns_id, self.trns_amount.to_decimal(2).to_string(), self.trns_date.to_string())
}
pub fn as_str(&self) -> Cow<str> {
return self.to_string().into()
}
}

View file

@ -1,9 +0,0 @@
use sqlx::FromRow;
#[derive(Clone, FromRow)]
pub struct Account {
pub acnt_id: i32,
pub acnt_dsply_name: String,
pub acnt_description: String,
}

View file

@ -1,34 +0,0 @@
use std::borrow::Cow;
// cargo add crust_decimal
#[derive(Clone)]
pub struct TransactionRecord {
pub id: i64,
// pub amount: Decimal,
pub amount: String,
//pub record_date: Date,
pub date: String,
}
impl TransactionRecord {
pub fn new(id: i64, amount: String, record_date: String) -> TransactionRecord {
TransactionRecord {
id,
amount,
date: record_date
}
}
pub fn get_header() -> String {
return format!(" {:<7} | {:<9} | {:<10}", "Id", "Amount", "Date")
}
pub fn to_string(&self) -> String {
return format!(" T{:0>6} | {:>9} | {:>10}", self.id, self.amount, self.date)
}
pub fn as_str(&self) -> Cow<str> {
return self.to_string().into()
}
}

View file

@ -1,10 +1,6 @@
use std::io; use std::io;
use std::fs::File; use std::fs::File;
use std::time::Duration; use std::time::Duration;
use std::sync::Mutex;
use std::sync::Arc;
use crossbeam_channel::bounded;
use simplelog::*; use simplelog::*;
@ -13,10 +9,10 @@ use ratatui::{
Terminal, Terminal,
}; };
use recount::db::DB;
use recount::db::data_cache::DataCache;
use recount::app::{App, AppResult}; use recount::app::{App, AppResult};
use recount::tui::Tui; use recount::tui::Tui;
use recount::db::DB;
use recount::db::tables;
#[tokio::main] #[tokio::main]
async fn main() -> AppResult<()> { async fn main() -> AppResult<()> {
@ -24,13 +20,12 @@ async fn main() -> AppResult<()> {
let log_file = "testing_log.txt".to_string(); let log_file = "testing_log.txt".to_string();
init_logger(log_file); init_logger(log_file);
let records = Arc::new(Mutex::new(Vec::new())); let data_cache = DataCache::new();
let (s, r) = bounded::<bool>(2); let db = DB::new(data_cache.clone()).await?;
let db = DB::new(Arc::clone(&records), s).await?;
// Create an application. // Create an application.
let mut app = App::new(db, records, r); let mut app = App::new(db, data_cache);
app.refresh();
// Initialize the terminal user interface. // Initialize the terminal user interface.
let backend = CrosstermBackend::new(io::stderr()); let backend = CrosstermBackend::new(io::stderr());

View file

@ -1,123 +1,135 @@
use crossterm::event::{KeyEvent, KeyEventKind, KeyCode}; use crossterm::event::{KeyEvent, KeyEventKind, KeyCode};
use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Block, Borders, List, ListItem, Paragraph}, text::{Text, Line, Span}, style::{Style, Color, Modifier}}; use ratatui::{backend::Backend, Frame, layout::{Rect, Layout, Direction, Constraint}, widgets::{Block, Borders, List, ListItem, Paragraph}, text::{Text, Line, Span}, style::{Style, Color}};
use crate::{app::{App, StatefulList}, db::TransactionRecord}; use crate::{app::App, db::tables::Transaction};
pub struct HistoryState { pub struct HistoryState {
pub transacts_list: StatefulList<TransactionRecord> pub selected_index: Option<usize>,
} }
impl HistoryState { impl HistoryState {
pub fn new() -> HistoryState { pub fn new() -> HistoryState {
HistoryState { HistoryState {
transacts_list: StatefulList::with_items(vec![ selected_index: None,
TransactionRecord::new(1, "$10.00".to_string(), "05-01-2020".to_string()),
TransactionRecord::new(2, "$10.00".to_string(), "05-01-2020".to_string()),
TransactionRecord::new(3, "$10.00".to_string(), "05-01-2020".to_string()),
TransactionRecord::new(4, "$10.00".to_string(), "05-01-2020".to_string()),
TransactionRecord::new(5, "$10.00".to_string(), "05-01-2020".to_string()),
])
} }
} }
pub fn handle_event(event: KeyEvent, app: &mut App) { pub fn handle_event(event: KeyEvent, app: &mut App) {
if app.states.history.selected_index.is_none() {
app.states.history.selected_index = Some(0);
}
let max_index = app.data_cache.transactions.lock().unwrap().len();
if event.kind == KeyEventKind::Press { if event.kind == KeyEventKind::Press {
match event.code { match event.code {
KeyCode::Tab => app.states.transactions.next_tab(), KeyCode::Up | KeyCode::Tab => app.states.history.next(max_index),
KeyCode::Up => app.states.history.transacts_list.previous(), KeyCode::Down => app.states.history.previous(),
KeyCode::Down => app.states.history.transacts_list.next(),
_ => {} _ => {}
} }
} }
} }
pub fn update_transactions(app: &mut App) { pub fn next(&mut self, max_index: usize) {
app.states.history.transacts_list.items.clear();
for i in app.records.lock().unwrap().iter() { if self.selected_index.is_some() {
app.states.history.transacts_list.items.push(i.clone()); let cur_index = self.selected_index.unwrap();
if cur_index + 1 < max_index {
self.selected_index = Some(cur_index + 1);
}
} }
} }
pub fn previous(&mut self) {
if self.selected_index.is_some() {
let cur_index = self.selected_index.unwrap();
if cur_index > 0 {
self.selected_index = Some(cur_index - 1);
}
}
}
} }
pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &mut App) { pub fn render_history_tab<B: Backend> (f: &mut Frame<B>, body_rect: Rect, app: &mut App) {
let do_update = match app.states.new_data.try_recv() {
Ok(val) => val,
Err(_) => false,
};
if do_update {
HistoryState::update_transactions(app);
}
let split_body = Layout::default() let split_body = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(body_rect); .split(body_rect);
let mut lines: Vec<Line> = vec![]; let mut lines: Vec<Line> = vec![];
if app.states.history.transacts_list.state.selected().is_some(){ {
let selected_item: &TransactionRecord = &app.states.history.transacts_list.items[app.states.history.transacts_list.state.selected().unwrap()]; let transacts_list = app.data_cache.transactions.lock().unwrap();
if app.states.history.selected_index.is_some() {
let selected_item: &Transaction = &transacts_list[app.states.history.selected_index.unwrap()];
let ident_style: Style = Style::default().fg(Color::Yellow); let ident_style: Style = Style::default().fg(Color::Yellow);
let value_style: Style = Style::default().fg(Color::LightBlue); let value_style: Style = Style::default().fg(Color::LightBlue);
lines.push( lines.push(
Line::from(vec![ Line::from(vec![
Span::styled("Transaction Id: ", ident_style), Span::styled("Transaction Id: ", ident_style),
Span::styled(format!("T{:0>6}", selected_item.id), value_style), Span::styled(format!("T{:0>6}", selected_item.trns_id), value_style),
]) ])
); );
lines.push( lines.push(
Line::from(vec![ Line::from(vec![
Span::styled("Amount: ", ident_style), Span::styled("Amount: ", ident_style),
Span::styled(format!("{}", selected_item.amount), value_style) Span::styled(format!("{}", selected_item.trns_amount.to_decimal(2).to_string()), value_style)
]) ])
); );
lines.push( lines.push(
Line::from(vec![ Line::from(vec![
Span::styled("Transaction Date: ", ident_style), Span::styled("Transaction Date: ", ident_style),
Span::styled(format!("{}", selected_item.date), value_style) Span::styled(format!("{}", selected_item.trns_date.to_string()), value_style)
]) ])
); );
} }
let paragraph = Paragraph::new(lines.clone()) let paragraph = Paragraph::new(lines.clone())
.block(Block::default().borders(Borders::ALL).title("Details").title_alignment(ratatui::layout::Alignment::Left)); .block(Block::default().borders(Borders::ALL).title("Details").title_alignment(ratatui::layout::Alignment::Left));
f.render_widget(paragraph, split_body[1]); f.render_widget(paragraph, split_body[1]);
let list_chunks = Layout::default() let list_chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(3)]) .constraints([Constraint::Length(1), Constraint::Min(3)])
.split(split_body[0]); .split(split_body[0]);
let list_header = Paragraph::new( let list_header = Paragraph::new(
Text::styled(TransactionRecord::get_header(), Style::default().fg(Color::Black).bg(Color::Cyan)) Text::styled(Transaction::get_header(), Style::default().fg(Color::Black).bg(Color::Cyan))
).block(Block::default().borders(Borders::NONE)); ).block(Block::default().borders(Borders::NONE));
f.render_widget(list_header, list_chunks[0]); f.render_widget(list_header, list_chunks[0]);
let mut items: Vec<ListItem> = Vec::new();
for i in 0..transacts_list.len() {
let style = match app.states.history.selected_index {
Some(val) => {
if val == i {
Style::default().fg(Color::Black).bg(Color::Yellow)
} else {
Style::default().fg(Color::Black).bg(Color::White)
}
},
None => Style::default().fg(Color::Black).bg(Color::White),
};
items.push(ListItem::new(
Text::from(transacts_list.get(i).unwrap().as_str())
).style(style));
}
// Iterate through all elements in the `items` app and append some debug text to it.
let items: Vec<ListItem> = app
.states.history.transacts_list
.items
.iter()
.map(|i| {
ListItem::new(Text::from(i.as_str())).style(Style::default().fg(Color::Black).bg(Color::White))
})
.collect();
let history_items = List::new(items) let history_items = List::new(items)
.block(Block::default().borders(Borders::NONE)) .block(Block::default().borders(Borders::NONE));
.highlight_style(
Style::default()
.bg(Color::Gray)
.add_modifier(Modifier::BOLD)
);
f.render_stateful_widget(history_items, list_chunks[1], &mut app.states.history.transacts_list.state) f.render_widget(history_items, list_chunks[1]);
}
} }