diff --git a/Cargo.lock b/Cargo.lock index 96436f5e..79406cd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -936,6 +936,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", + "unicode-width", "windows-sys 0.52.0", ] @@ -1951,6 +1952,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.29" @@ -2025,6 +2032,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "inherent" version = "1.0.11" @@ -2461,6 +2481,8 @@ dependencies = [ "futures", "fuzzy-matcher", "human-panic", + "humantime", + "indicatif", "iso8601-duration", "itertools 0.13.0", "lazy_static", @@ -2642,6 +2664,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.35.0" @@ -3046,6 +3074,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml index 2c13b398..a46d9d2d 100644 --- a/apps/cli/Cargo.toml +++ b/apps/cli/Cargo.toml @@ -43,6 +43,8 @@ iso8601-duration = { version = "0.2.0",features = ["serde", "chrono"] } measurements = { version = "0.11.0",features = ["serde", "std", "from_str"] } termimad = "0.29.2" itertools = "0.13.0" +indicatif = "0.17.8" +humantime = "2.1.0" [features] default = [] diff --git a/apps/cli/src/cli/dashboard/mod.rs b/apps/cli/src/cli/dashboard/mod.rs index 8ac33127..92c178ea 100644 --- a/apps/cli/src/cli/dashboard/mod.rs +++ b/apps/cli/src/cli/dashboard/mod.rs @@ -1,21 +1,147 @@ // TODO: Get active ingestions along their progress // TODO: Get analisis of side-effects and when they will likely occur -use itertools::Itertools; +use std::collections::HashMap; +use std::str::FromStr; + +use chrono::{Duration, Local, Utc}; +use futures::stream; +use futures::stream::StreamExt; +use humantime::format_duration; +use tabled::Table; use db::sea_orm::*; use db::sea_orm::DatabaseConnection; +use crate::core::dosage::Dosage; +use crate::core::ingestion::Ingestion; +use crate::core::phase::PhaseClassification; +use crate::service::ingestion::get_ingestion_by_id; + pub async fn handle_show_dashboard(database_connection: &DatabaseConnection) { println!("Dashboard is not implemented yet."); - // Show average dosage per day of each substance that was ingested - let ingestions = db::ingestion::Entity::find().all(database_connection).await.unwrap(); + // Fetch and map all ingestions from database + let ingestions = db::ingestion::Entity::find().all(database_connection).await.unwrap(); + let ingestions_stream = stream::iter(ingestions.into_iter()); + + let ingestions = futures::future::join_all(ingestions_stream + .map(|ingestion| { + let ingestion_id = ingestion.id; + async move { + get_ingestion_by_id(ingestion_id).await.unwrap() + } + }).collect::>().await).await; // Group ingestions by substance name - let ingestions_by_substance = ingestions + let ingestions_by_substance = ingestions.clone() + .into_iter() + .fold(HashMap::>::new(), |mut acc, ingestion| { + let substance_name = ingestion.substance_name.clone(); + let ingestion_entry = acc.entry(substance_name).or_insert(vec![]); + ingestion_entry.push(ingestion); + acc + }); + + // Calculate average dosage per day per substance + let average_dosage_per_substance_for_last_7_days = ingestions_by_substance.clone() + .into_iter() + .map(|(substance_name, ingestions)| { + let total_dosage: Dosage = ingestions + .iter() + .map(|ingestion| ingestion.dosage) + .collect::>() + .into_iter() + // Use custom summing implementation + .fold(Dosage::from_str( + "0.0 mg" + ).unwrap(), |acc, dosage| acc + dosage); + + let average_dosage = total_dosage / ingestions.len() as f64; + (substance_name, average_dosage) + }) + .collect::>(); + + println!("Average dosage per substance for last 7 days"); + for (substance_name, average_dosage) in average_dosage_per_substance_for_last_7_days { + println!("{}: {1:.0}", substance_name, average_dosage); + } + + // Calculate total dosage per substance for last 7 days + let total_dosage_per_substance_for_last_7_days = ingestions_by_substance + .into_iter() + .map(|(substance_name, ingestions)| { + let total_dosage: Dosage = ingestions + .iter() + .filter(|ingestion| ingestion.ingested_at >= Utc::now() - Duration::days(7)) + .map(|ingestion| ingestion.dosage) + .collect::>() + .into_iter() + // Use custom summing implementation + .fold(Dosage::from_str( + "0.0 mg" + ).unwrap(), |acc, dosage| acc + dosage); + + (substance_name, total_dosage) + }) + .collect::>(); + + println!("Total dosage per substance for last 7 days"); + + let table = Table::from_iter(total_dosage_per_substance_for_last_7_days.into_iter() + .map(|(substance_name, total_dosage)| { + vec![ + substance_name, + format!("{:.0}", total_dosage).to_string() + ] + }) + .collect::>().iter().cloned()); + + println!("{}", table.to_string()); + + // Filter all ingestions to find those which are active (onset, comeup, peak, offset) + let active_ingestions = ingestions .into_iter() - .chunk_by(|ingestion| ingestion.substance_name.as_ref().unwrap().clone()).into_iter(); + .filter(|ingestion| { + let ingestion_start = ingestion.phases.get(&PhaseClassification::Onset).unwrap().start_time; + let ingestion_end = ingestion.phases.get(&PhaseClassification::Offset).unwrap().end_time; + let daterange = ingestion_start..ingestion_end; + daterange.contains( &Local::now() ) + }) + .collect::>(); + + println!("Active ingestions: {:#?}", active_ingestions.len()); + + // Iterate through active ingestions to print procentage of completion, + // time left and other useful information such as ingestion id and substance name. + + active_ingestions.into_iter().for_each(|ingestion| { + let ingestion_start = ingestion.phases.get(&PhaseClassification::Onset).unwrap().start_time; + let ingestion_end = ingestion.phases.get(&PhaseClassification::Offset).unwrap().end_time; + let now = Local::now(); + let total_duration = ingestion_end - ingestion_start; + let elapsed_duration = now - ingestion_start; + let remaining_duration = total_duration - elapsed_duration; + + println!("#{} {} ({:.0}) | {}", ingestion.id, ingestion.substance_name, ingestion.dosage, format_duration(remaining_duration.to_std().unwrap())); + }); + + // We need to create progress bar for each active ingestion and + // put them all into easy-readable table allowing end-user to + // quickly see how long each ingestion will last. - println!("Ingestions by substance:"); + // active_ingestions.into_iter().for_each(|ingestion| { + // let ingestion_start = ingestion.phases.get(&PhaseClassification::Onset).unwrap().start_time; + // let ingestion_end = ingestion.phases.get(&PhaseClassification::Offset).unwrap().end_time; + // let now = Local::now(); + // let total_duration = (ingestion_end - ingestion_start).num_seconds() as u64; + // let elapsed_duration = (now - ingestion_start).num_seconds() as u64; + // + // println!("{}:", ingestion.substance_name); + // let pb = ProgressBar::new(total_duration).with_prefix(Cow::from(format!("{}:", ingestion.substance_name))); + // pb.set_style(ProgressStyle::default_bar()); + // pb.set_position(elapsed_duration); + // + // pb.finish_and_clear(); + // }); } \ No newline at end of file diff --git a/apps/cli/src/cli/import/import_from_psychonautwiki_journal.ts.rs b/apps/cli/src/cli/import/import_from_psychonautwiki_journal.ts.rs new file mode 100644 index 00000000..69814b87 --- /dev/null +++ b/apps/cli/src/cli/import/import_from_psychonautwiki_journal.ts.rs @@ -0,0 +1,3 @@ +pub fn import_from_psychonautwiki_journal() { + todo!() +} \ No newline at end of file diff --git a/apps/cli/src/cli/main.rs b/apps/cli/src/cli/main.rs index ab27810f..5722c249 100644 --- a/apps/cli/src/cli/main.rs +++ b/apps/cli/src/cli/main.rs @@ -58,7 +58,8 @@ enum DataManagementCommand { } #[derive(StructOpt, Debug)] -enum DashboardCommand { +#[structopt(name = "dashboard")] +struct DashboardCommand { } pub async fn cli() { diff --git a/apps/cli/src/core/dosage.rs b/apps/cli/src/core/dosage.rs index 7105f043..d0fdd197 100644 --- a/apps/cli/src/core/dosage.rs +++ b/apps/cli/src/core/dosage.rs @@ -1,7 +1,11 @@ +extern crate measurements; + use std::ops::{Range, RangeFrom, RangeTo}; use std::str::FromStr; +use measurements::*; use serde::{Deserialize, Serialize}; + pub type Dosage = measurements::Mass; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, Hash, Copy)] @@ -65,9 +69,6 @@ impl DosageRange { } -extern crate measurements; -use measurements::*; - pub fn test_measurements() { for power in -12..12 { let val: f64 = 123.456 * (10f64.powf(f64::from(power))); diff --git a/apps/cli/src/core/ingestion.rs b/apps/cli/src/core/ingestion.rs index 3a4a7e30..2a99b116 100644 --- a/apps/cli/src/core/ingestion.rs +++ b/apps/cli/src/core/ingestion.rs @@ -2,13 +2,12 @@ use std::collections::HashMap; use chrono::{DateTime, Local, Utc}; use serde::{Deserialize, Serialize}; -use tabled::Tabled; + use crate::core::dosage::Dosage; use crate::core::phase::{DurationRange, PhaseClassification}; use crate::core::route_of_administration::RouteOfAdministrationClassification; -use crate::ingestion_analyzer::IngestionAnalysis; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct IngestionPhase { pub(crate) phase_classification: PhaseClassification, pub(crate) duration: DurationRange, @@ -18,7 +17,7 @@ pub struct IngestionPhase { pub type IngestionPhases = HashMap; -#[derive( Serialize, Debug, Deserialize)] +#[derive( Serialize, Debug, Deserialize, Clone)] pub struct Ingestion { pub(crate) id: i32, pub(crate) substance_name: String, diff --git a/docs/Architecture/context-diagram.d2 b/docs/Architecture/context-diagram.d2 index 5b45e4f0..419c346b 100644 --- a/docs/Architecture/context-diagram.d2 +++ b/docs/Architecture/context-diagram.d2 @@ -1,42 +1,54 @@ +direction: left +iam: { + label: "Identity and Access Management (IAM)" -experience: "Experience" -ingestion: "Ingestion" - -dosage: "Dosage" -phase: "Phase" - + account: "Account" +} -# Account -account: "Account" +subject_database: { + label: "Subject" -# Subject -subject: "Subject" + subject: "Subject" -subject -> account + subject -> _.iam.account: (optionally) +} substance_database: { - label: "Substance Database" + label: "Public Substance Information" # Substance substance: "Substance" route_of_administration: "Route of Administration" - route_of_administration_dosage: "Route of Administration Dosage" - route_of_administration_phase: "Route of Administration Phase" + dosage: "Dosage" + phase: "Phase" route_of_administration -> substance - route_of_administration_dosage -> route_of_administration - route_of_administration_phase -> route_of_administration + dosage -> route_of_administration + phase -> route_of_administration } +journal: { + label: "Journal" + ingestion: "Ingestion" + ingestion_phase: "Ingestion Phase" -# Route of Administration + ingestion -> _.substance_database.substance + ingestion -> _.subject_database.subject + ingestion_phase -> ingestion +} -route_of_administration -> substance +experience_db: { + label: "Experience Dataset" + experience: "Expereince" -# Ingestion -ingestion -> subject -ingestion -> substance + experience -> _.subject_database.subject: { + label: "may contain" + } + experience -> _.journal.ingestion: { + label: "may contain" + } +}