Enhance application configuration and functionality

- Updated `.env` file to include `DB_MAX_CONNECTIONS` and `ENVIRONMENT` variables.
- Modified `Cargo.toml` to add `thiserror` and `time` dependencies, and updated `sqlx` features.
- Refactored `main.rs` to implement periodic tasks for checking future shifts and printing the current hospital.
- Expanded routing in `routes.rs` to include a new endpoint for current hospital status.
- Improved database initialization in `init.rs` to conditionally drop tables based on the environment.
- Enhanced error handling in database operations and added logging for better traceability.
- Updated hospital management logic in `hospital.rs` to fetch and store shifts with improved timestamp logging.
This commit is contained in:
Ben Melchior
2025-04-12 21:52:34 +02:00
parent 19833ad384
commit ba78627169
9 changed files with 455 additions and 67 deletions

3
.env
View File

@@ -4,3 +4,6 @@ PORT=3000
# DB # DB
DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi DATABASE_URL=postgres://hospitalapi:NVtqiNsQq2t7Lss@kn0x.tech/hospitalapi
DB_MAX_CONNECTIONS=5
ENVIRONMENT=production

View File

@@ -9,6 +9,8 @@ tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "postgres"] } sqlx = { version = "0.8.2", features = ["runtime-tokio-native-tls", "postgres", "time"] }
chrono = "0.4.38" chrono = "0.4.38"
chrono-tz = "0.10.0" chrono-tz = "0.10.0"
thiserror = "1.0"
time = "0.3"

View File

@@ -1,6 +1,18 @@
use sqlx::PgPool; use sqlx::PgPool;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::{fs, env};
use thiserror::Error;
use crate::utilities::hospital;
#[derive(Error, Debug)]
pub enum InitError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("File error: {0}")]
File(#[from] std::io::Error),
#[error("JSON parsing error: {0}")]
Json(#[from] serde_json::Error),
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct HospitalData { struct HospitalData {
@@ -15,15 +27,17 @@ struct Hospital {
phone: String, phone: String,
} }
pub async fn initiate_database(pool: &PgPool) -> Result<(), sqlx::Error> { pub async fn initiate_database(pool: &PgPool) -> Result<(), InitError> {
// Drop the hospitals table for testing // Only drop tables in development environment
sqlx::query("DROP TABLE IF EXISTS shifts;") if env::var("ENVIRONMENT").unwrap_or_else(|_| "development".to_string()) == "development" {
.execute(pool) sqlx::query("DROP TABLE IF EXISTS shifts;")
.await?; .execute(pool)
.await?;
sqlx::query("DROP TABLE IF EXISTS hospitals;") sqlx::query("DROP TABLE IF EXISTS hospitals;")
.execute(pool) .execute(pool)
.await?; .await?;
}
if !table_exists(pool).await? { if !table_exists(pool).await? {
println!("Initializing database..."); println!("Initializing database...");
@@ -35,6 +49,10 @@ pub async fn initiate_database(pool: &PgPool) -> Result<(), sqlx::Error> {
create_shifts_table(pool).await?; create_shifts_table(pool).await?;
println!("Database initialization completed successfully"); println!("Database initialization completed successfully");
println!("Generating hospital schedule...");
let pool_arc = std::sync::Arc::new(pool.clone());
hospital::create_hospital_list(pool_arc).await.map_err(|e| InitError::Database(e))?;
} }
Ok(()) Ok(())
@@ -56,7 +74,7 @@ async fn table_exists(pool: &PgPool) -> Result<bool, sqlx::Error> {
Ok(exists) Ok(exists)
} }
async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> { async fn create_hospital_table(pool: &PgPool) -> Result<(), InitError> {
// Create the hospitals table if it doesn't exist // Create the hospitals table if it doesn't exist
sqlx::query( sqlx::query(
r#" r#"
@@ -64,7 +82,9 @@ async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> {
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
address VARCHAR(255) NOT NULL, address VARCHAR(255) NOT NULL,
phone VARCHAR(20) NOT NULL phone VARCHAR(20) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
) )
"#, "#,
) )
@@ -79,10 +99,8 @@ async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> {
// Only insert default hospitals if the table is empty // Only insert default hospitals if the table is empty
if count == 0 { if count == 0 {
// Read and parse the hospital.json file // Read and parse the hospital.json file
let data = fs::read_to_string("hospital.json") let data = fs::read_to_string("hospital.json")?;
.expect("Unable to read hospital.json"); let hospital_data: HospitalData = serde_json::from_str(&data)?;
let hospital_data: HospitalData = serde_json::from_str(&data)
.expect("Unable to parse hospital.json");
// Insert each hospital from the JSON file // Insert each hospital from the JSON file
for hospital in hospital_data.hospitals { for hospital in hospital_data.hospitals {
@@ -104,7 +122,7 @@ async fn create_hospital_table(pool: &PgPool) -> Result<(), sqlx::Error> {
Ok(()) Ok(())
} }
async fn create_shifts_table(pool: &PgPool) -> Result<(), sqlx::Error> { async fn create_shifts_table(pool: &PgPool) -> Result<(), InitError> {
// Create the shifts table if it doesn't exist // Create the shifts table if it doesn't exist
sqlx::query("CREATE EXTENSION IF NOT EXISTS btree_gist") sqlx::query("CREATE EXTENSION IF NOT EXISTS btree_gist")
.execute(pool) .execute(pool)
@@ -117,7 +135,6 @@ async fn create_shifts_table(pool: &PgPool) -> Result<(), sqlx::Error> {
hospital_id INTEGER NOT NULL REFERENCES hospitals(id), hospital_id INTEGER NOT NULL REFERENCES hospitals(id),
start_time TIMESTAMP WITH TIME ZONE NOT NULL, start_time TIMESTAMP WITH TIME ZONE NOT NULL,
end_time TIMESTAMP WITH TIME ZONE NOT NULL, end_time TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT no_overlapping_shifts EXCLUDE USING gist ( CONSTRAINT no_overlapping_shifts EXCLUDE USING gist (
tstzrange(start_time, end_time) WITH && tstzrange(start_time, end_time) WITH &&
) )

View File

@@ -1,27 +1,43 @@
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool; use sqlx::PgPool;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use thiserror::Error;
mod init; mod init;
pub use init::initiate_database; pub use init::initiate_database;
#[derive(Error, Debug)]
pub enum DatabaseError {
#[error("Database URL not found in environment")]
MissingDatabaseUrl,
#[error("Failed to create connection pool: {0}")]
PoolCreationError(#[from] sqlx::Error),
#[error("Database initialization failed: {0}")]
InitializationError(String),
}
// initialize and return an Arc wrapped PgPool instance // initialize and return an Arc wrapped PgPool instance
pub async fn create_pool() -> Arc<PgPool> { pub async fn create_pool() -> Result<Arc<PgPool>, DatabaseError> {
// load environments variables from `.env` file // load environments variables from `.env` file
let database_url: String = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let database_url = env::var("DATABASE_URL").map_err(|_| DatabaseError::MissingDatabaseUrl)?;
// Get configuration from environment or use defaults
let max_connections = env::var("DB_MAX_CONNECTIONS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(5);
// create postgres connection pool // create postgres connection pool
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.max_connections(5) .max_connections(max_connections)
.connect(&database_url) .connect(&database_url)
.await .await
.expect("Failed to create connection pool"); .map_err(DatabaseError::PoolCreationError)?;
// Initialize database tables // Initialize database tables
if let Err(e) = initiate_database(&pool).await { initiate_database(&pool)
eprintln!("Error initializing database: {}", e); .await
panic!("Database initialization failed"); .map_err(|e| DatabaseError::InitializationError(e.to_string()))?;
}
Arc::new(pool) Ok(Arc::new(pool))
} }

View File

@@ -0,0 +1,79 @@
use axum::{
extract::State,
Json,
};
use serde::Serialize;
use std::sync::Arc;
use sqlx::PgPool;
use chrono::Local;
use time::OffsetDateTime;
#[derive(Serialize)]
pub struct CurrentHospital {
id: i32,
name: String,
start_time: String,
end_time: String,
}
#[derive(Serialize)]
pub struct HospitalResponse {
success: bool,
hospital: Option<CurrentHospital>,
message: String,
}
pub async fn current_hospital_handler(
State(pool): State<Arc<PgPool>>,
) -> Json<HospitalResponse> {
// Get the current time
let now = Local::now();
// Convert chrono DateTime to time OffsetDateTime for SQLx
let now_offset = OffsetDateTime::from_unix_timestamp(now.timestamp())
.expect("Failed to convert to OffsetDateTime");
// Get the current hospital on duty
let result = sqlx::query!(
r#"
SELECT h.id, h.name, s.start_time, s.end_time
FROM hospitals h
JOIN shifts s ON h.id = s.hospital_id
WHERE s.start_time <= $1 AND s.end_time > $1
ORDER BY s.end_time ASC
LIMIT 1
"#,
now_offset
)
.fetch_optional(&*pool)
.await;
match result {
Ok(Some(hospital)) => {
Json(HospitalResponse {
success: true,
hospital: Some(CurrentHospital {
id: hospital.id,
name: hospital.name,
start_time: hospital.start_time.to_string(),
end_time: hospital.end_time.to_string(),
}),
message: "Current hospital retrieved successfully".to_string(),
})
},
Ok(None) => {
Json(HospitalResponse {
success: false,
hospital: None,
message: "No hospital currently on duty".to_string(),
})
},
Err(e) => {
Json(HospitalResponse {
success: false,
hospital: None,
message: format!("Error retrieving current hospital: {}", e),
})
}
}
}

2
src/handlers/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod root;
pub mod current_hospital;

View File

@@ -1,19 +1,24 @@
use std::{env, net::SocketAddrV4}; use std::{env, net::SocketAddrV4, time::Duration as StdDuration};
use dotenvy::dotenv; use dotenvy::dotenv;
use utilities::hospital; use utilities::hospital;
use tokio::time;
use chrono::Local;
mod db; mod db;
mod routes; mod routes;
mod handlers { mod handlers;
pub mod root;
}
mod utilities { mod utilities {
pub mod hospital; pub mod hospital;
} }
// Helper function to print with timestamp
fn log_with_timestamp(message: &str) {
let now = Local::now();
println!("[{}] {}", now.format("%Y-%m-%d %H:%M:%S"), message);
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// load environment variables from `.env` file // load environment variables from `.env` file
dotenv().ok(); dotenv().ok();
@@ -25,21 +30,42 @@ async fn main() {
let addr: SocketAddrV4 = format!("{}:{}", host, port).parse::<SocketAddrV4>().expect("Invalid address"); let addr: SocketAddrV4 = format!("{}:{}", host, port).parse::<SocketAddrV4>().expect("Invalid address");
// initialize the database pool connection // initialize the database pool connection
let pool: std::sync::Arc<sqlx::Pool<sqlx::Postgres>> = db::create_pool().await; let pool: std::sync::Arc<sqlx::Pool<sqlx::Postgres>> = db::create_pool()
.await
.expect("Failed to create database pool");
// check if the programm was called with any arguments // Set up a periodic task to check for future shifts every 48 hours
let args: Vec<String> = env::args().collect(); let pool_clone = pool.clone();
if args.len() > 1 && args[1] == "create-hospital-list" { tokio::spawn(async move {
hospital::create_hospital_list(); let mut interval = time::interval(StdDuration::from_secs(48 * 60 * 60)); // 48 hours
return; loop {
} interval.tick().await;
log_with_timestamp("Periodic check for future shifts...");
if let Err(e) = hospital::check_and_create_future_shifts(pool_clone.clone()).await {
eprintln!("[{}] Error checking for future shifts: {}",
Local::now().format("%Y-%m-%d %H:%M:%S"), e);
}
}
});
// Set up a task to print the current hospital every minute
let pool_clone = pool.clone();
tokio::spawn(async move {
let mut interval = time::interval(StdDuration::from_secs(60)); // 1 minute
loop {
interval.tick().await;
if let Err(e) = hospital::print_current_hospital(pool_clone.clone()).await {
eprintln!("[{}] Error printing current hospital: {}",
Local::now().format("%Y-%m-%d %H:%M:%S"), e);
}
}
});
// set up the application routes // set up the application routes
let app: axum::Router = routes::create_router(pool); let app: axum::Router = routes::create_router(pool);
println!("Server is running on https://{addr}"); log_with_timestamp(&format!("Server is running on https://{}", addr));
// bind and serve the application // bind and serve the application
let listener: tokio::net::TcpListener = tokio::net::TcpListener::bind(&addr).await.unwrap(); let listener: tokio::net::TcpListener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await.unwrap();
} }

View File

@@ -3,9 +3,11 @@ use std::sync::Arc;
use sqlx::PgPool; use sqlx::PgPool;
use crate::handlers::root::root_handler; use crate::handlers::root::root_handler;
use crate::handlers::current_hospital::current_hospital_handler;
pub fn create_router(pool: Arc<PgPool>) -> Router { pub fn create_router(pool: Arc<PgPool>) -> Router {
Router::new() Router::new()
.route("/", get(root_handler)) .route("/", get(root_handler))
.route("/current-hospital", get(current_hospital_handler))
.with_state(pool) .with_state(pool)
} }

View File

@@ -1,9 +1,14 @@
use core::panic; use core::panic;
use std::io; use std::io;
use chrono::{offset::LocalResult, DateTime, Datelike, Duration, Local, TimeZone}; use chrono::{offset::LocalResult, DateTime, Datelike, Duration, Local, TimeZone};
use sqlx::PgPool;
use std::sync::Arc;
use time::OffsetDateTime;
use std::sync::Mutex;
#[derive(Debug)]
struct Hospital { struct Hospital {
id: u32, id: i32,
name: String, name: String,
} }
@@ -12,30 +17,43 @@ struct Hospital {
// to: DateTime<chrono_tz::Tz>, // to: DateTime<chrono_tz::Tz>,
// } // }
pub fn create_hospital_list() { // Helper function to print with timestamp
fn log_with_timestamp(message: &str) {
let now = Local::now();
println!("[{}] {}", now.format("%Y-%m-%d %H:%M:%S"), message);
}
// create the hospitals // Static variable to store the last printed hospital ID
let hospitals: [Hospital; 2] = [ static LAST_HOSPITAL_ID: Mutex<Option<i32>> = Mutex::new(None);
Hospital {
id: 1, pub async fn create_hospital_list(pool: Arc<PgPool>) -> Result<(), sqlx::Error> {
name: "Centre Hospitalier".to_string(), // Fetch hospitals from the database
}, let hospitals = sqlx::query_as!(
Hospital { Hospital,
id: 2, r#"
name: "Hopital Kirchberg".to_string(), SELECT id, name FROM hospitals ORDER BY id
}, "#
]; )
.fetch_all(&*pool)
.await?;
if hospitals.is_empty() {
log_with_timestamp("No hospitals found in the database.");
return Ok(());
}
// let mut start: DateTime<Local> = Local.with_ymd_and_hms(2025, 1, 6, 8, 0, 0).unwrap(); // set date for testing // let mut start: DateTime<Local> = Local.with_ymd_and_hms(2025, 1, 6, 8, 0, 0).unwrap(); // set date for testing
let mut start: DateTime<Local> = Local::now(); let mut start: DateTime<Local> = Local::now();
let mut end: DateTime<Local> = get_end_of_shift(start); let mut end: DateTime<Local> = get_end_of_shift(start);
println!("It is now {} and the current Shift is ending on {}", start.format("%A %d/%m/%Y %H:%M"), end.format("%A %d/%m/%Y %H:%M")); log_with_timestamp(&format!("It is now {} and the current Shift is ending on {}",
println!("Whitch Hospital is actualy on duty?"); start.format("%A %d/%m/%Y %H:%M"),
end.format("%A %d/%m/%Y %H:%M")));
log_with_timestamp("Which Hospital is currently on duty?");
// print out the hospitals // print out the hospitals
for hospital in &hospitals { for hospital in &hospitals {
println!("{}) {}", hospital.id, hospital.name) log_with_timestamp(&format!("{}) {}", hospital.id, hospital.name));
} }
let mut hospital_id_input = String::new(); let mut hospital_id_input = String::new();
@@ -46,26 +64,249 @@ pub fn create_hospital_list() {
let hospital_id: i32 = match hospital_id_input.parse() { let hospital_id: i32 = match hospital_id_input.parse() {
Ok(num) => num, Ok(num) => num,
Err(_) => { Err(_) => {
println!("Please enter a valid number!"); log_with_timestamp("Please enter a valid number!");
return; return Ok(());
} }
}; };
let mut hospital_index: usize = (hospital_id - 1) as usize;
for _ in 1..=100 { // Find the index of the selected hospital
let mut hospital_index: usize = 0;
let mut found = false;
for (i, hospital) in hospitals.iter().enumerate() {
if hospital.id == hospital_id {
hospital_index = i;
found = true;
break;
}
}
// Print the current hospital on duty if !found {
println!("From {} to {} is {} on duty.", start.format("%A %d/%m/%Y %H:%M"), end.format("%A %d/%m/%Y %H:%M"), hospitals[hospital_index].name ); log_with_timestamp(&format!("Hospital with ID {} not found.", hospital_id));
return Ok(());
}
log_with_timestamp("Generating and storing shifts in the database...");
// Clear existing shifts
sqlx::query("DELETE FROM shifts")
.execute(&*pool)
.await?;
// Generate and store 100 shifts
for i in 1..=100 {
// Format the timestamps as strings for database storage
let start_str = start.format("%Y-%m-%d %H:%M:%S").to_string();
let end_str = end.format("%Y-%m-%d %H:%M:%S").to_string();
// Store the current shift in the database
sqlx::query(
r#"
INSERT INTO shifts (hospital_id, start_time, end_time)
VALUES ($1, $2::timestamp with time zone, $3::timestamp with time zone)
"#,
)
.bind(hospitals[hospital_index].id)
.bind(start_str)
.bind(end_str)
.execute(&*pool)
.await?;
// Print progress only every 10 shifts
if i % 10 == 0 {
log_with_timestamp(&format!("Stored {} shifts...", i));
}
// new shift // new shift
start = end; start = end;
end = get_end_of_shift(start); end = get_end_of_shift(start);
// change hospital // change hospital
hospital_index = if hospital_index == 0 { 1 } else { 0 } hospital_index = if hospital_index == 0 { 1 } else { 0 };
// If we only have one hospital, don't switch
if hospitals.len() == 1 {
hospital_index = 0;
}
} }
log_with_timestamp("Successfully stored 100 shifts in the database.");
Ok(())
}
pub async fn check_and_create_future_shifts(pool: Arc<PgPool>) -> Result<(), sqlx::Error> {
// Get the current time
let now = Local::now();
// Calculate the date two months from now
let two_months_from_now = now + Duration::days(60);
// Convert chrono DateTime to time OffsetDateTime for SQLx
let now_offset = OffsetDateTime::from_unix_timestamp(now.timestamp())
.expect("Failed to convert to OffsetDateTime");
let two_months_offset = OffsetDateTime::from_unix_timestamp(two_months_from_now.timestamp())
.expect("Failed to convert to OffsetDateTime");
// Check if there are any shifts in the next two months
let future_shifts_count = sqlx::query_scalar::<_, i64>(
r#"
SELECT COUNT(*) FROM shifts
WHERE start_time > $1 AND start_time < $2
"#,
)
.bind(now_offset)
.bind(two_months_offset)
.fetch_one(&*pool)
.await?;
// If there are no future shifts, create them
if future_shifts_count == 0 {
log_with_timestamp("No future shifts found. Creating shifts for the next two months...");
// Get the current hospital on duty
let current_hospital = sqlx::query_as!(
Hospital,
r#"
SELECT h.id, h.name
FROM hospitals h
JOIN shifts s ON h.id = s.hospital_id
WHERE s.end_time > $1
ORDER BY s.end_time ASC
LIMIT 1
"#,
now_offset
)
.fetch_optional(&*pool)
.await?;
// If no current hospital is found, get the first hospital from the database
let hospital_id = match current_hospital {
Some(hospital) => hospital.id,
None => {
let first_hospital = sqlx::query_as!(
Hospital,
r#"
SELECT id, name FROM hospitals ORDER BY id LIMIT 1
"#
)
.fetch_optional(&*pool)
.await?
.ok_or_else(|| sqlx::Error::RowNotFound)?;
first_hospital.id
}
};
// Find the index of the selected hospital
let hospitals = sqlx::query_as!(
Hospital,
r#"
SELECT id, name FROM hospitals ORDER BY id
"#
)
.fetch_all(&*pool)
.await?;
let mut hospital_index = 0;
for (i, hospital) in hospitals.iter().enumerate() {
if hospital.id == hospital_id {
hospital_index = i;
break;
}
}
// Generate shifts starting from now
let mut start = now;
let mut end = get_end_of_shift(start);
// Generate shifts for the next two months (approximately 60 days)
for _ in 1..=120 {
// Convert chrono DateTime to time OffsetDateTime for SQLx
let start_offset = OffsetDateTime::from_unix_timestamp(start.timestamp())
.expect("Failed to convert to OffsetDateTime");
let end_offset = OffsetDateTime::from_unix_timestamp(end.timestamp())
.expect("Failed to convert to OffsetDateTime");
// Store the current shift in the database
sqlx::query(
r#"
INSERT INTO shifts (hospital_id, start_time, end_time)
VALUES ($1, $2, $3)
"#,
)
.bind(hospitals[hospital_index].id)
.bind(start_offset)
.bind(end_offset)
.execute(&*pool)
.await?;
// new shift
start = end;
end = get_end_of_shift(start);
// change hospital
hospital_index = if hospital_index == 0 { 1 } else { 0 };
// If we only have one hospital, don't switch
if hospitals.len() == 1 {
hospital_index = 0;
}
}
log_with_timestamp("Successfully created future shifts for the next two months.");
} else {
log_with_timestamp(&format!("Found {} future shifts. No need to create more.", future_shifts_count));
}
Ok(())
}
pub async fn print_current_hospital(pool: Arc<PgPool>) -> Result<(), sqlx::Error> {
// Get the current time
let now = Local::now();
// Convert chrono DateTime to time OffsetDateTime for SQLx
let now_offset = OffsetDateTime::from_unix_timestamp(now.timestamp())
.expect("Failed to convert to OffsetDateTime");
// Get the current hospital on duty
let current_hospital = sqlx::query_as!(
Hospital,
r#"
SELECT h.id, h.name
FROM hospitals h
JOIN shifts s ON h.id = s.hospital_id
WHERE s.start_time <= $1 AND s.end_time > $1
ORDER BY s.end_time ASC
LIMIT 1
"#,
now_offset
)
.fetch_optional(&*pool)
.await?;
// Get the last printed hospital ID
let mut last_id = LAST_HOSPITAL_ID.lock().unwrap();
// Check if the hospital has changed
let current_id = current_hospital.as_ref().map(|h| h.id);
let has_changed = *last_id != current_id;
// Only print if the hospital has changed
if has_changed {
match &current_hospital {
Some(hospital) => {
log_with_timestamp(&format!("Current hospital on duty: {} (ID: {})", hospital.name, hospital.id));
},
None => {
log_with_timestamp("No hospital currently on duty.");
}
}
// Update the last printed hospital ID
*last_id = current_id;
}
Ok(())
} }
fn get_end_of_shift(start: DateTime<Local>) -> DateTime<Local> { fn get_end_of_shift(start: DateTime<Local>) -> DateTime<Local> {