diff --git a/src/pages/loaner_page.rs b/src/pages/loaner_page.rs index 416e09a..9e224e0 100644 --- a/src/pages/loaner_page.rs +++ b/src/pages/loaner_page.rs @@ -1,21 +1,22 @@ #[cfg(feature = "ssr")] -use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata}; +use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue}; #[cfg(feature = "ssr")] use serde_dynamo::{from_item, to_item}; -use chrono::{DateTime, Utc}; -use leptos::either::EitherOf3; +use leptos::either::{Either, EitherOf3}; use leptos::prelude::*; use serde::{Deserialize, Serialize}; use leptos::logging::log; use leptos_router::params::Params; -use leptos_router::hooks::{use_navigate, use_params, use_query}; +use leptos_router::hooks::{use_navigate, use_params}; use leptos::Params; use traits::{AsReactive, ReactiveCapture}; use crate::common::{ExpandableInfo, ValueType}; use crate::components::{ActionButton, Banner, DashboardButton, OutlinedTextField, Panel, Row, Select}; use std::collections::HashMap; +use chrono::{DateTime, FixedOffset, TimeZone, Utc}; +use leptos::html::Dialog; #[derive(Params, PartialEq)] struct LoanerParams { @@ -30,6 +31,8 @@ struct LoanerReturnOutput { first_name: String, /// The last name of the student that is borrowing. last_name: String, + /// The date that the student borrowed the loaner. + date: String, } #[server] @@ -43,16 +46,18 @@ async fn get_loaners_return_list() -> Result, ServerFnEr match dbclient .scan() .table_name("loaner-info") - .projection_expression("subject, first_name, last_name") + .projection_expression("subject, first_name, last_name, date_taken") .send() .await { Ok(output) => { if let Some(items) = output.items { Ok(items.into_iter().map(|item| { let expandable: ExpandableInfo = from_item(item).unwrap(); + // This set of function calls is ridiculous, we need to make a better way to do this. LoanerReturnOutput { first_name: expandable.data.get("first_name").unwrap().as_string().unwrap_or_default().unwrap_or_default(), last_name: expandable.data.get("last_name").unwrap().as_string().unwrap_or_default().unwrap_or_default(), + date: expandable.data.get("date_taken").unwrap().as_string().unwrap_or_default().unwrap_or_default(), subject: expandable.subject } }).collect()) @@ -68,11 +73,37 @@ async fn get_loaners_return_list() -> Result, ServerFnEr } } +#[server] +async fn check_in_loaner(student: LoanerReturnOutput) -> Result<(), ServerFnError> { + let dbclient = Client::new(&aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await); + + match dbclient + .delete_item() + .table_name("loaner-info") + .key("subject", AttributeValue::S(student.subject)) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => { + log!("{:?}", err); + let msg = err.message().unwrap_or("Unknown error"); + Err(ServerFnError::new(msg)) + } + } +} + #[server] async fn create_borrow_entry(input: ExpandableInfo) -> Result<(), ServerFnError> { let mut input = input; let dbclient = Client::new(&aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await); - + + let current_time = chrono::Utc::now().format("%H:%M, %m/%d/%Y").to_string(); + log!("Creating entry with timestamp {}", current_time); + + // Add the timestamp to the data map as a string and then push it to the database. + input.data.insert("date_taken".to_string(), ValueType::String(Some(current_time.clone()))); + loop { let item: serde_dynamo::Item = to_item(&input)?; match dbclient @@ -88,7 +119,7 @@ async fn create_borrow_entry(input: ExpandableInfo) -> Result<(), ServerFnError> log!("{:?}", err); match err.code() { Some("ConditionalCheckFailedException") => { - // Retry database write until we have a good subject ID. + // Retry database writes until we have a good subject ID. log!("Failed conditional check, forcing new subject ID."); input.subject = uuid::Uuid::new_v4().to_string(); continue @@ -105,7 +136,7 @@ async fn create_borrow_entry(input: ExpandableInfo) -> Result<(), ServerFnError> /// # Loaner Page /// -/// This page is going to temporarily piggy-back on the general scholarship app, at least until +/// This page is going to temporarily piggyback on the general scholarship app, at least until /// we spin off the components used here into a new standalone crate. /// /// The loaner app handles management of Chromebook loaners to students. It was originally hosted @@ -187,11 +218,15 @@ pub fn LoanerBorrowForm() -> impl IntoView { let create_entry_action = ServerAction::::new(); let navigate = use_navigate(); - let temp = ExpandableInfo::new("test"); + let temp = ExpandableInfo::new(uuid::Uuid::new_v4().to_string()); let temp_reactive = temp.as_reactive(); let elements_disabled = RwSignal::new(false); view! { +
+

"Borrower Information"

+

"Please fill out the form below."

+
impl IntoView { #[component] pub fn LoanerReturnForm() -> impl IntoView { + // Register server actions + let check_in_action = ServerAction::::new(); + // Register server resources + let return_list_refresh = RwSignal::new(0); let return_list_resource: Resource> = Resource::new( - move || (), + move || return_list_refresh.get(), async |_| get_loaners_return_list().await.unwrap_or_else(|e| { log!("There was an error getting the loaner list: {}", e); vec![] }) ); - + + let dialog_ref = NodeRef::::new(); + let chosen_student = RwSignal::new(None::); + let dialog_disabled = RwSignal::new(false); + + // Create an effect to run after check-in - refreshes the list with another scan request. + // This could be beneficial if we ever decided to make multiple loaner kiosks (think SAT days) + Effect::new(move || { + if let Some(Ok(_)) = check_in_action.value().get() { + // Check the local loaner list and remove the indicated student from it. + return_list_refresh.update(|n| *n += 1); + if let Some(dialog) = dialog_ref.get() { + dialog.close(); + } + } + }); + + // We can't use a Memo in our custom components for the disabled signal. + Effect::new(move || { + if check_in_action.pending().get() { + dialog_disabled.set(true); + } else { + dialog_disabled.set(false); + } + }); + + // Handles checking in a student. + let dialog_check_in = move |_| { + chosen_student.with(|opt_student| { + if let Some(student) = opt_student.as_ref() { + check_in_action.dispatch(CheckInLoaner { student: student.clone() }); + } + }) + }; + + // Handles closing the dialog menu. + let close_dialog = move |_| { + if let Some(dialog) = dialog_ref.get() { + dialog.close(); + } + }; + view! { + +

"Confirmation"

+

+ {move || chosen_student.with(|opt_student| { + if let Some(student) = opt_student.as_ref() { + format!("Are you sure want to check in {} {}?", student.first_name, student.last_name) + } else { + "There is no student selected.".to_string() + } + })} +

+ "Close" + "Check In" +
+
+

"Student List"

+

"Please select your name from the list."

+
-
- {move || return_list_resource.get() - .map(move |loaner_list| { - loaner_list.into_iter().map(move |student| { - view! { - {student.first_name}" "{student.last_name} - } - }).collect_view() - }) - } +
+ {move || { + match return_list_resource.get() { + Some(loaner_list) => { + let list_view = loaner_list.iter().map(|student| { + let on_click = { + let student_click = student.clone(); + move |_| { + log!("Running on_click using student {:?}", student_click); + if let Some(dialog) = dialog_ref.get() { + chosen_student.set(Some(student_click.clone())); + dialog.show_modal().expect("Couldn't show modal dialog."); + } + } + }; + + let naive_time = chrono::NaiveDateTime::parse_from_str(&student.date, "%H:%M, %m/%d/%Y") + .unwrap(); + let time_zone = FixedOffset::west_opt(5 * 3600).unwrap(); + let student_date = time_zone.from_utc_datetime(&naive_time) + .format("%l:%M %p, %m/%d/%Y") + .to_string(); + + log!("Found current date {:?}", student_date); + + view! { +
+
+ {student.first_name.clone()}" "{student.last_name.clone()} +
+
+ {student_date.clone()} +
+
+ } + }).collect_view(); + + Either::Left(list_view) + }, + None => Either::Right("No loaners found.".into_view()) + } + }}
}