Create provider-side applicant forms #77
No files matched your search
@@ -25,7 +25,6 @@ leptos_oidc = "0.9.0"
|
|||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
leptos_animate = "0.1"
|
leptos_animate = "0.1"
|
||||||
|
|
||||||
macros = { path = "./macros" }
|
macros = { path = "./macros" }
|
||||||
traits = { path = "./traits" }
|
traits = { path = "./traits" }
|
||||||
icondata = { version = "0.7.0", features = ["font-awesome"] }
|
icondata = { version = "0.7.0", features = ["font-awesome"] }
|
||||||
|
|||||||
+6
-5
@@ -93,11 +93,11 @@ fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
<Route path=path!("/callback") view=AuthCallbackPage />
|
<Route path=path!("/callback") view=AuthCallbackPage />
|
||||||
<Route path=path!("/profile") view=ProviderContactPage />
|
<Route path=path!("/profile") view=ProviderContactPage />
|
||||||
<Route path=path!("/scholarships") view=FormClosedPage />
|
// <Route path=path!("/scholarships") view=FormClosedPage />
|
||||||
// <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
|
<ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
|
||||||
// <Route path=path!(":id") view=ScholarshipInfoPage />
|
<Route path=path!(":id") view=ScholarshipInfoPage />
|
||||||
// <Route path=path!("") view=ScholarshipInfoPage />
|
<Route path=path!("") view=ScholarshipInfoPage />
|
||||||
// </ParentRoute>
|
</ParentRoute>
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
}
|
}
|
||||||
.into_inner()
|
.into_inner()
|
||||||
@@ -158,6 +158,7 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
|||||||
path=path!("additional/specifics")
|
path=path!("additional/specifics")
|
||||||
view=student::StudentSpecificRequirementsPage
|
view=student::StudentSpecificRequirementsPage
|
||||||
/>
|
/>
|
||||||
|
<Route path=path!("testing") view=student::StudentTestDisplay />
|
||||||
</ParentRoute>
|
</ParentRoute>
|
||||||
}
|
}
|
||||||
.into_inner()
|
.into_inner()
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ use std::collections::HashMap;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn ValueDisplay(
|
pub fn ValueDisplay(
|
||||||
#[prop(into)] schema: Signal<SchemaNode>,
|
#[prop(into)] schema: Signal<SchemaNode>,
|
||||||
#[prop(into, optional)] data_member: Signal<String>,
|
/// The starting member. Starts display from this member in the `data_map`, if it exists.
|
||||||
|
#[prop(into, optional)]
|
||||||
|
data_member: Signal<String>,
|
||||||
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let data_value = Memo::new(move |_| {
|
let data_value = Memo::new(move |_| {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::server_fn::codec::Json;
|
use leptos::server_fn::codec::Json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
mod imports {
|
mod imports {
|
||||||
pub use super::super::MAIN_TABLE_NAME;
|
pub use super::super::MAIN_TABLE_NAME;
|
||||||
pub use crate::common::ValueType;
|
pub use crate::common::ValueType;
|
||||||
|
pub use crate::pages::api::tokens::validate_and_get_token_info;
|
||||||
pub use crate::utils::server::*;
|
pub use crate::utils::server::*;
|
||||||
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||||
pub use aws_sdk_dynamodb::types::AttributeValue;
|
pub use aws_sdk_dynamodb::types::AttributeValue;
|
||||||
@@ -16,7 +18,7 @@ mod imports {
|
|||||||
pub async fn put_student_data(
|
pub async fn put_student_data(
|
||||||
subject: String,
|
subject: String,
|
||||||
data_type: String,
|
data_type: String,
|
||||||
data_map: std::collections::HashMap<String, crate::common::ValueType>,
|
data_map: HashMap<String, crate::common::ValueType>,
|
||||||
) -> Result<(), ServerFnError> {
|
) -> Result<(), ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ pub async fn put_student_data(
|
|||||||
pub async fn get_student_data(
|
pub async fn get_student_data(
|
||||||
subject: String,
|
subject: String,
|
||||||
data_type: String,
|
data_type: String,
|
||||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
) -> Result<HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
@@ -99,7 +101,7 @@ pub async fn get_student_data(
|
|||||||
#[server]
|
#[server]
|
||||||
pub async fn get_all_student_data(
|
pub async fn get_all_student_data(
|
||||||
subject: String,
|
subject: String,
|
||||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
) -> Result<HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||||
use imports::*;
|
use imports::*;
|
||||||
|
|
||||||
let client = create_dynamo_client().await;
|
let client = create_dynamo_client().await;
|
||||||
@@ -131,3 +133,80 @@ pub async fn get_all_student_data(
|
|||||||
ServerFnError::new(msg)
|
ServerFnError::new(msg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub async fn get_completed_students(
|
||||||
|
access_token: String,
|
||||||
|
) -> Result<HashMap<String, HashMap<String, crate::common::ValueType>>, ServerFnError> {
|
||||||
|
use imports::*;
|
||||||
|
|
||||||
|
// We want to get all student information. The requirements are that the students have completed
|
||||||
|
// the demographics form - everything else may bed from this.
|
||||||
|
// The easiest way is to get all the information and filter on this side, instead of bookkeeping
|
||||||
|
// on the database's side.
|
||||||
|
|
||||||
|
// We want to verify the access token first, and make sure that the user has the correct access.
|
||||||
|
let claims = validate_and_get_token_info(access_token).await?;
|
||||||
|
if !claims.groups.contains(&"ScholarshipProviders".to_string()) {
|
||||||
|
return Err(ServerFnError::new(
|
||||||
|
"User is not in the ScholarshipProviders group",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = create_dynamo_client().await;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.scan()
|
||||||
|
.table_name(MAIN_TABLE_NAME)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
let msg = err.message().unwrap_or("Unknown error occurred");
|
||||||
|
error!("{}", msg);
|
||||||
|
ServerFnError::new(msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(items) = response.items else {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut output = HashMap::<String, HashMap<String, ValueType>>::new();
|
||||||
|
|
||||||
|
items.into_iter().for_each(|mut form_info| {
|
||||||
|
let student_id_full = form_info
|
||||||
|
.get("HK")
|
||||||
|
.map(|v| v.as_s().cloned().unwrap_or_default())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let student_id = student_id_full.split("STUDENT#").collect::<String>();
|
||||||
|
|
||||||
|
form_info.remove("HK");
|
||||||
|
form_info.remove("SK");
|
||||||
|
|
||||||
|
let form_info_convert = form_info
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (k, ValueType::from(&v)))
|
||||||
|
.collect::<HashMap<String, ValueType>>();
|
||||||
|
|
||||||
|
// We want to insert all the remaining information into the output map
|
||||||
|
output
|
||||||
|
.entry(student_id)
|
||||||
|
.and_modify(|v| {
|
||||||
|
v.extend(form_info_convert.clone());
|
||||||
|
})
|
||||||
|
.or_insert(form_info_convert);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't love this, but it does verify that the student has completed the demographics form
|
||||||
|
let output = output
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(k, student_info)| {
|
||||||
|
student_info
|
||||||
|
.get("first_name")
|
||||||
|
.and_then(|_| Some((k, student_info.clone())))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
use crate::common::{ExpandableInfo, ValueType};
|
use crate::common::{ComparisonData, ExpandableInfo, SchemaNode, SchemaType, ValueType};
|
||||||
use crate::components::{Banner, Loading};
|
use crate::components::{Banner, Loading, ValueDisplay};
|
||||||
|
use crate::pages::UnauthenticatedPage;
|
||||||
|
use crate::pages::api::get_comparison_info;
|
||||||
|
use crate::pages::api::students::{get_all_student_data, get_student_data};
|
||||||
|
use leptos::ev::{Event, MouseEvent};
|
||||||
|
use leptos::html::Dialog;
|
||||||
|
use leptos::logging::debug_log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_oidc::AuthSignal;
|
use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated};
|
||||||
use leptos_router::components::Outlet;
|
use leptos_router::components::Outlet;
|
||||||
use leptos_router::hooks::use_navigate;
|
use leptos_router::hooks::{query_signal, use_navigate};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ApplicantsPageFallback() -> impl IntoView {
|
pub fn ApplicantsPageFallback() -> impl IntoView {
|
||||||
@@ -28,10 +35,16 @@ pub fn ApplicantsPageShell() -> impl IntoView {
|
|||||||
// each of which is still considered a single page.
|
// each of which is still considered a single page.
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/providers" />
|
<AuthLoaded fallback=Loading>
|
||||||
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||||
|
<Banner
|
||||||
|
title="R15 Scholarships"
|
||||||
|
logo="/PHS_Stacked_Acronym.png"
|
||||||
|
path="/providers"
|
||||||
|
/>
|
||||||
<div class="flex flex-row mt-5">
|
<div class="flex flex-row mt-5">
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<div class="flex-3 flex flex-row">
|
<div class="flex-3 flex flex-row items-start">
|
||||||
<ApplicantsScholarshipList />
|
<ApplicantsScholarshipList />
|
||||||
<div class="flex-3 text-center">
|
<div class="flex-3 text-center">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -39,6 +52,8 @@ pub fn ApplicantsPageShell() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
|
</Authenticated>
|
||||||
|
</AuthLoaded>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +113,8 @@ fn ApplicantsScholarshipList() -> impl IntoView {
|
|||||||
// It avoids having to define it within the Transition component. It's wrapped in a Callback
|
// It avoids having to define it within the Transition component. It's wrapped in a Callback
|
||||||
// because this makes it Send + Sync.
|
// because this makes it Send + Sync.
|
||||||
let on_view_click = Callback::new(move |scholarship_id: String| {
|
let on_view_click = Callback::new(move |scholarship_id: String| {
|
||||||
navigate(&*scholarship_id, Default::default());
|
let path = format!("/providers/applicants/{}", scholarship_id);
|
||||||
|
navigate(&path, Default::default());
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@@ -175,4 +191,389 @@ fn ApplicantsScholarshipEntry(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn ApplicantsStudentList() -> impl IntoView {}
|
pub fn ApplicantsStudentList() -> impl IntoView {
|
||||||
|
use crate::pages::api::get_scholarship_info;
|
||||||
|
use crate::pages::api::students::get_completed_students;
|
||||||
|
use leptos_router::hooks::use_params;
|
||||||
|
use leptos_router::params::Params;
|
||||||
|
|
||||||
|
/// The parameters for the page that displays this component. This may be moved to the page-level
|
||||||
|
/// rather than the component-level.
|
||||||
|
#[derive(Params, PartialEq)]
|
||||||
|
struct StudentListParams {
|
||||||
|
scholarship_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is in charge of finding all student information and running the comparisons.
|
||||||
|
// It will then display all the eligible students.
|
||||||
|
|
||||||
|
let params = use_params::<StudentListParams>();
|
||||||
|
let auth = expect_context::<AuthSignal>();
|
||||||
|
let access_token =
|
||||||
|
Memo::new(move |_| auth.with(|a| a.authenticated().map(|a| a.access_token())));
|
||||||
|
|
||||||
|
let scholarship_id = Memo::new(move |_| {
|
||||||
|
params
|
||||||
|
.read()
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|params| params.scholarship_id.clone())
|
||||||
|
});
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
async fn get_eligibility_info(
|
||||||
|
token: String,
|
||||||
|
scholarship_id: String,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
HashMap<String, HashMap<String, ValueType>>,
|
||||||
|
ExpandableInfo,
|
||||||
|
Vec<ComparisonData>,
|
||||||
|
),
|
||||||
|
ServerFnError,
|
||||||
|
> {
|
||||||
|
let (students, scholarships, requirements_list) = tokio::join!(
|
||||||
|
get_completed_students(token),
|
||||||
|
get_scholarship_info(scholarship_id),
|
||||||
|
get_comparison_info()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((students?, scholarships?, requirements_list?))
|
||||||
|
}
|
||||||
|
|
||||||
|
let resource = Resource::new(
|
||||||
|
move || (access_token.get(), scholarship_id.get()),
|
||||||
|
move |(access_token, scholarship_id)| async move {
|
||||||
|
let Some(token) = access_token else {
|
||||||
|
return Err(ServerFnError::new("Couldn't find access token"));
|
||||||
|
};
|
||||||
|
let Some(scholarship_id) = scholarship_id else {
|
||||||
|
return Err(ServerFnError::new("Couldn't find scholarship ID"));
|
||||||
|
};
|
||||||
|
|
||||||
|
get_eligibility_info(token, scholarship_id).await
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<Suspense fallback=Loading>
|
||||||
|
{move || {
|
||||||
|
resource
|
||||||
|
.get()
|
||||||
|
.map(|result| {
|
||||||
|
match result {
|
||||||
|
Ok((students, scholarship, requirements_list)) => {
|
||||||
|
view! {
|
||||||
|
<div>"Showing students here."</div>
|
||||||
|
<ApplicantsStudentListView
|
||||||
|
students=students
|
||||||
|
scholarship=scholarship
|
||||||
|
requirements=requirements_list
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
"Error while getting eligible students: "{e.to_string()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ApplicantsStudentListView(
|
||||||
|
#[prop(into)] students: Signal<HashMap<String, HashMap<String, ValueType>>>,
|
||||||
|
#[prop(into)] scholarship: Signal<ExpandableInfo>,
|
||||||
|
#[prop(into)] requirements: Signal<Vec<ComparisonData>>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let eligible_students = RwSignal::new(HashMap::new());
|
||||||
|
let error_msg = RwSignal::new(String::new());
|
||||||
|
// let (current_student_id, set_current_student_id) = query_signal::<String>("student_id");
|
||||||
|
let current_student_id = RwSignal::new(None);
|
||||||
|
let navigate_student = Callback::new(move |id: String| {
|
||||||
|
current_student_id.set(Some(id));
|
||||||
|
});
|
||||||
|
let dialog_visible =
|
||||||
|
Signal::derive(move || !current_student_id.get().unwrap_or_default().is_empty());
|
||||||
|
let on_dialog_close = Callback::new(move |_| {
|
||||||
|
current_student_id.set(None);
|
||||||
|
});
|
||||||
|
|
||||||
|
fn get_eligible_students(
|
||||||
|
students: HashMap<String, HashMap<String, ValueType>>,
|
||||||
|
scholarship: ExpandableInfo,
|
||||||
|
requirements: Vec<ComparisonData>,
|
||||||
|
) -> Result<HashMap<String, HashMap<String, ValueType>>, Error> {
|
||||||
|
let requires_essay = !scholarship
|
||||||
|
.data
|
||||||
|
.get("essay_prompt")
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.as_string()
|
||||||
|
.map_err(|_| "Couldn't get essay as a string")?
|
||||||
|
.unwrap_or_default()
|
||||||
|
.is_empty();
|
||||||
|
|
||||||
|
// Get the full map of requirements. Each key contains a list of IDs.
|
||||||
|
let scholarship_requirements = scholarship
|
||||||
|
.data
|
||||||
|
.get("requirements")
|
||||||
|
.map(|val| val.as_map().ok()?)
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Get the lists of requirements. Maps into string IDs and discards invalid ValueTypes.
|
||||||
|
let requirement_lists = scholarship_requirements
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(_, list_val)| {
|
||||||
|
let list = list_val.as_list().ok().flatten()?;
|
||||||
|
let string_list = list
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|v| v.as_string().ok().flatten())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
Some(string_list)
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<String>>>();
|
||||||
|
|
||||||
|
// Resolve the requirement IDs. Discards all IDs that do not resolve.
|
||||||
|
let resolved_requirements = requirement_lists
|
||||||
|
.into_iter()
|
||||||
|
.map(|list| {
|
||||||
|
list.into_iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
requirements
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.find(|requirement| requirement.id == id)
|
||||||
|
})
|
||||||
|
.collect::<Vec<ComparisonData>>()
|
||||||
|
})
|
||||||
|
.collect::<Vec<Vec<ComparisonData>>>();
|
||||||
|
|
||||||
|
// Check student eligibility. These are all students that have completed the demographics
|
||||||
|
// forms.
|
||||||
|
let eligible_students = students
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(id, student)| {
|
||||||
|
// The student needs to pass one requirement from each list.
|
||||||
|
let result = resolved_requirements.iter().all(|list| {
|
||||||
|
if list.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.iter()
|
||||||
|
.any(|requirement| requirement.compare(&student).unwrap_or(false))
|
||||||
|
});
|
||||||
|
|
||||||
|
if result { Some((id, student)) } else { None }
|
||||||
|
})
|
||||||
|
.collect::<HashMap<String, HashMap<String, ValueType>>>();
|
||||||
|
|
||||||
|
// debug_log!("Total number of eligible students: {}", eligible_students.len());
|
||||||
|
|
||||||
|
Ok(eligible_students)
|
||||||
|
}
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
debug_log!("Running eligibility check on students");
|
||||||
|
let eligible_res =
|
||||||
|
get_eligible_students(students.get(), scholarship.get(), requirements.get());
|
||||||
|
|
||||||
|
match eligible_res {
|
||||||
|
Ok(list) => {
|
||||||
|
debug_log!("Total number of eligible students: {}", list.len());
|
||||||
|
eligible_students.set(list);
|
||||||
|
}
|
||||||
|
Err(e) => error_msg.set(format!("Failed to get eligible students list: {e}")),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is actually just a display of all the information from the students list.
|
||||||
|
// As defined in the ApplicantsStudentList component, this will already be wrapped in a
|
||||||
|
// flex container.
|
||||||
|
view! {
|
||||||
|
<StudentInformationDialog
|
||||||
|
student_id=current_student_id
|
||||||
|
visible=dialog_visible
|
||||||
|
on_close=on_dialog_close
|
||||||
|
/>
|
||||||
|
<Show when=move || !error_msg.get().is_empty()>
|
||||||
|
<div>"Error checking student eligibility: "{error_msg}</div>
|
||||||
|
</Show>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<For
|
||||||
|
each=move || eligible_students.get()
|
||||||
|
key=|(id, _)| id.clone()
|
||||||
|
children=move |(student_id, student)| {
|
||||||
|
let student = StoredValue::new(student);
|
||||||
|
let first_name = Memo::new(move |_| {
|
||||||
|
student
|
||||||
|
.get_value()
|
||||||
|
.get("first_name")
|
||||||
|
.map(|n| n.as_string().ok().flatten())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let last_name = Memo::new(move |_| {
|
||||||
|
student
|
||||||
|
.get_value()
|
||||||
|
.get("last_name")
|
||||||
|
.map(|n| n.as_string().ok().flatten())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
let on_click = move |_| {
|
||||||
|
navigate_student.run(student_id.clone());
|
||||||
|
};
|
||||||
|
// For now, we'll just display each student's first and last name.
|
||||||
|
// Now I'll add a container with some links. These links will just be appended
|
||||||
|
// to the end of the current path.
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="text-lg" on:click=on_click>
|
||||||
|
{first_name}
|
||||||
|
" "
|
||||||
|
{last_name}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn StudentInformationDialog(
|
||||||
|
#[prop(into)] student_id: Signal<Option<String>>,
|
||||||
|
// #[prop(into)] scholarship_info: StoredValue<ExpandableInfo>,
|
||||||
|
#[prop(into)] visible: Signal<bool>,
|
||||||
|
#[prop(into)] on_close: Callback<()>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
// This component needs to just display all student information.
|
||||||
|
// Specifically, it needs to know what information to display, like whether the scholarship
|
||||||
|
// requested an essay or financial information.
|
||||||
|
|
||||||
|
let dialog_ref = NodeRef::<Dialog>::new();
|
||||||
|
|
||||||
|
let demographics_schema = StoredValue::new(
|
||||||
|
SchemaNode::new(SchemaType::Map)
|
||||||
|
.header("Demographics Information")
|
||||||
|
.child(
|
||||||
|
"first_name",
|
||||||
|
SchemaNode::new(SchemaType::String).header("First Name"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"last_name",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Last Name"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"dob",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Date of Birth"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"gender",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Gender"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"phone_number",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Preferred Phone Number"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"email",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Preferred Email Address"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"street_address",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Street Address"),
|
||||||
|
)
|
||||||
|
.child("town", SchemaNode::new(SchemaType::String).header("Town")),
|
||||||
|
);
|
||||||
|
|
||||||
|
let sports_schema = StoredValue::new(
|
||||||
|
SchemaNode::new(SchemaType::List)
|
||||||
|
.header("Sports Activities")
|
||||||
|
.item_template(
|
||||||
|
SchemaNode::new(SchemaType::Map)
|
||||||
|
.header("Sport")
|
||||||
|
.child(
|
||||||
|
"sport_name",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Sport Name"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
"achievements",
|
||||||
|
SchemaNode::new(SchemaType::String).header("Special Achievements"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let extracurricular_schema =
|
||||||
|
StoredValue::new(SchemaNode::new(SchemaType::Map).header("Extracurricular Activities"));
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
if let Some(dialog) = dialog_ref.get() {
|
||||||
|
let open = visible.get();
|
||||||
|
if open {
|
||||||
|
let _ = dialog.show_modal();
|
||||||
|
debug_log!("Modal should be visible.");
|
||||||
|
} else {
|
||||||
|
dialog.close();
|
||||||
|
debug_log!("Closed modal.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<dialog
|
||||||
|
node_ref=dialog_ref
|
||||||
|
on:cancel=move |_: Event| on_close.run(())
|
||||||
|
on:click=move |_| on_close.run(())
|
||||||
|
class="w-1/2 m-auto rounded-lg shadow-xl/50
|
||||||
|
backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter
|
||||||
|
[open]:flex flex-col items-center transition-all duration-200"
|
||||||
|
>
|
||||||
|
<div class="p-5" on:click=move |e: MouseEvent| e.stop_propagation()>
|
||||||
|
<Suspense fallback=Loading>
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
get_student_data(
|
||||||
|
student_id.get().unwrap_or_default(),
|
||||||
|
"demographics".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|student_info| {
|
||||||
|
view! {
|
||||||
|
<ValueDisplay
|
||||||
|
schema=demographics_schema.get_value()
|
||||||
|
data_map=student_info
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
{move || Suspend::new(async move {
|
||||||
|
get_student_data(
|
||||||
|
student_id.get().unwrap_or_default(),
|
||||||
|
"athletics".to_string(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map(|student_info| {
|
||||||
|
view! {
|
||||||
|
<ValueDisplay
|
||||||
|
schema=sports_schema.get_value()
|
||||||
|
data_map=student_info
|
||||||
|
data_member="sports_participation"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user