feat: implement basic structure of provider applicants page
This commit is contained in:
1 parent
3baee8cac7
commit
469f192feb
5 files changed
+508
-26
No files matched your search
@@ -25,7 +25,6 @@ leptos_oidc = "0.9.0"
|
||||
base64 = "0.22"
|
||||
chrono = "0.4"
|
||||
leptos_animate = "0.1"
|
||||
|
||||
macros = { path = "./macros" }
|
||||
traits = { path = "./traits" }
|
||||
icondata = { version = "0.7.0", features = ["font-awesome"] }
|
||||
|
||||
+6
-5
@@ -93,11 +93,11 @@ fn ProviderRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
</ParentRoute>
|
||||
<Route path=path!("/callback") view=AuthCallbackPage />
|
||||
<Route path=path!("/profile") view=ProviderContactPage />
|
||||
<Route path=path!("/scholarships") view=FormClosedPage />
|
||||
// <ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
|
||||
// <Route path=path!(":id") view=ScholarshipInfoPage />
|
||||
// <Route path=path!("") view=ScholarshipInfoPage />
|
||||
// </ParentRoute>
|
||||
// <Route path=path!("/scholarships") view=FormClosedPage />
|
||||
<ParentRoute path=path!("/scholarships") view=ScholarshipInfoPage>
|
||||
<Route path=path!(":id") view=ScholarshipInfoPage />
|
||||
<Route path=path!("") view=ScholarshipInfoPage />
|
||||
</ParentRoute>
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
@@ -158,6 +158,7 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
path=path!("additional/specifics")
|
||||
view=student::StudentSpecificRequirementsPage
|
||||
/>
|
||||
<Route path=path!("testing") view=student::StudentTestDisplay />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
|
||||
@@ -10,7 +10,9 @@ use std::collections::HashMap;
|
||||
#[component]
|
||||
pub fn ValueDisplay(
|
||||
#[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>>,
|
||||
) -> impl IntoView {
|
||||
let data_value = Memo::new(move |_| {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::server_fn::codec::Json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
mod imports {
|
||||
pub use super::super::MAIN_TABLE_NAME;
|
||||
pub use crate::common::ValueType;
|
||||
pub use crate::pages::api::tokens::validate_and_get_token_info;
|
||||
pub use crate::utils::server::*;
|
||||
pub use aws_sdk_dynamodb::error::ProvideErrorMetadata;
|
||||
pub use aws_sdk_dynamodb::types::AttributeValue;
|
||||
@@ -16,7 +18,7 @@ mod imports {
|
||||
pub async fn put_student_data(
|
||||
subject: String,
|
||||
data_type: String,
|
||||
data_map: std::collections::HashMap<String, crate::common::ValueType>,
|
||||
data_map: HashMap<String, crate::common::ValueType>,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
@@ -52,7 +54,7 @@ pub async fn put_student_data(
|
||||
pub async fn get_student_data(
|
||||
subject: String,
|
||||
data_type: String,
|
||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||
) -> Result<HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
@@ -99,7 +101,7 @@ pub async fn get_student_data(
|
||||
#[server]
|
||||
pub async fn get_all_student_data(
|
||||
subject: String,
|
||||
) -> Result<std::collections::HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||
) -> Result<HashMap<String, crate::common::ValueType>, ServerFnError> {
|
||||
use imports::*;
|
||||
|
||||
let client = create_dynamo_client().await;
|
||||
@@ -131,3 +133,80 @@ pub async fn get_all_student_data(
|
||||
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::components::{Banner, Loading};
|
||||
use crate::common::{ComparisonData, ExpandableInfo, SchemaNode, SchemaType, ValueType};
|
||||
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_oidc::AuthSignal;
|
||||
use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated};
|
||||
use leptos_router::components::Outlet;
|
||||
use leptos_router::hooks::use_navigate;
|
||||
use leptos_router::hooks::{query_signal, use_navigate};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[component]
|
||||
pub fn ApplicantsPageFallback() -> impl IntoView {
|
||||
@@ -28,17 +35,25 @@ pub fn ApplicantsPageShell() -> impl IntoView {
|
||||
// each of which is still considered a single page.
|
||||
|
||||
view! {
|
||||
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/providers" />
|
||||
<div class="flex flex-row mt-5">
|
||||
<div class="flex-1" />
|
||||
<div class="flex-3 flex flex-row">
|
||||
<ApplicantsScholarshipList />
|
||||
<div class="flex-3 text-center">
|
||||
<Outlet />
|
||||
<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-1" />
|
||||
<div class="flex-3 flex flex-row items-start">
|
||||
<ApplicantsScholarshipList />
|
||||
<div class="flex-3 text-center">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1" />
|
||||
</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
|
||||
// because this makes it Send + Sync.
|
||||
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! {
|
||||
@@ -175,4 +191,389 @@ fn ApplicantsScholarshipEntry(
|
||||
}
|
||||
|
||||
#[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