diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 56084e3c..5180b34e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -933,6 +933,7 @@ name = "coldchain" version = "0.1.0" dependencies = [ "chrono", + "log", "repository", "serde", "serde_json", @@ -3283,6 +3284,7 @@ dependencies = [ "actix-http", "actix-multipart", "actix-web", + "coldchain", "config", "datasource", "env_logger", diff --git a/backend/coldchain/Cargo.toml b/backend/coldchain/Cargo.toml index 0f5c2a8f..b9d27847 100644 --- a/backend/coldchain/Cargo.toml +++ b/backend/coldchain/Cargo.toml @@ -11,6 +11,7 @@ chrono = { version = "0.4", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["macros"] } +log = "0.4" [features] telegram-tests = ["service/telegram-tests"] diff --git a/backend/coldchain/src/alerts.rs b/backend/coldchain/src/alerts.rs new file mode 100644 index 00000000..8a11ab03 --- /dev/null +++ b/backend/coldchain/src/alerts.rs @@ -0,0 +1,154 @@ +use chrono::NaiveDateTime; +use serde::Serialize; +use service::{ + notification::{ + self, + enqueue::{ + create_notification_events, NotificationContext, NotificationTarget, TemplateDefinition, + }, + }, + service_provider::ServiceContext, +}; + +/* + +Temperature Alerts will look something like this... +----------------------- +High temperature alert! + +Facility: Store A +Location: Fridge 1 +Sensor: E5:4G:D4:6D:A4 + +Date: 17 Jul 2023 +Time: 17:04 + +Temperature: 10° C +----------------------- +*/ + +#[derive(Clone, Debug, Serialize)] +pub struct TemperatureAlert { + pub store_id: String, + pub store_name: String, + pub location_id: String, + pub location_name: String, + pub sensor_id: String, + pub sensor_name: String, + pub datetime: NaiveDateTime, + pub temperature: f64, +} + +// Later this function probably won't exist, but serves as a reminder/POC... +pub async fn send_high_temperature_alert_telegram( + ctx: &ServiceContext, + alert: TemperatureAlert, + recipients: Vec, +) -> Result<(), notification::NotificationServiceError> { + let notification = NotificationContext { + title_template: Some(TemplateDefinition::TemplateName( + "coldchain/telegram/temperature_title.html".to_string(), + )), + body_template: TemplateDefinition::TemplateName( + "coldchain/telegram/temperature.html".to_string(), + ), + recipients, + template_data: serde_json::to_value(alert).map_err(|e| { + notification::NotificationServiceError::InternalError(format!( + "Error serializing template data: {}", + e + )) + })?, + }; + + // TODO : Get the config ID for this notification + + create_notification_events(ctx, None, notification) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use repository::{ + mock::MockDataInserts, test_db::setup_all, NotificationEventRowRepository, NotificationType, + }; + use service::test_utils::email_test::send_test_emails; + use service::test_utils::get_test_settings; + use service::test_utils::telegram_test::get_default_telegram_chat_id; + use service::test_utils::telegram_test::send_test_notifications; + + use service::notification::enqueue::NotificationTarget; + use std::str::FromStr; + + use service::service_provider::ServiceContext; + use service::service_provider::ServiceProvider; + + use super::*; + + #[tokio::test] + async fn test_send_high_temperature_alert() { + let (_, _, connection_manager, _) = + setup_all("test_enqueue_telegram_and_email", MockDataInserts::none()).await; + + let connection = connection_manager.connection().unwrap(); + let service_provider = Arc::new(ServiceProvider::new( + connection_manager, + get_test_settings(""), + )); + let context = ServiceContext::as_server_admin(service_provider).unwrap(); + + let example_alert = TemperatureAlert { + store_id: "6a3399dd-10a9-40b7-853e-3ac0634ce6b1".to_string(), + store_name: "Store A".to_string(), + location_id: "6a3399dd-10a9-40b7-853e-3ac0634ce6b2".to_string(), + location_name: "Fridge 1".to_string(), + sensor_id: "6a3399dd-10a9-40b7-853e-3ac0634ce6b3".to_string(), + sensor_name: "E5:4G:D4:6D:A4".to_string(), + datetime: NaiveDateTime::from_str("2023-07-17T17:04:00").unwrap(), + temperature: 10.12345, + }; + + let recipient1 = NotificationTarget { + name: "test".to_string(), + to_address: get_default_telegram_chat_id(), + notification_type: NotificationType::Telegram, + }; + let recipient2 = NotificationTarget { + name: "test-email".to_string(), + to_address: "test@example.com".to_string(), + notification_type: NotificationType::Email, + }; + + let result = send_high_temperature_alert_telegram( + &context, + example_alert.clone(), + vec![recipient1, recipient2], + ) + .await; + + assert!(result.is_ok()); + + // Check we have a notification event + let notification_event_row_repository = NotificationEventRowRepository::new(&connection); + let notification_event_rows = notification_event_row_repository.un_sent().unwrap(); + + assert_eq!(notification_event_rows.len(), 2); + assert_eq!( + notification_event_rows[0].to_address, + get_default_telegram_chat_id() + ); + assert!(notification_event_rows[0] + .message + .contains(&example_alert.store_name)); + + // Check email recipient + assert_eq!(notification_event_rows[1].to_address, "test@example.com"); + assert!(notification_event_rows[1] + .message + .contains(&example_alert.store_name)); + + send_test_notifications(&context).await; + send_test_emails(&context); + } +} diff --git a/backend/coldchain/src/lib.rs b/backend/coldchain/src/lib.rs index ae2b9ee4..cf5d45e0 100644 --- a/backend/coldchain/src/lib.rs +++ b/backend/coldchain/src/lib.rs @@ -1,154 +1,61 @@ -use chrono::NaiveDateTime; -use serde::Serialize; use service::{ - notification::{ - self, - enqueue::{ - create_notification_events, NotificationContext, NotificationTarget, TemplateDefinition, - }, - }, + plugin::{PluginError, PluginTrait}, service_provider::ServiceContext, }; -/* +pub mod alerts; -Temperature Alerts will look something like this... ------------------------ -High temperature alert! +pub struct ColdChainPlugin {} -Facility: Store A -Location: Fridge 1 -Sensor: E5:4G:D4:6D:A4 - -Date: 17 Jul 2023 -Time: 17:04 - -Temperature: 10° C ------------------------ -*/ - -#[derive(Clone, Debug, Serialize)] -pub struct TemperatureAlert { - pub store_id: String, - pub store_name: String, - pub location_id: String, - pub location_name: String, - pub sensor_id: String, - pub sensor_name: String, - pub datetime: NaiveDateTime, - pub temperature: f64, -} - -// Later this function probably won't exist, but serves as a reminder/POC... -pub async fn send_high_temperature_alert_telegram( - ctx: &ServiceContext, - alert: TemperatureAlert, - recipients: Vec, -) -> Result<(), notification::NotificationServiceError> { - let notification = NotificationContext { - title_template: Some(TemplateDefinition::TemplateName( - "coldchain/telegram/temperature_title.html".to_string(), - )), - body_template: TemplateDefinition::TemplateName( - "coldchain/telegram/temperature.html".to_string(), - ), - recipients, - template_data: serde_json::to_value(alert).map_err(|e| { - notification::NotificationServiceError::InternalError(format!( - "Error serializing template data: {}", - e - )) - })?, - }; +impl PluginTrait for ColdChainPlugin { + fn new() -> Self + where + Self: Sized, + { + ColdChainPlugin {} + } - // TODO : Get the config ID for this notification + fn name(&self) -> String { + "ColdChain".to_string() + } - create_notification_events(ctx, None, notification) + fn tick(&self, _ctx: &ServiceContext) -> Result<(), PluginError> { + log::debug!("Running ColdChainPlugin"); + log::error!("COLD CHAIN NOT YET IMPLEMENTED"); + Ok(()) + } } #[cfg(test)] mod tests { use std::sync::Arc; - use repository::{ - mock::MockDataInserts, test_db::setup_all, NotificationEventRowRepository, NotificationType, - }; - use service::test_utils::email_test::send_test_emails; + use repository::{mock::MockDataInserts, test_db::setup_all}; use service::test_utils::get_test_settings; - use service::test_utils::telegram_test::get_default_telegram_chat_id; - use service::test_utils::telegram_test::send_test_notifications; - use crate::notification::enqueue::NotificationTarget; - use std::str::FromStr; - - use service::service_provider::ServiceContext; use service::service_provider::ServiceProvider; use super::*; #[tokio::test] - async fn test_send_high_temperature_alert() { + async fn cold_chain_plugin_has_a_name() { + let plugin = ColdChainPlugin::new(); + assert_eq!(plugin.name(), "ColdChain"); + } + + #[tokio::test] + async fn cold_chain_plugin_can_tick() { let (_, _, connection_manager, _) = - setup_all("test_enqueue_telegram_and_email", MockDataInserts::none()).await; + setup_all("cold_chain_plugin_can_start", MockDataInserts::none()).await; - let connection = connection_manager.connection().unwrap(); let service_provider = Arc::new(ServiceProvider::new( connection_manager, get_test_settings(""), )); - let context = ServiceContext::as_server_admin(service_provider).unwrap(); - - let example_alert = TemperatureAlert { - store_id: "6a3399dd-10a9-40b7-853e-3ac0634ce6b1".to_string(), - store_name: "Store A".to_string(), - location_id: "6a3399dd-10a9-40b7-853e-3ac0634ce6b2".to_string(), - location_name: "Fridge 1".to_string(), - sensor_id: "6a3399dd-10a9-40b7-853e-3ac0634ce6b3".to_string(), - sensor_name: "E5:4G:D4:6D:A4".to_string(), - datetime: NaiveDateTime::from_str("2023-07-17T17:04:00").unwrap(), - temperature: 10.12345, - }; - - let recipient1 = NotificationTarget { - name: "test".to_string(), - to_address: get_default_telegram_chat_id(), - notification_type: NotificationType::Telegram, - }; - let recipient2 = NotificationTarget { - name: "test-email".to_string(), - to_address: "test@example.com".to_string(), - notification_type: NotificationType::Email, - }; - - let result = send_high_temperature_alert_telegram( - &context, - example_alert.clone(), - vec![recipient1, recipient2], - ) - .await; + let ctx = ServiceContext::as_server_admin(service_provider).unwrap(); + let plugin = ColdChainPlugin::new(); + let result = plugin.tick(&ctx); assert!(result.is_ok()); - - // Check we have a notification event - let notification_event_row_repository = NotificationEventRowRepository::new(&connection); - let notification_event_rows = notification_event_row_repository.un_sent().unwrap(); - - assert_eq!(notification_event_rows.len(), 2); - assert_eq!( - notification_event_rows[0].to_address, - get_default_telegram_chat_id() - ); - assert!(notification_event_rows[0] - .message - .contains(&example_alert.store_name)); - - // Check email recipient - assert_eq!(notification_event_rows[1].to_address, "test@example.com"); - assert!(notification_event_rows[1] - .message - .contains(&example_alert.store_name)); - - send_test_notifications(&context).await; - send_test_emails(&context); } } diff --git a/backend/server/Cargo.toml b/backend/server/Cargo.toml index d73669cd..dbb1092c 100644 --- a/backend/server/Cargo.toml +++ b/backend/server/Cargo.toml @@ -39,6 +39,7 @@ datasource = {path = "../datasource"} repository = { path = "../repository" } service = { path = "../service" } scheduled = { path = "../scheduled" } +coldchain = { path = "../coldchain" } telegram = { path = "../telegram" } util = { path = "../util" } diff --git a/backend/server/src/lib.rs b/backend/server/src/lib.rs index a7b1c18b..a9012b58 100644 --- a/backend/server/src/lib.rs +++ b/backend/server/src/lib.rs @@ -13,6 +13,7 @@ use middleware::{add_authentication_context, limit_content_length}; use repository::{get_storage_connection_manager, run_db_migrations, StorageConnectionManager}; use actix_web::{web::Data, App, HttpServer}; +use coldchain::ColdChainPlugin; use scheduled::ScheduledNotificationPlugin; use std::{ ops::DerefMut, @@ -89,8 +90,10 @@ async fn run_server( }; // Setup plugins - let plugins: Vec> = - vec![Box::new(ScheduledNotificationPlugin::new())]; + let plugins: Vec> = vec![ + Box::new(ScheduledNotificationPlugin::new()), + Box::new(ColdChainPlugin::new()), + ]; let scheduled_task_handle = actix_web::rt::spawn(async move { scheduled_task_runner(scheduled_task_context, plugins).await; diff --git a/frontend/packages/common/src/intl/locales/en/common.json b/frontend/packages/common/src/intl/locales/en/common.json index 16f74188..2011673b 100644 --- a/frontend/packages/common/src/intl/locales/en/common.json +++ b/frontend/packages/common/src/intl/locales/en/common.json @@ -50,6 +50,8 @@ "heading.quote-withdraw-title": "Specify reason", "heading.select-items": "Select Items", "heading.select-locations": "Stores and Locations", + "heading.select-sensors": "Select Sensors", + "heading.selected-sensors": "Selected Sensors", "heading.settings-display": "Display settings", "heading.username": "Username", "label.actions": "Actions", @@ -94,6 +96,7 @@ "label.kind": "Kind", "label.line": "Line {{line, number}}", "label.line-total": "Line total", + "label.location": "Location", "label.log": "Log", "label.manage": "Manage", "label.message": "Message", @@ -123,6 +126,7 @@ "label.showing": "Showing", "label.status": "Status", "label.status-reason": "Enter reason", + "label.store": "Store", "label.time": "Time", "label.title": "Title", "label.toggle-password-visibility": "Toggle Password Visibility", diff --git a/frontend/packages/common/src/intl/locales/en/system.json b/frontend/packages/common/src/intl/locales/en/system.json index 6b829aec..ffdae0a4 100644 --- a/frontend/packages/common/src/intl/locales/en/system.json +++ b/frontend/packages/common/src/intl/locales/en/system.json @@ -86,5 +86,6 @@ "helper-text.sql-query": "Your sql query can contain parameters, which are created using double curly braces within the query. For example: SELECT * FROM my_table WHERE id = {{param1}} AND name = {{param2}}", "label.test-sql-query": "Test SQL Query", "tooltip.manage-recipient-list": "Manage Recipient List", - "message.no-parameters": "No parameters found" + "message.no-parameters": "No parameters found", + "message.no-sensors-selected": "No sensors selected" } \ No newline at end of file diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx index 99f996ab..ec102620 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx @@ -15,37 +15,22 @@ import { getReminderUnitsAsOptions, getReminderUnitsFromString, } from '../../types'; +import { useColdChainSensors } from '../../api'; +import { SensorSelector } from './SensorSelector'; type CCNotificationEditFormProps = { onUpdate: (patch: Partial) => void; draft: CCNotification; }; -const dummyLocations = [ - { id: 'store-1-location-A', name: 'Central Medical Store, Cool room - East' }, - { id: 'store-1-location-B', name: 'Central Medical Store,Cool room - West' }, - { - id: 'store-1-location-C', - name: 'Central Medical Store, Ultra cold freezer 1', - }, - { id: 'store-2-location-A', name: 'Central Medical Store, Location A' }, - { - id: 'store-2-location-B', - name: 'Central Medical Store, Ultra cold freezer 2', - }, - { - id: 'store-2-location-C', - name: 'Central Medical Store, Ultra cold freezer 3', - }, - { id: 'store-2-location-D', name: 'District Hospital, Mini Fridge 1' }, - { id: 'store-2-location-E', name: 'Rural Pharmacy, Mini Fridge 2' }, -]; - export const CCNotificationEditForm = ({ onUpdate, draft, }: CCNotificationEditFormProps) => { const t = useTranslation('system'); + + const { data: sensors, isLoading: sensorsLoading } = useColdChainSensors(); + return ( <> {t('heading.preference')} @@ -194,7 +179,9 @@ export const CCNotificationEditForm = ({ onUpdate({ messageAlertResolved: !draft.messageAlertResolved })} + onClick={() => + onUpdate({ messageAlertResolved: !draft.messageAlertResolved }) + } />