From 9124e66e284bb88e67188109670cf561e83959ac Mon Sep 17 00:00:00 2001 From: Wes Holland Date: Fri, 17 Jan 2025 13:59:27 -0600 Subject: [PATCH] Refactor for easier navigation --- src/app/adjustment.rs | 229 ------------------ src/app/common/query_args/datetime_range.rs | 7 +- src/app/history.rs | 7 +- src/app/item.rs | 87 ------- src/app/item/adjustment/mod.rs | 3 + src/app/item/adjustment/negative.rs | 114 +++++++++ src/app/item/adjustment/positive.rs | 109 +++++++++ src/app/item/adjustment/table.rs | 87 +++++++ src/app/item/count.rs | 16 ++ src/app/item/item.rs | 48 ++++ src/app/item/mod.rs | 4 + src/app/item/stats.rs | 36 +++ src/app/mod.rs | 64 +---- src/app/routes.rs | 70 ++++++ src/auth.rs | 2 +- src/db/adjustment/adjustment_reason.rs | 6 +- src/error.rs | 2 +- src/main.rs | 9 +- src/static_routes.rs | 2 +- templates/adjustment-sale-form.html | 27 --- .../adjustment/adjustment-table.html} | 0 .../adjustment/negative-adjustment-form.html | 65 +++++ .../adjustment/positive-adjustment-form.html} | 2 +- templates/{ => item}/item.html | 12 +- .../stats-fragment.html} | 0 templates/main.html | 7 +- 26 files changed, 584 insertions(+), 431 deletions(-) delete mode 100644 src/app/adjustment.rs delete mode 100644 src/app/item.rs create mode 100644 src/app/item/adjustment/mod.rs create mode 100644 src/app/item/adjustment/negative.rs create mode 100644 src/app/item/adjustment/positive.rs create mode 100644 src/app/item/adjustment/table.rs create mode 100644 src/app/item/count.rs create mode 100644 src/app/item/item.rs create mode 100644 src/app/item/mod.rs create mode 100644 src/app/item/stats.rs create mode 100644 src/app/routes.rs delete mode 100644 templates/adjustment-sale-form.html rename templates/{adjustments-table-fragment.html => item/adjustment/adjustment-table.html} (100%) create mode 100644 templates/item/adjustment/negative-adjustment-form.html rename templates/{adjustment-new-stock-form.html => item/adjustment/positive-adjustment-form.html} (93%) rename templates/{ => item}/item.html (73%) rename templates/{item-stats-fragment.html => item/stats-fragment.html} (100%) diff --git a/src/app/adjustment.rs b/src/app/adjustment.rs deleted file mode 100644 index e9afd08..0000000 --- a/src/app/adjustment.rs +++ /dev/null @@ -1,229 +0,0 @@ -use askama::Template; -use askama_axum::Response; -use axum::extract::{Path, State}; -use axum::{debug_handler, Form}; -use axum::response::IntoResponse; -use axum_htmx::{HxEvent, HxResponseTrigger}; -use oauth2::http; -use serde::Deserialize; -use sqlx::SqlitePool; -use tracing::info; -use chrono::FixedOffset; -use crate::db::adjustment::{add_adjustment, get_item_adjustment_valuation_weighted_mean, DbAdjustmentWithValuation}; -use crate::db::adjustment::adjustment_reason::DbAdjustmentReason; -use crate::db::inventory_item::{does_inventory_item_allow_fractional_units, inventory_item_get_by_id_with_unit}; -use crate::error::AppError; -use crate::session::SessionUser; -use crate::util::currency; -use crate::util::currency::{dollars_string_to_int_cents, int_cents_to_dollars_string}; -use crate::util::formatting::format_either; - - -#[derive(Template)] -#[template(path = "adjustments-table-fragment.html")] -struct AdjustmentTableTemplate { - item_id: i64, - adjustments: Vec, -} - -pub async fn get_adjustments_for_item(State(db): State, - Path(id): Path, - user: SessionUser -) -> Result { - - let timezone = user.get_timezone()?; - - let allow_fractional = - does_inventory_item_allow_fractional_units(&db, id).await?; - - let adjustments: Vec = get_item_adjustment_valuation_weighted_mean(&db, id).await?; - - - let adjustments: Vec = adjustments.into_iter() - .map(|x| AdjustmentDisplayItem::from_db_item(x, timezone, allow_fractional)) - .collect(); - - Ok(AdjustmentTableTemplate { item_id: id, adjustments }.into_response()) -} - -#[derive(Template)] -#[template(path = "adjustment-sale-form.html")] -pub struct AdjustmentSaleFormTemplate { - pub item_id: i64, - pub amount_error: &'static str, -} - -#[derive(Deserialize, Debug)] -pub struct AdjustmentSaleFormData { - pub amount: f64, -} - -#[debug_handler] -pub async fn adjustment_sale_form_post(State(db): State, - Path(id): Path, - user: SessionUser, - form_data: Form -) -> Result { - - let adjustment_amount = if form_data.amount > 0.0 { - -1.0 * form_data.amount - } else { - form_data.amount - }; - - let fractional_units_allowed = - does_inventory_item_allow_fractional_units(&db, id).await?; - - if adjustment_amount == 0.0 { - return Ok(AdjustmentSaleFormTemplate { - item_id: id, - amount_error: "Please input a non-zero amount" - }.into_response()) - } else if !(fractional_units_allowed || adjustment_amount.fract() == 0.0) { - return Ok(AdjustmentSaleFormTemplate { - item_id: id, - amount_error: "Please input a whole number" - }.into_response()) - } - - let trigger_events = HxResponseTrigger::normal( - std::iter::once(HxEvent::from("new-adjustment")) - ); - - let timestamp = chrono::Utc::now(); - - info!("Add adjustment form: {:?} for user {}", form_data, user.name); - - let _new_id = add_adjustment(&db, id, user.id, - timestamp, - timestamp, - adjustment_amount, - None, - DbAdjustmentReason::Sale).await?; - - - Ok((trigger_events, AdjustmentSaleFormTemplate { item_id: id, amount_error: "" }.into_response()).into_response()) -} - -pub async fn adjustment_sale_form_get( - Path(id): Path -) -> Result { - Ok(AdjustmentSaleFormTemplate { item_id: id, amount_error: "" }.into_response()) -} - -#[derive(Template)] -#[template(path = "adjustment-new-stock-form.html")] -pub struct AdjustmentNewStockFormTemplate { - pub item_id: i64, - pub amount_error: &'static str, - pub price_error: &'static str, -} - -#[derive(Deserialize, Debug)] -pub struct AdjustmentNewStockFormData { - pub amount: f64, - pub price: String, -} - -#[debug_handler] -pub async fn adjustment_new_stock_form_post(State(db): State, - Path(id): Path, - user: SessionUser, - form_data: Form -) -> Result { - - let fractional_units_allowed = - does_inventory_item_allow_fractional_units(&db, id).await?; - - let amount_error = if form_data.amount == 0.0 { - "Please input a non-zero amount" - } else if !(fractional_units_allowed || form_data.amount.fract() == 0.0) { - "Please input a whole number" - } else { - "" - }; - - let price = dollars_string_to_int_cents(&form_data.price); - let price_error = if price.is_err() { - "Please input a dollar amount" - } else { - "" - }; - - if !(amount_error.is_empty() && price_error.is_empty()) { - return Ok(AdjustmentNewStockFormTemplate { - item_id: id, - amount_error, - price_error, - }.into_response()) - } - - let price = price?; - let unit_price = (price as f64 / form_data.amount) as i64; - - let trigger_events = HxResponseTrigger::normal( - std::iter::once(HxEvent::from("new-adjustment")) - ); - - let timestamp = chrono::Utc::now(); - - let adjustment_amount = form_data.amount; - - info!("Add adjustment form: {:?} for user {}", form_data, user.name); - - let _new_id = add_adjustment(&db, id, user.id, - timestamp, - timestamp, - adjustment_amount, - Some(unit_price), - DbAdjustmentReason::NewStock).await?; - - - Ok((trigger_events, AdjustmentNewStockFormTemplate { item_id: id, - amount_error: "", price_error: "" }.into_response()).into_response()) -} - -pub async fn adjustment_new_stock_form_get( - Path(id): Path -) -> Result { - Ok(AdjustmentNewStockFormTemplate { item_id: id, - amount_error: "", price_error: "" }.into_response()) -} - - -#[derive(Clone, Debug)] -pub struct AdjustmentDisplayItem { - pub date: String, - pub amount: String, - pub unit_value: String, - pub tally: String, - pub tally_value: String, - pub reason: String, -} - - -impl AdjustmentDisplayItem { - pub fn from_db_item(db_entry: DbAdjustmentWithValuation, timezone: FixedOffset - , allow_fractional: bool) -> Self { - let precision : usize = if allow_fractional { 2 } else { 0 }; - let date = db_entry.adjustment.target_date.with_timezone(&timezone) - .format("%Y-%m-%d %l:%M:%S %p").to_string(); - let amount = format!("{:.*}", precision, db_entry.adjustment.amount); - let unit_price = db_entry.adjustment.unit_price.unwrap_or_else(|| - (db_entry.value as f64 / db_entry.tally) as i64); - let unit_value = currency::int_cents_to_dollars_string(unit_price); - let tally = format!("{:.*}", precision, db_entry.tally); - let tally_value = currency::int_cents_to_dollars_string(db_entry.value); - let reason = db_entry.adjustment.reason.into(); - - Self { - date, - amount, - unit_value, - tally, - tally_value, - reason, - } - - } -} diff --git a/src/app/common/query_args/datetime_range.rs b/src/app/common/query_args/datetime_range.rs index a25a5c5..42658d2 100644 --- a/src/app/common/query_args/datetime_range.rs +++ b/src/app/common/query_args/datetime_range.rs @@ -1,9 +1,8 @@ -use std::str::FromStr; -use serde::Deserialize; -use anyhow::Error; -use chrono::{FixedOffset, NaiveDate, NaiveTime}; use crate::util::parsing; use crate::util::time::{LocalTimestampRange, DEFAULT_TIMEZONE_OFFSET}; +use anyhow::Error; +use chrono::{FixedOffset, NaiveDate, NaiveTime}; +use serde::Deserialize; /// Common query args for datetime ranges. Times assumed to be /// in local time. This usually means a user configured time, diff --git a/src/app/history.rs b/src/app/history.rs index 9e1036d..4ace9d2 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -1,7 +1,9 @@ use crate::app::common::query_args::datetime_range::DatetimeRangeQueryArgs; -use crate::db::adjustment::{get_adjustments_target_date_range, DbAdjustment}; +use crate::db::adjustment::adjustment_joins::DbAdjustmentWithUserAndItem; +use crate::db::adjustment::DbAdjustment; use crate::error::{AppError, QueryExtractor}; use crate::session::SessionUser; +use crate::util::currency; use crate::util::time::{tz_offset_to_string, LocalTimestampRange, UtcTimestampRange}; use anyhow::Result; use askama::Template; @@ -9,11 +11,8 @@ use askama_axum::{IntoResponse, Response}; use axum::extract::State; use axum_htmx::HxRequest; use chrono::prelude::*; -use serde::Deserialize; use sqlx::SqlitePool; use tracing::info; -use crate::db::adjustment::adjustment_joins::DbAdjustmentWithUserAndItem; -use crate::util::currency; #[derive(Template)] #[template(path = "history.html")] diff --git a/src/app/item.rs b/src/app/item.rs deleted file mode 100644 index 44d3be8..0000000 --- a/src/app/item.rs +++ /dev/null @@ -1,87 +0,0 @@ -use anyhow::anyhow; -use askama::Template; -use askama_axum::IntoResponse; -use axum::extract::{Path, State}; -use sqlx::SqlitePool; -use axum::response::Response; -use crate::app::adjustment::AdjustmentDisplayItem; -use crate::db::adjustment::{get_item_adjustment_valuation_weighted_mean, sum_all_adjustments_for_item, DbAdjustmentWithValuation}; -use crate::db::inventory_item::{inventory_item_get_by_id_with_unit, inventory_item_get_unit_abbreviation, DbInventoryItemWithCount}; -use crate::error::AppError; -use crate::session::SessionUser; -use crate::util::currency::int_cents_to_dollars_string; -use crate::util::formatting::format_either; - -#[derive(Template)] -#[template(path = "item.html")] -struct ItemTemplate { - pub item_id: i64, - pub item: ItemDisplayItem, -} - -#[derive(Clone, Debug)] -struct ItemDisplayItem { - pub name: String, - pub reorder_point: f64, - pub allow_fractional_units: bool, - pub active: bool, - pub display_unit: String, - pub display_unit_short: String, - pub amount: String, -} - -impl From for ItemDisplayItem { - fn from(item: DbInventoryItemWithCount) -> Self { - let precision = if item.allow_fractional_units { 2 } else { 0 }; - let amount = format!("{:.*}", precision, item.amount.unwrap_or_default()); - Self { - name: item.name, - reorder_point: item.reorder_point, - allow_fractional_units: item.allow_fractional_units, - display_unit: item.display_unit_str, - display_unit_short: item.display_unit_abbreviation, - active: item.active, - amount, - } - } -} - -pub async fn item(State(db): State, - Path(id): Path, - user: SessionUser -) -> Result { - let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into(); - - Ok(ItemTemplate { item_id: id, item }.into_response()) -} - - -pub async fn item_count(State(db): State, Path(id): Path) -> Result { - let count = sum_all_adjustments_for_item(&db, id).await?; - - Ok(count.to_string().into_response()) -} - - -#[derive(Template)] -#[template(path = "item-stats-fragment.html")] -struct ItemStatsTemplate { - pub item_id: i64, - pub item: ItemDisplayItem, - pub value: String, -} -pub async fn item_stats(State(db): State, Path(id): Path) -> Result { - //TODO This is pretty chatty with the database. Might could cut down the - // number of queries - let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into(); - - let value = get_item_adjustment_valuation_weighted_mean(&db, id) - .await? - .into_iter() - .last().ok_or_else(|| anyhow!("No value"))? - .value; - - let value = int_cents_to_dollars_string(value); - - Ok(ItemStatsTemplate { item_id: id, item, value }.into_response()) -} diff --git a/src/app/item/adjustment/mod.rs b/src/app/item/adjustment/mod.rs new file mode 100644 index 0000000..b76660b --- /dev/null +++ b/src/app/item/adjustment/mod.rs @@ -0,0 +1,3 @@ +pub mod table; +pub mod negative; +pub mod positive; \ No newline at end of file diff --git a/src/app/item/adjustment/negative.rs b/src/app/item/adjustment/negative.rs new file mode 100644 index 0000000..29cd3ea --- /dev/null +++ b/src/app/item/adjustment/negative.rs @@ -0,0 +1,114 @@ +use crate::db::adjustment::add_adjustment; +use crate::db::adjustment::adjustment_reason::DbAdjustmentReason; +use crate::db::inventory_item::does_inventory_item_allow_fractional_units; +use crate::error::AppError; +use crate::session::SessionUser; +use askama::Template; +use askama_axum::{IntoResponse, Response}; +use axum::extract::{Path, State}; +use axum::{debug_handler, Form}; +use axum_htmx::{HxEvent, HxResponseTrigger}; +use serde::Deserialize; +use sqlx::SqlitePool; +use tracing::info; + +#[derive(Template)] +#[template(path = "item/adjustment/negative-adjustment-form.html")] +pub struct NegativeAdjustmentFormTemplate { + pub item_id: i64, + pub amount_error: &'static str, + pub reason_error: &'static str, +} + +#[derive(Deserialize, Debug)] +pub struct NegativeAdjustmentFormData { + pub amount: f64, + pub reason: Option, +} + +#[debug_handler] +pub async fn negative_adjustment_form_post( + State(db): State, + Path(id): Path, + user: SessionUser, + mut form_data: Form, +) -> Result { + let adjustment_amount = if form_data.amount > 0.0 { + -1.0 * form_data.amount + } else { + form_data.amount + }; + + let fractional_units_allowed = does_inventory_item_allow_fractional_units(&db, id).await?; + + let reason = form_data + .reason + .take() + .and_then(|s| DbAdjustmentReason::try_from(s.as_str()).ok()) + .unwrap_or_else(|| DbAdjustmentReason::Unknown); + + let amount_error = if adjustment_amount == 0.0 { + "Please input a non-zero amount" + } else if !(fractional_units_allowed || adjustment_amount.fract() == 0.0) { + "Please input a whole number" + } else { + "" + }; + + let reason_error = if reason == DbAdjustmentReason::Unknown { + "Unknown adjustment reason" + } else { + "" + }; + + if !(amount_error.is_empty() && reason_error.is_empty()) { + return Ok(NegativeAdjustmentFormTemplate { + item_id: id, + amount_error, + reason_error, + } + .into_response()); + } + + let trigger_events = + HxResponseTrigger::normal(std::iter::once(HxEvent::from("new-adjustment"))); + + let timestamp = chrono::Utc::now(); + + info!( + "Add adjustment form: (Amount {}, Reason {:?}, for user {}", + form_data.amount, reason, user.name + ); + + let _new_id = add_adjustment( + &db, + id, + user.id, + timestamp, + timestamp, + adjustment_amount, + None, + reason, + ) + .await?; + + Ok(( + trigger_events, + NegativeAdjustmentFormTemplate { + item_id: id, + amount_error: "", + reason_error: "", + } + .into_response(), + ) + .into_response()) +} + +pub async fn negative_adjustment_form_get(Path(id): Path) -> Result { + Ok(NegativeAdjustmentFormTemplate { + item_id: id, + amount_error: "", + reason_error: "", + } + .into_response()) +} diff --git a/src/app/item/adjustment/positive.rs b/src/app/item/adjustment/positive.rs new file mode 100644 index 0000000..8b1d672 --- /dev/null +++ b/src/app/item/adjustment/positive.rs @@ -0,0 +1,109 @@ +use crate::db::adjustment::add_adjustment; +use crate::db::adjustment::adjustment_reason::DbAdjustmentReason; +use crate::db::inventory_item::does_inventory_item_allow_fractional_units; +use crate::error::AppError; +use crate::session::SessionUser; +use crate::util::currency::dollars_string_to_int_cents; +use askama::Template; +use askama_axum::{IntoResponse, Response}; +use axum::extract::{Path, State}; +use axum::{debug_handler, Form}; +use axum_htmx::{HxEvent, HxResponseTrigger}; +use serde::Deserialize; +use sqlx::SqlitePool; +use tracing::info; + +#[derive(Template)] +#[template(path = "item/adjustment/positive-adjustment-form.html")] +pub struct PositiveAdjustmentFormTemplate { + pub item_id: i64, + pub amount_error: &'static str, + pub price_error: &'static str, +} + +#[derive(Deserialize, Debug)] +pub struct PositiveAdjustmentFormData { + pub amount: f64, + pub price: String, +} + +#[debug_handler] +pub async fn positive_adjustment_form_post( + State(db): State, + Path(id): Path, + user: SessionUser, + form_data: Form, +) -> Result { + let fractional_units_allowed = does_inventory_item_allow_fractional_units(&db, id).await?; + + let amount_error = if form_data.amount == 0.0 { + "Please input a non-zero amount" + } else if !(fractional_units_allowed || form_data.amount.fract() == 0.0) { + "Please input a whole number" + } else { + "" + }; + + let price = dollars_string_to_int_cents(&form_data.price); + let price_error = if price.is_err() { + "Please input a dollar amount" + } else { + "" + }; + + if !(amount_error.is_empty() && price_error.is_empty()) { + return Ok(PositiveAdjustmentFormTemplate { + item_id: id, + amount_error, + price_error, + } + .into_response()); + } + + let price = price?; + let unit_price = (price as f64 / form_data.amount) as i64; + + let trigger_events = + HxResponseTrigger::normal(std::iter::once(HxEvent::from("new-adjustment"))); + + let timestamp = chrono::Utc::now(); + + let adjustment_amount = form_data.amount; + + info!( + "Add adjustment form: {:?} for user {}", + form_data, user.name + ); + + let _new_id = add_adjustment( + &db, + id, + user.id, + timestamp, + timestamp, + adjustment_amount, + Some(unit_price), + DbAdjustmentReason::NewStock, + ) + .await?; + + Ok(( + trigger_events, + PositiveAdjustmentFormTemplate { + item_id: id, + amount_error: "", + price_error: "", + } + .into_response(), + ) + .into_response()) +} + +pub async fn positive_adjustment_form_get(Path(id): Path) -> Result { + Ok(PositiveAdjustmentFormTemplate { + item_id: id, + amount_error: "", + price_error: "", + } + .into_response()) +} diff --git a/src/app/item/adjustment/table.rs b/src/app/item/adjustment/table.rs new file mode 100644 index 0000000..e4a0202 --- /dev/null +++ b/src/app/item/adjustment/table.rs @@ -0,0 +1,87 @@ +use crate::db::adjustment::{ + get_item_adjustment_valuation_weighted_mean, DbAdjustmentWithValuation, +}; +use crate::db::inventory_item::does_inventory_item_allow_fractional_units; +use crate::error::AppError; +use crate::session::SessionUser; +use crate::util::currency; +use askama::Template; +use askama_axum::{IntoResponse, Response}; +use axum::extract::{Path, State}; +use chrono::FixedOffset; +use sqlx::SqlitePool; + +#[derive(Template)] +#[template(path = "item/adjustment/adjustment-table.html")] +struct AdjustmentTableTemplate { + item_id: i64, + adjustments: Vec, +} + +pub async fn get_adjustments_table( + State(db): State, + Path(id): Path, + user: SessionUser, +) -> Result { + let timezone = user.get_timezone()?; + + let allow_fractional = does_inventory_item_allow_fractional_units(&db, id).await?; + + let adjustments: Vec = + get_item_adjustment_valuation_weighted_mean(&db, id).await?; + + let adjustments: Vec = adjustments + .into_iter() + .map(|x| AdjustmentDisplayItem::from_db_item(x, timezone, allow_fractional)) + .collect(); + + Ok(AdjustmentTableTemplate { + item_id: id, + adjustments, + } + .into_response()) +} + +#[derive(Clone, Debug)] +pub struct AdjustmentDisplayItem { + pub date: String, + pub amount: String, + pub unit_value: String, + pub tally: String, + pub tally_value: String, + pub reason: String, +} + +impl AdjustmentDisplayItem { + pub fn from_db_item( + db_entry: DbAdjustmentWithValuation, + timezone: FixedOffset, + allow_fractional: bool, + ) -> Self { + let precision: usize = if allow_fractional { 2 } else { 0 }; + let date = db_entry + .adjustment + .target_date + .with_timezone(&timezone) + .format("%Y-%m-%d %l:%M:%S %p") + .to_string(); + let amount = format!("{:.*}", precision, db_entry.adjustment.amount); + let unit_price = db_entry + .adjustment + .unit_price + .unwrap_or_else(|| (db_entry.value as f64 / db_entry.tally) as i64); + let unit_value = currency::int_cents_to_dollars_string(unit_price); + let tally = format!("{:.*}", precision, db_entry.tally); + let tally_value = currency::int_cents_to_dollars_string(db_entry.value); + let reason = db_entry.adjustment.reason.into(); + + Self { + date, + amount, + unit_value, + tally, + tally_value, + reason, + } + } +} diff --git a/src/app/item/count.rs b/src/app/item/count.rs new file mode 100644 index 0000000..4300e45 --- /dev/null +++ b/src/app/item/count.rs @@ -0,0 +1,16 @@ +use crate::db::adjustment::sum_all_adjustments_for_item; +use crate::error::AppError; +use askama_axum::{IntoResponse, Response}; +use axum::debug_handler; +use axum::extract::{Path, State}; +use sqlx::SqlitePool; + +#[debug_handler] +pub async fn item_count( + State(db): State, + Path(id): Path, +) -> Result { + let count = sum_all_adjustments_for_item(&db, id).await?; + + Ok(count.to_string().into_response()) +} diff --git a/src/app/item/item.rs b/src/app/item/item.rs new file mode 100644 index 0000000..381ccf8 --- /dev/null +++ b/src/app/item/item.rs @@ -0,0 +1,48 @@ +use crate::db::inventory_item::{inventory_item_get_by_id_with_unit, DbInventoryItemWithCount}; +use crate::error::AppError; +use askama::Template; +use askama_axum::{IntoResponse, Response}; +use axum::debug_handler; +use axum::extract::{Path, State}; +use sqlx::SqlitePool; + +#[derive(Template)] +#[template(path = "item/item.html")] +struct ItemTemplate { + pub item_id: i64, + pub item: ItemDisplayItem, +} + +#[debug_handler] +pub async fn item(State(db): State, Path(id): Path) -> Result { + let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into(); + + Ok(ItemTemplate { item_id: id, item }.into_response()) +} + +#[derive(Clone, Debug)] +pub struct ItemDisplayItem { + pub name: String, + pub reorder_point: f64, + pub allow_fractional_units: bool, + pub active: bool, + pub display_unit: String, + pub display_unit_short: String, + pub amount: String, +} + +impl From for ItemDisplayItem { + fn from(item: DbInventoryItemWithCount) -> Self { + let precision = if item.allow_fractional_units { 2 } else { 0 }; + let amount = format!("{:.*}", precision, item.amount.unwrap_or_default()); + Self { + name: item.name, + reorder_point: item.reorder_point, + allow_fractional_units: item.allow_fractional_units, + display_unit: item.display_unit_str, + display_unit_short: item.display_unit_abbreviation, + active: item.active, + amount, + } + } +} diff --git a/src/app/item/mod.rs b/src/app/item/mod.rs new file mode 100644 index 0000000..40c7dec --- /dev/null +++ b/src/app/item/mod.rs @@ -0,0 +1,4 @@ +pub mod adjustment; +pub mod item; +pub mod stats; +pub mod count; diff --git a/src/app/item/stats.rs b/src/app/item/stats.rs new file mode 100644 index 0000000..82ba29e --- /dev/null +++ b/src/app/item/stats.rs @@ -0,0 +1,36 @@ +use askama::Template; +use axum::extract::{Path, State}; +use sqlx::SqlitePool; +use askama_axum::{IntoResponse, Response}; +use anyhow::anyhow; +use axum::debug_handler; +use crate::app::item::item::ItemDisplayItem; +use crate::db::adjustment::get_item_adjustment_valuation_weighted_mean; +use crate::db::inventory_item::inventory_item_get_by_id_with_unit; +use crate::error::AppError; +use crate::util::currency::int_cents_to_dollars_string; + +#[derive(Template)] +#[template(path = "item/stats-fragment.html")] +struct ItemStatsTemplate { + pub item_id: i64, + pub item: ItemDisplayItem, + pub value: String, +} + +#[debug_handler] +pub async fn item_stats(State(db): State, Path(id): Path) -> Result { + //TODO This is pretty chatty with the database. Might could cut down the + // number of queries + let item: ItemDisplayItem = inventory_item_get_by_id_with_unit(&db, id).await?.into(); + + let value = get_item_adjustment_valuation_weighted_mean(&db, id) + .await? + .into_iter() + .last().ok_or_else(|| anyhow!("No value"))? + .value; + + let value = int_cents_to_dollars_string(value); + + Ok(ItemStatsTemplate { item_id: id, item, value }.into_response()) +} \ No newline at end of file diff --git a/src/app/mod.rs b/src/app/mod.rs index a280e31..59a5ed3 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,13 +1,3 @@ -use axum::middleware::from_extractor; -use axum::Router; -use axum::routing::{get, post}; -use axum_htmx::AutoVaryLayer; -use sqlx::SqlitePool; -use oauth2::basic::BasicClient; -use axum::extract::FromRef; -use serde::Deserialize; -use crate::session::SessionUser; - pub mod item; mod upload; pub mod catalog; @@ -16,57 +6,5 @@ mod overview; mod reports; mod history; pub mod common; -pub mod adjustment; - -pub fn routes() -> Router { - Router::new() - .route("/", get(home::home)) - .route("/index.html", get(home::home)) - .route("/home", get(home::home)) - .route("/home/search", get(home::home)) - .route("/catalog", get(catalog::catalog)) - .route("/catalog/", get(catalog::catalog)) - .route("/item/:item_id", get(item::item)) - .route("/item/:item_id/", get(item::item)) - .route("/item/:item_id/count", get(item::item_count)) - .route("/item/:item_id/stats", get(item::item_stats)) - .route("/item/:item_id/adjustments", get(adjustment::get_adjustments_for_item)) - .route("/item/:item_id/adjustment/sale", post(adjustment::adjustment_sale_form_post)) - .route("/item/:item_id/adjustment/sale", get(adjustment::adjustment_sale_form_get)) - .route("/item/:item_id/adjustment/new-stock", post(adjustment::adjustment_new_stock_form_post)) - .route("/item/:item_id/adjustment/new-stock", get(adjustment::adjustment_new_stock_form_get)) - .route("/upload", get(upload::index::upload_index_handler)) - .route("/upload/catalog", post(upload::catalog::catalog_import)) - .route("/overview", get(overview::overview_handler)) - .route("/reports", get(reports::reports_handler)) - .route("/history", get(history::history_log_handler)) - // Ensure that all routes here require an authenticated user - // whether explicitly asked or not - .route_layer(from_extractor::()) - .layer(AutoVaryLayer) -} - -// App state. Pretty basic stuff. Gets passed around by the server to the handlers and whatnot -// Use in a handler with the state enum: -// async fn handler(State(my_app_state): State) -#[derive(Clone)] -pub struct AppState { - pub db: SqlitePool, - pub oauth_client: BasicClient, -} - -// Axum extractors for app state. These allow the handler to just use -// pieces of the App state -// async fn handler(State(my_db): State) -impl FromRef for SqlitePool { - fn from_ref(input: &AppState) -> Self { - input.db.clone() - } -} - -impl FromRef for BasicClient { - fn from_ref(input: &AppState) -> Self { - input.oauth_client.clone() - } -} +pub mod routes; diff --git a/src/app/routes.rs b/src/app/routes.rs new file mode 100644 index 0000000..39085e1 --- /dev/null +++ b/src/app/routes.rs @@ -0,0 +1,70 @@ +use crate::app::{catalog, history, home, item, overview, reports, upload}; +use crate::session::SessionUser; +use axum::extract::FromRef; +use axum::middleware::from_extractor; +use axum::routing::{get, post}; +use axum::Router; +use axum_htmx::AutoVaryLayer; +use oauth2::basic::BasicClient; +use sqlx::SqlitePool; + +pub fn routes() -> Router { + Router::new() + .route("/", get(home::home)) + .route("/index.html", get(home::home)) + .route("/home", get(home::home)) + .route("/home/search", get(home::home)) + .route("/catalog", get(catalog::catalog)) + .route("/catalog/", get(catalog::catalog)) + .route("/item/:item_id", get(item::item::item)) + .route("/item/:item_id/", get(item::item::item)) + .route("/item/:item_id/count", get(item::count::item_count)) + .route("/item/:item_id/stats", get(item::stats::item_stats)) + .route( + "/item/:item_id/adjustments", + get(item::adjustment::table::get_adjustments_table), + ) + .route( + "/item/:item_id/adjustment/negative", + get(item::adjustment::negative::negative_adjustment_form_get) + .post(item::adjustment::negative::negative_adjustment_form_post), + ) + .route( + "/item/:item_id/adjustment/positive", + get(item::adjustment::positive::positive_adjustment_form_get) + .post(item::adjustment::positive::positive_adjustment_form_post), + ) + .route("/upload", get(upload::index::upload_index_handler)) + .route("/upload/catalog", post(upload::catalog::catalog_import)) + .route("/overview", get(overview::overview_handler)) + .route("/reports", get(reports::reports_handler)) + .route("/history", get(history::history_log_handler)) + // Ensure that all routes here require an authenticated user + // whether explicitly asked or not + .route_layer(from_extractor::()) + .layer(AutoVaryLayer) +} + +// App state. Pretty basic stuff. Gets passed around by the server to the handlers and whatnot +// Use in a handler with the state enum: +// async fn handler(State(my_app_state): State) +#[derive(Clone)] +pub struct AppState { + pub db: SqlitePool, + pub oauth_client: BasicClient, +} + +// Axum extractors for app state. These allow the handler to just use +// pieces of the App state +// async fn handler(State(my_db): State) +impl FromRef for SqlitePool { + fn from_ref(input: &AppState) -> Self { + input.db.clone() + } +} + +impl FromRef for BasicClient { + fn from_ref(input: &AppState) -> Self { + input.oauth_client.clone() + } +} diff --git a/src/auth.rs b/src/auth.rs index 60e6767..8484331 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -13,7 +13,7 @@ use tower_sessions::Session; use crate::error::{AppError, AppForbiddenResponse}; use crate::error::QueryExtractor; -use crate::app::AppState; +use crate::app::routes::AppState; use crate::session::{SessionUser, USER_SESSION}; use crate::db; // This module is all the stuff related to authentication and authorization diff --git a/src/db/adjustment/adjustment_reason.rs b/src/db/adjustment/adjustment_reason.rs index afdfec6..e7f4ae6 100644 --- a/src/db/adjustment/adjustment_reason.rs +++ b/src/db/adjustment/adjustment_reason.rs @@ -1,7 +1,7 @@ use tracing::error; use serde::Serialize; -#[derive(Debug, Clone, Copy, Serialize)] +#[derive(Debug, Clone, Copy, Serialize, PartialEq)] pub enum DbAdjustmentReason { Unknown, Sale, @@ -44,8 +44,8 @@ impl From for DbAdjustmentReason { impl TryFrom<&str> for DbAdjustmentReason { type Error = anyhow::Error; - fn try_from(value: &str) -> std::result::Result { - match value { + fn try_from(value: &str) -> Result { + match value.to_lowercase().as_str() { "unknown" => Ok(Self::Unknown), "sale" => Ok(Self::Sale), "destruction" => Ok(Self::Destruction), diff --git a/src/error.rs b/src/error.rs index 651fefa..936e6f8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use axum::response::{IntoResponse, Response}; use axum::Router; use axum::routing::get; use axum::extract::FromRequestParts; -use crate::app::AppState; +use crate::app::routes::AppState; use crate::session::SessionUser; // This module is all the stuff related to handling error responses diff --git a/src/main.rs b/src/main.rs index 058cd06..11d91b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,12 @@ -use app::AppState; +use app::routes; use anyhow::{Context, Result}; use axum::Router; use tokio::signal; use tokio::task::{AbortHandle, JoinHandle}; -use tower_http::{ - trace::TraceLayer, -}; +use tower_http::trace::TraceLayer; use tracing::info; use tracing_subscriber::EnvFilter; +use app::routes::AppState; mod app_state; mod db; @@ -67,7 +66,7 @@ async fn main() -> Result<()>{ let auth_routes = auth::routes(); let static_routes = static_routes::routes(); let error_routes = error::routes(); - let app_routes = app::routes(); + let app_routes = routes::routes(); // Top level router let router = Router::new() diff --git a/src/static_routes.rs b/src/static_routes.rs index 508014f..2f5a108 100644 --- a/src/static_routes.rs +++ b/src/static_routes.rs @@ -1,6 +1,6 @@ use axum::Router; use tower_http::services::ServeFile; -use crate::app::AppState; +use crate::app::routes::AppState; pub fn routes() -> Router { Router::new() diff --git a/templates/adjustment-sale-form.html b/templates/adjustment-sale-form.html deleted file mode 100644 index b6ed7d5..0000000 --- a/templates/adjustment-sale-form.html +++ /dev/null @@ -1,27 +0,0 @@ - -
-
-
- - - {% if !amount_error.is_empty() -%} - {{ amount_error }} - {% endif -%} -
-
- -
-
-
diff --git a/templates/adjustments-table-fragment.html b/templates/item/adjustment/adjustment-table.html similarity index 100% rename from templates/adjustments-table-fragment.html rename to templates/item/adjustment/adjustment-table.html diff --git a/templates/item/adjustment/negative-adjustment-form.html b/templates/item/adjustment/negative-adjustment-form.html new file mode 100644 index 0000000..a620bc2 --- /dev/null +++ b/templates/item/adjustment/negative-adjustment-form.html @@ -0,0 +1,65 @@ +
+ +
+
+ + + {% if !amount_error.is_empty() -%} + {{ amount_error }} + {% endif -%} +
+
+ + +
+
+ +
+
+
diff --git a/templates/adjustment-new-stock-form.html b/templates/item/adjustment/positive-adjustment-form.html similarity index 93% rename from templates/adjustment-new-stock-form.html rename to templates/item/adjustment/positive-adjustment-form.html index 55f0467..40e6f12 100644 --- a/templates/adjustment-new-stock-form.html +++ b/templates/item/adjustment/positive-adjustment-form.html @@ -1,5 +1,5 @@ -
+
- +
-
+
@@ -30,10 +32,12 @@
- +
-
+
diff --git a/templates/item-stats-fragment.html b/templates/item/stats-fragment.html similarity index 100% rename from templates/item-stats-fragment.html rename to templates/item/stats-fragment.html diff --git a/templates/main.html b/templates/main.html index 5350f0a..5da2638 100644 --- a/templates/main.html +++ b/templates/main.html @@ -12,6 +12,7 @@ + {% block title %}Title{% endblock %} @@ -42,7 +43,11 @@ {% block content %}

Content Missing

{% endblock %}
+
- \ No newline at end of file