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 crossterm::event::{Event, self, KeyCode};
use tokio;
use ratatui::widgets::ListState;
use crossbeam_channel::Receiver;
use log::warn;
use crate::db::DB;
use crate::db::data_cache::DataCache;
use crate::uis::history::HistoryState;
use crate::uis::new_transaction::NewTransactionTabState;
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 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 {
Insert,
Normal,
@ -77,17 +26,17 @@ pub struct App<'a> {
pub input_mode: InputMode,
pub db: Arc<tokio::sync::Mutex<DB>>,
pub records: Arc<Mutex<Vec<TransactionRecord>>>,
pub data_cache: DataCache,
}
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 {
running: true,
states: States::new(r),
states: States::new(),
input_mode: InputMode::Normal,
db: Arc::new(tokio::sync::Mutex::new(db)),
records,
data_cache,
}
}
@ -160,20 +109,16 @@ pub struct States<'a> {
pub transactions: NewTransactionTabState<'a>,
pub history: HistoryState,
pub new_data: Receiver<bool>,
pub active_frame: ActiveFrame,
}
impl<'a> States<'a> {
pub fn new(r: Receiver<bool>) -> States<'a> {
pub fn new() -> States<'a> {
States {
nav_state: NavigationState::new(),
transactions: NewTransactionTabState::new(),
history: HistoryState::new(),
new_data: r,
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 sqlx::Error;
use sqlx::PgPool;
use sqlx::Row;
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 {
conn_pool: PgPool,
new_data_notify: Sender<bool>,
records: Arc<Mutex<Vec<TransactionRecord>>>,
data_cache: DataCache,
}
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()
.max_connections(3)
.connect("postgres://rcntuser:Devel@pmentPa$$w0rd@10.0.0.183/Borealis").await?;
Ok(DB {
conn_pool: connection_pool,
new_data_notify: s,
records,
data_cache,
})
}
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);
while let Some(row) = rows.try_next().await? {
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();
let mut temp_transactions: Vec<Transaction> = Vec::new();
self.records.lock().unwrap().push(
TransactionRecord {
id: id.into(),
amount: amount.to_decimal(2).to_string(),
date: date.to_string()
}
);
while let Some(row) = rows.try_next().await? {
temp_transactions.push(row);
}
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(());

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

View file

@ -1,123 +1,135 @@
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 crate::{app::{App, StatefulList}, db::TransactionRecord};
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, db::tables::Transaction};
pub struct HistoryState {
pub transacts_list: StatefulList<TransactionRecord>
pub selected_index: Option<usize>,
}
impl HistoryState {
pub fn new() -> HistoryState {
HistoryState {
transacts_list: StatefulList::with_items(vec![
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()),
])
HistoryState {
selected_index: None,
}
}
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 {
match event.code {
KeyCode::Tab => app.states.transactions.next_tab(),
KeyCode::Up => app.states.history.transacts_list.previous(),
KeyCode::Down => app.states.history.transacts_list.next(),
KeyCode::Up | KeyCode::Tab => app.states.history.next(max_index),
KeyCode::Down => app.states.history.previous(),
_ => {}
}
}
}
pub fn update_transactions(app: &mut App) {
app.states.history.transacts_list.items.clear();
for i in app.records.lock().unwrap().iter() {
app.states.history.transacts_list.items.push(i.clone());
pub fn next(&mut self, max_index: usize) {
if self.selected_index.is_some() {
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) {
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()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(body_rect);
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 value_style: Style = Style::default().fg(Color::LightBlue);
let ident_style: Style = Style::default().fg(Color::Yellow);
let value_style: Style = Style::default().fg(Color::LightBlue);
lines.push(
Line::from(vec![
Span::styled("Transaction Id: ", ident_style),
Span::styled(format!("T{:0>6}", selected_item.id), value_style),
])
);
lines.push(
Line::from(vec![
Span::styled("Amount: ", ident_style),
Span::styled(format!("{}", selected_item.amount), value_style)
])
);
lines.push(
Line::from(vec![
Span::styled("Transaction Date: ", ident_style),
Span::styled(format!("{}", selected_item.date), value_style)
])
);
}
lines.push(
Line::from(vec![
Span::styled("Transaction Id: ", ident_style),
Span::styled(format!("T{:0>6}", selected_item.trns_id), value_style),
])
);
lines.push(
Line::from(vec![
Span::styled("Amount: ", ident_style),
Span::styled(format!("{}", selected_item.trns_amount.to_decimal(2).to_string()), value_style)
])
);
lines.push(
Line::from(vec![
Span::styled("Transaction Date: ", ident_style),
Span::styled(format!("{}", selected_item.trns_date.to_string()), value_style)
])
);
}
let paragraph = Paragraph::new(lines.clone())
.block(Block::default().borders(Borders::ALL).title("Details").title_alignment(ratatui::layout::Alignment::Left));
let paragraph = Paragraph::new(lines.clone())
.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()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(3)])
.split(split_body[0]);
let list_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(3)])
.split(split_body[0]);
let list_header = Paragraph::new(
Text::styled(TransactionRecord::get_header(), Style::default().fg(Color::Black).bg(Color::Cyan))
).block(Block::default().borders(Borders::NONE));
let list_header = Paragraph::new(
Text::styled(Transaction::get_header(), Style::default().fg(Color::Black).bg(Color::Cyan))
).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)
.block(Block::default().borders(Borders::NONE))
.highlight_style(
Style::default()
.bg(Color::Gray)
.add_modifier(Modifier::BOLD)
);
.block(Block::default().borders(Borders::NONE));
f.render_stateful_widget(history_items, list_chunks[1], &mut app.states.history.transacts_list.state)
f.render_widget(history_items, list_chunks[1]);
}
}