diff --git a/code/Cargo.lock b/code/Cargo.lock index 5ebff1a..ef0ecd6 100644 --- a/code/Cargo.lock +++ b/code/Cargo.lock @@ -777,6 +777,7 @@ dependencies = [ "egui_extras", "hex", "jiff", + "ordered-float", "rust_decimal", "rust_xlsxwriter", "sha2 0.11.0", diff --git a/code/Cargo.toml b/code/Cargo.toml index 0232def..36bf6d1 100644 --- a/code/Cargo.toml +++ b/code/Cargo.toml @@ -15,6 +15,7 @@ egui_dock = "0.19.1" egui_extras = {version = "0.34.2", features = ["datepicker"]} hex = "0.4.3" jiff = "0.2.29" +ordered-float = "5.3.0" rust_decimal = {version = "1.42.0", features = ["macros"]} rust_xlsxwriter = {version="0.95.0",features=["chrono"]} sha2 = "0.11.0" diff --git a/code/src/app.rs b/code/src/app.rs index c794deb..1c66bed 100644 --- a/code/src/app.rs +++ b/code/src/app.rs @@ -388,6 +388,7 @@ impl MainTabViewer { worker_tabs: WorkerTabViewer::new(is_admin), material_tabs: MaterialTabViewer::new(is_admin), recipe_tab: RecipeTabViewer::new(is_admin), + order_tabs: OrderTabViewer::new(is_admin), ..Default::default() } } diff --git a/code/src/database.rs b/code/src/database.rs index b6547f5..128bb82 100644 --- a/code/src/database.rs +++ b/code/src/database.rs @@ -5,6 +5,7 @@ use sqlx::mysql::MySqlPool; use sqlx::mysql::MySqlPoolOptions; use dotenvy::dotenv_override; +use std::collections::BTreeSet; use std::collections::HashMap; use std::collections::HashSet; use std::env; @@ -524,7 +525,7 @@ impl DBOperator { .await .unwrap(); let pelems = self.get_recipe_elements().await.unwrap_or(Vec::new()); - let mut elems: HashSet = HashSet::new(); + let mut elems: BTreeSet = BTreeSet::new(); for pr in pelems { if pr.recipe_id == pret.id { elems.insert(pr.clone()); @@ -536,8 +537,10 @@ impl DBOperator { elements: elems, } } - pub async fn get_recipes(&self) -> Result, sqlx::Error> { - let pre_ret = sqlx::query_as::<_, RecipeRow>("SELECT * FROM `recipe`") + pub async fn get_recipes(&self, start: i32, length: i32) -> Result, sqlx::Error> { + let pre_ret = sqlx::query_as::<_, RecipeRow>("SELECT * FROM `recipe` LIMIT ? OFFSET ?") + .bind(length) + .bind(start) .fetch_all(&self.pool) .await .unwrap_or(Vec::new()); @@ -624,7 +627,7 @@ impl DBOperator { } pub async fn add_recipe_elems( &self, - elems: HashSet, + elems: BTreeSet, rec_id: i32, ) -> Result { let mut tx = self.pool.begin().await?; @@ -665,11 +668,17 @@ impl DBOperator { let mut ret: Vec = Vec::new(); for or in pret { + let hs: BTreeSet = self + .get_order_elements_by_order_id(or.id) + .await + .iter() + .cloned() + .collect(); ret.push(Order { id: or.id, order_date: or.order_date, client: self.get_client_by_id(or.client_id).await.unwrap(), - elements: self.get_order_elements_by_order_id(or.id).await, + elements: hs, total_price: or.total_price, }) } @@ -706,17 +715,11 @@ impl DBOperator { .fetch_one(&self.pool) .await .unwrap(); - let mut recipe: Recipe = Recipe::default(); - for rec in self.get_recipes().await.unwrap() { - if rec.id == pret.recipe_id { - recipe = rec.clone(); - } - } Product { id: pret.id, - recipe, + recipe: self.get_recipe_by_id(pret.recipe_id).await, price_per_unit: pret.price_per_unit, - volume: pret.volume, + volume: ordered_float::OrderedFloat(pret.volume), } } pub async fn get_products(&self, start: i32, length: i32) -> Vec { @@ -732,7 +735,7 @@ impl DBOperator { ret.push(Product { id: pr.id, recipe: self.get_recipe_by_id(pr.recipe_id).await, - volume: pr.volume, + volume: ordered_float::OrderedFloat(pr.volume), price_per_unit: pr.price_per_unit, }); } @@ -744,7 +747,7 @@ impl DBOperator { "INSERT INTO `product` (recipe_id, volume, price_per_unit) VALUES(?, ?, ?);", ) .bind(prod.recipe.id) - .bind(prod.volume) + .bind(prod.volume.into_inner()) .bind(prod.price_per_unit) .execute(&self.pool) .await @@ -756,7 +759,7 @@ impl DBOperator { pub async fn update_product(&self, prod: Product) -> Result<(), sqlx::Error> { match sqlx::query("UPDATE product SET recipe_id=?, volume=?, price_per_unit=? WHERE id=?;") .bind(prod.recipe.id) - .bind(prod.volume) + .bind(prod.volume.into_inner()) .bind(prod.price_per_unit) .bind(prod.id) .execute(&self.pool) diff --git a/code/src/models.rs b/code/src/models.rs index 830c3c2..168bca4 100644 --- a/code/src/models.rs +++ b/code/src/models.rs @@ -1,4 +1,8 @@ -use std::{any::TypeId, collections::HashSet, ops::Deref}; +use std::{ + any::TypeId, + collections::{BTreeSet, HashSet}, + ops::Deref, +}; use chrono::Local; use egui::TextBuffer; @@ -7,7 +11,7 @@ use sqlx::{ types::{BigDecimal, chrono::DateTime}, }; -#[derive(Clone, Hash, sqlx::FromRow, PartialEq, std::cmp::Eq)] +#[derive(Clone, Hash, sqlx::FromRow, PartialEq, std::cmp::Eq, PartialOrd, Ord)] pub struct Position { pub id: i32, pub name: String, @@ -22,7 +26,7 @@ impl Default for Position { } } } -#[derive(Default, Clone, Hash, std::cmp::Eq, PartialEq)] +#[derive(Default, Clone, Hash, std::cmp::Eq, PartialEq, PartialOrd, Ord)] pub struct Worker { pub id: i32, pub full_name: String, @@ -57,7 +61,7 @@ pub struct Salary { pub comment: String, } -#[derive(Default, Clone, Hash, PartialEq, Eq)] +#[derive(Default, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct Equipment { pub id: i32, pub name: String, @@ -75,7 +79,7 @@ pub struct EquipmentRow { pub worker_id: i32, pub is_written_off: bool, } -#[derive(Clone, PartialEq, Eq, Hash, Default)] +#[derive(Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] pub struct Material { pub id: i32, pub name: String, @@ -89,12 +93,12 @@ pub struct MaterialRow { pub quantity: i32, pub category_id: i32, } -#[derive(sqlx::FromRow, Default, Clone, PartialEq, Eq, Hash)] +#[derive(sqlx::FromRow, Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct MaterialCategory { pub id: i32, pub name: String, } -#[derive(Clone, PartialEq, Eq, Hash, Default)] +#[derive(Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)] pub struct RecipeElement { pub id: i32, pub material: Material, @@ -110,29 +114,30 @@ pub struct RecipeElementRow { pub quantity: i32, pub recipe_id: i32, } -#[derive(Default, Clone)] +#[derive(Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Recipe { pub id: i32, pub name: String, - pub elements: HashSet, + pub elements: BTreeSet, } #[derive(FromRow)] pub struct RecipeRow { pub id: i32, pub name: String, } -#[derive(FromRow)] +#[derive(FromRow, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct Client { pub id: i32, pub name: String, pub address: String, } +#[derive(Default, PartialEq, PartialOrd, Eq, Ord)] pub struct Order { pub id: i32, pub client: Client, pub order_date: chrono::DateTime, pub total_price: BigDecimal, - pub elements: std::vec::Vec, + pub elements: BTreeSet, } #[derive(FromRow)] pub struct OrderRow { @@ -141,6 +146,7 @@ pub struct OrderRow { pub order_date: chrono::DateTime, pub total_price: BigDecimal, } +#[derive(PartialEq, PartialOrd, Ord, Eq, Clone, Default)] pub struct OrderElement { pub id: i32, pub product: Product, @@ -155,11 +161,11 @@ pub struct OrderElementRow { pub total_price: BigDecimal, pub is_incoming: bool, } -#[derive(Default, Clone)] +#[derive(Default, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Product { pub id: i32, pub recipe: Recipe, - pub volume: f64, + pub volume: ordered_float::OrderedFloat, pub price_per_unit: BigDecimal, } #[derive(FromRow)] @@ -236,13 +242,20 @@ pub struct RecipeModalState { } #[derive(Default)] -pub struct ElementModalState { +pub struct RecipeElementModalState { pub is_open: bool, pub can_finish: bool, pub element: RecipeElement, pub status: ModalStatus, } #[derive(Default)] +pub struct OrderElementModalState { + pub is_open: bool, + pub can_finish: bool, + pub element: OrderElement, + pub status: ModalStatus, +} +#[derive(Default)] pub struct ProductModalState { pub is_open: bool, pub can_finish: bool, @@ -250,6 +263,13 @@ pub struct ProductModalState { pub recipe: Recipe, pub status: ModalStatus, } +#[derive(Default)] +pub struct OrderModalState { + pub is_open: bool, + pub can_finish: bool, + pub order: Order, + pub status: ModalStatus, +} #[derive(Default, PartialEq)] pub enum ModalStatus { diff --git a/code/src/order_tab.rs b/code/src/order_tab.rs index 0eeefef..6f9976c 100644 --- a/code/src/order_tab.rs +++ b/code/src/order_tab.rs @@ -1,12 +1,13 @@ use std::sync::Arc; +use bigdecimal::ToPrimitive; use egui::{Sense, mutex::RwLock}; use egui_extras::Column; use crate::{ database::DBOperator, misc, - models::Order, + models::{ModalStatus, Order, OrderElement, OrderElementModalState, OrderModalState}, tab::{TABS_CAN_BE_WINDOWS, Tab, TabTypes}, }; @@ -20,6 +21,8 @@ pub struct OrderTabViewer { outcoming_order_page: i32, is_admin: bool, + process_order_state: OrderModalState, + process_element_state: OrderElementModalState, } impl OrderTabViewer { @@ -29,21 +32,105 @@ impl OrderTabViewer { ..Default::default() } } + fn update_orders(&mut self) { + self.rt.block_on(async { + self.incoming_orders = Arc::new(RwLock::new( + self.db_oper + .get_orders( + self.incoming_order_page * misc::ELEMENTS_PER_PAGE, + misc::ELEMENTS_PER_PAGE, + true, + ) + .await, + )); + self.outcoming_orders = Arc::new(RwLock::new( + self.db_oper + .get_orders( + self.outcoming_order_page * misc::ELEMENTS_PER_PAGE, + misc::ELEMENTS_PER_PAGE, + false, + ) + .await, + )); + }); + } + fn show_order_element_modal(&mut self, ui: &mut egui::Ui) { + egui::Modal::new(egui::Id::new("order_element_modal")).show(ui.ctx(), |ui| { + if ui.button("Закрыть").clicked() { + self.process_element_state.is_open = false; + } + egui::Grid::new("order_element_grid").show(ui, |ui| { + ui.label("Продукт:"); + /// + ui.end_row(); + + ui.label("Количество:"); + ui.add( + egui::Slider::new(&mut self.process_element_state.element.quantity, 0..=100) + .clamping(egui::SliderClamping::Never), + ); + ui.end_row(); + + ui.label("Итоговая цена:"); + ui.label(format!( + "{}", + self.process_element_state + .element + .product + .price_per_unit + .clone() + * self.process_element_state.element.quantity + )) + }); + }); + } pub fn show_orders(&mut self, ui: &mut egui::Ui, incoming_orders: bool) { let orders = if incoming_orders { - &self.incoming_orders + self.incoming_orders.clone() } else { - &self.outcoming_orders + self.outcoming_orders.clone() }; ui.horizontal(|ui| { - if ui.button("Добавить").clicked() {} - if ui.button("<-").clicked() {} + if self.is_admin { + if ui.button("Добавить").clicked() { + self.process_order_state.is_open = true; + self.process_order_state.order = Order::default(); + } + } + + if ui.button("<-").clicked() { + if incoming_orders { + if self.incoming_orders.read().len() > 0 { + self.incoming_order_page -= 1; + self.update_orders(); + } + } else { + if self.outcoming_orders.read().len() > 0 { + self.outcoming_order_page -= 1; + self.update_orders(); + } + } + } ui.monospace(if incoming_orders { self.incoming_order_page.to_string() } else { self.outcoming_order_page.to_string() }); - if ui.button("->").clicked() {} + if ui.button("->").clicked() { + if incoming_orders { + if self.incoming_orders.read().len().to_i32().unwrap() + == misc::ELEMENTS_PER_PAGE + { + self.incoming_order_page += 1; + } + } else { + if self.outcoming_orders.read().len().to_i32().unwrap() + == misc::ELEMENTS_PER_PAGE + { + self.outcoming_order_page += 1; + } + } + } }); egui_extras::TableBuilder::new(ui) @@ -92,6 +179,58 @@ impl OrderTabViewer { }); } }); + if self.process_order_state.is_open { + egui::Modal::new(egui::Id::new("order_process_modal")).show(ui.ctx(), |ui| { + if ui.button("Закрыть").clicked() { + self.process_order_state.is_open = false + } + egui::Grid::new("order_process_grid").show(ui, |ui| { + ui.label("ID заказа:"); + ui.label(self.process_order_state.order.id.to_string()); + ui.end_row(); + + ui.label(if incoming_orders { + "Заказчик" + } else { + "Клиент" + }); + //ВЫБОР КЛИЕНТА + ui.end_row(); + + ui.label("Элементы"); + egui::ComboBox::new("order_process_combobox", "") + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .selected_text(match self.process_order_state.order.elements.len() { + 0 => "Пусто".to_owned(), + _ => format!("{} элем.", self.process_order_state.order.elements.len()), + }) + .show_ui(ui, |ui| { + if ui.button("Новый элемент").clicked() { + self.process_element_state.is_open = true; + self.process_element_state.status = ModalStatus::Add; + self.process_element_state.element = OrderElement::default(); + } + + for el in self.process_order_state.order.elements.clone().iter() { + ui.horizontal(|ui| { + if ui.button("x").clicked() { + self.process_order_state.order.elements.remove(el); + } + ui.label(format!( + "{} ({})\n{}/шт.", + el.product.recipe.name, + el.quantity, + el.product.price_per_unit + )); + }); + } + }); + }); + }); + } + if self.process_element_state.is_open { + self.show_order_element_modal(ui); + } } } @@ -140,6 +279,8 @@ impl Default for OrderTabViewer { db_oper, rt, is_admin: false, + process_order_state: OrderModalState::default(), + process_element_state: OrderElementModalState::default(), } } } diff --git a/code/src/recipe_tab.rs b/code/src/recipe_tab.rs index 24df4e5..344ba0d 100644 --- a/code/src/recipe_tab.rs +++ b/code/src/recipe_tab.rs @@ -5,7 +5,7 @@ use crate::{ database::DBOperator, misc, models::{ - ElementModalState, Equipment, Material, ModalStatus, Product, ProductModalState, Recipe, + RecipeElementModalState, Equipment, Material, ModalStatus, Product, ProductModalState, Recipe, RecipeElement, RecipeModalState, }, tab::{self, Tab, TabTypes}, @@ -29,10 +29,11 @@ pub struct RecipeTabViewer { products: Arc>>, process_recipe_status: RecipeModalState, - process_element_status: ElementModalState, + process_element_status: RecipeElementModalState, process_product_status: ProductModalState, old_price: BigDecimal, product_page: i32, + recipe_page: i32, } impl RecipeTabViewer { @@ -43,10 +44,12 @@ impl RecipeTabViewer { } } fn update_recipes(&mut self) { - self.recipes = - Arc::new(RwLock::new(self.rt.block_on(async { - self.db_oper.get_recipes().await.unwrap_or(Vec::new()) - }))); + self.recipes = Arc::new(RwLock::new(self.rt.block_on(async { + self.db_oper + .get_recipes(self.recipe_page, misc::ELEMENTS_PER_PAGE) + .await + .unwrap_or(Vec::new()) + }))); } pub fn update_products(&mut self) { self.products = Arc::new(RwLock::new(self.rt.block_on(async { @@ -229,6 +232,19 @@ impl RecipeTabViewer { self.process_recipe_status.status = ModalStatus::Add; } } + if ui.button("<-").clicked() { + if self.recipe_page > 0 { + self.recipe_page -= 1; + self.update_recipes(); + } + } + ui.monospace(self.recipe_page.to_string()); + if ui.button("->").clicked() { + if self.recipes.read().len().to_i32().unwrap() > misc::ELEMENTS_PER_PAGE { + self.recipe_page += 1; + self.update_recipes(); + } + } }); TableBuilder::new(ui) .striped(true) @@ -293,12 +309,14 @@ impl RecipeTabViewer { if ui.button("<-").clicked() { if self.product_page > 0 { self.product_page -= 1; + self.update_products(); } } ui.monospace(self.product_page.to_string()); if ui.button("->").clicked() { if self.products.read().len() == 5 { self.product_page += 1; + self.update_products(); } } }); @@ -319,7 +337,7 @@ impl RecipeTabViewer { ui.heading("Цена за ед."); }); head.col(|ui| { - ui.heading(" "); + ui.heading(" "); }); }) .body(|mut body| { @@ -386,7 +404,7 @@ impl egui_dock::TabViewer for RecipeTabViewer { ui.add_sized( size, egui::Slider::new( - &mut self.process_product_status.product.volume, + &mut self.process_product_status.product.volume.into_inner(), 0.0..=100.0, ) .clamping(egui::SliderClamping::Never), @@ -414,7 +432,7 @@ impl egui_dock::TabViewer for RecipeTabViewer { ModalStatus::Remove => true, _ => { self.process_product_status.product.price_per_unit > 0 - && self.process_product_status.product.volume > 0.0 + && self.process_product_status.product.volume.into_inner() > 0.0 } }; let btext = match self.process_product_status.status { @@ -475,9 +493,12 @@ impl Default for RecipeTabViewer { Self { is_admin: false, - recipes: Arc::new(RwLock::new( - rt.block_on(async { db_oper.get_recipes().await.unwrap_or(Vec::new()) }), - )), + recipes: Arc::new(RwLock::new(rt.block_on(async { + db_oper + .get_recipes(0, misc::ELEMENTS_PER_PAGE) + .await + .unwrap_or(Vec::new()) + }))), materials: Arc::new(RwLock::new( rt.block_on(async { db_oper.get_materials().await.unwrap_or(Vec::new()) }), )), @@ -498,7 +519,7 @@ impl Default for RecipeTabViewer { status: crate::models::ModalStatus::Add, elements: HashSet::new(), }, - process_element_status: ElementModalState { + process_element_status: RecipeElementModalState { is_open: false, can_finish: false, element: RecipeElement::default(), @@ -508,6 +529,7 @@ impl Default for RecipeTabViewer { old_price: BigDecimal::default(), product_page: 0, + recipe_page: 0, rt, db_oper, diff --git a/ЧЁСДЕЛАТЬ b/ЧЁСДЕЛАТЬ new file mode 100644 index 0000000..039b35d --- /dev/null +++ b/ЧЁСДЕЛАТЬ @@ -0,0 +1,4 @@ +1 - Добавить пагниацию там, где её ещё нет +2 - Доделать проверки при нажатии кнопок в модальных окнах +3 - Написать часть с клиентом +4 - доделать заказы