diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index e51ab4f..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[env] -DATABASE_URL = "postgres://postgres:postgres@localhost:19522/postgres" diff --git a/.gitignore b/.gitignore index 68d3068..c907f58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ /target .shuttle-storage Secrets*.toml +.env +.cargo # ignore virtualenv /bin diff --git a/.sqlx/query-59183ce536917b910db8f480b490771a7440e56fa6f09a36a02f3fa79516f95b.json b/.sqlx/query-59183ce536917b910db8f480b490771a7440e56fa6f09a36a02f3fa79516f95b.json new file mode 100644 index 0000000..4c2a909 --- /dev/null +++ b/.sqlx/query-59183ce536917b910db8f480b490771a7440e56fa6f09a36a02f3fa79516f95b.json @@ -0,0 +1,67 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, description, price, expense_type as \"expense_type: ExpenseType\", is_essencial, date\n FROM expenses WHERE date BETWEEN $1 AND $2 ORDER BY date ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "price", + "type_info": "Float4" + }, + { + "ordinal": 3, + "name": "expense_type: ExpenseType", + "type_info": { + "Custom": { + "name": "expense_type", + "kind": { + "Enum": [ + "food", + "transport", + "health", + "education", + "entertainment", + "other" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "is_essencial", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "date", + "type_info": "Date" + } + ], + "parameters": { + "Left": [ + "Date", + "Date" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "59183ce536917b910db8f480b490771a7440e56fa6f09a36a02f3fa79516f95b" +} diff --git a/.sqlx/query-c6a4023380d34cfa6892168ae56ef7c3951792638f4d6779394922fa18516e61.json b/.sqlx/query-c6a4023380d34cfa6892168ae56ef7c3951792638f4d6779394922fa18516e61.json new file mode 100644 index 0000000..5976a57 --- /dev/null +++ b/.sqlx/query-c6a4023380d34cfa6892168ae56ef7c3951792638f4d6779394922fa18516e61.json @@ -0,0 +1,64 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT id, description, price, expense_type as \"expense_type: ExpenseType\", is_essencial, date\n FROM expenses ORDER BY date ASC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "description", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "price", + "type_info": "Float4" + }, + { + "ordinal": 3, + "name": "expense_type: ExpenseType", + "type_info": { + "Custom": { + "name": "expense_type", + "kind": { + "Enum": [ + "food", + "transport", + "health", + "education", + "entertainment", + "other" + ] + } + } + } + }, + { + "ordinal": 4, + "name": "is_essencial", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "date", + "type_info": "Date" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "c6a4023380d34cfa6892168ae56ef7c3951792638f4d6779394922fa18516e61" +} diff --git a/Cargo.lock b/Cargo.lock index 2fb7b5f..792a8bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2502,6 +2502,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "dotenvy", @@ -2587,6 +2588,7 @@ dependencies = [ "bitflags 2.4.1", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -2628,6 +2630,7 @@ dependencies = [ "base64", "bitflags 2.4.1", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -2664,6 +2667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 57b55f9..77d84cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ serde = { version = "1.0.193", features = ["derive"] } shuttle-axum = { version = "0.35.1", default-features = false, features = ["axum-0-7"] } shuttle-runtime = { version = "0.35.1", default-features = false, features = ["colored"] } shuttle-shared-db = { version = "0.35.1", features = ["postgres"] } -sqlx = { version = "0.7.3", features = ["runtime-tokio-rustls", "postgres"] } +sqlx = { version = "0.7.3", features = ["runtime-tokio-rustls", "postgres", "chrono"] } strum = { version = "0.25.0", features = ["strum_macros", "derive"] } tokio = { version = "1.35.1", features = ["full"] } tower = "0.4.13" diff --git a/migrations/1_schema.sql b/migrations/1_schema.sql index 7753487..78a1471 100644 --- a/migrations/1_schema.sql +++ b/migrations/1_schema.sql @@ -11,5 +11,6 @@ CREATE TABLE IF NOT EXISTS expenses ( description varchar(255) NOT NULL, price real NOT NULL, expense_type expense_type NOT NULL, - is_essencial boolean NOT NULL + is_essencial boolean NOT NULL, + date date NOT NULL DEFAULT CURRENT_DATE ); diff --git a/src/constant.rs b/src/constant.rs new file mode 100644 index 0000000..c33358e --- /dev/null +++ b/src/constant.rs @@ -0,0 +1,60 @@ +macro_rules! TABLE_ROW { + () => { + " + {} + {} + {} + {} + {} + + + + " + }; +} + +macro_rules! EDITABLE_TABLE_ROW { + () => { + " + + + + + + + + + + " + }; +} + +pub(crate) use EDITABLE_TABLE_ROW; +pub(crate) use TABLE_ROW; diff --git a/src/data.rs b/src/data.rs index 0f9729e..f5def97 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,9 +1,10 @@ -use std::fmt::Display; +use std::{fmt::Display, str::FromStr}; use chrono::Month; +use serde::Deserialize; use strum::EnumIter; -#[derive(EnumIter, Debug, PartialEq, Clone)] +#[derive(EnumIter, Debug, PartialEq, Clone, Deserialize)] pub enum Months { January, February, @@ -19,6 +20,28 @@ pub enum Months { December, } +impl FromStr for Months { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "January" => Ok(Months::January), + "February" => Ok(Months::February), + "March" => Ok(Months::March), + "April" => Ok(Months::April), + "May" => Ok(Months::May), + "June" => Ok(Months::June), + "July" => Ok(Months::July), + "August" => Ok(Months::August), + "September" => Ok(Months::September), + "October" => Ok(Months::October), + "November" => Ok(Months::November), + "December" => Ok(Months::December), + _ => Err(format!("{} is not a valid month", s)), + } + } +} + impl Display for Months { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/src/hypermedia_router.rs b/src/hypermedia_router.rs index 7664050..4c6aaee 100644 --- a/src/hypermedia_router.rs +++ b/src/hypermedia_router.rs @@ -1,15 +1,17 @@ +use crate::{ + constant::{EDITABLE_TABLE_ROW, TABLE_ROW}, + schema::{Expense, ExpenseType, GetExpense}, + util::{get_first_day_from_month, get_last_day_from_month}, + AppState, ExpensesTemplate, +}; use std::sync::Arc; use askama_axum::IntoResponse; -use axum::extract::Path; -use axum::routing::get; -use axum::Router; -use axum::{extract::State, response::Html}; - -use crate::{ - schema::Expense, - util::{EDITABLE_TABLE_ROW, TABLE_ROW}, - AppState, ExpensesTemplate, +use axum::{ + extract::{Path, Query, State}, + response::Html, + routing::get, + Router, }; pub fn hypermedia_router() -> Router> { @@ -26,15 +28,34 @@ pub async fn expenses_index() -> impl IntoResponse { } } -pub async fn get_expenses(State(shared_state): State>) -> impl IntoResponse { - // let expenses: Vec = sqlx::query_as!(Expense, "SELECT * FROM expenses") - // .fetch_all(&shared_state.pool) - // .await - // .unwrap(); - let expenses: Vec = sqlx::query_as("SELECT * FROM expenses") - .fetch_all(&shared_state.pool) - .await - .unwrap(); +pub async fn get_expenses( + State(shared_state): State>, + Query(get_expense_input): Query, +) -> impl IntoResponse { + let expenses: Vec = match get_expense_input.month { + Some(month) => { + sqlx::query_as!( + Expense, + r#"SELECT id, description, price, expense_type as "expense_type: ExpenseType", is_essencial, date + FROM expenses WHERE date BETWEEN $1 AND $2 ORDER BY date ASC"#, + get_first_day_from_month(month.clone() as u32 + 1), + get_last_day_from_month(month as u32 + 1) + ) + .fetch_all(&shared_state.pool) + .await + .unwrap() + } + None => { + sqlx::query_as!( + Expense, + r#"SELECT id, description, price, expense_type as "expense_type: ExpenseType", is_essencial, date + FROM expenses ORDER BY date ASC"# + ) + .fetch_all(&shared_state.pool) + .await + .unwrap() + } + }; Html( expenses @@ -42,6 +63,7 @@ pub async fn get_expenses(State(shared_state): State>) -> impl Int .map(|expense| { format!( TABLE_ROW!(), + expense.date, expense.description, expense.price, expense.expense_type, @@ -67,6 +89,7 @@ pub async fn edit_expense( Html(format!( EDITABLE_TABLE_ROW!(), expense.id, + expense.date, expense.description, expense.price, expense.is_essencial, @@ -87,7 +110,12 @@ pub async fn get_expense( Html(format!( TABLE_ROW!(), - expense.description, expense.price, expense.expense_type, expense.is_essencial, expense.id + expense.date, + expense.description, + expense.price, + expense.expense_type, + expense.is_essencial, + expense.id )) } @@ -103,6 +131,11 @@ pub async fn update_expense( Html(format!( TABLE_ROW!(), - expense.description, expense.price, expense.expense_type, expense.is_essencial, expense.id + expense.date, + expense.description, + expense.price, + expense.expense_type, + expense.is_essencial, + expense.id )) } diff --git a/src/main.rs b/src/main.rs index 29321a5..81f568e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod constant; mod data; mod data_router; mod hypermedia_router; diff --git a/src/schema.rs b/src/schema.rs index 9015436..07779fe 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,5 +1,7 @@ +use crate::Months; use std::{fmt::Display, str::FromStr}; +use chrono::NaiveDate; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use strum::EnumIter; @@ -51,4 +53,23 @@ pub struct Expense { pub price: f32, pub expense_type: ExpenseType, pub is_essencial: bool, + pub date: NaiveDate, +} + +#[derive(Deserialize, Debug)] +pub struct GetExpense { + #[serde(deserialize_with = "empty_string_to_none")] + pub month: Option, +} + +fn empty_string_to_none<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let s: String = serde::Deserialize::deserialize(deserializer)?; + if s.is_empty() { + Ok(None) + } else { + Ok(Some(Months::from_str(&s).unwrap())) + } } diff --git a/src/util.rs b/src/util.rs index eba9594..95f7421 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,58 +1,20 @@ -macro_rules! TABLE_ROW { - () => { - " - {} - {} - {} - {} - - - - " - }; -} +use chrono::{Datelike, NaiveDate, Utc}; -macro_rules! EDITABLE_TABLE_ROW { - () => { - " - - - - - - - - - " - }; +pub fn get_first_day_from_month(month: u32) -> NaiveDate { + Utc::now() + .with_day(1) + .unwrap() + .naive_utc() + .date() + .with_month(month) + .unwrap() } -pub(crate) use EDITABLE_TABLE_ROW; -pub(crate) use TABLE_ROW; +pub fn get_last_day_from_month(month: u32) -> NaiveDate { + let first_day_of_month = get_first_day_from_month(month); + + if first_day_of_month.month() == 12 { + return first_day_of_month.with_day(31).unwrap(); + } + first_day_of_month.with_month(month + 1).unwrap() - chrono::Duration::days(1) +} diff --git a/templates/expenses.html b/templates/expenses.html index 587d6b9..812fcde 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -16,6 +16,7 @@

Carol's Expenses

{% endif %} {% endfor %} + @@ -25,6 +26,7 @@

Carol's Expenses

+
Date Description Price Category