summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml15
-rw-r--r--src/banking/mod.rs59
-rw-r--r--src/exchange/mod.rs2
-rw-r--r--src/lib.rs0
-rw-r--r--src/main.rs46
-rw-r--r--src/parsers/csv/mod.rs (renamed from src/Csv/mod.rs)35
-rw-r--r--src/parsers/ini/mod.rs64
-rw-r--r--src/parsers/mod.rs2
-rw-r--r--src/web_frontend/balance.rs118
-rw-r--r--src/web_frontend/chart.rs61
-rw-r--r--src/web_frontend/mod.rs29
-rw-r--r--src/web_frontend/transactions.rs101
-rw-r--r--templates/account.html.tera63
-rw-r--r--templates/balance.html.tera109
-rw-r--r--templates/chart.html.tera83
-rw-r--r--templates/transaction.html.tera119
16 files changed, 881 insertions, 25 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 5efc7c0..5d465d2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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(&current_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>