From e336292db4727e111fa1b3c7af981cd2df29bc18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Forc=C3=A9n=20Mu=C3=B1oz?= Date: Wed, 20 Mar 2024 20:49:22 +0100 Subject: [PATCH] Condensed classifiers in one place --- src/models/transaction.rs | 26 +++ templates/base.html | 5 +- ...{categories_list.html => classifiers.html} | 24 ++- templates/index.html | 23 +++ templates/rules_list.html | 23 --- webserver/src/main.rs | 26 ++- webserver/src/routes/ui.rs | 8 +- webserver/src/routes/ui/classifier.rs | 155 ++++++++++++++++++ 8 files changed, 256 insertions(+), 34 deletions(-) rename templates/{categories_list.html => classifiers.html} (50%) delete mode 100644 templates/rules_list.html create mode 100644 webserver/src/routes/ui/classifier.rs diff --git a/src/models/transaction.rs b/src/models/transaction.rs index c80394c..13d4c1a 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -101,6 +101,32 @@ impl Transaction { Ok(res) } + pub async fn list_by_user( + pool: &SqlitePool, + user: i32, + limit: i32, + offset: i32, + asc: bool, + ) -> Result> { + let rows = sqlx::query( + if asc { + "SELECT t.* FROM transactions t JOIN accounts a ON a.account_id=t.account WHERE a.user=? ORDER BY transaction_timestamp ASC LIMIT ? OFFSET ?" + } else { + "SELECT t.* FROM transactions t JOIN accounts a ON a.account_id=t.account WHERE a.user=? ORDER BY transaction_timestamp DESC LIMIT ? OFFSET ?" + } + ).bind(user) + .bind(limit) + .bind(offset) + .fetch_all(pool) + .await?; + + let mut res = Vec::new(); + for r in &rows { + res.push(Transaction::from_row(r)?); + } + Ok(res) + } + pub fn query_by_date<'a>( account: i32, after: Option>, diff --git a/templates/base.html b/templates/base.html index 07c127a..53e5666 100644 --- a/templates/base.html +++ b/templates/base.html @@ -25,9 +25,10 @@
{% block body %} diff --git a/templates/categories_list.html b/templates/classifiers.html similarity index 50% rename from templates/categories_list.html rename to templates/classifiers.html index b603fa7..f0decd6 100644 --- a/templates/categories_list.html +++ b/templates/classifiers.html @@ -1,8 +1,8 @@ {% extends "base.html" %} -{% block title %}Categories{% endblock title %} +{% block title %}Rules{% endblock title %} {% block body %}
- New + New
@@ -22,4 +22,24 @@ {% endfor %}
+ +
+ New +
+ + + + + + + + + {% for rule in rules %} + + + + + {% endfor %} + +
CategoríaRegla
{{rule.category}}{{rule.regex}}
{% endblock body %} diff --git a/templates/index.html b/templates/index.html index 89ed83f..d66e104 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,4 +7,27 @@ {{account.account_name}} {% endfor %}
+

Last transactions

+
+ + + + + + + + + + + {% for tx in transactions %} + + + + + + + {% endfor %} + +
DescriptionDateAmountCategory
{{tx.description}}{{tx.transaction_timestamp}}{{tx.amount/100}}{{tx.category}}
+
{% endblock body %} diff --git a/templates/rules_list.html b/templates/rules_list.html deleted file mode 100644 index 16a9ebe..0000000 --- a/templates/rules_list.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} -{% block title %}Rules{% endblock title %} -{% block body %} -
- New -
- - - - - - - - - {% for rule in rules %} - - - - - {% endfor %} - -
CategoríaRegla
{{rule.category}}{{rule.regex}}
-{% endblock body %} diff --git a/webserver/src/main.rs b/webserver/src/main.rs index 3cb05e4..04aa77f 100644 --- a/webserver/src/main.rs +++ b/webserver/src/main.rs @@ -42,12 +42,26 @@ async fn main() { "/accounts/id/:id/transactions/add", post(routes::ui::account::add_transactions_action), ) - .route("/rules", get(routes::ui::rules::list)) - .route("/rules/new", get(routes::ui::rules::new_view)) - .route("/rules/new", post(routes::ui::rules::new_action)) - .route("/categories", get(routes::ui::categories::list)) - .route("/categories/new", get(routes::ui::categories::new_view)) - .route("/categories/new", post(routes::ui::categories::new_action)) + .route( + "/classifiers", + get(routes::ui::classifier::view_classifiers), + ) + .route( + "/classifiers/new_rule", + get(routes::ui::classifier::rules_new_view), + ) + .route( + "/classifiers/new_rule", + post(routes::ui::classifier::rules_new_action), + ) + .route( + "/classifiers/new_category", + get(routes::ui::classifier::category_new_view), + ) + .route( + "/classifiers/new_category", + post(routes::ui::classifier::category_new_action), + ) .nest( "/static", Router::new() diff --git a/webserver/src/routes/ui.rs b/webserver/src/routes/ui.rs index 1771bac..3e8a59a 100644 --- a/webserver/src/routes/ui.rs +++ b/webserver/src/routes/ui.rs @@ -6,10 +6,11 @@ use sqlx::SqlitePool; use tera::{Context, Tera}; use crate::users::UserToken; -use accounters::models::Account; +use accounters::models::{Account, Transaction}; pub mod account; pub mod categories; +pub mod classifier; pub mod rules; pub async fn index( @@ -22,6 +23,11 @@ pub async fn index( let accounts = Account::list(db.as_ref(), uid.user_id).await.unwrap(); ctx.insert("accounts", &accounts); + let transactions = Transaction::list_by_user(db.as_ref(), uid.user_id, 10, 0, false) + .await + .unwrap(); + ctx.insert("transactions", &transactions); + match tmpls.render("index.html", &ctx) { Ok(out) => ( StatusCode::OK, diff --git a/webserver/src/routes/ui/classifier.rs b/webserver/src/routes/ui/classifier.rs new file mode 100644 index 0000000..3fb91c3 --- /dev/null +++ b/webserver/src/routes/ui/classifier.rs @@ -0,0 +1,155 @@ +use std::sync::Arc; + +use accounters::models::{categories::Category, rules::Rule}; +use axum::{ + extract::{Form, State}, + response::IntoResponse, +}; +use hyper::{header::CONTENT_TYPE, StatusCode}; +use serde::Deserialize; +use sqlx::SqlitePool; +use tera::{Context, Tera}; + +use crate::users::UserToken; + +pub async fn view_classifiers( + State(db): State>, + State(tmpls): State>, + uid: UserToken, +) -> impl IntoResponse { + let rules = match Rule::list_by_user(db.as_ref(), uid.user_id).await { + Ok(r) => r, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain")], + format!("{e:?}"), + ); + } + }; + + let categories = match Category::list(db.as_ref()).await { + Ok(categories) => categories, + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain;charset=utf-8")], + format!("{e}"), + ) + } + }; + + let mut ctx = Context::new(); + + ctx.insert("rules", &rules); + ctx.insert("categories", &categories); + + ( + StatusCode::OK, + [(CONTENT_TYPE, "text/html;charset=utf-8")], + tmpls.render("classifiers.html", &ctx).unwrap(), + ) +} + +pub async fn rules_new_view( + State(db): State>, + State(tmpls): State>, + uid: UserToken, +) -> impl IntoResponse { + let categories = Category::list(db.as_ref()).await.unwrap(); + let mut ctx = Context::new(); + ctx.insert("categories", &categories); + ( + StatusCode::OK, + [(CONTENT_TYPE, "text/html;charset=utf-8")], + tmpls.render("rules_new.html", &ctx).unwrap(), + ) +} + +#[derive(Deserialize)] +pub struct NewRuleParams { + pub description: String, + pub regex: String, + pub category: i32, +} + +pub async fn rules_new_action( + State(db): State>, + State(tmpls): State>, + uid: UserToken, + Form(params): Form, +) -> impl IntoResponse { + match Rule::new(db.as_ref(), uid.user_id, params.regex, params.category).await { + Ok(_) => ( + StatusCode::OK, + [(CONTENT_TYPE, "text/html;charset=utf-8")], + tmpls + .render("rules_new_success.html", &Context::new()) + .unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain;charset=utf-8")], + format!("{e}"), + ), + } +} + +pub async fn category_list( + State(db): State>, + State(tmpl): State>, + uid: UserToken, +) -> impl IntoResponse { + match Category::list(db.as_ref()).await { + Ok(categories) => { + let mut ctx = Context::new(); + ctx.insert("categories", &categories); + ( + StatusCode::OK, + [(CONTENT_TYPE, "text/html;charset=utf-8")], + tmpl.render("categories_list.html", &ctx).unwrap(), + ) + } + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain;charset=utf-8")], + format!("{e}"), + ), + } +} + +pub async fn category_new_view(State(tmpl): State>, uid: UserToken) -> impl IntoResponse { + ( + StatusCode::OK, + [(CONTENT_TYPE, "text/html;charset=utf-8")], + tmpl.render("categories_new.html", &Context::new()).unwrap(), + ) +} + +#[derive(Deserialize)] +pub struct CategoryNewRuleParams { + pub name: String, + pub description: String, +} + +pub async fn category_new_action( + State(db): State>, + State(tmpls): State>, + uid: UserToken, + Form(params): Form, +) -> impl IntoResponse { + match Category::new(db.as_ref(), ¶ms.name, ¶ms.description).await { + Ok(_) => ( + StatusCode::OK, + [(CONTENT_TYPE, "text/html;charset=utf-8")], + tmpls + .render("rules_new_success.html", &Context::new()) + .unwrap(), + ), + Err(e) => ( + StatusCode::INTERNAL_SERVER_ERROR, + [(CONTENT_TYPE, "text/plain;charset=utf-8")], + format!("{e}"), + ), + } +}