Compare commits
10 Commits
6fda1192fa
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df4fd0202d | ||
|
|
2395d72c7e | ||
|
|
dd6b6f7b2c | ||
|
|
33fb39e732 | ||
|
|
55dfd16cd2 | ||
|
|
24ac0a0fbe | ||
|
|
e8b3eae889 | ||
|
|
d512dedd21 | ||
|
|
4c4967d50a | ||
|
|
83281e9199 |
@@ -5,4 +5,5 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
|
ratatui = "0.29.0"
|
||||||
sqlite = "0.37.0"
|
sqlite = "0.37.0"
|
||||||
|
|||||||
128
src/app.rs
Normal file
128
src/app.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use ratatui::widgets::ListState;
|
||||||
|
use sqlite::Connection;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
data_layer,
|
||||||
|
entities::{Account, AccountType, Transaction},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum CurrentScreen {
|
||||||
|
Main,
|
||||||
|
Exiting,
|
||||||
|
}
|
||||||
|
pub enum CurrentWidget {
|
||||||
|
AccountList,
|
||||||
|
TrxInfo,
|
||||||
|
TrxList,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AccountList {
|
||||||
|
accounts: Vec<Account>,
|
||||||
|
pub state: ListState,
|
||||||
|
}
|
||||||
|
impl AccountList {
|
||||||
|
fn new() -> AccountList {
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
list_state.select_first();
|
||||||
|
return AccountList {
|
||||||
|
accounts: Vec::new(),
|
||||||
|
state: list_state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_accounts(&self, con: &Connection) -> Vec<Account> {
|
||||||
|
if self.accounts.iter().count() == 0 {
|
||||||
|
return data_layer::get_accounts(con);
|
||||||
|
}
|
||||||
|
return self.accounts.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TrxList {
|
||||||
|
trx: Vec<Transaction>,
|
||||||
|
pub state: ListState,
|
||||||
|
}
|
||||||
|
impl TrxList {
|
||||||
|
fn new() -> TrxList {
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
list_state.select_first();
|
||||||
|
return TrxList {
|
||||||
|
trx: Vec::new(),
|
||||||
|
state: list_state,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_trx(&self, con: &Connection, app: &App) -> Vec<Transaction> {
|
||||||
|
if self.trx.iter().count() == 0 {
|
||||||
|
if let Some(i) = app.acc_list.state.selected() {
|
||||||
|
return data_layer::get_account_transactions(
|
||||||
|
con,
|
||||||
|
app.get_list_accounts()[i].get_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
return self.trx.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub current_screen: CurrentScreen,
|
||||||
|
pub current_widget: CurrentWidget,
|
||||||
|
pub acc_list: AccountList,
|
||||||
|
pub trx_list: TrxList,
|
||||||
|
pub connection: Connection,
|
||||||
|
exit: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub fn new() -> App {
|
||||||
|
let con = match Connection::open("ft_rs.db") {
|
||||||
|
Ok(con) => con,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error opening database: {}", e);
|
||||||
|
panic!("stopping");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return App {
|
||||||
|
current_screen: CurrentScreen::Main,
|
||||||
|
current_widget: CurrentWidget::AccountList,
|
||||||
|
acc_list: AccountList::new(),
|
||||||
|
trx_list: TrxList::new(),
|
||||||
|
connection: con,
|
||||||
|
exit: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_list_accounts(&self) -> Vec<Account> {
|
||||||
|
let mut accounts = self.acc_list.get_accounts(&self.connection);
|
||||||
|
let all = Account::new(0, "All".to_string(), AccountType::Cash);
|
||||||
|
accounts.insert(0, all);
|
||||||
|
return accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_ac(&mut self) {
|
||||||
|
self.acc_list.state.select_first();
|
||||||
|
}
|
||||||
|
pub fn next_ac(&mut self) {
|
||||||
|
self.acc_list.state.select_next();
|
||||||
|
}
|
||||||
|
pub fn previous_ac(&mut self) {
|
||||||
|
self.acc_list.state.select_previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_list_trx(&self) -> Vec<Transaction> {
|
||||||
|
let accounts = &self.trx_list.get_trx(&self.connection, self);
|
||||||
|
return accounts.to_vec();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn first_tr(&mut self) {
|
||||||
|
self.trx_list.state.select_first();
|
||||||
|
}
|
||||||
|
pub fn next_tr(&mut self) {
|
||||||
|
self.trx_list.state.select_next();
|
||||||
|
}
|
||||||
|
pub fn previous_tr(&mut self) {
|
||||||
|
self.trx_list.state.select_previous();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::entities::*;
|
use crate::entities::*;
|
||||||
|
use chrono::DateTime;
|
||||||
use sqlite::{Connection, State};
|
use sqlite::{Connection, State};
|
||||||
|
|
||||||
pub fn setup(con: &Connection) {
|
pub fn setup(con: &Connection) {
|
||||||
@@ -20,7 +21,7 @@ pub fn setup(con: &Connection) {
|
|||||||
account_id INTEGER NOT NULL,
|
account_id INTEGER NOT NULL,
|
||||||
type_id INTEGER NOT NULL,
|
type_id INTEGER NOT NULL,
|
||||||
amount FLOAT NOT NULL,
|
amount FLOAT NOT NULL,
|
||||||
date DATE NOT NULL,
|
date INTEGER NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
FOREIGN KEY(account_id) REFERENCES Accounts(id),
|
FOREIGN KEY(account_id) REFERENCES Accounts(id),
|
||||||
FOREIGN KEY(type_id) REFERENCES TransactionTypes(id)
|
FOREIGN KEY(type_id) REFERENCES TransactionTypes(id)
|
||||||
@@ -76,6 +77,25 @@ pub fn get_account(con: &Connection, id: i64) -> Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_account_total(id: i64, con: &Connection) -> f64 {
|
||||||
|
let mut query = "SELECT SUM(amount) as total FROM Transactions".to_owned();
|
||||||
|
if id != 0 {
|
||||||
|
query.push_str(" WHERE account_id = ?")
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut statement = con.prepare(query).unwrap();
|
||||||
|
|
||||||
|
if id != 0 {
|
||||||
|
statement.bind((1, id)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(State::Row) = statement.next() {
|
||||||
|
return statement.read::<f64, _>("total").unwrap();
|
||||||
|
} else {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_accounts(con: &Connection) -> Vec<Account> {
|
pub fn get_accounts(con: &Connection) -> Vec<Account> {
|
||||||
let query = "SELECT * FROM Accounts";
|
let query = "SELECT * FROM Accounts";
|
||||||
let mut statement = con.prepare(query).unwrap();
|
let mut statement = con.prepare(query).unwrap();
|
||||||
@@ -150,3 +170,110 @@ pub fn get_transaction_types(con: &Connection) -> Vec<TransactionType> {
|
|||||||
|
|
||||||
return vec;
|
return vec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn upsert_transaction(con: &Connection, tr: Transaction) -> Transaction {
|
||||||
|
let query;
|
||||||
|
if tr.get_id() == 0 {
|
||||||
|
query = "INSERT INTO Transactions
|
||||||
|
(account_id, type_id, amount, date, description)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
RETURNING id;";
|
||||||
|
} else {
|
||||||
|
query = "UPDATE Transactions
|
||||||
|
SET account_id = ?, type_id = ?, amount = ?, date = ?, description = ?
|
||||||
|
WHERE id = ? RETURNING id;";
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut statement = con.prepare(query).unwrap();
|
||||||
|
statement.bind((1, tr.get_account().get_id())).unwrap();
|
||||||
|
statement.bind((2, tr.get_type().get_id())).unwrap();
|
||||||
|
statement.bind((3, tr.get_amount())).unwrap();
|
||||||
|
statement.bind((4, tr.get_date().timestamp())).unwrap();
|
||||||
|
statement.bind((5, &tr.get_desc() as &str)).unwrap();
|
||||||
|
|
||||||
|
if tr.get_id() != 0 {
|
||||||
|
statement.bind((6, tr.get_id())).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let id;
|
||||||
|
if let Ok(State::Row) = statement.next() {
|
||||||
|
id = statement.read::<i64, _>("id").unwrap();
|
||||||
|
} else {
|
||||||
|
id = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Transaction::new(
|
||||||
|
id,
|
||||||
|
tr.get_account().get_id(),
|
||||||
|
tr.get_amount(),
|
||||||
|
tr.get_date(),
|
||||||
|
tr.get_desc(),
|
||||||
|
tr.get_type().get_id(),
|
||||||
|
con,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transaction(con: &Connection, id: i64) -> Transaction {
|
||||||
|
let query = "SELECT * FROM Transactions WHERE id = ?";
|
||||||
|
let mut statement = con.prepare(query).unwrap();
|
||||||
|
statement.bind((1, id)).unwrap();
|
||||||
|
|
||||||
|
if let Ok(State::Row) = statement.next() {
|
||||||
|
return Transaction::new(
|
||||||
|
statement.read::<i64, _>("id").unwrap(),
|
||||||
|
statement.read::<i64, _>("account_id").unwrap(),
|
||||||
|
statement.read::<f64, _>("amount").unwrap(),
|
||||||
|
DateTime::from_timestamp(statement.read::<i64, _>("date").unwrap(), 0).unwrap(),
|
||||||
|
statement.read::<String, _>("description").unwrap(),
|
||||||
|
statement.read::<i64, _>("type_id").unwrap(),
|
||||||
|
con,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Transaction::new_empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_account_transactions(con: &Connection, ac_id: i64) -> Vec<Transaction> {
|
||||||
|
if ac_id == 0 {
|
||||||
|
return get_transactions(con);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = "SELECT * FROM Transactions WHERE account_id = ?";
|
||||||
|
let mut statement = con.prepare(query).unwrap();
|
||||||
|
statement.bind((1, ac_id)).unwrap();
|
||||||
|
let mut vec = Vec::<Transaction>::new();
|
||||||
|
|
||||||
|
while let Ok(State::Row) = statement.next() {
|
||||||
|
vec.push(Transaction::new(
|
||||||
|
statement.read::<i64, _>("id").unwrap(),
|
||||||
|
statement.read::<i64, _>("account_id").unwrap(),
|
||||||
|
statement.read::<f64, _>("amount").unwrap(),
|
||||||
|
DateTime::from_timestamp(statement.read::<i64, _>("date").unwrap(), 0).unwrap(),
|
||||||
|
statement.read::<String, _>("description").unwrap(),
|
||||||
|
statement.read::<i64, _>("type_id").unwrap(),
|
||||||
|
con,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_transactions(con: &Connection) -> Vec<Transaction> {
|
||||||
|
let query = "SELECT * FROM TransactionTypes";
|
||||||
|
let mut statement = con.prepare(query).unwrap();
|
||||||
|
let mut vec = Vec::<Transaction>::new();
|
||||||
|
|
||||||
|
while let Ok(State::Row) = statement.next() {
|
||||||
|
vec.push(Transaction::new(
|
||||||
|
statement.read::<i64, _>("id").unwrap(),
|
||||||
|
statement.read::<i64, _>("account_id").unwrap(),
|
||||||
|
statement.read::<f64, _>("amount").unwrap(),
|
||||||
|
DateTime::from_timestamp(statement.read::<i64, _>("date").unwrap(), 0).unwrap(),
|
||||||
|
statement.read::<String, _>("description").unwrap(),
|
||||||
|
statement.read::<i64, _>("type_id").unwrap(),
|
||||||
|
con,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use sqlite::Connection;
|
||||||
|
|
||||||
|
use crate::data_layer;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
id: i64,
|
id: i64,
|
||||||
name: String,
|
name: String,
|
||||||
@@ -22,6 +28,9 @@ impl Account {
|
|||||||
pub fn get_ac_type(&self) -> AccountType {
|
pub fn get_ac_type(&self) -> AccountType {
|
||||||
return AccountType::try_from(self.ac_type as i64).unwrap();
|
return AccountType::try_from(self.ac_type as i64).unwrap();
|
||||||
}
|
}
|
||||||
|
pub fn get_total(&self, con: &Connection) -> f64 {
|
||||||
|
return data_layer::get_account_total(self.id, con);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use crate::{data_layer, entities::Account};
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlite::Connection;
|
use sqlite::Connection;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct Transaction {
|
pub struct Transaction {
|
||||||
id: i64,
|
id: i64,
|
||||||
account_id: i64,
|
account_id: i64,
|
||||||
@@ -31,11 +32,24 @@ impl Transaction {
|
|||||||
date: date,
|
date: date,
|
||||||
description: desc,
|
description: desc,
|
||||||
type_id: type_id,
|
type_id: type_id,
|
||||||
account: data_layer::get_account(con, id),
|
account: data_layer::get_account(con, ac_id),
|
||||||
tr_type: data_layer::get_transaction_type(con, type_id),
|
tr_type: data_layer::get_transaction_type(con, type_id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_empty() -> Self {
|
||||||
|
Transaction {
|
||||||
|
id: 0,
|
||||||
|
account_id: 0,
|
||||||
|
amount: 0.0,
|
||||||
|
date: DateTime::from_timestamp(0, 0).unwrap(),
|
||||||
|
description: "".to_string(),
|
||||||
|
type_id: 0,
|
||||||
|
account: Account::new(0, "".to_string(), super::AccountType::Cash),
|
||||||
|
tr_type: TransactionType::new(0, "".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_id(&self) -> i64 {
|
pub fn get_id(&self) -> i64 {
|
||||||
return self.id;
|
return self.id;
|
||||||
}
|
}
|
||||||
@@ -49,18 +63,19 @@ impl Transaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_desc(&self) -> String {
|
pub fn get_desc(&self) -> String {
|
||||||
return self.description;
|
return self.description.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_account(&self) -> Account {
|
pub fn get_account(&self) -> Account {
|
||||||
return self.account;
|
return self.account.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_type(&self) -> TransactionType {
|
pub fn get_type(&self) -> TransactionType {
|
||||||
return self.tr_type;
|
return self.tr_type.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct TransactionType {
|
pub struct TransactionType {
|
||||||
id: i64,
|
id: i64,
|
||||||
description: String,
|
description: String,
|
||||||
|
|||||||
256
src/main.rs
256
src/main.rs
@@ -1,25 +1,243 @@
|
|||||||
|
mod app;
|
||||||
mod data_layer;
|
mod data_layer;
|
||||||
mod entities;
|
mod entities;
|
||||||
use sqlite::Connection;
|
mod ui;
|
||||||
|
use crate::{
|
||||||
use crate::entities::Account;
|
app::{App, CurrentScreen},
|
||||||
|
ui::ui,
|
||||||
fn main() {
|
|
||||||
println!("Hello, world!");
|
|
||||||
|
|
||||||
let connection = match Connection::open("ft_rs.db") {
|
|
||||||
Ok(con) => con,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error opening database: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
use app::CurrentWidget;
|
||||||
|
use ratatui::{
|
||||||
|
Terminal,
|
||||||
|
crossterm::{
|
||||||
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||||
|
},
|
||||||
|
prelude::Backend,
|
||||||
|
};
|
||||||
|
use std::{error::Error, io};
|
||||||
|
|
||||||
data_layer::setup(&connection);
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let mut account = Account::new(0, "test".to_string(), entities::AccountType::Cash);
|
enable_raw_mode()?;
|
||||||
account = data_layer::upsert_account(&connection, account);
|
let mut stderr = io::stderr();
|
||||||
let accounts = data_layer::get_accounts(&connection);
|
execute!(stderr, EnterAlternateScreen, EnableMouseCapture)?;
|
||||||
for ac in accounts.iter() {
|
|
||||||
println!("name: {}", ac.get_name())
|
let mut terminal = ratatui::init();
|
||||||
|
let mut app = App::new();
|
||||||
|
let res = run_app(&mut terminal, &mut app);
|
||||||
|
|
||||||
|
disable_raw_mode()?;
|
||||||
|
execute!(
|
||||||
|
terminal.backend_mut(),
|
||||||
|
LeaveAlternateScreen,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
|
||||||
|
if let Err(err) = res {
|
||||||
|
println!("{err:?}");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<bool> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| ui(f, app))?;
|
||||||
|
|
||||||
|
// S'occupe des keyevents
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == event::KeyEventKind::Release {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match app.current_screen {
|
||||||
|
CurrentScreen::Main => match key.code {
|
||||||
|
KeyCode::Char('q') => {
|
||||||
|
app.current_screen = CurrentScreen::Exiting;
|
||||||
|
}
|
||||||
|
KeyCode::Right => {
|
||||||
|
app.current_widget = CurrentWidget::TrxList;
|
||||||
|
}
|
||||||
|
KeyCode::Left => {
|
||||||
|
app.current_widget = CurrentWidget::AccountList;
|
||||||
|
}
|
||||||
|
KeyCode::Char('j') => {
|
||||||
|
match app.current_widget {
|
||||||
|
CurrentWidget::AccountList => {
|
||||||
|
app.next_ac();
|
||||||
|
}
|
||||||
|
CurrentWidget::TrxList => {
|
||||||
|
//app.next_trx();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('k') => {
|
||||||
|
match app.current_widget {
|
||||||
|
CurrentWidget::AccountList => {
|
||||||
|
app.previous_ac();
|
||||||
|
}
|
||||||
|
CurrentWidget::TrxList => {
|
||||||
|
//app.previous_trx();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
CurrentScreen::Exiting => match key.code {
|
||||||
|
KeyCode::Char('y') => {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
app.current_screen = CurrentScreen::Main;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn main() {
|
||||||
|
// let connection = match Connection::open("ft_rs.db") {
|
||||||
|
// Ok(con) => con,
|
||||||
|
// Err(e) => {
|
||||||
|
// eprintln!("Error opening database: {}", e);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// data_layer::setup(&connection);
|
||||||
|
|
||||||
|
// loop {
|
||||||
|
// println!("Please choose an option:");
|
||||||
|
// println!("1. List accounts");
|
||||||
|
// println!("2. Add an account");
|
||||||
|
// println!("3. List transaction types");
|
||||||
|
// println!("4. Add a transaction type");
|
||||||
|
// println!("5. List transactions");
|
||||||
|
// println!("6. Add a transaction");
|
||||||
|
// println!("0. Exit");
|
||||||
|
|
||||||
|
// let mut choice = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut choice)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
|
||||||
|
// let choice = choice.trim();
|
||||||
|
|
||||||
|
// match choice {
|
||||||
|
// "1" => {
|
||||||
|
// let accounts = data_layer::get_accounts)(&connection);
|
||||||
|
// let mut total = 0.0;
|
||||||
|
// for ac in accounts.iter() {
|
||||||
|
// let ac_total = ac.get_total(&connection);
|
||||||
|
// println!(
|
||||||
|
// "Id: {}, Name: {}, Total: {}",
|
||||||
|
// ac.get_id(),
|
||||||
|
// ac.get_name(),
|
||||||
|
// ac_total
|
||||||
|
// );
|
||||||
|
// total += ac_total;
|
||||||
|
// }
|
||||||
|
// println!("Total: {}", total);
|
||||||
|
// }
|
||||||
|
// "2" => {
|
||||||
|
// println!("Please enter the account name:");
|
||||||
|
// let mut ac_name = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut ac_name)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// let ac_name = ac_name.trim();
|
||||||
|
// let new_ac = Account::new(0, ac_name.to_string(), entities::AccountType::Cash);
|
||||||
|
// data_layer::upsert_account(&connection, new_ac);
|
||||||
|
// }
|
||||||
|
// "3" => {
|
||||||
|
// let types = data_layer::get_transaction_types(&connection);
|
||||||
|
// for t in types.iter() {
|
||||||
|
// println!("Name: {}", t.get_description());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// "4" => {
|
||||||
|
// println!("Please enter the transaction type:");
|
||||||
|
// let mut desc = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut desc)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// let desc = desc.trim();
|
||||||
|
// let tr_type = TransactionType::new(0, desc.to_string());
|
||||||
|
// data_layer::upsert_transaction_type(&connection, tr_type);
|
||||||
|
// }
|
||||||
|
// "5" => {
|
||||||
|
// println!("Please enter the account id:");
|
||||||
|
// let mut ac_id_str = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut ac_id_str)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// ac_id_str = ac_id_str.trim().to_string();
|
||||||
|
// let ac_id = ac_id_str.parse::<i64>().unwrap();
|
||||||
|
// let trx: Vec<entities::Transaction> =
|
||||||
|
// data_layer::get_account_transactions(&connection, ac_id);
|
||||||
|
// // .sort_by(|a, b| b.get_date().cmp(&a.get_date()))
|
||||||
|
// // .try_into()
|
||||||
|
// // .unwrap();
|
||||||
|
// for t in trx.iter() {
|
||||||
|
// println!(
|
||||||
|
// "Date: {}, Type: {}, Description: {}, Amount: {}$",
|
||||||
|
// Local.from_utc_datetime(&t.get_date().naive_local()),
|
||||||
|
// t.get_type().get_description(),
|
||||||
|
// t.get_desc(),
|
||||||
|
// t.get_amount()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// "6" => {
|
||||||
|
// println!("Please enter the account id:");
|
||||||
|
// let mut ac_id_str = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut ac_id_str)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// ac_id_str = ac_id_str.trim().to_string();
|
||||||
|
// let ac_id = ac_id_str.parse::<i64>().unwrap();
|
||||||
|
// println!("Please enter the transaction type id:");
|
||||||
|
// let mut tr_type_id_str = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut tr_type_id_str)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// tr_type_id_str = tr_type_id_str.trim().to_string();
|
||||||
|
// let type_id = tr_type_id_str.parse::<i64>().unwrap();
|
||||||
|
// println!("Please enter the amount:");
|
||||||
|
// let mut amnt_str = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut amnt_str)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// amnt_str = amnt_str.trim().to_string();
|
||||||
|
// let amount = amnt_str.parse::<f64>().unwrap();
|
||||||
|
// println!("Please enter the description:");
|
||||||
|
// let mut desc = String::new();
|
||||||
|
// io::stdin()
|
||||||
|
// .read_line(&mut desc)
|
||||||
|
// .expect("Failed to read line");
|
||||||
|
// desc = desc.trim().to_string();
|
||||||
|
// let tr = Transaction::new(
|
||||||
|
// 0,
|
||||||
|
// ac_id,
|
||||||
|
// amount,
|
||||||
|
// chrono::offset::Utc::now(),
|
||||||
|
// desc,
|
||||||
|
// type_id,
|
||||||
|
// &connection,
|
||||||
|
// );
|
||||||
|
// data_layer::upsert_transaction(&connection, tr);
|
||||||
|
// }
|
||||||
|
// "0" => {
|
||||||
|
// println!("Exiting...");
|
||||||
|
// break; // Exit the loop
|
||||||
|
// }
|
||||||
|
// _ => {
|
||||||
|
// println!("Invalid choice.");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
164
src/ui.rs
Normal file
164
src/ui.rs
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
use crate::{
|
||||||
|
app::{App, CurrentScreen, CurrentWidget},
|
||||||
|
entities::{Account, Transaction},
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
Frame,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::{
|
||||||
|
Color, Modifier, Style, Stylize,
|
||||||
|
palette::tailwind::{BLUE, GREEN, SLATE},
|
||||||
|
},
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::{
|
||||||
|
Block, Borders, Clear, HighlightSpacing, List, ListItem, Paragraph, StatefulWidget, Widget,
|
||||||
|
Wrap,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const NORMAL_ROW_BG: Color = SLATE.c950;
|
||||||
|
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
|
||||||
|
const TEXT_FG_COLOR: Color = SLATE.c200;
|
||||||
|
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
|
||||||
|
|
||||||
|
pub fn ui(frame: &mut Frame, app: &mut App) {
|
||||||
|
let layout = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||||
|
.split(frame.area());
|
||||||
|
let right_layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(vec![Constraint::Percentage(10), Constraint::Percentage(90)])
|
||||||
|
.split(layout[1]);
|
||||||
|
|
||||||
|
let mut ac_block = Block::default()
|
||||||
|
.title("Accounts")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::new().fg(Color::DarkGray));
|
||||||
|
let mut info_block = Block::default()
|
||||||
|
.title("Account info")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::new().fg(Color::DarkGray));
|
||||||
|
let mut trx_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::new().fg(Color::DarkGray));
|
||||||
|
|
||||||
|
let active_style = Style::default();
|
||||||
|
match app.current_widget {
|
||||||
|
CurrentWidget::AccountList => ac_block = ac_block.border_style(active_style),
|
||||||
|
CurrentWidget::TrxInfo => info_block = info_block.border_style(active_style),
|
||||||
|
CurrentWidget::TrxList => trx_block = trx_block.border_style(active_style),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ac_items: Vec<ListItem> = app
|
||||||
|
.get_list_accounts()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, acc)| {
|
||||||
|
let color = alternate_colors(i);
|
||||||
|
ListItem::from(acc).bg(color)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let list = List::new(ac_items)
|
||||||
|
.block(ac_block)
|
||||||
|
.highlight_style(SELECTED_STYLE)
|
||||||
|
.highlight_symbol(">")
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
|
||||||
|
StatefulWidget::render(list, layout[0], frame.buffer_mut(), &mut app.acc_list.state);
|
||||||
|
|
||||||
|
let info = if let Some(i) = app.acc_list.state.selected() {
|
||||||
|
format!(
|
||||||
|
"Total: {}",
|
||||||
|
app.get_list_accounts()[i]
|
||||||
|
.get_total(&app.connection)
|
||||||
|
.to_string()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"No account selected...".to_string()
|
||||||
|
};
|
||||||
|
frame.render_widget(Paragraph::new(info).block(info_block), right_layout[0]);
|
||||||
|
|
||||||
|
let trx_items: Vec<ListItem> = app
|
||||||
|
.get_list_trx()
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, tr)| {
|
||||||
|
let color = alternate_colors(i);
|
||||||
|
ListItem::from(tr).bg(color)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let trx_list = List::new(trx_items)
|
||||||
|
.block(trx_block)
|
||||||
|
.highlight_style(SELECTED_STYLE)
|
||||||
|
.highlight_symbol(">")
|
||||||
|
.highlight_spacing(HighlightSpacing::Always);
|
||||||
|
|
||||||
|
StatefulWidget::render(
|
||||||
|
trx_list,
|
||||||
|
right_layout[1],
|
||||||
|
frame.buffer_mut(),
|
||||||
|
&mut app.trx_list.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let CurrentScreen::Exiting = app.current_screen {
|
||||||
|
let popup = Block::default()
|
||||||
|
.title("Exiting program")
|
||||||
|
.borders(Borders::all())
|
||||||
|
.style(Style::default());
|
||||||
|
|
||||||
|
let exit_text = Text::styled(
|
||||||
|
"Are you sure you want to close the program? (y/n)",
|
||||||
|
Style::default().fg(Color::Red),
|
||||||
|
);
|
||||||
|
|
||||||
|
let exit_paragraph = Paragraph::new(exit_text)
|
||||||
|
.block(popup)
|
||||||
|
.wrap(Wrap { trim: false });
|
||||||
|
|
||||||
|
let area = centered_rect(50, 20, frame.area());
|
||||||
|
frame.render_widget(exit_paragraph, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||||
|
let popup_layout = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - percent_y) / 2),
|
||||||
|
Constraint::Percentage(percent_y),
|
||||||
|
Constraint::Percentage((100 - percent_y) / 2),
|
||||||
|
])
|
||||||
|
.split(r);
|
||||||
|
|
||||||
|
return Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
Constraint::Percentage(percent_x),
|
||||||
|
Constraint::Percentage((100 - percent_x) / 2),
|
||||||
|
])
|
||||||
|
.split(popup_layout[1])[1];
|
||||||
|
}
|
||||||
|
const fn alternate_colors(i: usize) -> Color {
|
||||||
|
if i % 2 == 0 {
|
||||||
|
NORMAL_ROW_BG
|
||||||
|
} else {
|
||||||
|
ALT_ROW_BG_COLOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Account> for ListItem<'_> {
|
||||||
|
fn from(value: &Account) -> Self {
|
||||||
|
let line = Line::styled(value.get_name(), TEXT_FG_COLOR);
|
||||||
|
ListItem::new(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&Transaction> for ListItem<'_> {
|
||||||
|
fn from(value: &Transaction) -> Self {
|
||||||
|
let line = Line::styled(value.get_desc(), TEXT_FG_COLOR);
|
||||||
|
ListItem::new(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user