Refactor for easier navigation

demo-mode
Wes Holland 11 months ago
parent a22a052c99
commit 9124e66e28

@ -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<AdjustmentDisplayItem>,
}
pub async fn get_adjustments_for_item(State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser
) -> Result<Response, AppError> {
let timezone = user.get_timezone()?;
let allow_fractional =
does_inventory_item_allow_fractional_units(&db, id).await?;
let adjustments: Vec<DbAdjustmentWithValuation> = get_item_adjustment_valuation_weighted_mean(&db, id).await?;
let adjustments: Vec<AdjustmentDisplayItem> = 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<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<AdjustmentSaleFormData>
) -> Result<Response, AppError> {
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<i64>
) -> Result<Response, AppError> {
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<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<AdjustmentNewStockFormData>
) -> Result<Response, AppError> {
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<i64>
) -> Result<Response, AppError> {
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,
}
}
}

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

@ -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")]

@ -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<DbInventoryItemWithCount> 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<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser
) -> Result<Response, AppError> {
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<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
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<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
//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())
}

@ -0,0 +1,3 @@
pub mod table;
pub mod negative;
pub mod positive;

@ -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<String>,
}
#[debug_handler]
pub async fn negative_adjustment_form_post(
State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
mut form_data: Form<NegativeAdjustmentFormData>,
) -> Result<Response, AppError> {
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<i64>) -> Result<Response, AppError> {
Ok(NegativeAdjustmentFormTemplate {
item_id: id,
amount_error: "",
reason_error: "",
}
.into_response())
}

@ -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<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
form_data: Form<PositiveAdjustmentFormData>,
) -> Result<Response, AppError> {
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<i64>) -> Result<Response, AppError> {
Ok(PositiveAdjustmentFormTemplate {
item_id: id,
amount_error: "",
price_error: "",
}
.into_response())
}

@ -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<AdjustmentDisplayItem>,
}
pub async fn get_adjustments_table(
State(db): State<SqlitePool>,
Path(id): Path<i64>,
user: SessionUser,
) -> Result<Response, AppError> {
let timezone = user.get_timezone()?;
let allow_fractional = does_inventory_item_allow_fractional_units(&db, id).await?;
let adjustments: Vec<DbAdjustmentWithValuation> =
get_item_adjustment_valuation_weighted_mean(&db, id).await?;
let adjustments: Vec<AdjustmentDisplayItem> = 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,
}
}
}

@ -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<SqlitePool>,
Path(id): Path<i64>,
) -> Result<Response, AppError> {
let count = sum_all_adjustments_for_item(&db, id).await?;
Ok(count.to_string().into_response())
}

@ -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<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
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<DbInventoryItemWithCount> 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,
}
}
}

@ -0,0 +1,4 @@
pub mod adjustment;
pub mod item;
pub mod stats;
pub mod count;

@ -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<SqlitePool>, Path(id): Path<i64>) -> Result<Response, AppError> {
//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())
}

@ -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<AppState> {
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::<SessionUser>())
.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<AppState>)
#[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<SqlitePool>)
impl FromRef<AppState> for SqlitePool {
fn from_ref(input: &AppState) -> Self {
input.db.clone()
}
}
impl FromRef<AppState> for BasicClient {
fn from_ref(input: &AppState) -> Self {
input.oauth_client.clone()
}
}
pub mod routes;

@ -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<AppState> {
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::<SessionUser>())
.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<AppState>)
#[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<SqlitePool>)
impl FromRef<AppState> for SqlitePool {
fn from_ref(input: &AppState) -> Self {
input.db.clone()
}
}
impl FromRef<AppState> for BasicClient {
fn from_ref(input: &AppState) -> Self {
input.oauth_client.clone()
}
}

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

@ -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<i64> for DbAdjustmentReason {
impl TryFrom<&str> for DbAdjustmentReason {
type Error = anyhow::Error;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
match value {
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"unknown" => Ok(Self::Unknown),
"sale" => Ok(Self::Sale),
"destruction" => Ok(Self::Destruction),

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

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

@ -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<AppState> {
Router::new()

@ -1,27 +0,0 @@
<form hx-post="/item/{{item_id}}/adjustment/sale" hx-target="this" hx-swap="outerHTML" >
<div class="row">
<div class="col">
<!--<label for="amount" class="form-label">Amount</label>-->
<input type="number"
id="amount"
name="amount"
step="0.01"
class="form-control"
placeholder="Amount"
aria-label="amount"
required
{% if !amount_error.is_empty() -%}
aria-invalid="true"
aria-describedby="invalid-amount"
{% endif -%}
/>
{% if !amount_error.is_empty() -%}
<small id="invalid-amount" class="invalid-feedback">{{ amount_error }}</small>
{% endif -%}
</div>
<div class="col">
<button class="btn btn-primary">Record Sale</button>
</div>
</div>
</form>

@ -0,0 +1,65 @@
<form hx-post="/item/{{item_id}}/adjustment/negative"
hx-target="this" hx-swap="outerHTML"
x-ref="formNegativeAdjustment"
x-data="{ reason: 'Sale' }">
<input id="reason" name="reason" type="hidden" x-model="reason"/>
<div class="row">
<div class="col">
<!--<label for="amount" class="form-label">Amount</label>-->
<input type="number"
id="amount"
name="amount"
step="0.01"
class="form-control"
placeholder="Amount"
aria-label="amount"
required
{% if !amount_error.is_empty() -%}
aria-invalid="true"
aria-describedby="invalid-amount"
{% endif -%}
/>
{% if !amount_error.is_empty() -%}
<small id="invalid-amount" class="invalid-feedback">{{ amount_error }}</small>
{% endif -%}
</div>
<div class="col">
<button class="btn btn-primary" x-text="reason">Sale</button>
</div>
<div class="col">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
<span class="visually-hidden">Other</span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item"
@click="reason = 'Sale'">
Sale
</a>
</li>
<li>
<a class="dropdown-item"
@click="reason = 'Destruction'">
Destruction
</a>
</li>
<li>
<a class="dropdown-item"
@click="reason = 'Expiration'">
Expiration
</a>
</li>
<li>
<a class="dropdown-item"
@click="reason = 'Theft'">
Theft
</a>
</li>
</ul>
</div>
</div>
</div>
</form>

@ -1,5 +1,5 @@
<form hx-post="/item/{{item_id}}/adjustment/new-stock" hx-target="this" hx-swap="outerHTML" >
<form hx-post="/item/{{item_id}}/adjustment/positive" hx-target="this" hx-swap="outerHTML" >
<div class="row">
<div class="col">
<input type="number"

@ -19,10 +19,12 @@
<div x-data="{ open: false }" class="mb-3">
<div class="d-flex justify-content-start">
<div class="me-3">
<button class="btn btn-primary" @click="open = ! open">Sale</button>
<button class="btn btn-primary display-6" @click="open = ! open">
Minus
</button>
</div>
<div x-show="open" @click.outside="open = false">
<div hx-get="/item/{{item_id}}/adjustment/sale" hx-trigger="load" hx-swap="outerHTML"></div>
<div hx-get="/item/{{item_id}}/adjustment/negative" hx-trigger="load" hx-swap="outerHTML"></div>
</div>
</div>
</div>
@ -30,10 +32,12 @@
<div x-data="{ open: false }" class="mb-3">
<div class="d-flex justify-content-start">
<div class="me-3">
<button class="btn btn-primary" @click="open = ! open">Stock</button>
<button class="btn btn-primary" @click="open = ! open">
Plus
</button>
</div>
<div x-show="open" @click.outside="open = false">
<div hx-get="/item/{{item_id}}/adjustment/new-stock" hx-trigger="load" hx-swap="outerHTML">
<div hx-get="/item/{{item_id}}/adjustment/positive" hx-trigger="load" hx-swap="outerHTML">
</div>
</div>
</div>

@ -12,6 +12,7 @@
<script src="/js/htmx.min.js"></script>
<!--TODO Vendor this script -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<title>{% block title %}Title{% endblock %}</title>
</head>
<body>
@ -42,7 +43,11 @@
{% block content %}<p>Content Missing</p>{% endblock %}
</main>
<footer class="container-fluid">
<script>
// Needed for nice bootstrap dropdowns
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="dropdown"]');
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
</script>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</body>
</html>
Loading…
Cancel
Save

Powered by TurnKey Linux.