Added new views, joined rules and categories

This commit is contained in:
Manuel Forcén Muñoz 2024-04-24 23:20:37 +02:00
parent bbc61cfe45
commit 759f91a9a2
12 changed files with 469 additions and 83 deletions

View file

@ -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),

View file

@ -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:?}"),
),
}
}

View file

@ -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
{

View 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(),
)
}