diff --git a/.gitignore b/.gitignore index fedaa2b..771ad09 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .env +Session.vim diff --git a/src/lib/mod.rs b/src/lib/mod.rs index d5cbad7..309584f 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,2 +1,3 @@ pub mod models; pub mod schema; +pub mod report; diff --git a/src/lib/models.rs b/src/lib/models.rs index 037d690..185a7f6 100644 --- a/src/lib/models.rs +++ b/src/lib/models.rs @@ -1,6 +1,6 @@ use diesel::prelude::*; -#[derive(Queryable, Selectable, Debug)] +#[derive(Identifiable, Queryable, Selectable, Debug)] #[diesel(table_name = crate::schema::shifts)] #[diesel(check_for_backend(diesel::mysql::Mysql))] pub struct Shift { @@ -9,7 +9,8 @@ pub struct Shift { pub end: Option, } -#[derive(Queryable, Selectable, Debug)] +#[derive(Identifiable, Queryable, Associations, PartialEq, Selectable, Debug)] +#[diesel(belongs_to(Shift, foreign_key = shift))] #[diesel(table_name = crate::schema::drinks)] #[diesel(check_for_backend(diesel::mysql::Mysql))] pub struct Drink { diff --git a/src/lib/report.rs b/src/lib/report.rs new file mode 100644 index 0000000..2030a51 --- /dev/null +++ b/src/lib/report.rs @@ -0,0 +1,40 @@ +use crate::models::Drink; +use std::collections::HashMap; + +pub struct DrinkReport { + pub summary: HashMap, +} + +impl DrinkReport { + pub fn into_sorted(self) -> impl Iterator { + let mut array: Vec<(u32, u32)> = self.summary.into_iter().collect(); + + array.sort_by(|(a, _), (b, _)| a.cmp(b)); + + array.into_iter() + } +} + +pub trait GenerateDrinkReport { + fn generate_report(&self) -> DrinkReport; +} + +impl GenerateDrinkReport for Vec { + fn generate_report(&self) -> DrinkReport { + let mut summary: HashMap = HashMap::new(); + + // init with all default drink prices + for price in [2u32, 3u32, 5u32, 8u32, 15u32].into_iter() { + summary.insert(price, 0); + } + + for drink in self { + summary + .entry(drink.price) + .and_modify(|v| *v += drink.quantity) + .or_insert(drink.quantity); + } + + DrinkReport { summary } + } +} diff --git a/src/main.rs b/src/main.rs index 2e0ea9a..615167b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,37 @@ mod api; mod axum_ructe; -use axum::extract::Path; use axum_ructe::render; -use axum::{extract::State, response::IntoResponse, routing::get, Router}; -use cm_lib::models::Shift; -use diesel::result::OptionalExtension; -use diesel::{ExpressionMethods, QueryDsl, SelectableHelper}; +use cm_lib::{ + models::{Drink, Shift}, + report::GenerateDrinkReport, + schema::shifts, +}; + +use axum::{ + extract::{Path, State}, + response::IntoResponse, + routing::get, + Router, +}; + +use diesel::{ + result::OptionalExtension, BelongingToDsl, ExpressionMethods, QueryDsl, SelectableHelper, +}; + use diesel_async::{ pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager}, AsyncMysqlConnection, RunQueryDsl, }; + use dotenvy::dotenv; use std::net::SocketAddr; use tower_http::services::{ServeDir, ServeFile}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use crate::templates::shift_reports_html; + include!(concat!(env!("OUT_DIR"), "/templates.rs")); async fn establish_connection() -> Pool { @@ -63,7 +78,9 @@ async fn main() { let app = Router::new() .nest("/api", api::router()) .route("/", get(root)) + .route("/shift_reports", get(shift_reports)) .route("/shifts/:id/drinks", get(drinks)) + .route("/shifts/:id/report", get(shift_report)) .fallback_service(fallback_handler) .with_state(state); @@ -100,3 +117,28 @@ async fn root(State(state): State) -> impl IntoResponse { async fn drinks(Path(id): Path) -> impl IntoResponse { render!(templates::drinks_html, id) } + +async fn shift_report(State(state): State, Path(id): Path) -> impl IntoResponse { + let mut conn = state.connection.get().await.unwrap(); + let shift: Shift = shifts::table.find(id).first(&mut conn).await.unwrap(); + let drinks: Vec = Drink::belonging_to(&shift).load(&mut conn).await.unwrap(); + + render!( + crate::templates::shift_report_html, + drinks.generate_report() + ) +} + +async fn shift_reports(State(state): State) -> impl IntoResponse { + let mut conn = state.connection.get().await.unwrap(); + + let mut shifts: Vec = shifts::table + .select(Shift::as_select()) + .load(&mut conn) + .await + .unwrap(); + + shifts.sort_by(|a, b| b.start.cmp(&a.start)); + + render!(shift_reports_html, shifts) +} diff --git a/templates/home.rs.html b/templates/home.rs.html index 3346720..4859dc8 100644 --- a/templates/home.rs.html +++ b/templates/home.rs.html @@ -11,12 +11,17 @@ @:shift_button_html(open_shift.as_ref()) @if open_shift.is_some() { - + } + }) diff --git a/templates/shift_report.rs.html b/templates/shift_report.rs.html new file mode 100644 index 0000000..a20472f --- /dev/null +++ b/templates/shift_report.rs.html @@ -0,0 +1,41 @@ +@use super::base_html; +@use cm_lib::report::DrinkReport; + +@(drinks: DrinkReport) + + +@:base_html({ + + + + + + + + + + + + @for (price, quantity) in drinks.into_sorted() { + + + + + + + } + +
PriceQuantity
@price@quantity
+ +}) diff --git a/templates/shift_reports.rs.html b/templates/shift_reports.rs.html new file mode 100644 index 0000000..ffe31d8 --- /dev/null +++ b/templates/shift_reports.rs.html @@ -0,0 +1,47 @@ +@use super::base_html; +@use cm_lib::models::Shift; + +@(shifts: Vec) + +@:base_html({ + + + + + + + + + + + + + + @for shift in shifts { + + + + + + + + + } + +
Shift IDShift StartShift End
@shift.id@shift.start.format("%Y-%m-%d %H:%M:%S") + @if let Some(end) = shift.end { + @end.format("%Y-%m-%d %H:%M:%S") + } else { + Currently Open + } + + + + +
+ +})