From f2c6de7137990efe2ec684a726f38209e335a4c5 Mon Sep 17 00:00:00 2001 From: ultrageese Date: Thu, 25 Jun 2026 23:38:40 +1000 Subject: [PATCH] =?UTF-8?q?=D0=9D=D0=B5=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B4=D1=83=D0=BA=D1=86=D0=B8=D0=B8=20(?= =?UTF-8?q?=D0=B2=D1=81=D1=8F)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- code/src/app.rs | 17 +++- code/src/database.rs | 74 +++++++++++++-- code/src/models.rs | 10 +- code/src/order_tab.rs | 35 ++++++- code/src/recipe_tab.rs | 210 +++++++++++++++++++++++++++++++++++++++-- 5 files changed, 326 insertions(+), 20 deletions(-) diff --git a/code/src/app.rs b/code/src/app.rs index 72edea5..c794deb 100644 --- a/code/src/app.rs +++ b/code/src/app.rs @@ -208,6 +208,7 @@ struct MainTabViewer { is_admin: bool, reporter: Reporter, only_hired_workers: bool, + recipe_tree: DockState, } impl Default for MainTabViewer { fn default() -> Self { @@ -258,7 +259,16 @@ impl Default for MainTabViewer { }, ]), recipe_tab: RecipeTabViewer::default(), - + recipe_tree: DockState::new(vec![ + Tab { + title: "Список".to_owned(), + tab_type: TabTypes::Recipe, + }, + Tab { + title: "Производство".to_owned(), + tab_type: TabTypes::Product, + }, + ]), order_tabs: OrderTabViewer::default(), order_tree: DockState::new(vec![ Tab { @@ -977,7 +987,10 @@ impl MainTabViewer { } fn show_recipe(&mut self, ui: &mut egui::Ui) { - self.recipe_tab.show_recipe_list(ui); + let id = ui.make_persistent_id("RecipeMenu"); + DockArea::new(&mut self.recipe_tree) + .id(id) + .show_inside(ui, &mut self.recipe_tab); } } diff --git a/code/src/database.rs b/code/src/database.rs index b9cd0d0..b6547f5 100644 --- a/code/src/database.rs +++ b/code/src/database.rs @@ -652,12 +652,7 @@ impl DBOperator { Err(_) => Ok(false), } } - pub async fn get_orders( - &self, - start: i32, - length: i32, - is_incoming: bool, - ) -> Vec{ + pub async fn get_orders(&self, start: i32, length: i32, is_incoming: bool) -> Vec { let pret = sqlx::query_as::<_, OrderRow>( "SELECT * FROM `order` WHERE is_incoming=? LIMIT ? OFFSET ?", ) @@ -724,6 +719,73 @@ impl DBOperator { volume: pret.volume, } } + pub async fn get_products(&self, start: i32, length: i32) -> Vec { + let pret = sqlx::query_as::<_, ProductRow>("SELECT * FROM `product` LIMIT ? OFFSET ?") + .bind(length) + .bind(start) + .fetch_all(&self.pool) + .await + .unwrap_or(Vec::new()); + + let mut ret: Vec = Vec::new(); + for pr in pret { + ret.push(Product { + id: pr.id, + recipe: self.get_recipe_by_id(pr.recipe_id).await, + volume: pr.volume, + price_per_unit: pr.price_per_unit, + }); + } + ret + } + pub async fn add_product(&self, prod: Product) -> Result<(), sqlx::Error> { + println!("{}", prod.recipe.id); + match sqlx::query( + "INSERT INTO `product` (recipe_id, volume, price_per_unit) VALUES(?, ?, ?);", + ) + .bind(prod.recipe.id) + .bind(prod.volume) + .bind(prod.price_per_unit) + .execute(&self.pool) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + 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.price_per_unit) + .bind(prod.id) + .execute(&self.pool) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + pub async fn remove_product(&self, prod_id: i32) -> Result<(), sqlx::Error> { + match sqlx::query("DELETE FROM `product` WHERE id=?") + .bind(prod_id) + .execute(&self.pool) + .await + { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + pub async fn check_product(&self, prod_id: i32) -> Result<(), sqlx::Error> { + match sqlx::query("SELECT * FROM `product` WHERE id=?") + .bind(prod_id) + .fetch_optional(&self.pool) + .await? + { + Some(_) => Ok(()), + None => Err(sqlx::Error::BeginFailed), + } + } } pub async fn estabilish_connection() -> Result<(), sqlx::Error> { diff --git a/code/src/models.rs b/code/src/models.rs index 33c62ef..830c3c2 100644 --- a/code/src/models.rs +++ b/code/src/models.rs @@ -155,7 +155,7 @@ pub struct OrderElementRow { pub total_price: BigDecimal, pub is_incoming: bool, } - +#[derive(Default, Clone)] pub struct Product { pub id: i32, pub recipe: Recipe, @@ -242,6 +242,14 @@ pub struct ElementModalState { pub element: RecipeElement, pub status: ModalStatus, } +#[derive(Default)] +pub struct ProductModalState { + pub is_open: bool, + pub can_finish: bool, + pub product: Product, + pub recipe: Recipe, + pub status: ModalStatus, +} #[derive(Default, PartialEq)] pub enum ModalStatus { diff --git a/code/src/order_tab.rs b/code/src/order_tab.rs index b08b3fd..0eeefef 100644 --- a/code/src/order_tab.rs +++ b/code/src/order_tab.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use egui::mutex::RwLock; +use egui::{Sense, mutex::RwLock}; use egui_extras::Column; use crate::{ @@ -18,9 +18,17 @@ pub struct OrderTabViewer { outcoming_orders: Arc>>, incoming_order_page: i32, outcoming_order_page: i32, + + is_admin: bool, } impl OrderTabViewer { + pub fn new(is_admin: bool) -> Self { + Self { + is_admin, + ..Default::default() + } + } pub fn show_orders(&mut self, ui: &mut egui::Ui, incoming_orders: bool) { let orders = if incoming_orders { &self.incoming_orders @@ -42,6 +50,7 @@ impl OrderTabViewer { .columns(Column::auto(), 5) .striped(true) .vscroll(true) + .sense(Sense::click()) .header(25.0, |mut heading| { heading.col(|ui| { ui.heading(if incoming_orders { @@ -59,6 +68,29 @@ impl OrderTabViewer { heading.col(|ui| { ui.heading("Дата создания"); }); + }) + .body(|mut body| { + for or in orders.read().iter() { + body.row(25.0, |mut row| { + let len = orders.read().iter().clone().len(); + row.col(|ui| { + ui.label(&or.client.name); + }); + row.col(|ui| { + ui.label(match len { + 0 => "Пусто".to_owned(), + _ => format!("{} пт.", len), + }); + }); + row.col(|ui| { + ui.label(len.to_string()); + }); + row.col(|ui| { + ui.label(or.order_date.date_naive().to_string()); + }); + row.col(|ui| if ui.button("Удалить").clicked() {}); + }); + } }); } } @@ -107,6 +139,7 @@ impl Default for OrderTabViewer { outcoming_order_page: 0, db_oper, rt, + is_admin: false, } } } diff --git a/code/src/recipe_tab.rs b/code/src/recipe_tab.rs index 053f1be..24df4e5 100644 --- a/code/src/recipe_tab.rs +++ b/code/src/recipe_tab.rs @@ -1,13 +1,14 @@ -use bigdecimal::ToPrimitive; +use bigdecimal::{BigDecimal, FromPrimitive, ToPrimitive}; use egui_extras::{Column, TableBuilder}; use crate::{ database::DBOperator, + misc, models::{ - ElementModalState, Equipment, Material, ModalStatus, Recipe, RecipeElement, - RecipeModalState, + ElementModalState, Equipment, Material, ModalStatus, Product, ProductModalState, Recipe, + RecipeElement, RecipeModalState, }, - tab::{Tab, TabTypes}, + tab::{self, Tab, TabTypes}, }; use egui::{Sense, mutex::RwLock}; use std::{ @@ -25,9 +26,13 @@ pub struct RecipeTabViewer { recipes: Arc>>, materials: Arc>>, equipment: Arc>>, + products: Arc>>, process_recipe_status: RecipeModalState, process_element_status: ElementModalState, + process_product_status: ProductModalState, + old_price: BigDecimal, + product_page: i32, } impl RecipeTabViewer { @@ -43,6 +48,13 @@ impl RecipeTabViewer { self.db_oper.get_recipes().await.unwrap_or(Vec::new()) }))); } + pub fn update_products(&mut self) { + self.products = Arc::new(RwLock::new(self.rt.block_on(async { + self.db_oper + .get_products(self.product_page, misc::ELEMENTS_PER_PAGE) + .await + }))); + } fn new_element_modal(&mut self, ui: &mut egui::Ui) { egui::Modal::new(egui::Id::new("new_element_modal")).show(ui.ctx(), |ui| { if ui.button("Закрыть").clicked() { @@ -220,7 +232,7 @@ impl RecipeTabViewer { }); TableBuilder::new(ui) .striped(true) - .columns(Column::auto(), 3) + .columns(Column::auto(), 4) .vscroll(false) .sense(Sense::click()) .header(30.0, |mut head| { @@ -255,8 +267,16 @@ impl RecipeTabViewer { crate::models::ModalStatus::Remove; } }); + row.col(|ui| { + if ui.button("Произвести").clicked() { + self.process_product_status.is_open = true; + self.process_product_status.status = ModalStatus::Add; + self.process_product_status.product = Product::default(); + self.process_product_status.product.recipe = rec.clone(); + } + }); } - if row.response().clicked() { + if row.response().double_clicked() { self.process_recipe_status.is_open = true; self.process_recipe_status.recipe = rec.clone(); self.process_recipe_status.status = crate::models::ModalStatus::Edit; @@ -268,6 +288,70 @@ impl RecipeTabViewer { self.process_recipe_modal(ui); } } + fn show_product_list(&mut self, ui: &mut egui::Ui) { + ui.horizontal(|ui| { + if ui.button("<-").clicked() { + if self.product_page > 0 { + self.product_page -= 1; + } + } + ui.monospace(self.product_page.to_string()); + if ui.button("->").clicked() { + if self.products.read().len() == 5 { + self.product_page += 1; + } + } + }); + egui_extras::TableBuilder::new(ui) + .striped(false) + .vscroll(false) + .sense(Sense::click()) + .columns(Column::auto(), 4) + .column(Column::exact(100.0).at_least(100.0)) + .header(25.0, |mut head| { + head.col(|ui| { + ui.heading("Рецепт"); + }); + head.col(|ui| { + ui.heading("Количество"); + }); + head.col(|ui| { + ui.heading("Цена за ед."); + }); + head.col(|ui| { + ui.heading(" "); + }); + }) + .body(|mut body| { + for pr in self.products.read().iter() { + body.row(25.0, |mut row| { + row.col(|ui| { + ui.label(&pr.recipe.name); + }); + row.col(|ui| { + ui.label(pr.volume.to_string()); + }); + row.col(|ui| { + ui.label(pr.price_per_unit.to_string()); + }); + if self.is_admin { + row.col(|ui| { + if ui.add(egui::Button::new("Удалить")).clicked() { + self.process_product_status.is_open = true; + self.process_product_status.status = ModalStatus::Remove; + self.process_product_status.product = pr.clone(); + } + }); + if row.response().double_clicked() { + self.process_product_status.is_open = true; + self.process_product_status.status = ModalStatus::Edit; + self.process_product_status.product = pr.clone(); + } + } + }); + } + }); + } } impl egui_dock::TabViewer for RecipeTabViewer { type Tab = Tab; @@ -278,13 +362,109 @@ impl egui_dock::TabViewer for RecipeTabViewer { fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) { match &tab.tab_type { - TabTypes::RecipeList => { + TabTypes::Recipe => { self.show_recipe_list(ui); } + TabTypes::Product => { + self.show_product_list(ui); + } _ => { ui.label("Помогите!!"); } } + if self.process_product_status.is_open { + egui::Modal::new(egui::Id::new("process_product_modal")).show(ui.ctx(), |ui| { + if ui.button("Закрыть").clicked() { + self.process_product_status.is_open = false; + } + egui::Grid::new("process_product_grid").show(ui, |ui| { + let size = ui.available_size(); + ui.heading(&self.process_product_status.product.recipe.name); + ui.end_row(); + if self.process_product_status.status != ModalStatus::Remove { + ui.label("Объём/кол-во:"); + ui.add_sized( + size, + egui::Slider::new( + &mut self.process_product_status.product.volume, + 0.0..=100.0, + ) + .clamping(egui::SliderClamping::Never), + ); + ui.end_row(); + + ui.label("Цена за штуку:"); + let mut price = self + .process_product_status + .product + .price_per_unit + .to_f64() + .unwrap(); + ui.add_sized(size, egui::DragValue::new(&mut price)); + if BigDecimal::from_f64(price).unwrap() + != self.process_product_status.product.price_per_unit + { + self.process_product_status.product.price_per_unit = + BigDecimal::from_f64(price).unwrap(); + } + ui.end_row(); + } + }); + self.process_product_status.can_finish = match self.process_product_status.status { + ModalStatus::Remove => true, + _ => { + self.process_product_status.product.price_per_unit > 0 + && self.process_product_status.product.volume > 0.0 + } + }; + let btext = match self.process_product_status.status { + ModalStatus::Add => "Добавить", + ModalStatus::Edit => "Сохранить", + ModalStatus::Remove => "Удалить", + }; + if ui + .add_enabled( + self.process_product_status.can_finish, + egui::Button::new(btext), + ) + .clicked() + { + match self.process_product_status.status { + ModalStatus::Add => { + self.rt.block_on(async { + println!("yaya"); + self.db_oper + .add_product(self.process_product_status.product.clone()) + .await + .unwrap(); + }); + } + ModalStatus::Edit => { + self.rt.block_on(async { + self.db_oper + .update_product(self.process_product_status.product.clone()) + .await + .ok(); + }); + } + ModalStatus::Remove => self.rt.block_on(async { + self.db_oper + .remove_product(self.process_product_status.product.id) + .await + .ok(); + }), + } + self.process_product_status.is_open = false; + self.update_products(); + } + }); + } + } + fn allowed_in_windows(&self, _tab: &mut Self::Tab) -> bool { + tab::TABS_CAN_BE_WINDOWS + } + fn is_closeable(&self, _tab: &Self::Tab) -> bool { + false } } @@ -301,9 +481,15 @@ impl Default for RecipeTabViewer { materials: Arc::new(RwLock::new( rt.block_on(async { db_oper.get_materials().await.unwrap_or(Vec::new()) }), )), - equipment: Arc::new(RwLock::new( - rt.block_on(async { db_oper.get_equipment(0,5, false).await.unwrap_or(Vec::new()) }), - )), + equipment: Arc::new(RwLock::new(rt.block_on(async { + db_oper + .get_equipment(0, 5, false) + .await + .unwrap_or(Vec::new()) + }))), + products: Arc::new(RwLock::new(rt.block_on(async { + db_oper.get_products(0, misc::ELEMENTS_PER_PAGE).await + }))), process_recipe_status: RecipeModalState { is_open: false, @@ -318,6 +504,10 @@ impl Default for RecipeTabViewer { element: RecipeElement::default(), status: crate::models::ModalStatus::Add, }, + process_product_status: ProductModalState::default(), + old_price: BigDecimal::default(), + + product_page: 0, rt, db_oper,