-
-
+
+
}
}
@@ -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
,
+ }
+
+ // 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::();
+ let auth = expect_context::();
+ 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>,
+ ExpandableInfo,
+ Vec,
+ ),
+ 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! {
+
+
+ {move || {
+ resource
+ .get()
+ .map(|result| {
+ match result {
+ Ok((students, scholarship, requirements_list)) => {
+ view! {
+ "Showing students here."
+
+ }
+ .into_any()
+ }
+ Err(e) => {
+ view! {
+
+ "Error while getting eligible students: "{e.to_string()}
+
+ }
+ .into_any()
+ }
+ }
+ })
+ }}
+
+
+ }
+}
+
+#[component]
+fn ApplicantsStudentListView(
+ #[prop(into)] students: Signal>>,
+ #[prop(into)] scholarship: Signal,
+ #[prop(into)] requirements: Signal>,
+) -> 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::("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>,
+ scholarship: ExpandableInfo,
+ requirements: Vec,
+ ) -> Result>, 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::>();
+
+ Some(string_list)
+ })
+ .collect::>>();
+
+ // 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::>()
+ })
+ .collect::>>();
+
+ // 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::>>();
+
+ // 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! {
+
+
+ "Error checking student eligibility: "{error_msg}
+
+
+
+ {first_name}
+ " "
+ {last_name}
+
+ }
+ }
+ />
+
+ }
+}
+
+#[component]
+fn StudentInformationDialog(
+ #[prop(into)] student_id: Signal