- 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.
418 lines
13 KiB
Rust
418 lines
13 KiB
Rust
use core::panic;
|
|
use std::io;
|
|
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 {
|
|
id: i32,
|
|
name: String,
|
|
}
|
|
|
|
// struct Schedule {
|
|
// from: DateTime<chrono_tz::Tz>,
|
|
// to: DateTime<chrono_tz::Tz>,
|
|
// }
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Static variable to store the last printed hospital ID
|
|
static LAST_HOSPITAL_ID: Mutex<Option<i32>> = Mutex::new(None);
|
|
|
|
pub async fn create_hospital_list(pool: Arc<PgPool>) -> Result<(), sqlx::Error> {
|
|
// Fetch hospitals from the database
|
|
let hospitals = sqlx::query_as!(
|
|
Hospital,
|
|
r#"
|
|
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::now();
|
|
let mut end: DateTime<Local> = get_end_of_shift(start);
|
|
|
|
log_with_timestamp(&format!("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("Which Hospital is currently on duty?");
|
|
|
|
// print out the hospitals
|
|
for hospital in &hospitals {
|
|
log_with_timestamp(&format!("{}) {}", hospital.id, hospital.name));
|
|
}
|
|
|
|
let mut hospital_id_input = String::new();
|
|
io::stdin()
|
|
.read_line(&mut hospital_id_input)
|
|
.expect("Failed to read the line");
|
|
let hospital_id_input = hospital_id_input.trim();
|
|
let hospital_id: i32 = match hospital_id_input.parse() {
|
|
Ok(num) => num,
|
|
Err(_) => {
|
|
log_with_timestamp("Please enter a valid number!");
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
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
|
|
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 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 ¤t_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> {
|
|
|
|
let end: DateTime<Local>;
|
|
|
|
// define weekday & hour
|
|
let weekday_str: String = start.format("%u").to_string();
|
|
let weekday: u32 = weekday_str.parse().unwrap();
|
|
let hour_str: String = start.format("%H").to_string();
|
|
let hour: u32 = hour_str.parse().unwrap();
|
|
|
|
// From Monday to Thursday before 7:00
|
|
if weekday >= 1 && weekday <= 4 && hour < 7 {
|
|
|
|
let next_day: DateTime<Local> = start;
|
|
let year: i32 = next_day.year();
|
|
let month: u32 = next_day.month();
|
|
let day: u32 = next_day.day();
|
|
|
|
let local_result: LocalResult<DateTime<Local>> = Local.with_ymd_and_hms(year, month, day, 7, 0, 0);
|
|
end = match local_result {
|
|
LocalResult::None => {
|
|
panic!("The specified date and time do not exist.");
|
|
},
|
|
LocalResult::Single(datetime) => datetime,
|
|
LocalResult::Ambiguous(datetime1, _datetime2, ) => {
|
|
datetime1
|
|
}
|
|
};
|
|
|
|
// From Monday to Wednesday after 7:00
|
|
} else if weekday >= 1 && weekday <= 3 && hour >= 7 {
|
|
|
|
let next_day: DateTime<Local> = start + Duration::days(1);
|
|
let year: i32 = next_day.year();
|
|
let month: u32 = next_day.month();
|
|
let day: u32 = next_day.day();
|
|
|
|
let local_result: LocalResult<DateTime<Local>> = Local.with_ymd_and_hms(year, month, day, 7, 0, 0);
|
|
end = match local_result {
|
|
LocalResult::None => {
|
|
panic!("The specified date and time do not exist.");
|
|
},
|
|
LocalResult::Single(datetime) => datetime,
|
|
LocalResult::Ambiguous(datetime1, _datetime2, ) => {
|
|
datetime1
|
|
}
|
|
};
|
|
|
|
} else if weekday == 4 && hour >= 7 {
|
|
|
|
let next_day: DateTime<Local> = start + Duration::days(1);
|
|
let year: i32 = next_day.year();
|
|
let month: u32 = next_day.month();
|
|
let day: u32 = next_day.day();
|
|
|
|
let local_result: LocalResult<DateTime<Local>> = Local.with_ymd_and_hms(year, month, day, 17, 0, 0);
|
|
end = match local_result {
|
|
LocalResult::None => {
|
|
panic!("The specified date and time do not exist.");
|
|
},
|
|
LocalResult::Single(datetime) => datetime,
|
|
LocalResult::Ambiguous(datetime1, _datetime2, ) => {
|
|
datetime1
|
|
}
|
|
};
|
|
|
|
} else if weekday == 5 && hour < 17 {
|
|
|
|
let next_day: DateTime<Local> = start;
|
|
let year: i32 = next_day.year();
|
|
let month: u32 = next_day.month();
|
|
let day: u32 = next_day.day();
|
|
|
|
let local_result: LocalResult<DateTime<Local>> = Local.with_ymd_and_hms(year, month, day, 17, 0, 0);
|
|
end = match local_result {
|
|
LocalResult::None => {
|
|
panic!("The specified date and time do not exist.");
|
|
},
|
|
LocalResult::Single(datetime) => datetime,
|
|
LocalResult::Ambiguous(datetime1, _datetime2, ) => {
|
|
datetime1
|
|
}
|
|
};
|
|
|
|
// Friday after 17:00
|
|
} else {
|
|
|
|
let count_days: i64 = 8 - weekday as i64;
|
|
let next_day: DateTime<Local> = start + Duration::days(count_days);
|
|
let year: i32 = next_day.year();
|
|
let month: u32 = next_day.month();
|
|
let day: u32 = next_day.day();
|
|
|
|
let local_result: LocalResult<DateTime<Local>> = Local.with_ymd_and_hms(year, month, day, 7, 0, 0);
|
|
end = match local_result {
|
|
LocalResult::None => {
|
|
panic!("The specified date and time do not exist.");
|
|
},
|
|
LocalResult::Single(datetime) => datetime,
|
|
LocalResult::Ambiguous(datetime1, _datetime2, ) => {
|
|
datetime1
|
|
}
|
|
};
|
|
}
|
|
|
|
end
|
|
} |