Edit; List; Delete functionality

This commit is contained in:
xqtc 2025-04-03 14:36:06 +02:00
parent 6a3620ae7d
commit a0f9877f54
Signed by: xqtc
GPG key ID: 2C064D095926D9D1
4 changed files with 345 additions and 45 deletions

Binary file not shown.

View file

@ -1,4 +1,4 @@
use crate::{SubstanceEntry, Unit};
use crate::{IngestionEntry, Unit};
use sqlite::{Connection, Result};
pub fn init_db_conn() -> Result<Connection> {
@ -45,7 +45,7 @@ pub fn create_db() -> Result<()> {
// Err(e) => e,
// };
// }
pub fn get_all_ingestions(conn: &sqlite::Connection) -> Result<Vec<SubstanceEntry>> {
pub fn get_all_ingestions(conn: &sqlite::Connection) -> Result<Vec<IngestionEntry>> {
let mut statement = conn.prepare(
"SELECT substance, route, amount, unit, ingestion_time FROM ingestions ORDER BY ingestion_time DESC"
)?;
@ -80,7 +80,7 @@ pub fn get_all_ingestions(conn: &sqlite::Connection) -> Result<Vec<SubstanceEntr
Err(_) => continue, // Skip invalid dates
};
result.push(SubstanceEntry {
result.push(IngestionEntry {
substance,
route,
amount,
@ -93,7 +93,7 @@ pub fn get_all_ingestions(conn: &sqlite::Connection) -> Result<Vec<SubstanceEntr
Ok(result)
}
pub fn save_ingestion_to_db(conn: &sqlite::Connection, entry: &SubstanceEntry) -> Result<()> {
pub fn save_ingestion_to_db(conn: &sqlite::Connection, entry: &IngestionEntry) -> Result<()> {
// Prepare the statement with named parameters
let query = format!(
"INSERT INTO ingestions (substance, route, amount, unit, ingestion_time, created_at)

View file

@ -3,6 +3,7 @@ use db::create_db;
use serde::{Deserialize, Serialize};
use std::error::Error;
use strum::EnumIter;
use strum::IntoEnumIterator;
use strum_macros::Display;
use util::gather_ingestion_data;
@ -12,6 +13,14 @@ use util::read_substances_from_file;
mod db;
use db::init_db_conn;
#[derive(Debug, Display, Serialize, Deserialize, EnumIter)]
pub enum Command {
AddIngestion,
EditIngestion,
ListIngestions,
DeleteIngestion,
}
#[derive(Debug, Clone, Display, Serialize, Deserialize, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum Unit {
@ -22,7 +31,7 @@ pub enum Unit {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubstanceEntry {
pub struct IngestionEntry {
substance: String,
route: String,
amount: f64,
@ -49,46 +58,59 @@ fn main() -> Result<(), Box<dyn Error>> {
println!("Successfully loaded {} substances", substances.len());
let ingestion = gather_ingestion_data(substances)?;
let command =
inquire::Select::new("What do you want to do?", Command::iter().collect()).prompt()?;
match command {
Command::AddIngestion => {
let ingestion = gather_ingestion_data(substances)?;
// Display the recorded information
println!("\nRecorded Information:");
println!("---------------------");
println!("Substance: {}", ingestion.entry.substance);
println!("Route: {}", ingestion.entry.route);
println!("Amount: {}{}", ingestion.entry.amount, ingestion.entry.unit);
println!(
"Time: {}\n",
ingestion.entry.ingestion_time.format("%Y-%m-%d %H:%M")
);
// Display the recorded information
println!("\nRecorded Information:");
println!("---------------------");
println!("Substance: {}", ingestion.entry.substance);
println!("Route: {}", ingestion.entry.route);
println!("Amount: {}{}", ingestion.entry.amount, ingestion.entry.unit);
println!(
"Time: {}\n",
ingestion.entry.ingestion_time.format("%Y-%m-%d %H:%M")
);
// Show onset, duration, and after-effects if available
if let Some(onset) = ingestion.data["formatted_onset"]["value"].as_str() {
let unit = ingestion.data["formatted_onset"]["_unit"]
.as_str()
.unwrap_or("");
println!("Expected onset: {} {}", onset, unit);
// Show onset, duration, and after-effects if available
if let Some(onset) = ingestion.data["formatted_onset"]["value"].as_str() {
let unit = ingestion.data["formatted_onset"]["_unit"]
.as_str()
.unwrap_or("");
println!("Expected onset: {} {}", onset, unit);
}
if let Some(duration) = ingestion.data["formatted_duration"]["value"].as_str() {
let unit = ingestion.data["formatted_duration"]["_unit"]
.as_str()
.unwrap_or("");
println!("Expected duration: {} {}", duration, unit);
}
if let Some(aftereffects) = ingestion.data["formatted_aftereffects"]["value"].as_str() {
let unit = ingestion.data["formatted_aftereffects"]["_unit"]
.as_str()
.unwrap_or("");
println!("Expected after-effects: {} {}", aftereffects, unit);
}
if inquire::prompt_confirmation("Save to DB?").unwrap() {
db::save_ingestion_to_db(&init_db_conn()?, &ingestion.entry)?;
let _ingestions = db::get_all_ingestions(&init_db_conn()?)?;
println!("Entry saved successfully");
};
}
Command::ListIngestions => {
util::list_ingestions()?;
}
Command::DeleteIngestion => {
util::delete_ingestion_entries(&init_db_conn()?)?;
}
Command::EditIngestion => util::edit_ingestion_entry(&init_db_conn()?)?,
}
if let Some(duration) = ingestion.data["formatted_duration"]["value"].as_str() {
let unit = ingestion.data["formatted_duration"]["_unit"]
.as_str()
.unwrap_or("");
println!("Expected duration: {} {}", duration, unit);
}
if let Some(aftereffects) = ingestion.data["formatted_aftereffects"]["value"].as_str() {
let unit = ingestion.data["formatted_aftereffects"]["_unit"]
.as_str()
.unwrap_or("");
println!("Expected after-effects: {} {}", aftereffects, unit);
}
if inquire::prompt_confirmation("Save to DB?").unwrap() {
db::save_ingestion_to_db(&init_db_conn()?, &ingestion.entry)?;
let _ingestions = db::get_all_ingestions(&init_db_conn()?)?;
println!("Entry saved successfully");
};
Ok(())
}

View file

@ -8,11 +8,11 @@ use std::io::BufReader;
use std::path::Path;
use strum::IntoEnumIterator;
use crate::{SubstanceEntry, Unit};
use crate::{IngestionEntry, Unit, db::init_db_conn};
#[derive(Clone)]
pub struct IngestionResponse {
pub entry: SubstanceEntry,
pub entry: IngestionEntry,
pub data: Value,
}
@ -130,7 +130,7 @@ pub fn gather_ingestion_data(
.unwrap();
// Create an entry
let entry = SubstanceEntry {
let entry = IngestionEntry {
substance: substance.clone(),
route,
amount,
@ -144,3 +144,281 @@ pub fn gather_ingestion_data(
};
Ok(resp)
}
pub fn list_ingestions() -> Result<(), Box<dyn Error>> {
let mut ingestions: Vec<IngestionEntry> = crate::db::get_all_ingestions(&init_db_conn()?)?;
ingestions.reverse();
for ingestion in ingestions {
println!(
"Date: {}",
ingestion.ingestion_time.format("%d/%m/%Y %H:%M")
);
println!(
"Substance: {}",
capitalize_first_letter(&ingestion.substance)
);
println!("Route of administration: {}", ingestion.route);
println!("Ingested amount: {}{}", ingestion.amount, ingestion.unit);
println!("");
}
Ok(())
}
pub fn delete_ingestion_entries(conn: &sqlite::Connection) -> Result<(), Box<dyn Error>> {
let entries = crate::db::get_all_ingestions(conn)?;
if entries.is_empty() {
println!("No ingestion entries found in the database.");
return Ok(());
}
let entry_options: Vec<String> = entries
.iter()
.enumerate()
.map(|(i, entry)| {
format!(
"#{}: {} {} of {} via {} on {}",
i + 1,
entry.amount,
entry.unit,
entry.substance,
entry.route,
entry.ingestion_time.format("%Y-%m-%d %H:%M")
)
})
.collect();
let mut options = entry_options.clone();
options.push("Cancel - Don't delete anything".to_string());
let selection = Select::new("Select an entry to delete:", options).prompt()?;
if selection == "Cancel - Don't delete anything" {
println!("Operation canceled. No entries were deleted.");
return Ok(());
}
// Find the selected entry index
let selected_index = entry_options.iter().position(|e| e == &selection).unwrap();
let selected_entry = &entries[selected_index];
let confirm = Select::new(
&format!("Are you sure you want to delete this entry: {}?", selection),
vec!["No, cancel", "Yes, delete it"],
)
.prompt()?;
if confirm == "Yes, delete it" {
let query = format!(
"DELETE FROM ingestions WHERE substance = '{}' AND route = '{}' AND amount = {} AND unit = '{}' AND ingestion_time = '{}'",
selected_entry.substance,
selected_entry.route,
selected_entry.amount,
selected_entry.unit.to_string().to_lowercase(),
selected_entry.ingestion_time.to_rfc3339()
);
match conn.execute(query) {
Ok(_) => println!("Entry deleted successfully."),
Err(e) => println!("Failed to delete entry: {}", e),
}
} else {
println!("Deletion canceled.");
}
Ok(())
}
pub fn edit_ingestion_entry(conn: &sqlite::Connection) -> Result<(), Box<dyn Error>> {
// First, get all entries to display them
let entries = crate::db::get_all_ingestions(conn)?;
if entries.is_empty() {
println!("No ingestion entries found in the database.");
return Ok(());
}
// Create formatted strings for selection
let entry_options: Vec<String> = entries
.iter()
.enumerate()
.map(|(i, entry)| {
format!(
"#{}: {} {} of {} via {} on {}",
i + 1,
entry.amount,
entry.unit,
entry.substance,
entry.route,
entry.ingestion_time.format("%Y-%m-%d %H:%M")
)
})
.collect();
// Add an option to cancel
let mut options = entry_options.clone();
options.push("Cancel - Don't edit anything".to_string());
// Prompt the user to select an entry to edit
let selection = Select::new("Select an entry to edit:", options).prompt()?;
// Check if the user selected the cancel option
if selection == "Cancel - Don't edit anything" {
println!("Operation canceled. No entries were edited.");
return Ok(());
}
// Find the selected entry index
let selected_index = entry_options.iter().position(|e| e == &selection).unwrap();
let selected_entry = &entries[selected_index];
// Store the original values for the WHERE clause in the update query
let original_substance = selected_entry.substance.clone();
let original_route = selected_entry.route.clone();
let original_amount = selected_entry.amount;
let original_unit = selected_entry.unit.to_string().to_lowercase();
let original_time = selected_entry.ingestion_time.to_rfc3339();
// Create a new entry with the original values as defaults
// Load substances from JSON file for selection
let substances = read_substances_from_file("drugs.json")?;
// let substances = match read_substances_from_file("drugs_example.json") {
// Ok(s) => s,
// Err(_) => {
// eprintln!("Error loading substances file: {}", e);
// return Err(());
// }
// };
// Create a list of substance names for selection
let mut substance_names: Vec<String> = substances.keys().map(|k| k.to_string()).collect();
substance_names.sort();
// Select a substance (default to the original)
let substance = Select::new("Select a substance:", substance_names.clone())
.with_starting_cursor(
substance_names
.clone()
.iter()
.position(|s| s == &selected_entry.substance)
.unwrap_or(0),
)
.prompt()?;
// Get substance data
let substance_data = &substances[&substance];
// Extract routes of administration
let mut routes = Vec::new();
if let Some(formatted_dose) = substance_data["formatted_dose"].as_object() {
routes = formatted_dose.keys().map(|k| k.to_string()).collect();
} else {
// Default routes if not specified in the JSON
routes = vec![
"Oral".to_string(),
"Insufflated".to_string(),
"Sublingual".to_string(),
];
}
// Select route of administration (default to the original if it exists in the routes)
let default_route_index = routes
.iter()
.position(|r| r == &selected_entry.route)
.unwrap_or(0);
let route = Select::new("Select route of administration:", routes)
.with_starting_cursor(default_route_index)
.prompt()?;
// Ask for the amount (default to the original)
let amount = CustomType::<f64>::new("Enter the amount ingested (numeric value):")
.with_parser(&|input| match input.parse::<f64>() {
Ok(value) => Ok(value),
Err(_) => Err(()),
})
.with_error_message("Please enter a valid number")
.with_default(selected_entry.amount)
.prompt()?;
// Ask for the unit (default to the original)
let unit_options = vec![Unit::Ug, Unit::Mg, Unit::G, Unit::Ml];
let default_unit_index = unit_options
.iter()
.position(|u| u.to_string().to_lowercase() == original_unit)
.unwrap_or(0);
let unit = Select::new("Select unit:", unit_options)
.with_starting_cursor(default_unit_index)
.prompt()?;
// Ask for the ingestion time (default to the original)
let default_date = selected_entry.ingestion_time.date().naive_local();
let ingestion_date = DateSelect::new("Select ingestion date:")
.with_default(default_date)
.prompt()?;
let default_time = selected_entry.ingestion_time.time();
let ingestion_time = CustomType::<chrono::NaiveTime>::new("Enter ingestion time (HH:MM):")
.with_parser(
&|input| match chrono::NaiveTime::parse_from_str(input, "%H:%M") {
Ok(time) => Ok(time),
Err(_) => Err(()),
},
)
.with_error_message("Please enter a valid time in the format HH:MM")
.with_default(default_time)
.prompt()?;
// Combine date and time
let datetime = chrono::Local
.from_local_datetime(&ingestion_date.and_time(ingestion_time))
.single()
.unwrap();
// Create updated entry
let updated_entry = IngestionEntry {
substance: substance.clone(),
route,
amount,
unit,
ingestion_time: datetime,
created_at: None,
};
// Confirm update
let confirm = Select::new(
"Are you sure you want to update this entry?",
vec!["No, cancel", "Yes, update it"],
)
.prompt()?;
if confirm == "Yes, update it" {
// Update the entry in the database
let query = format!(
"UPDATE ingestions SET substance = '{}', route = '{}', amount = {}, unit = '{}', ingestion_time = '{}'
WHERE substance = '{}' AND route = '{}' AND amount = {} AND unit = '{}' AND ingestion_time = '{}'",
updated_entry.substance,
updated_entry.route,
updated_entry.amount,
updated_entry.unit.to_string().to_lowercase(),
updated_entry.ingestion_time.to_rfc3339(),
original_substance,
original_route,
original_amount,
original_unit,
original_time
);
match conn.execute(query) {
Ok(_) => println!("Entry updated successfully."),
Err(e) => println!("Failed to update entry: {}", e),
}
} else {
println!("Update canceled.");
}
Ok(())
}
fn capitalize_first_letter(s: &str) -> String {
s[0..1].to_uppercase() + &s[1..]
}