Condensed classifiers in one place
This commit is contained in:
parent
cf7fc2ec87
commit
e336292db4
8 changed files with 256 additions and 34 deletions
|
|
@ -101,6 +101,32 @@ impl Transaction {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_by_user(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
user: i32,
|
||||||
|
limit: i32,
|
||||||
|
offset: i32,
|
||||||
|
asc: bool,
|
||||||
|
) -> Result<Vec<Self>> {
|
||||||
|
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>(
|
pub fn query_by_date<'a>(
|
||||||
account: i32,
|
account: i32,
|
||||||
after: Option<DateTime<Utc>>,
|
after: Option<DateTime<Utc>>,
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,10 @@
|
||||||
<body>
|
<body>
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<aside class="sidebar bg-stone-300 p-4 flex flex-col">
|
<aside class="sidebar bg-stone-300 p-4 flex flex-col">
|
||||||
<a class="hover:bg-stone-400" href="/">Start</a>
|
<a class="hover:bg-stone-400" href="/">Summary</a>
|
||||||
<a class="hover:bg-stone-400" href="/rules">Rules</a>
|
<a class="hover:bg-stone-400" href="/accounts">Accounts</a>
|
||||||
<a class="hover:bg-stone-400" href="/categories">Categories</a>
|
<a class="hover:bg-stone-400" href="/categories">Categories</a>
|
||||||
|
<a class="hover:bg-stone-400" href="/classifiers">Classifiers</a>
|
||||||
</aside>
|
</aside>
|
||||||
<div class="p-4 grow h-full overflow-auto">
|
<div class="p-4 grow h-full overflow-auto">
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Categories{% endblock title %}
|
{% block title %}Rules{% endblock title %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div>
|
<div>
|
||||||
<a href="/categories/new">New</a>
|
<a href="/classifiers/new_category">New</a>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
|
|
@ -22,4 +22,24 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<a href="/classifiers/new_rule">New</a>
|
||||||
|
</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Categoría</th>
|
||||||
|
<th>Regla</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rule in rules %}
|
||||||
|
<tr>
|
||||||
|
<td>{{rule.category}}</td>
|
||||||
|
<td>{{rule.regex}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
@ -7,4 +7,27 @@
|
||||||
<a class="p-2 hover:bg-stone-200" href="/accounts/id/{{account.account_id}}">{{account.account_name}}</a>
|
<a class="p-2 hover:bg-stone-200" href="/accounts/id/{{account.account_id}}">{{account.account_name}}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<div><h2 class="text-lg">Last transactions</h2></div>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Amount</th>
|
||||||
|
<th>Category</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for tx in transactions %}
|
||||||
|
<tr>
|
||||||
|
<td>{{tx.description}}</td>
|
||||||
|
<td>{{tx.transaction_timestamp}}</td>
|
||||||
|
<td>{{tx.amount/100}}</td>
|
||||||
|
<td>{{tx.category}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}Rules{% endblock title %}
|
|
||||||
{% block body %}
|
|
||||||
<div>
|
|
||||||
<a href="/rules/new">New</a>
|
|
||||||
</div>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Categoría</th>
|
|
||||||
<th>Regla</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for rule in rules %}
|
|
||||||
<tr>
|
|
||||||
<td>{{rule.category}}</td>
|
|
||||||
<td>{{rule.regex}}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% endblock body %}
|
|
||||||
|
|
@ -42,12 +42,26 @@ async fn main() {
|
||||||
"/accounts/id/:id/transactions/add",
|
"/accounts/id/:id/transactions/add",
|
||||||
post(routes::ui::account::add_transactions_action),
|
post(routes::ui::account::add_transactions_action),
|
||||||
)
|
)
|
||||||
.route("/rules", get(routes::ui::rules::list))
|
.route(
|
||||||
.route("/rules/new", get(routes::ui::rules::new_view))
|
"/classifiers",
|
||||||
.route("/rules/new", post(routes::ui::rules::new_action))
|
get(routes::ui::classifier::view_classifiers),
|
||||||
.route("/categories", get(routes::ui::categories::list))
|
)
|
||||||
.route("/categories/new", get(routes::ui::categories::new_view))
|
.route(
|
||||||
.route("/categories/new", post(routes::ui::categories::new_action))
|
"/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(
|
.nest(
|
||||||
"/static",
|
"/static",
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ use sqlx::SqlitePool;
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
|
|
||||||
use crate::users::UserToken;
|
use crate::users::UserToken;
|
||||||
use accounters::models::Account;
|
use accounters::models::{Account, Transaction};
|
||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod categories;
|
pub mod categories;
|
||||||
|
pub mod classifier;
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
|
|
||||||
pub async fn index(
|
pub async fn index(
|
||||||
|
|
@ -22,6 +23,11 @@ pub async fn index(
|
||||||
let accounts = Account::list(db.as_ref(), uid.user_id).await.unwrap();
|
let accounts = Account::list(db.as_ref(), uid.user_id).await.unwrap();
|
||||||
ctx.insert("accounts", &accounts);
|
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) {
|
match tmpls.render("index.html", &ctx) {
|
||||||
Ok(out) => (
|
Ok(out) => (
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
|
||||||
155
webserver/src/routes/ui/classifier.rs
Normal file
155
webserver/src/routes/ui/classifier.rs
Normal file
|
|
@ -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<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
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<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
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<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
Form(params): Form<NewRuleParams>,
|
||||||
|
) -> 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<Arc<SqlitePool>>,
|
||||||
|
State(tmpl): State<Arc<Tera>>,
|
||||||
|
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<Arc<Tera>>, 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<Arc<SqlitePool>>,
|
||||||
|
State(tmpls): State<Arc<Tera>>,
|
||||||
|
uid: UserToken,
|
||||||
|
Form(params): Form<CategoryNewRuleParams>,
|
||||||
|
) -> 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}"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue