diff options
| -rw-r--r-- | Cargo.toml | 15 | ||||
| -rw-r--r-- | src/banking/mod.rs | 59 | ||||
| -rw-r--r-- | src/exchange/mod.rs | 2 | ||||
| -rw-r--r-- | src/lib.rs | 0 | ||||
| -rw-r--r-- | src/main.rs | 46 | ||||
| -rw-r--r-- | src/parsers/csv/mod.rs (renamed from src/Csv/mod.rs) | 35 | ||||
| -rw-r--r-- | src/parsers/ini/mod.rs | 64 | ||||
| -rw-r--r-- | src/parsers/mod.rs | 2 | ||||
| -rw-r--r-- | src/web_frontend/balance.rs | 118 | ||||
| -rw-r--r-- | src/web_frontend/chart.rs | 61 | ||||
| -rw-r--r-- | src/web_frontend/mod.rs | 29 | ||||
| -rw-r--r-- | src/web_frontend/transactions.rs | 101 | ||||
| -rw-r--r-- | templates/account.html.tera | 63 | ||||
| -rw-r--r-- | templates/balance.html.tera | 109 | ||||
| -rw-r--r-- | templates/chart.html.tera | 83 | ||||
| -rw-r--r-- | templates/transaction.html.tera | 119 |
16 files changed, 881 insertions, 25 deletions
@@ -1,7 +1,18 @@ [package] name = "finz" version = "0.1.0" -authors = ["Benedict Börger <benedict@0xb8000.de>"] +authors = ["Benedict <benedict@0xb8000.de>"] [dependencies] -chrono = "0.4" +serde = "*" +serde_derive = "*" +serde_json = "*" +mysql = "*" +rocket = "*" +regex = "*" +chrono = {version = "*", features = ["serde", "rustc-serialize"] } + +[dependencies.rocket_contrib] +version = "*" +default-features = false +features = ["tera_templates"] diff --git a/src/banking/mod.rs b/src/banking/mod.rs new file mode 100644 index 0000000..410bae1 --- /dev/null +++ b/src/banking/mod.rs @@ -0,0 +1,59 @@ + +use parsers::csv::CsvFile; + + +pub struct Account { + name : String, + iban : String, + transactions : Vec<Transaction> +} + + +#[derive(Serialize)] +pub struct Transaction { + pub sender_name : String, + pub amount : f32, + pub reference : String, + pub date : chrono::NaiveDate +} + +impl Transaction { + pub fn from_sparkasse_csv_file(file : CsvFile) -> Vec<Transaction> { + let mut ret = Vec::new(); + for line in file.iter() { + let mut sender_name_f = String::from(""); + let mut sender_iban_f = String::from(""); + let mut amount_f : f32 = 0.0; + let mut reference_f = String::from(""); + let mut date_f = chrono::NaiveDate::parse_from_str("01.01.2019", "%d.%m.%Y").unwrap(); + match line.get(&String::from("Kontonummer")) { + Some(value) => sender_iban_f = value.to_string(), + None => println!("missing sender") + } + match line.get(&String::from("Beguenstigter/Zahlungspflichtiger")) { + Some(value) => sender_name_f = value.to_string(), + None => println!("missing sender") + } + match line.get(&String::from("Verwendungszweck")) { + Some(value) => reference_f = value.to_string(), + None => println!("missing refernce") + } + match line.get(&String::from("Betrag")) { + Some(value) => amount_f = value.parse().unwrap(), + None => println!("missing amount") + } + match line.get(&String::from("Valutadatum")) { + Some(value) => { + date_f = chrono::NaiveDate::parse_from_str(value, "%d.%m.%y").unwrap();} , + None => println!("missing date") + } + ret.push(Transaction { + sender_name : sender_name_f, + amount : amount_f, + reference : reference_f, + date : date_f }); + + } + ret + } +} diff --git a/src/exchange/mod.rs b/src/exchange/mod.rs index 8d93ac0..94cdbca 100644 --- a/src/exchange/mod.rs +++ b/src/exchange/mod.rs @@ -1,5 +1,5 @@ use std::io; -use Csv::CsvFile; +use parsers::csv::CsvFile; use std::collections::HashMap; pub struct Stock { diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index e69de29..0000000 --- a/src/lib.rs +++ /dev/null diff --git a/src/main.rs b/src/main.rs index 4c0768c..1325742 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,29 @@ -mod exchange; -mod Csv; -//use exchange::Stock; -use Csv::CsvFile; +#![feature(proc_macro_hygiene,decl_macro)] +#[macro_use] +extern crate rocket; +extern crate rocket_contrib; +extern crate regex; +extern crate chrono; -fn main() { - println!("Welcome to your investment calculator!"); +#[macro_use] +extern crate serde_derive; + +//mod exchange; +mod parsers; +mod banking; +mod web_frontend; - // TODO Compute how the money would have benn developed on historical data - //Stock::read_from_csv_file("~/hih"); - let config = CsvFile::read_file("test.csv", ";", true); - let config = match config { - Ok(c) => c, - Err(error) => panic!("Error reading CSV file: {}", error), - }; - let mut i = 0; - for line in config.content { - i += 1; - for f in line { - println!("{}: {}",i, f) - } - } +use rocket_contrib::templates::Template; + +fn main() { + // handle command line parameters + // e.g. wheres is the asset config ini file + // TODO how pass config to the modules/handler ? + // launch web frontend + rocket::ignite() + .attach(Template::fairing()) + .mount("/", routes![web_frontend::account_handler, web_frontend::transactions::transaction_handler, + web_frontend::transactions::transaction_handler_post, + web_frontend::balance::balance_handler, web_frontend::static_handler]) + .launch(); } diff --git a/src/Csv/mod.rs b/src/parsers/csv/mod.rs index 29e1aca..c906202 100644 --- a/src/Csv/mod.rs +++ b/src/parsers/csv/mod.rs @@ -1,6 +1,7 @@ use std::fs::File; use std::io; use std::io::BufRead; +use std::collections::HashMap; /** Represents a CSV file @@ -14,6 +15,27 @@ pub struct CsvFile { has_header : bool } +/* + * Iterator for the CSV file + */ +pub struct CsvFileIterator<'a> { + file : &'a CsvFile, + line : usize, + column : usize, + max_column : usize +} + +impl <'a> Iterator for CsvFileIterator<'a> { + type Item = HashMap<&'a String,&'a String>; + + fn next(&mut self) -> Option<Self::Item> { + let k = self.file.content.get(self.line)?; + let it : HashMap<&'a String, &'a String> = self.file.header.iter().zip(k.iter()).collect(); + self.line = self.line + 1; + Some(it) + } +} + impl CsvFile { pub fn new(path: &str, delim: &str, header: bool) -> CsvFile { CsvFile { @@ -30,13 +52,13 @@ impl CsvFile { fn parse_line(line : String, delim: &str) -> Vec<String> { let mut columns : Vec<String> = Vec::new(); for column in line.split(delim).collect::<Vec<&str>>() { - columns.push(column.to_string()); + columns.push(column.trim_matches(|c| c == '\'' || c == '\"').to_string()); } columns } - pub fn read_file(path: &str, delim: &str, header: bool) -> Result<CsvFile, io::Error> { + pub fn from_file(path: &str, delim: &str, header: bool) -> Result<CsvFile, io::Error> { let fd = File::open(path)?; let reader = io::BufReader::new(fd); let mut column_names : Vec<String> = Vec::new(); @@ -59,4 +81,13 @@ impl CsvFile { content : lines, has_header: header } ) } + + pub fn iter(&self) -> CsvFileIterator { + CsvFileIterator { file : self, + line : 0, + column : 0, + max_column : self.header.len() + } + } + } diff --git a/src/parsers/ini/mod.rs b/src/parsers/ini/mod.rs new file mode 100644 index 0000000..9633231 --- /dev/null +++ b/src/parsers/ini/mod.rs @@ -0,0 +1,64 @@ +use std::fs::File; +use std::io; +use std::io::BufRead; +use std::collections::HashMap; + +pub struct IniFile { + pub section : HashMap<String, Vec<IniSection>> +} + +pub struct IniSection { + pub section_name : String, + pub properties : HashMap<String, Vec<String>> +} + +impl IniFile { + pub fn from_file(path : &str) -> Result<IniFile, io::Error> { + let mut file = HashMap::new(); + let fd = File::open(path)?; + let reader = io::BufReader::new(fd); + let mut current_section = String::from(""); + + for line in reader.lines() { + let line = line?; + let line = line.trim().to_string(); + + if line.starts_with("#") || line.is_empty() { + println!("kommentar or empts: {}", line); + continue; + } + // another section begins + if line.starts_with("[") && line.ends_with("]") { + let nam = line.get(1..(line.len()-1)); + let n; + match nam { + Some(sec_name) => n = sec_name, + // TODO no name given, what should be done + None => n = "" + } + current_section = n.to_string(); + // TODO maybe the sections has been specified twice? + file.insert(n.to_string(), Vec::new() ); + continue; + + } + let kv : Vec<&str> = line.split("=").collect(); + let mut key = String::from(""); + if let Some(t) = kv.get(0) { + key = t.to_string(); + } + let mut value = String::from(""); + if let Some(t) = kv.get(1) { + value = t.to_string(); + } + if let Some(section) = file.get_mut(¤t_section) { + // get the entry with key from vector + if let Some(ent) = section.get_mut(&key) { + ent.insert(value.to_string()); + } + } + } + Ok(IniFile { sections : file }) + } + +} diff --git a/src/parsers/mod.rs b/src/parsers/mod.rs new file mode 100644 index 0000000..d8bce3c --- /dev/null +++ b/src/parsers/mod.rs @@ -0,0 +1,2 @@ +pub mod csv; +//pub mod ini; diff --git a/src/web_frontend/balance.rs b/src/web_frontend/balance.rs new file mode 100644 index 0000000..2cbebec --- /dev/null +++ b/src/web_frontend/balance.rs @@ -0,0 +1,118 @@ +use parsers::csv::CsvFile; +use banking::Account; +//use parsers::ini::IniFile; +use std::collections::HashMap; +use rocket_contrib::templates::Template; +use rocket::response::NamedFile; +use std::path::{PathBuf, Path}; +use rocket::request::Form; +use rocket::http::RawStr; +use regex::Regex; +use chrono::{NaiveDate, Utc}; +use chrono::Datelike; + +#[derive(Serialize)] +struct MonthEarnSpend { + name : String, + earned : f32, + spent : f32, +} + + +#[derive(Serialize)] +struct BalanceContext { + account_name : String, + months : Vec<MonthEarnSpend>, + date_start : String, + date_end : String +} + +#[derive(Debug)] +struct DateRange { + start_year : i32, + start_month : u32, + end_year : i32, + end_month : u32, +} + +impl DateRange { + fn new(start : chrono::NaiveDate, end : chrono::NaiveDate) -> DateRange { + DateRange { + start_year : start.year(), + start_month : start.month(), + end_year : end.year(), + end_month : end.month(), + } + } +} + +impl Iterator for DateRange { + type Item = chrono::NaiveDate; + + fn next(&mut self) -> Option<Self::Item> { + println!("next called"); + if (self.start_year <= self.end_year) { + if(self.start_month <= self.end_month) { + let mut tmp = self.start_year.to_string(); + if self.start_month < 10 { + tmp.push_str("-0"); + } else { + tmp.push_str("-"); + } + tmp.push_str(&self.start_month.to_string()); + tmp.push_str("-01"); + if self.start_month < 13 { + self.start_month = self.start_month + 1; + } else { + self.start_month = 1; + self.start_year = self.start_year + 1; + } + println!("{}", tmp); + return Some(chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap()) + } + } + None + } +} + +#[get("/balance?<start>&<end>")] +pub fn balance_handler(start : Option<&RawStr>, end : Option<&RawStr>) -> rocket_contrib::templates::Template { + let date_start = match start { + Some(s) => { let mut tmp = s.to_string(); + tmp.push_str("-01"); + chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap() }, + None => Utc::today().naive_utc() + }; + let date_end = match end { + Some(s) => { let mut tmp = s.to_string(); + tmp.push_str("-01"); + chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap() }, + None => Utc::today().naive_utc() + }; + let date_range = DateRange::new(date_start, date_end); + + let mut earn_spend_v = Vec::new(); + for date in date_range { + let transactions = CsvFile::from_file("data/t.csv", ";", true); + let t : Vec<crate::banking::Transaction> ; + match transactions { + Ok(trans) => t = crate::banking::Transaction::from_sparkasse_csv_file(trans), + Err(e) => panic!("could not read file {:?}", e) + } + let result : Vec<_> = t.iter().filter(|x| x.date.month() == date.month()).collect(); + let mut earn = 0.0; + let mut spend = 0.0; + for r in &result { + if r.amount > 0.0 { + earn = earn + r.amount; + } else { + spend = spend + r.amount.abs(); + } + } + earn_spend_v.push(MonthEarnSpend { name : date.to_string(), earned : earn, spent : spend}); + } + let context = BalanceContext { account_name : String::from("Girokonto"), + months : earn_spend_v , date_start : date_start.to_string()[0..7].to_string(), + date_end : date_end.to_string()[0..7].to_string()}; + Template::render("balance", context) +} diff --git a/src/web_frontend/chart.rs b/src/web_frontend/chart.rs new file mode 100644 index 0000000..c5948fe --- /dev/null +++ b/src/web_frontend/chart.rs @@ -0,0 +1,61 @@ +use parsers::csv::CsvFile; +use banking::Account; +//use parsers::ini::IniFile; +use std::collections::HashMap; +use rocket_contrib::templates::Template; +use rocket::response::NamedFile; +use std::path::{PathBuf, Path}; +use rocket::request::Form; +use rocket::http::RawStr; +use regex::Regex; +use chrono::{NaiveDate, Utc}; +use chrono::Datelike; + +#[derive(Serialize)] +struct ChartContext { + account_name : String, + groups : HashMap<String, f32> +} + + +#[get("/chart")] +fn chart_handler() -> rocket_contrib::templates::Template { + // read group config + let chart_file = "data/giro"; + let chart_config = IniFile::from_file(chart_file); + let ini_file; + match chart_config { + Ok(file) => ini_file = file, + Err(e) => panic!("could not read group file {:?}", e) + } + let mut groups = HashMap::new(); + for (section_name, entries) in ini_file.sections { + let mut complete = 0.0; + println!("section name: {}", section_name); + for entrie in entries { + for val in entrie.values { + if entrie.name.is_empty() || val.is_empty() { + continue + } + println!("entrie is : {}", entrie.name); + let transactions = CsvFile::from_file("data/t.csv", ";", true); + let t : Vec<banking::Transaction> ; + match transactions { + Ok(trans) => t = banking::Transaction::from_sparkasse_csv_file(trans), + Err(e) => panic!("could not read file {:?}", e) + } + let re = Regex::new(&val).unwrap(); + let tmp = t.into_iter().filter(|transaction| + re.is_match(&transaction.sender_name) ) + .fold(0.0, |acc, x| acc + x.amount); + complete = complete + tmp.abs(); + } + } + groups.insert(section_name, complete); + // ALSO INSERT OTHER, AKA THE REST + } + let context = ChartContext { account_name : String::from("Girokonto"), + groups : groups }; + Template::render("chart", context) +} + diff --git a/src/web_frontend/mod.rs b/src/web_frontend/mod.rs new file mode 100644 index 0000000..8577065 --- /dev/null +++ b/src/web_frontend/mod.rs @@ -0,0 +1,29 @@ +pub mod transactions; +pub mod balance; +use parsers::csv::CsvFile; +use crate::banking::Account; +//use parsers::ini::IniFile; +use std::collections::HashMap; +use rocket_contrib::templates::Template; +use rocket::response::NamedFile; +use std::path::{PathBuf, Path}; +use rocket::request::Form; +use rocket::http::RawStr; +use regex::Regex; +use chrono::{NaiveDate, Utc}; +use chrono::Datelike; +/* + * Overview over all accounts, complete asset overview? + */ +#[get("/")] +pub fn account_handler() -> rocket_contrib::templates::Template { + let context : HashMap<u32, u32> = HashMap::new(); + Template::render("account", context) +} + +// allow always access +#[get("/static/<file..>")] +pub fn static_handler(file: PathBuf) -> Option<NamedFile> { + NamedFile::open(Path::new("static/").join(file)).ok() +} + diff --git a/src/web_frontend/transactions.rs b/src/web_frontend/transactions.rs new file mode 100644 index 0000000..417ca96 --- /dev/null +++ b/src/web_frontend/transactions.rs @@ -0,0 +1,101 @@ +use parsers::csv::CsvFile; +use crate::banking::Account; +//use parsers::ini::IniFile; +use std::collections::HashMap; +use rocket_contrib::templates::Template; +use rocket::response::NamedFile; +use std::path::{PathBuf, Path}; +use rocket::request::Form; +use rocket::http::RawStr; +use regex::Regex; +use chrono::{NaiveDate, Utc}; +use chrono::Datelike; + +/* + This files contains the code to handle the /transactions requests +*/ + +/* + * This context is passed to the template rendering engine + * If you modify this structure, adapt also the template to include the + * changes! + */ +#[derive(Serialize)] +struct TransactionContext { + transactions : Vec<crate::banking::Transaction>, + account_name : String, + filter : String, + date_start : String, + date_end : String +} + +#[derive(FromForm)] +pub struct TransactionFilter { + filter : String +} + +impl TransactionFilter { + pub fn apply_filter(&self, transactions : Vec<crate::banking::Transaction>) -> Vec<crate::banking::Transaction> { + let transactions = CsvFile::from_file("data/t.csv", ";", true); + let t : Vec<crate::banking::Transaction> ; + match transactions { + Ok(trans) => t = crate::banking::Transaction::from_sparkasse_csv_file(trans), + Err(e) => panic!("could not read file {:?}", e) + } + //self.filter.split("=").collect::<Vec<&str>>(); + let re = Regex::new(&self.filter).unwrap(); + let tmp = t.into_iter().filter(|transaction| re.is_match(&transaction.sender_name) || re.is_match(&transaction.reference) ).collect(); + tmp + + } +} + +#[post("/transactions?<start>&<end>", data= "<transaction_filter>")] +pub fn transaction_handler_post(start : Option<&RawStr>, end : Option<&RawStr>, + transaction_filter : Form<TransactionFilter>) -> rocket_contrib::templates::Template { + let date_start = match start { + Some(s) => { let mut tmp = s.to_string(); + tmp.push_str("-01"); + chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap() }, + None => Utc::today().naive_utc() + }; + let date_end = match end { + Some(s) => { let mut tmp = s.to_string(); + tmp.push_str("-01"); + chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap() }, + None => Utc::today().naive_utc() + }; + let input : TransactionFilter = transaction_filter.into_inner(); + let tmp = Vec::new(); + let ft = input.apply_filter(tmp); + let context = TransactionContext { transactions : ft, account_name : String::from("TEST"), + filter : input.filter, date_start : date_start.to_string(), + date_end : date_end.to_string()}; + Template::render("transaction", context) +} + +#[get("/transactions?<start>&<end>")] +pub fn transaction_handler(start : Option<&RawStr>, end : Option<&RawStr>) -> rocket_contrib::templates::Template { + let date_start = match start { + Some(s) => { let mut tmp = s.to_string(); + tmp.push_str("-01"); + chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap() }, + None => Utc::today().naive_utc() + }; + let date_end = match end { + Some(s) => { let mut tmp = s.to_string(); + tmp.push_str("-01"); + chrono::NaiveDate::parse_from_str(&tmp, "%Y-%m-%d").unwrap() }, + None => Utc::today().naive_utc() + }; + let transactions = CsvFile::from_file("data/t.csv", ";", true); + let t : Vec<crate::banking::Transaction> ; + match transactions { + Ok(trans) => t = crate::banking::Transaction::from_sparkasse_csv_file(trans), + Err(e) => panic!("could not read file {:?}", e) + } + let context = TransactionContext { transactions: t, account_name : String::from("Girokonto"), + filter : String::from(""), date_start : date_start.to_string()[0..7].to_string(), + date_end : date_end.to_string()[0..7].to_string()}; + Template::render("transaction", context) +} diff --git a/templates/account.html.tera b/templates/account.html.tera new file mode 100644 index 0000000..c1dc15e --- /dev/null +++ b/templates/account.html.tera @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> --> + <link rel="stylesheet" href="/static/bootstrap.min.css"> +<title>Account Overview</title> +</head> +<body> +<div class="container"> + <h1>Your applications</h1> + <div class="row"> + <div class="col-sm-12"> + <h2>Summary:</h2> + <table class="table table-hover"> + <tr><th>Balacne</th><th></th></tr> + <tr><th>Transactions this month</th><th></th></tr> + </table> + </div> + </div> + {% if false %} + <div class="row"> + <div class="col-sm-12"> + <h2>List of applications:</h2> + <table class="table table-hover"> + <thead> + <tr> + <th>Company</th> + <th>Role</th> + <th>Level</th> + <th>Link</th> + <th>Status</th> + </tr> + </thead> + <tbody> + {% for application in applications %} + {% if application.status == "in progress" or + application.status == "send" %} + <tr class="info clickable-row" data-href="/view/application/{{ application.uid }}"> + {% elif application.status == "rejected" %} + <tr class="danger clickable-row" data-href="/view/application/{{ application.uid }}"> + {% elif application.status == "accepted" %} + <tr class="success clickable-row" data-href="/view/application/{{ application.uid }}"> + {% else %} + <tr> + {% endif %} + <td>{{ application.company }}</td> + <td>{{ application.role }}</td> + <td>{{ application.level}}</td> + <td>{{ application.link}}</td> + <td>{{ application.status | upper }}</td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + </div> + {% endif %} +</div> + +</body> +</html> diff --git a/templates/balance.html.tera b/templates/balance.html.tera new file mode 100644 index 0000000..13dc39d --- /dev/null +++ b/templates/balance.html.tera @@ -0,0 +1,109 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> --> + <link rel="stylesheet" href="/static/bootstrap.min.css"> + <link rel="stylesheet" href="/static/custom.css"> + <title>Transaction Overview for account {{ account_name }}</title> +</head> +<body> +<nav class="navbar navbar-expand-lg navbar-light bg-light"> + <a class="navbar-brand" href="#">Finz</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarNavDropdown"> + <ul class="navbar-nav"> + <li class="nav-item active"> + <li class="nav-item dropdown active"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {{ account_name }} + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="/transactions">All Transactions</a> + <a class="dropdown-item" href="/chart">Spending chart</a> + <a class="dropdown-item" href="/balance">Balance</a> + </div> + </li> + <li class="nav-item dropdown active"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Asset + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="/transactions">Overview</a> + <a class="dropdown-item" href="/chart">Girokonto</a> + </div> + </li> + <li class="nav-item"> + <a class="nav-link" href="#">Investing</a> + </li> + </ul> + </div> +</nav> +<!-- content --> +<div class="container"> +<h1>Balance <small class="text-muted">{{ account_name }}</small></h1> +<div style="text-align:center; margin: auto; width:100%; padding: 10px;"> + <form method="get" action="/balance"><input type="month" id="start" name="start" value="{{ date_start }}"> to <input type="month" id="end" name="end"value="{{ date_end }}"> + <input type="submit" value="Show" class="btn btn-light"> + </form> +</div> + <div class="row"> + <div class="col-sm-12"> + <table class="table table-hover"> + <thead> + <tr> + <th>Month</th> + <th>Earned</th> + <th>Spend</th> + <th>Balance</th> + </tr> + </thead> + <tbody> + {% set balance_sum = 0 %} + {% for month in months %} + <tr> + <td>{{ month.name }}</td> + <td>{{ month.earned | round(method="common", precision=2) }}</td> + <td>{{ month.spent | round(method="common", precision=2) }}</td> + {% set balance = month.earned - month.spent %} + {% if balance >= 0 %} + <td class="text-success">{{ balance | round(method="common", precision=2) }}</td> + {% else %} + <td class="text-danger">{{ balance | round(method="common", precision=2) }}</td> + {% endif %} + </tr> + {% set_global balance_sum = balance_sum + balance %} + {% endfor %} + <tr style="border-top:2px solid #000000;"> + <td></td> + <td></td> + <td></td> + {% if balance_sum >= 0 %} + <td class="text-success">{{ balance_sum| round(method="common", precision=2) }}</td> + {% else %} + <td class="text-danger">{{ balance_sum | round(method="common", precision=2) }}</td> + {% endif %} + </tr> + </tbody> + </table> + </div> + </div> +</div> + <script src="/static/jquery.min.js"></script> + <script src="/static/popper.min.js"></script> + <script src="/static/bootstrap.min.js"></script> + <script> + $(document).ready(function() { + $("#filter").focus(function() { + $("#help").css("visibility", "visible"); + }); + $("#filter").focusout(function() { + $("#help").css("visibility", "hidden"); + }); + }); + </script> +</body> +</html> diff --git a/templates/chart.html.tera b/templates/chart.html.tera new file mode 100644 index 0000000..6ce1613 --- /dev/null +++ b/templates/chart.html.tera @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> --> + <link rel="stylesheet" href="/static/bootstrap.min.css"> + <link rel="stylesheet" href="/static/custom.css"> + <script src="/static/chart.js"></script> +<script type="text/javascript"> + google.charts.load("current", {packages:["corechart"]}); + google.charts.setOnLoadCallback(drawChart); + function drawChart() { + var data = google.visualization.arrayToDataTable([ + ['Money spent', 'EUR'], + {% for name, value in groups %} + ['{{ name }}', {{ value }}], + {% endfor %} + ]); + + var options = { + pieHole: 0.4, + legend : { position : 'right', alignment : 'center' }, + }; + + var chart = new google.visualization.PieChart(document.getElementById('donutchart')); + chart.draw(data, options); + + } + </script> + <title>Graphic Overview for account {{ account_name }}</title> +</head> +<body> +<nav class="navbar navbar-expand-lg navbar-light bg-light"> + <a class="navbar-brand" href="#">Finz</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarNavDropdown"> + <ul class="navbar-nav"> + <li class="nav-item active"> + <li class="nav-item dropdown active"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {{ account_name }} + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="/transactions">All Transactions</a> + <a class="dropdown-item" href="/chart">Spending chart</a> + <a class="dropdown-item" href="/balance">Balance</a> + </div> + </li> + <li class="nav-item dropdown active"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Asset + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="/transactions">Overview</a> + <a class="dropdown-item" href="/chart">Girokonto</a> + </div> + </li> + <li class="nav-item"> + <a class="nav-link" href="#">Investing</a> + </li> + </ul> + </div> +</nav> +<!-- content --> +<div class="container"> +<h1>Spending Chart <small class="text-muted">{{ account_name }}</small></h1> +<div style="text-align:center; margin: auto; width:100%"> + <form method="post" action="/chart"><input type="month" id="start"> to <input type="month" id="end" value="today"> + <input type="submit" value="Show" class="btn btn-light"> + </form> +</div> + <!-- stack coln charts to compare months --> + <div id="donutchart" style="margin: auto; width: 100%; height: 500px;"></div> +</div> + + <script src="/static/jquery.min.js"></script> + <script src="/static/popper.min.js"></script> + <script src="/static/bootstrap.min.js"></script> +</body> +</html> diff --git a/templates/transaction.html.tera b/templates/transaction.html.tera new file mode 100644 index 0000000..a1fe899 --- /dev/null +++ b/templates/transaction.html.tera @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> --> + <link rel="stylesheet" href="/static/bootstrap.min.css"> + <link rel="stylesheet" href="/static/coustom.css"> + <title>Transaction Overview for account {{ account_name }}</title> +</head> +<body> +<nav class="navbar navbar-expand-lg navbar-light bg-light"> + <a class="navbar-brand" href="#">Finz</a> + <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarNavDropdown"> + <ul class="navbar-nav"> + <li class="nav-item active"> + <li class="nav-item dropdown active"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + {{ account_name }} + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="/transactions">All Transactions</a> + <a class="dropdown-item" href="/chart">Spending chart</a> + <a class="dropdown-item" href="/balance">Balance</a> + </div> + </li> + <li class="nav-item dropdown active"> + <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> + Asset + </a> + <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> + <a class="dropdown-item" href="/transactions">Overview</a> + <a class="dropdown-item" href="/chart">Girokonto</a> + </div> + </li> + <li class="nav-item"> + <a class="nav-link" href="#">Investing</a> + </li> + </ul> + </div> +</nav> +<!-- content --> +<div class="container"> +<div class="md-form mt-0" style="padding-top: 20px;"> + <form name="filter" method="post" action="/transactions"> + {% if filter == "" %} + <input id="filter" name="filter" class="form-control" type="text" placeholder="Filter"> + {% else %} + <input id="filter" name="filter" class="form-control" type="text" value="{{ filter }}"> + {% endif %} + <input type="submit" style="visibility: hidden; display: none" /> + </form> + <h6 style="padding-top:5px; visibility: hidden;" id="help" class="text-success">Help: search single rows with: sender=value; logical || and && are also possible</h6> +</div> +<div style="text-align:center; margin: auto; width:100%; padding: 10px;"> + <form method="get" action="/transactions"><input type="month" id="start" name="start" value="{{ date_start }}"> to <input type="month" id="end" name="end" value="{{ date_end }}"> + <input type="submit" value="Show" class="btn btn-light"> + </form> +</div> +<h1>Transactions <small class="text-muted">{{ account_name }}</small></h1> + <div class="row"> + <div class="col-sm-12"> + <table class="table table-hover"> + <thead> + <tr> + <th>Sender</th> + <th>Reference</th> + <th>Date</th> + <th>Amount</th> + </tr> + </thead> + <tbody> + {% set amount_sum = 0 %} + {% for transaction in transactions %} + <tr> + <td>{{ transaction.sender_name }}</td> + <td>{{ transaction.reference | truncate(length=40) }}</td> + <td>{{ transaction.date }}</td> + {% if transaction.amount >= 0 %} + <td class="text-success">{{ transaction.amount | round(method="common", precision=2) }}</td> + {% else %} + <td class="text-danger">{{ transaction.amount | round(method="common", precision=2) }}</td> + {% endif %} + </tr> + {% set_global amount_sum = amount_sum + transaction.amount %} + {% endfor %} + <tr style="border-top:2px solid #000000;"> + <td></td> + <td></td> + <td></td> + {% if amount_sum >= 0 %} + <td class="text-success">{{ amount_sum | round(method="common", precision=2) }}</td> + {% else %} + <td class="text-danger">{{ amount_sum | round(method="common", precision=2) }}</td> + {% endif %} + </tr> + </tbody> + </table> + </div> + </div> +</div> + <script src="/static/jquery.min.js"></script> + <script src="/static/popper.min.js"></script> + <script src="/static/bootstrap.min.js"></script> + <script> + $(document).ready(function() { + $("#filter").focus(function() { + $("#help").css("visibility", "visible"); + }); + $("#filter").focusout(function() { + $("#help").css("visibility", "hidden"); + }); + }); + </script> +</body> +</html> |
