From 58b7a74dd926ffd9186df86c48117b8a7abab082 Mon Sep 17 00:00:00 2001 From: James Brunskill Date: Tue, 3 Oct 2023 15:40:57 +1300 Subject: [PATCH 1/6] Make coldchain a plugin --- backend/Cargo.lock | 2 + backend/coldchain/Cargo.toml | 1 + backend/coldchain/src/alerts.rs | 147 ++++++++++++++++++++++++++++++++ backend/coldchain/src/lib.rs | 147 +++++++------------------------- backend/server/Cargo.toml | 1 + backend/server/src/lib.rs | 7 +- 6 files changed, 186 insertions(+), 119 deletions(-) create mode 100644 backend/coldchain/src/alerts.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index cbd47fbe..0daa7352 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", @@ -3225,6 +3226,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..5f51a3f2 --- /dev/null +++ b/backend/coldchain/src/alerts.rs @@ -0,0 +1,147 @@ +use chrono::NaiveDateTime; +use serde::Serialize; +use service::{ + notification::{ + self, + enqueue::{create_notification_events, NotificationContext, NotificationTarget}, + }, + 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_name: Some("coldchain/telegram/temperature_title.html".to_string()), + body_template_name: "coldchain/telegram/temperature.html".to_string(), + recipients, + template_data: serde_json::to_value(alert).map_err(|e| { + notification::NotificationServiceError::GenericError(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 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 630c80f3..cf5d45e0 100644 --- a/backend/coldchain/src/lib.rs +++ b/backend/coldchain/src/lib.rs @@ -1,148 +1,61 @@ -use chrono::NaiveDateTime; -use serde::Serialize; use service::{ - notification::{ - self, - enqueue::{create_notification_events, NotificationContext, NotificationTarget}, - }, + 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_name: Some("coldchain/telegram/temperature_title.html".to_string()), - body_template_name: "coldchain/telegram/temperature.html".to_string(), - recipients, - template_data: serde_json::to_value(alert).map_err(|e| { - notification::NotificationServiceError::GenericError(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 4eed8dbd..4f54802b 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; From 08a84fc664102304da742c6b6e2b41206db0dd99 Mon Sep 17 00:00:00 2001 From: James Brunskill Date: Tue, 3 Oct 2023 16:58:06 +1300 Subject: [PATCH 2/6] Configure sensor_ids for Coldchain --- .../ColdChain/CCNotificationEditForm.tsx | 43 ++++++------------- .../ColdChain/CCNotificationEditPage.tsx | 1 + .../Pages/ColdChain/parseConfig.ts | 2 + .../src/Notifications/api/hooks/index.ts | 1 + .../api/hooks/useColdChainSensors.ts | 30 +++++++++++++ .../Notifications/api/operations.generated.ts | 16 +++++++ .../src/Notifications/api/operations.graphql | 4 ++ .../system/src/Notifications/types.ts | 1 + 8 files changed, 69 insertions(+), 29 deletions(-) create mode 100644 frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx index 2018cfad..c58a896e 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx @@ -15,37 +15,21 @@ import { getReminderUnitsAsOptions, getReminderUnitsFromString, } from '../../types'; +import { useColdChainSensors } from '../../api'; 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 ( <> - {dummyLocations.map(location => { - const isSelected = draft.locationIds.includes(location.id); + {sensorsLoading && Loading...} + {sensors?.map(sensor => { + const isSelected = draft.sensorIds.includes(sensor.sensor_id); return ( { if (isSelected) { onUpdate({ - locationIds: draft.locationIds.filter( - id => id !== location.id + sensorIds: draft.sensorIds.filter( + id => id !== sensor.sensor_id ), }); } else { onUpdate({ - locationIds: [...draft.locationIds, location.id], + sensorIds: [...draft.sensorIds, sensor.sensor_id], }); } }} /> ); diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx index 12c4ae71..bc7c28e8 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx @@ -36,6 +36,7 @@ const createCCNotification = ( reminderInterval: seed?.reminderInterval ?? 15, reminderUnits: seed?.reminderUnits ?? ReminderUnits.MINUTES, locationIds: seed?.locationIds ?? [], + sensorIds: seed?.sensorIds ?? [], recipientIds: seed?.recipientIds ?? [], recipientListIds: seed?.recipientListIds ?? [], status: seed?.status ?? ConfigStatus.Disabled, diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts b/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts index 60f1928d..75aba0fe 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/parseConfig.ts @@ -23,6 +23,7 @@ export function parseColdChainNotificationConfig( reminderInterval, reminderUnits, locationIds, + sensorIds, recipientIds, recipientListIds, sqlRecipientListIds, @@ -42,6 +43,7 @@ export function parseColdChainNotificationConfig( reminderInterval, reminderUnits, locationIds, + sensorIds, recipientIds, recipientListIds, status: config.status, diff --git a/frontend/packages/system/src/Notifications/api/hooks/index.ts b/frontend/packages/system/src/Notifications/api/hooks/index.ts index 7278fe13..32ff8afa 100644 --- a/frontend/packages/system/src/Notifications/api/hooks/index.ts +++ b/frontend/packages/system/src/Notifications/api/hooks/index.ts @@ -1 +1,2 @@ export * from './useNotificationConfigs'; +export * from './useColdChainSensors'; diff --git a/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts b/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts new file mode 100644 index 00000000..f9f5c1c9 --- /dev/null +++ b/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts @@ -0,0 +1,30 @@ +import { useGql, useQuery } from '@notify-frontend/common'; +import { getSdk } from './../operations.generated'; + +type SensorData = { + sensor_id: string; + store_name: string; + location_name: string; + sensor_name: string; +}; + +export const useColdChainSensors = () => { + const { client } = useGql(); + const sdk = getSdk(client); + + const cacheKeys = ['COLDCHAIN_SENSORS']; + + return useQuery(cacheKeys, async () => { + const sensorQuery = + "SELECT sn.id as sensor_id, s.name as store_name,coalesce(l.description, '') as location_name, sn.name as sensor_name FROM SENSOR sn JOIN store s ON sn.storeid = s.id LEFT JOIN location l on sn.locationid = l.id WHERE sn.is_active = true ORDER BY 2,3,4 LIMIT 1000"; + const response = await sdk.getColdChainSensors({ + sqlQuery: sensorQuery, + params: '{}', + }); + + const sensors: SensorData[] = JSON.parse( + response?.runSqlQueryWithParameters + ); + return sensors; + }); +}; diff --git a/frontend/packages/system/src/Notifications/api/operations.generated.ts b/frontend/packages/system/src/Notifications/api/operations.generated.ts index af71e0d7..eb0693b3 100644 --- a/frontend/packages/system/src/Notifications/api/operations.generated.ts +++ b/frontend/packages/system/src/Notifications/api/operations.generated.ts @@ -35,6 +35,14 @@ export type DeleteNotificationConfigMutationVariables = Types.Exact<{ export type DeleteNotificationConfigMutation = { __typename: 'FullMutation', deleteNotificationConfig: { __typename: 'DeleteResponse', id: string } }; +export type GetColdChainSensorsQueryVariables = Types.Exact<{ + sqlQuery?: Types.InputMaybe; + params?: Types.InputMaybe; +}>; + + +export type GetColdChainSensorsQuery = { __typename: 'FullQuery', runSqlQueryWithParameters: string }; + export const NotificationConfigRowFragmentDoc = gql` fragment NotificationConfigRow on NotificationConfigNode { id @@ -84,6 +92,11 @@ export const DeleteNotificationConfigDocument = gql` } } `; +export const GetColdChainSensorsDocument = gql` + query getColdChainSensors($sqlQuery: String, $params: String) { + runSqlQueryWithParameters(sqlQuery: $sqlQuery, parameters: $params) +} + `; export type SdkFunctionWrapper = (action: (requestHeaders?:Record) => Promise, operationName: string, operationType?: string) => Promise; @@ -103,6 +116,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = }, deleteNotificationConfig(variables: DeleteNotificationConfigMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(DeleteNotificationConfigDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteNotificationConfig', 'mutation'); + }, + getColdChainSensors(variables?: GetColdChainSensorsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetColdChainSensorsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getColdChainSensors', 'query'); } }; } diff --git a/frontend/packages/system/src/Notifications/api/operations.graphql b/frontend/packages/system/src/Notifications/api/operations.graphql index 3417e137..a6d9925a 100644 --- a/frontend/packages/system/src/Notifications/api/operations.graphql +++ b/frontend/packages/system/src/Notifications/api/operations.graphql @@ -45,3 +45,7 @@ mutation deleteNotificationConfig($id: String!) { } } } + +query getColdChainSensors($sqlQuery: String, $params: String) { + runSqlQueryWithParameters(sqlQuery: $sqlQuery, parameters: $params) +} diff --git a/frontend/packages/system/src/Notifications/types.ts b/frontend/packages/system/src/Notifications/types.ts index b949869e..0c927444 100644 --- a/frontend/packages/system/src/Notifications/types.ts +++ b/frontend/packages/system/src/Notifications/types.ts @@ -53,6 +53,7 @@ export interface CCNotification extends BaseNotificationConfig { reminderInterval: number; reminderUnits: ReminderUnits; locationIds: string[]; + sensorIds: string[]; } export interface ScheduledNotification extends BaseNotificationConfig { From fbc7b4ce3ad56fc33e16ba3e52e9678dc4991dd6 Mon Sep 17 00:00:00 2001 From: James Brunskill Date: Tue, 3 Oct 2023 17:53:29 +1300 Subject: [PATCH 3/6] Modal for sensor selection --- .../common/src/intl/locales/en/common.json | 2 + .../common/src/intl/locales/en/system.json | 3 +- .../ColdChain/CCNotificationEditForm.tsx | 48 ++---- .../ColdChain/CCNotificationEditPage.tsx | 4 +- .../Pages/ColdChain/SensorSelectionModal.tsx | 154 ++++++++++++++++++ .../Pages/ColdChain/SensorSelector.tsx | 73 +++++++++ .../api/hooks/useColdChainSensors.ts | 13 +- 7 files changed, 254 insertions(+), 43 deletions(-) create mode 100644 frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelectionModal.tsx create mode 100644 frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelector.tsx diff --git a/frontend/packages/common/src/intl/locales/en/common.json b/frontend/packages/common/src/intl/locales/en/common.json index e266e893..004352bb 100644 --- a/frontend/packages/common/src/intl/locales/en/common.json +++ b/frontend/packages/common/src/intl/locales/en/common.json @@ -49,6 +49,8 @@ "heading.quote-withdraw-title": "Specify reason", "heading.select-items": "Select Items", "heading.select-locations": "Select Locations", + "heading.select-sensors": "Select Sensors", + "heading.selected-sensors": "Selected Sensors", "heading.settings-display": "Display settings", "heading.username": "Username", "label.actions": "Actions", diff --git a/frontend/packages/common/src/intl/locales/en/system.json b/frontend/packages/common/src/intl/locales/en/system.json index fbb688a8..8e16a7b4 100644 --- a/frontend/packages/common/src/intl/locales/en/system.json +++ b/frontend/packages/common/src/intl/locales/en/system.json @@ -85,5 +85,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 c58a896e..c112d6fc 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx @@ -16,6 +16,7 @@ import { getReminderUnitsFromString, } from '../../types'; import { useColdChainSensors } from '../../api'; +import { SensorSelector } from './SensorSelector'; type CCNotificationEditFormProps = { onUpdate: (patch: Partial) => void; @@ -169,45 +170,18 @@ export const CCNotificationEditForm = ({ - {t('heading.select-locations')} + {t('heading.selected-sensors')} - {sensorsLoading && Loading...} - {sensors?.map(sensor => { - const isSelected = draft.sensorIds.includes(sensor.sensor_id); - return ( - - { - if (isSelected) { - onUpdate({ - sensorIds: draft.sensorIds.filter( - id => id !== sensor.sensor_id - ), - }); - } else { - onUpdate({ - sensorIds: [...draft.sensorIds, sensor.sensor_id], - }); - } - }} - /> - - - ); - })} + { + console.log('props', props); + onUpdate(props as Partial); + }} + isLoading={sensorsLoading} + /> diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx index bc7c28e8..cb7fd626 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditPage.tsx @@ -88,8 +88,8 @@ export const CCNotificationEditPage = () => { !draft.lowTemp && draft.remind && draft.noData) || - // no locations selected - !draft.locationIds.length || + // no sensor selected + !draft.sensorIds.length || // no recipients selected (!draft.recipientListIds.length && !draft.recipientIds.length && diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelectionModal.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelectionModal.tsx new file mode 100644 index 00000000..4e1e6484 --- /dev/null +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelectionModal.tsx @@ -0,0 +1,154 @@ +import React, { FC, useMemo, useState } from 'react'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import { useDialog } from '@common/hooks'; +import { useTranslation } from '@common/intl'; + +import { + AutocompleteMultiList, + AutocompleteOptionRenderer, + Checkbox, + DialogButton, + LoadingButton, + Tooltip, +} from '@common/components'; +import { CheckIcon } from '@common/icons'; +import { Grid } from '@common/ui'; +import { SensorData, sensorDisplayName } from '../../api'; + +interface SensorSelectionSelectionModalProps { + sensors: SensorData[]; + initialSelectedIds: string[]; + isOpen: boolean; + onClose: () => void; + setSelection: (input: { sensorIds: string[] }) => void; +} + +interface SensorSelectionOption { + id: string; + name: string; +} + +export const SensorSelectionModal: FC = ({ + sensors, + initialSelectedIds, + isOpen, + onClose, + setSelection, +}) => { + const t = useTranslation('system'); + const [errorMessage, setErrorMessage] = useState(''); + const [selectedIds, setSelectedIds] = useState([]); + + const { Modal } = useDialog({ isOpen, onClose }); + + const options = useMemo(() => { + return sensors.map(sensor => ({ + id: sensor.id, + name: sensorDisplayName(sensor), + })); + }, [sensors]); + + const onChangeSelectedQueries = (ids: string[]) => { + setSelectedIds(ids); + }; + + const onSubmit = async () => { + if (selectedIds.length === 0) { + setErrorMessage(t('messages.nothing-selected')); + // return; + } + + setSelection({ sensorIds: selectedIds }); + onClose(); + }; + + const modalHeight = Math.min(window.innerHeight - 100, 700); + const modalWidth = Math.min(window.innerWidth - 100, 924); + + return ( + + + } + > + {t('heading.select-sensors')} + + + + } + cancelButton={} + title={t('heading.select-sensors')} + slideAnimation={false} + > + + {errorMessage ? ( + + { + setErrorMessage(''); + }} + > + {t('error')} + {errorMessage} + + + ) : null} + + `${option.name}`} + filterProperties={['name']} + filterPlaceholder={t('placeholder.search')} + width={modalWidth - 50} + height={modalHeight - 300} + defaultSelection={options.filter(o => + initialSelectedIds.includes(o.id) + )} + /> + + + + ); +}; + +const renderOption: AutocompleteOptionRenderer = ( + props, + option, + { selected } +): JSX.Element => ( +
  • + + + + {option.name} + + +
  • +); diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelector.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelector.tsx new file mode 100644 index 00000000..ea1c06a3 --- /dev/null +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/SensorSelector.tsx @@ -0,0 +1,73 @@ +import React, { FC } from 'react'; +import { + TableProvider, + DataTable, + useColumns, + createTableStore, + useTranslation, + useEditModal, + IconButton, + EditIcon, +} from '@notify-frontend/common'; +import { SensorData, sensorDisplayName } from '../../api'; +import { SensorSelectionModal } from './SensorSelectionModal'; + +type SensorSelectorProps = { + records: SensorData[]; + selectedIds: string[]; + setSelection: (input: { sensorIds: string[] }) => void; + isLoading: boolean; +}; + +export const SensorSelector: FC = ({ + records, + selectedIds, + setSelection, + isLoading, +}) => { + const t = useTranslation('system'); + + const { isOpen, onClose, onOpen } = useEditModal(); + + const columns = useColumns([ + { + key: 'name', + label: 'label.name', + width: 150, + sortable: true, + accessor: ({ rowData }) => { + return sensorDisplayName(rowData); + }, + }, + ]); + + const selectedRecords = (records ?? []).filter(s => + selectedIds.includes(s.id) + ); + + return ( + <> + + } + label={t('label.edit')} + onClick={onOpen} + /> + + + + + ); +}; diff --git a/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts b/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts index f9f5c1c9..67d16f45 100644 --- a/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts +++ b/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts @@ -1,13 +1,20 @@ import { useGql, useQuery } from '@notify-frontend/common'; import { getSdk } from './../operations.generated'; -type SensorData = { - sensor_id: string; +export type SensorData = { + id: string; store_name: string; location_name: string; sensor_name: string; }; +export function sensorDisplayName(sensor: SensorData): string { + if (!sensor.location_name) { + return `${sensor.store_name} - ${sensor.sensor_name}`; + } + return `${sensor.store_name} - ${sensor.location_name} - ${sensor.sensor_name}`; +} + export const useColdChainSensors = () => { const { client } = useGql(); const sdk = getSdk(client); @@ -16,7 +23,7 @@ export const useColdChainSensors = () => { return useQuery(cacheKeys, async () => { const sensorQuery = - "SELECT sn.id as sensor_id, s.name as store_name,coalesce(l.description, '') as location_name, sn.name as sensor_name FROM SENSOR sn JOIN store s ON sn.storeid = s.id LEFT JOIN location l on sn.locationid = l.id WHERE sn.is_active = true ORDER BY 2,3,4 LIMIT 1000"; + "SELECT sn.id as id, s.name as store_name,coalesce(l.description, '') as location_name, sn.name as sensor_name FROM SENSOR sn JOIN store s ON sn.storeid = s.id LEFT JOIN location l on sn.locationid = l.id WHERE sn.is_active = true ORDER BY 2,3,4 LIMIT 1000"; const response = await sdk.getColdChainSensors({ sqlQuery: sensorQuery, params: '{}', From 8b5e154585145be482e4b80ae60846a30e20cace Mon Sep 17 00:00:00 2001 From: James Brunskill Date: Wed, 11 Oct 2023 16:17:51 +1300 Subject: [PATCH 4/6] Rename runSqlQuery graphql request --- .../Notifications/api/hooks/useColdChainSensors.ts | 6 +++++- .../src/Notifications/api/operations.generated.ts | 12 ++++++------ .../system/src/Notifications/api/operations.graphql | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts b/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts index 67d16f45..9014fa29 100644 --- a/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts +++ b/frontend/packages/system/src/Notifications/api/hooks/useColdChainSensors.ts @@ -24,11 +24,15 @@ export const useColdChainSensors = () => { return useQuery(cacheKeys, async () => { const sensorQuery = "SELECT sn.id as id, s.name as store_name,coalesce(l.description, '') as location_name, sn.name as sensor_name FROM SENSOR sn JOIN store s ON sn.storeid = s.id LEFT JOIN location l on sn.locationid = l.id WHERE sn.is_active = true ORDER BY 2,3,4 LIMIT 1000"; - const response = await sdk.getColdChainSensors({ + const response = await sdk.runSqlQueryWithParameters({ sqlQuery: sensorQuery, params: '{}', }); + if (!response) { + return []; + } + const sensors: SensorData[] = JSON.parse( response?.runSqlQueryWithParameters ); diff --git a/frontend/packages/system/src/Notifications/api/operations.generated.ts b/frontend/packages/system/src/Notifications/api/operations.generated.ts index 55d159c0..520a97d6 100644 --- a/frontend/packages/system/src/Notifications/api/operations.generated.ts +++ b/frontend/packages/system/src/Notifications/api/operations.generated.ts @@ -35,13 +35,13 @@ export type DeleteNotificationConfigMutationVariables = Types.Exact<{ export type DeleteNotificationConfigMutation = { __typename: 'FullMutation', deleteNotificationConfig: { __typename: 'DeleteResponse', id: string } }; -export type GetColdChainSensorsQueryVariables = Types.Exact<{ +export type RunSqlQueryWithParametersQueryVariables = Types.Exact<{ sqlQuery?: Types.InputMaybe; params?: Types.InputMaybe; }>; -export type GetColdChainSensorsQuery = { __typename: 'FullQuery', runSqlQueryWithParameters: string }; +export type RunSqlQueryWithParametersQuery = { __typename: 'FullQuery', runSqlQueryWithParameters: string }; export const NotificationConfigRowFragmentDoc = gql` fragment NotificationConfigRow on NotificationConfigNode { @@ -95,8 +95,8 @@ export const DeleteNotificationConfigDocument = gql` } } `; -export const GetColdChainSensorsDocument = gql` - query getColdChainSensors($sqlQuery: String, $params: String) { +export const RunSqlQueryWithParametersDocument = gql` + query runSqlQueryWithParameters($sqlQuery: String, $params: String) { runSqlQueryWithParameters(sqlQuery: $sqlQuery, parameters: $params) } `; @@ -120,8 +120,8 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = deleteNotificationConfig(variables: DeleteNotificationConfigMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(DeleteNotificationConfigDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'deleteNotificationConfig', 'mutation'); }, - getColdChainSensors(variables?: GetColdChainSensorsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { - return withWrapper((wrappedRequestHeaders) => client.request(GetColdChainSensorsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getColdChainSensors', 'query'); + runSqlQueryWithParameters(variables?: RunSqlQueryWithParametersQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(RunSqlQueryWithParametersDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'runSqlQueryWithParameters', 'query'); } }; } diff --git a/frontend/packages/system/src/Notifications/api/operations.graphql b/frontend/packages/system/src/Notifications/api/operations.graphql index 3a72b923..28682fc0 100644 --- a/frontend/packages/system/src/Notifications/api/operations.graphql +++ b/frontend/packages/system/src/Notifications/api/operations.graphql @@ -49,6 +49,6 @@ mutation deleteNotificationConfig($id: String!) { } } -query getColdChainSensors($sqlQuery: String, $params: String) { +query runSqlQueryWithParameters($sqlQuery: String, $params: String) { runSqlQueryWithParameters(sqlQuery: $sqlQuery, parameters: $params) } From c9c12eb3cdd2833655fe5b7650c57f1213b72fa5 Mon Sep 17 00:00:00 2001 From: James Brunskill Date: Wed, 11 Oct 2023 16:18:18 +1300 Subject: [PATCH 5/6] Clean up debug output --- .../Pages/ColdChain/CCNotificationEditForm.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx index 269e458a..ec102620 100644 --- a/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx +++ b/frontend/packages/system/src/Notifications/Pages/ColdChain/CCNotificationEditForm.tsx @@ -130,12 +130,12 @@ export const CCNotificationEditForm = ({ {t('heading.preference')} @@ -179,7 +179,9 @@ export const CCNotificationEditForm = ({ onUpdate({ messageAlertResolved: !draft.messageAlertResolved })} + onClick={() => + onUpdate({ messageAlertResolved: !draft.messageAlertResolved }) + } />