diff --git a/code/database.sql b/code/database.sql index 8c656a5..769aaf1 100644 --- a/code/database.sql +++ b/code/database.sql @@ -60,7 +60,7 @@ CREATE TABLE `equipment` ( PRIMARY KEY (`id`), KEY `fk_worker1` (`worker_id`), CONSTRAINT `fk_worker1` FOREIGN KEY (`worker_id`) REFERENCES `worker` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -71,8 +71,12 @@ SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0; LOCK TABLES `equipment` WRITE; /*!40000 ALTER TABLE `equipment` DISABLE KEYS */; INSERT INTO `equipment` VALUES -(1,'М-1','1970-01-01 00:00:00',1,'Метла деревяная',0), -(2,'М-2','1970-01-01 00:00:00',2,'Метла деревяная',0); +(3,'ПВ-11','2026-06-19 14:00:00',6,'Пивоварня \"Моисей\"',0), +(4,'ПВ-12','2001-04-30 13:00:00',5,'Пивоварня \"Дружба\"',0), +(5,'СД-1','2005-05-24 13:00:00',7,'Солододробилка',0), +(6,'СД-2','2006-06-25 13:00:00',2,'Солододробилка',0), +(7,'КТЛ-5','1991-02-01 14:00:00',1,'Сусловарный котёл',0), +(8,'КТЛ-10','2026-02-01 14:00:00',5,'Сусловарный котёл',0); /*!40000 ALTER TABLE `equipment` ENABLE KEYS */; UNLOCK TABLES; COMMIT; @@ -123,7 +127,7 @@ CREATE TABLE `material_category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -313,7 +317,7 @@ CREATE TABLE `recipe` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -323,6 +327,8 @@ CREATE TABLE `recipe` ( SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0; LOCK TABLES `recipe` WRITE; /*!40000 ALTER TABLE `recipe` DISABLE KEYS */; +INSERT INTO `recipe` VALUES +(1,'Пиво светлое'); /*!40000 ALTER TABLE `recipe` ENABLE KEYS */; UNLOCK TABLES; COMMIT; @@ -348,7 +354,7 @@ CREATE TABLE `recipe_element` ( CONSTRAINT `fk_equipment` FOREIGN KEY (`equipment_id`) REFERENCES `equipment` (`id`), CONSTRAINT `fk_material` FOREIGN KEY (`material_id`) REFERENCES `material` (`id`), CONSTRAINT `fk_recipe` FOREIGN KEY (`recipe_id`) REFERENCES `recipe` (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -379,7 +385,7 @@ CREATE TABLE `salary` ( PRIMARY KEY (`id`), KEY `fk_worker` (`worker_id`), CONSTRAINT `fk_worker` FOREIGN KEY (`worker_id`) REFERENCES `worker` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -390,7 +396,12 @@ SET @OLD_AUTOCOMMIT=@@AUTOCOMMIT, @@AUTOCOMMIT=0; LOCK TABLES `salary` WRITE; /*!40000 ALTER TABLE `salary` DISABLE KEYS */; INSERT INTO `salary` VALUES -(2,3,30000,'1970-01-20 00:00:00','Премия за пиво'); +(2,3,30000,'1970-01-20 00:00:00','Премия за пиво'), +(3,5,2000,'1980-02-19 14:00:00','За красивые глаза'), +(4,4,2000,'1980-02-19 14:00:00','За красивые глаза'), +(5,10,1000,'2026-06-21 14:00:00','да поможет нам бог'), +(6,2,1000,'2026-06-21 14:00:00','да поможет нам бог'), +(7,8,1000,'2026-06-21 14:00:00','да поможет нам бог'); /*!40000 ALTER TABLE `salary` ENABLE KEYS */; UNLOCK TABLES; COMMIT; @@ -438,7 +449,7 @@ CREATE TABLE `worker` ( PRIMARY KEY (`id`), KEY `fk_position` (`position_id`), CONSTRAINT `fk_position` FOREIGN KEY (`position_id`) REFERENCES `position` (`id`) -) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_uca1400_ai_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -459,8 +470,7 @@ INSERT INTO `worker` VALUES (8,4,'2026-04-24 00:00:00',0,'Иванов Иван'), (9,3,'2024-04-21 14:00:00',0,'Власов Иннокентий'), (10,2,'1970-01-19 14:00:00',0,'Петров Пётр Петрович'), -(11,2,'2003-02-01 14:00:00',0,'Никитин Олег Михайлович'), -(12,2,'2008-02-01 14:00:00',1,'Славик'); +(11,2,'2003-02-01 14:00:00',0,'Никитин Олег Михайлович'); /*!40000 ALTER TABLE `worker` ENABLE KEYS */; UNLOCK TABLES; COMMIT; @@ -475,4 +485,4 @@ SET AUTOCOMMIT=@OLD_AUTOCOMMIT; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */; --- Dump completed on 2026-06-19 20:04:08 +-- Dump completed on 2026-06-22 8:47:40 diff --git a/code/src/app.rs b/code/src/app.rs index 03513f5..bf6175b 100644 --- a/code/src/app.rs +++ b/code/src/app.rs @@ -330,10 +330,13 @@ impl MainTabViewer { .insert("name".into(), "".into()); self.equipment_modal_state .data - .insert("inv_numver".into(), "".into()); + .insert("inv_number".into(), "".into()); self.equipment_modal_state .data .insert("worker_id".into(), "0".into()); + self.equipment_modal_state + .data + .insert("maintenance_date".into(), "".into()); } }); } @@ -347,6 +350,7 @@ impl MainTabViewer { .column(Column::auto()) .column(Column::auto()) .column(Column::auto()) + .column(Column::auto()) .sense(egui::Sense::click()) .header(30.0, |mut header| { header.col(|ui| { @@ -361,6 +365,9 @@ impl MainTabViewer { header.col(|ui| { ui.heading("Дата последнего\nтехобслуживания"); }); + header.col(|ui| { + ui.heading("Списано"); + }); }) .body(|mut body| { body.ui_mut().style_mut().interaction.selectable_labels = false; @@ -378,6 +385,9 @@ impl MainTabViewer { row.col(|ui| { ui.label(eq.maintenance_date.date_naive().to_string()); }); + row.col(|ui| { + ui.label(if eq.is_written_off { "Да" } else { "Нет" }); + }); row.col(|ui| { if self.is_admin { if ui.button("Списать").clicked() { @@ -431,15 +441,21 @@ impl MainTabViewer { egui::Grid::new("process_equipment_grid").show(ui, |ui| { ui.label("Название"); let mut name = self.equipment_modal_state.data.get_mut("name").unwrap(); - ui.add_sized(Vec2::new(200.0, 20.0), egui::TextEdit::singleline(name)); + ui.add_sized( + Vec2::new(200.0, 20.0), + egui::TextEdit::singleline(name).hint_text("Название оборудования"), + ); ui.end_row(); ui.label("Инв. номер"); - ui.text_edit_singleline( - self.equipment_modal_state - .data - .get_mut("inv_number") - .unwrap(), + ui.add( + egui::TextEdit::singleline( + self.equipment_modal_state + .data + .get_mut("inv_number") + .unwrap(), + ) + .hint_text("НН-00"), ); ui.end_row(); @@ -478,11 +494,14 @@ impl MainTabViewer { ui.end_row(); ui.label("Дата последнего\nтехобслуживания"); - ui.text_edit_singleline( - self.equipment_modal_state - .data - .get_mut("maintenance_date") - .unwrap(), + ui.add( + egui::TextEdit::singleline( + self.equipment_modal_state + .data + .get_mut("maintenance_date") + .unwrap(), + ) + .hint_text("1970-01-01"), ); }); } else { diff --git a/code/src/lib.rs b/code/src/lib.rs index 2794306..fe13481 100644 --- a/code/src/lib.rs +++ b/code/src/lib.rs @@ -1,6 +1,353 @@ #[cfg(test)] -mod tests{ - use super::*; +mod tests { + use super::database::DBOperator; + use crate::models::*; + use bigdecimal::BigDecimal; + use chrono::Local; + use std::collections::HashSet; - -} \ No newline at end of file + // ============================================================ + // UNIT TESTS (no database required) + // ============================================================ + + /// Test 1: Position default values and construction + #[test] + fn test_position_default_and_construction() { + let default_pos = Position::default(); + assert_eq!(default_pos.id, 1); + assert_eq!(default_pos.name, ""); + assert_eq!(default_pos.wage, BigDecimal::from(0)); + + let custom_pos = Position { + id: 42, + name: "Пивовар".to_string(), + wage: BigDecimal::from(65000), + }; + assert_eq!(custom_pos.id, 42); + assert_eq!(custom_pos.name, "Пивовар"); + assert_eq!(custom_pos.wage, BigDecimal::from(65000)); + } + + /// Test 2: Worker default values and construction with nested Position + #[test] + fn test_worker_default_and_construction() { + let default_worker = Worker::default(); + assert_eq!(default_worker.id, 0); + assert_eq!(default_worker.full_name, ""); + assert!(!default_worker.is_fired); + + let pos = Position { + id: 3, + name: "Пивовар".to_string(), + wage: BigDecimal::from(65000), + }; + let worker = Worker { + id: 1, + full_name: "Иванов Иван".to_string(), + hire_date: Local::now(), + position: pos.clone(), + is_fired: false, + }; + assert_eq!(worker.full_name, "Иванов Иван"); + assert_eq!(worker.position.name, "Пивовар"); + assert_eq!(worker.position.wage, BigDecimal::from(65000)); + assert!(!worker.is_fired); + } + + /// Test 3: MaterialCategory default and Material construction with category + #[test] + fn test_material_and_category_construction() { + let default_cat = MaterialCategory::default(); + assert_eq!(default_cat.id, 0); + assert_eq!(default_cat.name, ""); + + let cat = MaterialCategory { + id: 2, + name: "Солод".to_string(), + }; + let mat = Material { + id: 1, + name: "Курский тёмный".to_string(), + quantity: 20, + category: cat.clone(), + }; + assert_eq!(mat.name, "Курский тёмный"); + assert_eq!(mat.quantity, 20); + assert_eq!(mat.category.name, "Солод"); + } + + /// Test 4: ModalStatus default and equality (using matches! to avoid Debug) + #[test] + fn test_modal_status_default_and_equality() { + let default_status = ModalStatus::default(); + assert!(matches!(default_status, ModalStatus::Add)); + assert!(!matches!(default_status, ModalStatus::Edit)); + assert!(!matches!(default_status, ModalStatus::Remove)); + + let edit_status = ModalStatus::Edit; + assert!(matches!(edit_status, ModalStatus::Edit)); + assert!(!matches!(edit_status, ModalStatus::Add)); + } + + /// Test 5: Equipment default and construction with nested Worker + #[test] + fn test_equipment_default_and_construction() { + let default_eq = Equipment::default(); + assert_eq!(default_eq.id, 0); + assert_eq!(default_eq.name, ""); + assert_eq!(default_eq.inv_number, ""); + assert!(!default_eq.is_written_off); + + let pos = Position { + id: 1, + name: "Уборщик".to_string(), + wage: BigDecimal::from(20000), + }; + let worker = Worker { + id: 1, + full_name: "Иван Петров".to_string(), + hire_date: Local::now(), + position: pos, + is_fired: false, + }; + let equipment = Equipment { + id: 1, + name: "Метла деревяная".to_string(), + inv_number: "М-1".to_string(), + maintenance_date: Local::now(), + worker: worker.clone(), + is_written_off: false, + }; + assert_eq!(equipment.name, "Метла деревяная"); + assert_eq!(equipment.inv_number, "М-1"); + assert_eq!(equipment.worker.full_name, "Иван Петров"); + assert!(!equipment.is_written_off); + } + + // ============================================================ + // INTEGRATION TESTS (require database connection) + // ============================================================ + + /// Test 6: Integration — get_position returns positions from DB + #[tokio::test] + async fn test_integration_get_position() { + let db = DBOperator::new().await; + let positions = db.get_position().await.expect("get_position failed"); + // The seed data has 4 positions: Уборщик, Кладовщик, Пивовар, Менеджер + assert!( + positions.len() >= 4, + "Expected at least 4 positions, got {}", + positions.len() + ); + let names: Vec<&str> = positions.iter().map(|p| p.name.as_str()).collect(); + assert!(names.contains(&"Уборщик"), "Should contain Уборщик"); + assert!(names.contains(&"Пивовар"), "Should contain Пивовар"); + } + + /// Test 7: Integration — add_worker and get_workers round-trip + #[tokio::test] + async fn test_integration_add_and_get_worker() { + let db = DBOperator::new().await; + + // Add a new worker + let new_worker = WorkerRow { + id: 0, // auto-increment + full_name: "Тестовый Работник Для Теста".to_string(), + hire_date: Local::now(), + position_id: 1, // Уборщик + is_fired: false, + }; + let add_result = db.add_worker(new_worker).await.expect("add_worker failed"); + assert!(add_result, "add_worker should return true"); + + // Verify the worker appears in get_workers + let workers = db.get_workers().await.expect("get_workers failed"); + let found = workers + .iter() + .any(|w| w.full_name == "Тестовый Работник Для Теста"); + assert!(found, "Newly added worker should appear in get_workers"); + + // Cleanup: find and mark the test worker as fired + let test_worker = workers + .iter() + .find(|w| w.full_name == "Тестовый Работник Для Теста") + .expect("Test worker not found for cleanup"); + let fired_row = WorkerRow { + id: test_worker.id, + full_name: test_worker.full_name.clone(), + hire_date: test_worker.hire_date, + position_id: test_worker.position.id, + is_fired: true, + }; + db.update_worker(fired_row) + .await + .expect("update_worker (fire) failed"); + } + + /// Test 8: Integration — material_category full CRUD round-trip + #[tokio::test] + async fn test_integration_material_category_crud() { + let db = DBOperator::new().await; + + // Add a new category + let new_cat = MaterialCategory { + id: 0, + name: "Тестовая Категория Интеграция".to_string(), + }; + let add_result = db + .add_material_category(new_cat) + .await + .expect("add_material_category failed"); + assert!(add_result, "add_material_category should return true"); + + // Verify it appears in get_mcat + let cats = db.get_mcat().await.expect("get_mcat failed"); + let found_cat = cats + .iter() + .find(|c| c.name == "Тестовая Категория Интеграция") + .expect("New category not found in get_mcat"); + + // Update the category + let mut updated_cat = found_cat.clone(); + updated_cat.name = "Обновлённая Тестовая Категория".to_string(); + let update_result = db + .update_material_category(updated_cat.clone()) + .await + .expect("update_material_category failed"); + assert!(update_result, "update_material_category should return true"); + + // Verify update + let cats_after = db.get_mcat().await.expect("get_mcat after update failed"); + let updated = cats_after + .iter() + .find(|c| c.id == found_cat.id) + .expect("Category not found after update"); + assert_eq!(updated.name, "Обновлённая Тестовая Категория"); + + // Delete the category + let delete_result = db + .delete_material_category(updated_cat) + .await + .expect("delete_material_category failed"); + assert!(delete_result, "delete_material_category should return true"); + + // Verify deletion + let cats_final = db.get_mcat().await.expect("get_mcat after delete failed"); + let not_found = cats_final.iter().find(|c| c.id == found_cat.id).is_none(); + assert!(not_found, "Deleted category should not appear in get_mcat"); + } + + /// Test 9: Integration — salary full CRUD round-trip + #[tokio::test] + async fn test_integration_salary_crud() { + let db = DBOperator::new().await; + + // Get existing workers to use a valid worker_id + let workers = db.get_workers().await.expect("get_workers failed"); + let worker = workers.first().expect("No workers in DB for salary test"); + + let salary_date = Local::now(); + let salary_size = BigDecimal::from(99999); + let comment = Some("Интеграционный тест зарплаты".to_string()); + + // Add salary + let add_result = db + .add_salary(worker.id, salary_date, salary_size.clone(), comment.clone()) + .await + .expect("add_salary failed"); + assert!(add_result, "add_salary should return true"); + + // Verify it appears in get_salaries + let salaries = db.get_salaries().await.expect("get_salaries failed"); + let found_sal = salaries + .iter() + .find(|s| { + s.worker.id == worker.id + && s.salary_size == salary_size + && s.comment == "Интеграционный тест зарплаты" + }) + .expect("Newly added salary not found in get_salaries"); + + // Remove the salary + let remove_result = db + .remove_salary(found_sal.id) + .await + .expect("remove_salary failed"); + assert!(remove_result, "remove_salary should return true"); + + // Verify removal + let salaries_after = db + .get_salaries() + .await + .expect("get_salaries after remove failed"); + let not_found = salaries_after + .iter() + .find(|s| s.id == found_sal.id) + .is_none(); + assert!( + not_found, + "Removed salary should not appear in get_salaries" + ); + } + + /// Test 10: Integration — recipe full CRUD round-trip + #[tokio::test] + async fn test_integration_recipe_crud() { + let db = DBOperator::new().await; + + // Add a new recipe + let new_recipe = Recipe { + id: 0, + name: "Тестовый Рецепт Интеграция".to_string(), + elements: HashSet::new(), + }; + let recipe_id = db.add_recipe(new_recipe).await.expect("add_recipe failed"); + assert!(recipe_id > 0, "add_recipe should return a valid ID"); + + // Verify it appears in get_recipes + let recipes = db.get_recipes().await.expect("get_recipes failed"); + let found_recipe = recipes + .iter() + .find(|r| r.name == "Тестовый Рецепт Интеграция") + .expect("Newly added recipe not found in get_recipes"); + assert_eq!(found_recipe.id as u64, recipe_id); + + // Update the recipe + let mut updated_recipe = found_recipe.clone(); + updated_recipe.name = "Обновлённый Тестовый Рецепт".to_string(); + let update_result = db + .update_recipe(updated_recipe.clone()) + .await + .expect("update_recipe failed"); + assert!(update_result, "update_recipe should return true"); + + // Verify update + let recipes_after = db + .get_recipes() + .await + .expect("get_recipes after update failed"); + let updated = recipes_after + .iter() + .find(|r| r.id == found_recipe.id) + .expect("Recipe not found after update"); + assert_eq!(updated.name, "Обновлённый Тестовый Рецепт"); + + // Remove the recipe + let remove_result = db + .remove_recipe(found_recipe.id) + .await + .expect("remove_recipe failed"); + assert!(remove_result, "remove_recipe should return true"); + + // Verify removal + let recipes_final = db + .get_recipes() + .await + .expect("get_recipes after remove failed"); + let not_found = recipes_final + .iter() + .find(|r| r.id == found_recipe.id) + .is_none(); + assert!(not_found, "Removed recipe should not appear in get_recipes"); + } +} diff --git a/code/src/main.rs b/code/src/main.rs index 8fc69ef..99e59d6 100644 --- a/code/src/main.rs +++ b/code/src/main.rs @@ -11,7 +11,8 @@ mod reports; mod tab; mod worker_database; mod worker_tab; -// mod schema; +mod lib; + fn main() { dotenvy::dotenv().ok(); database::estabilish_connection();