Added new views, joined rules and categories
This commit is contained in:
parent
bbc61cfe45
commit
759f91a9a2
12 changed files with 469 additions and 83 deletions
|
|
@ -42,6 +42,8 @@ async fn main() {
|
|||
"/accounts/id/:id/transactions/add",
|
||||
post(routes::ui::account::add_transactions_action),
|
||||
)
|
||||
.route("/transaction/:id", get(routes::ui::transaction::view))
|
||||
.route("/transaction/:id", post(routes::ui::transaction::update))
|
||||
.route(
|
||||
"/classifiers",
|
||||
get(routes::ui::classifier::view_classifiers),
|
||||
|
|
|
|||
|
|
@ -1,17 +1,57 @@
|
|||
use std::sync::Arc;
|
||||
use std::{borrow::BorrowMut, collections::HashMap, sync::Arc};
|
||||
|
||||
use axum::{extract::State, response::IntoResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use hyper::{header::CONTENT_TYPE, StatusCode};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::{Context, Tera};
|
||||
|
||||
use crate::users::UserToken;
|
||||
use accounters::models::{account::Account, transaction::Transaction};
|
||||
use accounters::models::{account::Account, categories::Category, transaction::Transaction};
|
||||
|
||||
pub mod account;
|
||||
pub mod categories;
|
||||
pub mod classifier;
|
||||
pub mod rules;
|
||||
pub mod transaction;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AccountRender {
|
||||
id: i32,
|
||||
description: String,
|
||||
accumulated: f32,
|
||||
}
|
||||
|
||||
impl AccountRender {
|
||||
async fn from_account(pool: &SqlitePool, acc: Account) -> Self {
|
||||
let last_acc = Transaction::list(pool, acc.get_id(), 1, 0, false)
|
||||
.await
|
||||
.map_or(0.0, |x| {
|
||||
x.get(0)
|
||||
.map_or(0.0, |x| (x.get_accumulated() as f32) / 100.0)
|
||||
});
|
||||
Self {
|
||||
id: acc.get_id(),
|
||||
description: acc.get_account_name().to_string(),
|
||||
accumulated: last_acc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hm_sort(hm: HashMap<i32, i64>, collapse: usize) -> Vec<(i32, i64)> {
|
||||
let mut res: Vec<(i32, i64)> = hm.into_iter().collect();
|
||||
res.sort_unstable_by(|a, b| b.1.cmp(&a.1));
|
||||
if res.len() > collapse {
|
||||
let rest = res
|
||||
.split_off(collapse)
|
||||
.iter()
|
||||
.fold(0i64, |acc, item| acc + item.1);
|
||||
let last = res.last_mut().unwrap();
|
||||
*last = (-1, last.1 + rest);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub async fn index(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
|
|
@ -21,7 +61,65 @@ pub async fn index(
|
|||
let mut ctx = Context::new();
|
||||
|
||||
let accounts = Account::list(db.as_ref(), uid.user_id).await.unwrap();
|
||||
ctx.insert("accounts", &accounts);
|
||||
let mut acc_render = Vec::new();
|
||||
|
||||
for acc in accounts.into_iter() {
|
||||
acc_render.push(AccountRender::from_account(db.as_ref(), acc).await);
|
||||
}
|
||||
|
||||
ctx.insert("accounts", &acc_render);
|
||||
|
||||
let last_month = Transaction::list_by_date(
|
||||
db.as_ref(),
|
||||
uid.user_id,
|
||||
Some(Utc::now() - chrono::Duration::days(30)),
|
||||
Some(Utc::now()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut categories: HashMap<i32, String> = Category::list(db.as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| (x.category_id, x.name.clone()))
|
||||
.collect();
|
||||
categories.insert(0, String::from("Unclassified"));
|
||||
ctx.insert("categories", &categories);
|
||||
|
||||
let mut income: HashMap<i32, i64> = HashMap::new();
|
||||
let mut expenses: HashMap<i32, i64> = HashMap::new();
|
||||
|
||||
for tx in last_month.iter() {
|
||||
if tx.get_amount() > 0 {
|
||||
let acc = income
|
||||
.entry(tx.get_category().unwrap_or(0))
|
||||
.or_default()
|
||||
.borrow_mut();
|
||||
*acc = *acc + tx.get_amount() as i64;
|
||||
} else {
|
||||
let acc = expenses
|
||||
.entry(tx.get_category().unwrap_or(0))
|
||||
.or_default()
|
||||
.borrow_mut();
|
||||
*acc = *acc - tx.get_amount() as i64;
|
||||
}
|
||||
}
|
||||
|
||||
let income = hm_sort(income, 5);
|
||||
let expenses = hm_sort(expenses, 5);
|
||||
ctx.insert("income", &income);
|
||||
ctx.insert("expenses", &expenses);
|
||||
|
||||
let mut colors = Vec::new();
|
||||
colors.extend_from_slice(&(["85FF33", "60F000", "46AF00", "2C6D00", "1A4200"][..income.len()]));
|
||||
|
||||
colors
|
||||
.extend_from_slice(&(["FF3333", "C50000", "830000", "570000", "420000"][..expenses.len()]));
|
||||
|
||||
ctx.insert("colors", &colors);
|
||||
|
||||
let transactions = Transaction::list_by_user(db.as_ref(), uid.user_id, 10, 0, false)
|
||||
.await
|
||||
|
|
@ -37,7 +135,7 @@ pub async fn index(
|
|||
Err(e) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
[(CONTENT_TYPE, "text/plain")],
|
||||
format!("{e}"),
|
||||
format!("{e:?}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
|
|
@ -12,10 +12,7 @@ use sqlx::SqlitePool;
|
|||
use tera::{Context, Tera};
|
||||
|
||||
use crate::users::UserToken;
|
||||
use accounters::models::{
|
||||
account::Account,
|
||||
transaction::{Transaction, TxConflictResolutionMode},
|
||||
};
|
||||
use accounters::models::{account::Account, categories::Category, transaction::Transaction};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AccountViewParams {
|
||||
|
|
@ -51,6 +48,14 @@ pub async fn list(
|
|||
);
|
||||
}
|
||||
|
||||
let categories: HashMap<i32, String> = Category::list(db.as_ref())
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| (x.category_id, x.name.clone()))
|
||||
.collect();
|
||||
ctx.insert("categories", &categories);
|
||||
|
||||
let n_entries = entries.unwrap_or(10).max(10);
|
||||
let page = page.unwrap_or(0).max(0);
|
||||
|
||||
|
|
@ -146,7 +151,6 @@ pub async fn add_transactions_action(
|
|||
&tx.date,
|
||||
None,
|
||||
(tx.amount * 100.0).round() as i32,
|
||||
TxConflictResolutionMode::Nothing,
|
||||
)
|
||||
.await
|
||||
{
|
||||
|
|
|
|||
99
webserver/src/routes/ui/transaction.rs
Normal file
99
webserver/src/routes/ui/transaction.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use accounters::models::{categories::Category, transaction::Transaction};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
Form,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use hyper::{header, StatusCode};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Tera;
|
||||
|
||||
use crate::users::UserToken;
|
||||
|
||||
pub async fn view(
|
||||
db: State<Arc<SqlitePool>>,
|
||||
tmpl: State<Arc<Tera>>,
|
||||
user: UserToken,
|
||||
Path(id): Path<i32>,
|
||||
) -> impl IntoResponse {
|
||||
let tx = Transaction::get_by_id(db.as_ref(), id).await.unwrap();
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("tx_id", &id);
|
||||
ctx.insert("tx", &tx);
|
||||
|
||||
let categories = Category::list(db.as_ref()).await.unwrap();
|
||||
ctx.insert("categories", &categories);
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "text/html;charset=utf-8")],
|
||||
tmpl.render("transaction.html", &ctx).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
fn deserialize_optional<'de, D>(data: D) -> Result<Option<i32>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let str = String::deserialize(data)?;
|
||||
if str.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(str.parse().unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct TxUpdateRequest {
|
||||
description: String,
|
||||
date: DateTime<Utc>,
|
||||
amount: f32,
|
||||
#[serde(deserialize_with = "deserialize_optional")]
|
||||
category: Option<i32>,
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
db: State<Arc<SqlitePool>>,
|
||||
tmpl: State<Arc<Tera>>,
|
||||
user: UserToken,
|
||||
Path(id): Path<i32>,
|
||||
Form(req): Form<TxUpdateRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let ret_str = format!("/transaction/{id}");
|
||||
let mut tx = match Transaction::get_by_id(db.as_ref(), id).await {
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
return (
|
||||
StatusCode::NOT_FOUND,
|
||||
[(header::LOCATION, ret_str)],
|
||||
format!("{e:?}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let amount = (req.amount * 100.0).round() as i32;
|
||||
|
||||
if tx.get_amount() != amount {
|
||||
tx.set_amount(db.as_ref(), amount).await.unwrap();
|
||||
}
|
||||
|
||||
if tx.get_description() != req.description {
|
||||
tx.set_description(db.as_ref(), &req.description)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if tx.get_category() != req.category {
|
||||
tx.set_category(db.as_ref(), req.category).await.unwrap();
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::MOVED_PERMANENTLY,
|
||||
[(header::LOCATION, ret_str)],
|
||||
String::new(),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue