From c012683af1e6ea5bd9e8f3a63757949ee71f04ce Mon Sep 17 00:00:00 2001 From: JMARyA Date: Tue, 16 Jan 2024 09:02:23 +0100 Subject: [PATCH] work --- Cargo.lock | 19 ++++ Cargo.toml | 1 + README.md | 10 ++ src/item.rs | 175 +++++++++++++++++++++++++++++-- src/main.rs | 36 +++---- src/pages/item/inventory_page.rs | 70 +++++++++++++ src/pages/item/mod.rs | 50 +++++++++ src/pages/item/variant_pages.rs | 43 ++++++++ src/pages/mod.rs | 1 + src/ui/mod.rs | 9 ++ 10 files changed, 384 insertions(+), 30 deletions(-) create mode 100644 README.md create mode 100644 src/pages/item/inventory_page.rs create mode 100644 src/pages/item/mod.rs create mode 100644 src/pages/item/variant_pages.rs create mode 100644 src/pages/mod.rs create mode 100644 src/ui/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a1ae359..7530500 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -475,6 +475,7 @@ version = "0.1.0" dependencies = [ "actix-web", "env_logger", + "futures", "log", "maud 0.25.0", "mdq", @@ -848,6 +849,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -855,6 +871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -909,9 +926,11 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", diff --git a/Cargo.toml b/Cargo.toml index 672f00d..58fc1d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] actix-web = "4.4.1" env_logger = "0.10.1" +futures = "0.3.30" log = "0.4.20" maud = "0.25.0" mdq = { git = "https://git.hydrar.de/mdtools/mdq", version = "0.1.0" } diff --git a/README.md b/README.md new file mode 100644 index 0000000..375ca19 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# CDB +cdb is a powerful software tool designed to automate economic calculations by managing a database of items, their relations, quantities, and tracking demands. This tool facilitates the estimation of demand and supply, aiding in making informed decisions on production, imports, and purchases to meet market needs efficiently. + +## Features +- Data Input: Start with a root dataset of items and their relations, including details such as item names, categories, and dependencies. +- Inventory Tracking: Record and update the quantities of items within the database based on real-world transactions, production, and consumption. +- Demand Tracking: Monitor the demand for each item, keeping track of how many units are required over time. +- Supply Calculation: Automatically calculate and update the estimated supply needed to meet the current demand. +- Automated Economics: Utilize the collected data to automate economic decisions, determining optimal production levels, imports, and purchases to fulfill market needs. +- Flexible Reporting: Generate reports and visualizations to analyze the economic trends \ No newline at end of file diff --git a/src/item.rs b/src/item.rs index 180785b..6b9ce93 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,22 +1,163 @@ -use mongodb::{Collection, bson::doc}; +use std::collections::HashSet; + +use futures::TryStreamExt; +use mongodb::{bson::doc, Collection}; + +macro_rules! collect_results { + ($res:expr) => {{ + let mut ret = vec![]; + + while let Some(doc) = $res.try_next().await.unwrap() { + ret.push(doc); + } + + ret + }}; +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct ItemVariantEntry { + pub item_name: String, + pub variant_name: String, +} + +#[derive(serde::Deserialize, serde::Serialize)] +pub struct ItemInventoryEntry { + pub item_name: String, + pub variant_name: String, + pub origin: String, + pub price: f64, +} #[derive(serde::Deserialize, serde::Serialize)] pub struct ItemEntry { pub name: String, - pub category: String + pub category: String, } impl ItemEntry { pub fn new(doc: mdq::Document) -> Self { - let name = std::path::Path::new(&doc.path).file_stem().unwrap().to_str().unwrap().to_string(); - let category = doc.frontmatter.as_mapping().unwrap().get("category").unwrap().as_str().unwrap().to_string(); + let name = std::path::Path::new(&doc.path) + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string(); + let category = doc + .frontmatter + .as_mapping() + .unwrap() + .get("category") + .unwrap() + .as_str() + .unwrap() + .to_string(); Self { name, category } } } +pub struct Item { + db: mongodb::Client, + pub item: ItemEntry, +} + +impl Item { + pub fn new(db: mongodb::Client, item: ItemEntry) -> Self { + Self { db, item } + } + + pub async fn get_inventory_entries(&self) -> Vec { + let quantities: Collection = + self.db.database("cdb").collection("item_quantities"); + + let mut found = + quantities + .find(doc! { "item_name": self.item.name.clone()}, None) + .await + .unwrap(); + + collect_results!(found) + } + + pub async fn get_origins(&self) -> Vec { + let quantities: Collection = + self.db.database("cdb").collection("item_quantities"); + + let mut found = + quantities + .find(doc! { "item_name": self.item.name.clone()}, None) + .await + .unwrap(); + + let mut hashset = HashSet::new(); + + while let Some(res) = found.try_next().await.unwrap() { + hashset.insert(res.origin); + } + + hashset.into_iter().collect() + } + + pub async fn add_inventory(&self, variant: &str, origin: &str, price: f64) -> String { + // todo : implement + let quantities: Collection = + self.db.database("cdb").collection("item_quantities"); + let id = quantities + .insert_one( + ItemInventoryEntry { + item_name: self.item.name.clone(), + variant_name: variant.to_owned(), + origin: origin.to_owned(), + price, + }, + None, + ) + .await + .unwrap(); + return id.inserted_id.as_object_id().unwrap().to_string(); + } + + pub async fn get_variants(&self) -> Vec { + let variants: Collection = + self.db.database("cdb").collection("item_variants"); + let mut found_variants = variants + .find( + doc! { + "item_name": self.item.name.clone() + }, + None, + ) + .await + .unwrap(); + + let mut ret = vec!["Generic".to_string()]; + + while let Some(doc) = found_variants.try_next().await.unwrap() { + ret.push(doc.variant_name); + } + + ret + } + + pub async fn add_variant(&self, name: &str) { + let variants: Collection = + self.db.database("cdb").collection("item_variants"); + variants + .insert_one( + ItemVariantEntry { + item_name: self.item.name.clone(), + variant_name: name.to_owned(), + }, + None, + ) + .await + .unwrap(); + } +} + pub struct ItemDB { index: mdq::Index, - mongodb: mongodb::Client + mongodb: mongodb::Client, } impl ItemDB { @@ -29,14 +170,30 @@ impl ItemDB { let item = ItemEntry::new(item.clone()); log::info!("Adding item {} to DB", item.name); let items: Collection = mongodb.database("cdb").collection("items"); - items.insert_one(item, None).await.unwrap(); + if let None = + items + .find_one(doc! { "name": item.name.clone()}, None) + .await + .unwrap() + { + items.insert_one(item, None).await.unwrap(); + } else { + // todo : update values + } } Self { index, mongodb } } - pub fn get_item(&self, item: &str) -> Option { - self.index.documents.iter().map(|x| ItemEntry::new(x.clone())).find(|x| x.name == item) + pub fn get_item(&self, item: &str) -> Option { + Some(Item::new( + self.mongodb.clone(), + self.index + .documents + .iter() + .map(|x| ItemEntry::new(x.clone())) + .find(|x| x.name == item)?, + )) } pub fn items(&self) -> Vec { @@ -47,4 +204,4 @@ impl ItemDB { } ret } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index a03c2c6..c794606 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,17 @@ use actix_web::{get, HttpRequest, Responder}; use maud::html; + mod item; +mod pages; +mod ui; // ░░░░░░░░░░▀▀▀██████▄▄▄░░░░░░░░░░ // ░░░░░░░░░░░░░░░░░▀▀▀████▄░░░░░░░ // ░░░░░░░░░░▄███████▀░░░▀███▄░░░░░ // ░░░░░░░░▄███████▀░░░░░░░▀███▄░░░ // ░░░░░░▄████████░░░░░░░░░░░███▄░░ -// ░░░░░██████████▄░░░░░░░░░░░███▌░ ▒█▀▀█ █▀▀█ █▀▄▀█ █▀▄▀█ ▒█▀▀▄ ▒█▀▀█ -// ░░░░░▀█████▀░▀███▄░░░░░░░░░▐███░ ▒█░░░ █░░█ █░▀░█ █░▀░█ ▒█░▒█ ▒█▀▀▄ +// ░░░░░██████████▄░░░░░░░░░░░███▌░ ▒█▀▀█ █▀▀█ █▀▄▀█ █▀▄▀█ ▒█▀▀▄ ▒█▀▀█ +// ░░░░░▀█████▀░▀███▄░░░░░░░░░▐███░ ▒█░░░ █░░█ █░▀░█ █░▀░█ ▒█░▒█ ▒█▀▀▄ // ░░░░░░░▀█▀░░░░░▀███▄░░░░░░░▐███░ ▒█▄▄█ ▀▀▀▀ ▀░░░▀ ▀░░░▀ ▒█▄▄▀ ▒█▄▄█ // ░░░░░░░░░░░░░░░░░▀███▄░░░░░███▌░ // ░░░░▄██▄░░░░░░░░░░░▀███▄░░▐███░░ @@ -17,23 +20,6 @@ mod item; // ░████▀░░░▀▀█████▄▄▄▄█████████▄░░ // ░░▀▀░░░░░░░░░▀▀██████▀▀░░░▀▀██░░ -#[get("/item/{item_id}")] -pub async fn item_page(r: HttpRequest) -> impl Responder { - let id = r.match_info().query("item_id"); - println!("{}", id); - - let itemdb: &actix_web::web::Data = r.app_data().unwrap(); - - let item = itemdb.get_item(id).unwrap(); - - let content = html!( - p { "Item" }; - p { (format!("Category: {}", item.category))} - ).into_string(); - - web_base::build_site(&r, "Item", &content) -} - #[get("/")] pub(crate) async fn index(r: HttpRequest) -> impl Responder { let itemdb: &actix_web::web::Data = r.app_data().unwrap(); @@ -52,13 +38,21 @@ pub(crate) async fn index(r: HttpRequest) -> impl Responder { async fn main() -> std::io::Result<()> { env_logger::init(); - let itemdb = item::ItemDB::new("./itemdb", "mongodb://user:pass@mongodb:27017").await; + let itemdb = item::ItemDB::new("./itemdb", "mongodb://user:pass@127.0.0.1:27017").await; let itemdb = actix_web::web::Data::new(itemdb); web_base::map!( web_base::Site::new() .head_content("".to_string()), - |app: actix_web::App<_>| { app.app_data(itemdb.clone()).service(index).service(item_page) } + |app: actix_web::App<_>| { + app.app_data(itemdb.clone()) + .service(index) + .service(pages::item::item_page) + .service(pages::item::variant_pages::add_item_variant_page) + .service(pages::item::variant_pages::add_item_variant_page_post) + .service(pages::item::inventory_page::add_item_inventory_page) + .service(pages::item::inventory_page::add_item_inventory_page_post) + } ) .bind(("0.0.0.0".to_string(), 8080))? .run() diff --git a/src/pages/item/inventory_page.rs b/src/pages/item/inventory_page.rs new file mode 100644 index 0000000..7e85072 --- /dev/null +++ b/src/pages/item/inventory_page.rs @@ -0,0 +1,70 @@ +use actix_web::{get, post, HttpRequest, Responder}; +use maud::html; + +use crate::item; + +#[derive(serde::Serialize, serde::Deserialize)] +struct AddinventoryForm { + variant: String, + origin: String, + price: f64, +} + +#[post("/item/{item_id}/inventory/add")] +pub async fn add_item_inventory_page_post( + r: HttpRequest, + f: actix_web::web::Form, +) -> impl Responder { + let id = r.match_info().query("item_id"); + let itemdb: &actix_web::web::Data = r.app_data().unwrap(); + let item = itemdb.get_item(id).unwrap(); + let rec_id = item.add_inventory(&f.variant, &f.origin, f.price).await; + + // todo : generate receipts + + web_base::redirect(&format!("/receipt/{}", rec_id)) +} + +#[get("/item/{item_id}/inventory/add")] +pub async fn add_item_inventory_page(r: HttpRequest) -> impl Responder { + let id = r.match_info().query("item_id"); + + let itemdb: &actix_web::web::Data = r.app_data().unwrap(); + + let item = itemdb.get_item(id).unwrap(); + let item_variants = item.get_variants().await; + + // todo : get previous origins + let item_origins = item.get_origins().await; + + let content = html!( + div class="bg-white p-8 rounded shadow-md max-w-md grid items-center justify-center m-auto mt-5" { + h1 class="text-2xl font-bold mb-4" { "Add Item inventory" }; + form action=(format!("/item/{}/inventory/add", id)) method="post" { + div { + label class="text-sm font-medium text-gray-600 mr-3" for="variant" { "Variant:" }; + select class="mt-1 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500" id="variant" name="variant" { + @for variant in item_variants { + option value=(variant) { (variant) }; + }; + }; + }; + br; + label class="text-sm font-medium text-gray-600" for="origin" { "Origin:" }; + input class="mt-1 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500" type="text" name="origin" list="origin_options" placeholder="Origin"; + datalist id="origin_options" { + @for origin in item_origins { + option value=(origin); + } + }; + br; + label class="text-sm font-medium text-gray-600" for="price" { "Price: "}; + input class="mt-1 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500" type="number" step="0.01" name="price" placeholder="0.00"; + br; + input class="hover:cursor-pointer bg-blue-500 text-white rounded px-4 py-2 mt-5" type="submit" name="submit" value="Add"; + }; + }; + ).into_string(); + + web_base::build_site(&r, "Add Item inventory", &content) +} diff --git a/src/pages/item/mod.rs b/src/pages/item/mod.rs new file mode 100644 index 0000000..89464db --- /dev/null +++ b/src/pages/item/mod.rs @@ -0,0 +1,50 @@ +use actix_web::{get, HttpRequest, Responder}; +use maud::html; +pub mod inventory_page; +pub mod variant_pages; + +use crate::{item, ui::button}; + +#[get("/item/{item_id}")] +pub async fn item_page(r: HttpRequest) -> impl Responder { + let id = r.match_info().query("item_id"); + println!("{}", id); + + let itemdb: &actix_web::web::Data = r.app_data().unwrap(); + + let item = itemdb.get_item(id).unwrap(); + + let content = + html!( + div class="bg-gray-100 min-h-screen min-w-screen items-center justify-center py-5" { + div class="w-screen px-5" { + div { + p class="mb-2 font-bold text-2xl" { (format!("Item : {}", item.item.name)) }; + p class="font-bold" { (format!("Category: {}", item.item.category))}; + }; + div class="mt-5" { + (button("Add Variant", &format!("/item/{}/variant/add", item.item.name))) + (button("Add inventory", &format!("/item/{}/inventory/add", item.item.name))); + }; + }; + + + div class="mt-5 px-5" { + h1 class="text-2xl font-bold mb-4" { "Variants" }; + @for variant in item.get_variants().await { + p class="mb-2" { (variant) }; + } + @for q in item.get_inventory_entries().await { + div class="p-2" { + p { (format!("Variant {}", q.variant_name))}; + p { (format!("Origin {}", q.origin))}; + p { (format!("Price {}", q.price))}; + }; + } + }; + }; + ) + .into_string(); + + web_base::build_site(&r, "Item", &content) +} diff --git a/src/pages/item/variant_pages.rs b/src/pages/item/variant_pages.rs new file mode 100644 index 0000000..7f2e996 --- /dev/null +++ b/src/pages/item/variant_pages.rs @@ -0,0 +1,43 @@ +use actix_web::{get, post, HttpRequest, Responder}; +use maud::html; + +use crate::item; + +#[derive(serde::Serialize, serde::Deserialize)] +struct AddVariantForm { + variant_name: String, +} + +#[post("/item/{item_id}/variant/add")] +pub async fn add_item_variant_page_post( + r: HttpRequest, + f: actix_web::web::Form, +) -> impl Responder { + let id = r.match_info().query("item_id"); + let itemdb: &actix_web::web::Data = r.app_data().unwrap(); + let item = itemdb.get_item(id).unwrap(); + item.add_variant(&f.variant_name).await; + + web_base::redirect(&format!("/item/{}", id)) +} + +#[get("/item/{item_id}/variant/add")] +pub async fn add_item_variant_page(r: HttpRequest) -> impl Responder { + let id = r.match_info().query("item_id"); + + let itemdb: &actix_web::web::Data = r.app_data().unwrap(); + + let item = itemdb.get_item(id).unwrap(); + + let content = html!( + div class="bg-white p-8 rounded shadow-md max-w-md grid items-center justify-center m-auto mt-5" { + h1 class="text-2xl font-bold mb-4" { "Add Variant for " (item.item.name) }; + form action=(format!("/item/{}/variant/add", id)) method="post" { + input class="mt-1 p-2 border border-gray-300 rounded focus:outline-none focus:border-blue-500" type="text" name="variant_name" placeholder="Name" { }; + input class="ml-5 hover:cursor-pointer bg-blue-500 text-white rounded px-4 py-2 mt-5" type="submit" name="submit" value="Add"; + }; + }; + ).into_string(); + + web_base::build_site(&r, "Add Item Variant", &content) +} diff --git a/src/pages/mod.rs b/src/pages/mod.rs new file mode 100644 index 0000000..a35e98d --- /dev/null +++ b/src/pages/mod.rs @@ -0,0 +1 @@ +pub mod item; diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..b57c6f7 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,9 @@ +use maud::html; + +pub fn button(title: &str, redir: &str) -> maud::PreEscaped { + html!( + a class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded hover:cursor-pointer mt-1 mr-1 mb-1" href=(redir) { + (title) + }; + ) +}