use crate::common::{
ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode,
SchemaType, ValueType,
};
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
use crate::pages::{StudentInformationDialog, UnauthenticatedPage};
use crate::pages::api::get_comparison_info;
use crate::pages::api::students::{GetStudentFiles, admin_get_all_input_files, get_student_data};
use base64::Engine;
use leptos::ev::{Event, MouseEvent};
use leptos::html::Dialog;
use leptos::logging::debug_log;
use leptos::prelude::*;
use leptos::wasm_bindgen::JsCast;
use leptos::web_sys;
use leptos::web_sys::{HtmlAnchorElement, ScrollBehavior};
use leptos_icons::Icon;
use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated};
use leptos_router::components::Outlet;
use leptos_router::hooks::use_navigate;
use std::collections::HashMap;
use leptos_router::NavigateOptions;
#[component]
pub fn AdminApplicantsPageFallback() -> impl IntoView {
view! {
"Scholarship Applicants"
"Please select a scholarship from the left to view the eligible applicants."
}
}
#[component]
pub fn AdminApplicantsPageShell() -> impl IntoView {
// This shell needs to simply contain everything that will stay consistent across paths.
// The path will change to contain each scholarship's id, which will directly affect the content
// shown in a subroute for this page.
// As a result, this page likely only needs to contain the banner and the list of scholarships.
// The content will be shown within the right side, but only as a result of some subroutes,
// each of which is still considered a single page.
view! {
}
}
#[component]
fn AdminApplicantsScholarshipList() -> impl IntoView {
use crate::common::ExpandableInfo;
#[server]
async fn admin_get_provider_scholarships(
access_token: String,
) -> Result, ServerFnError> {
use crate::pages::api::SCHOLARSHIPS_TABLE;
use crate::pages::api::tokens::validate_and_get_token_info;
let claims = validate_and_get_token_info(access_token, "us-east-1_rvCU4Xy4j", "us-east-1").await?;
// Get the information from the database.
let client = crate::utils::server::create_dynamo_client().await;
let output = client
.scan()
.table_name(SCHOLARSHIPS_TABLE)
.send()
.await?
.items
.unwrap_or_default()
.into_iter()
.filter_map(|item| serde_dynamo::from_item(item).ok())
.collect::>();
Ok(output)
}
// Use access token for user identification
let auth = expect_context::();
let access_token =
Memo::new(move |_| auth.with(|auth| auth.authenticated().map(|a| a.access_token())));
// We need to get the scholarships from the API. We don't need the whole scholarship, just the
// name and ID.
let trigger = Trigger::new();
let scholarships_res = Resource::new(
move || (trigger.track(), access_token.get()),
async move |(_, access_token)| {
admin_get_provider_scholarships(access_token.unwrap_or_default()).await
},
);
let navigate = use_navigate();
// This closure returns another closure that's using the correct scholarship ID.
// 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| {
let path = format!("/admin/applicants/{}", scholarship_id);
navigate(&*path, Default::default());
});
view! {
}
}
#[component]
fn ApplicantsScholarshipEntry(
#[prop()] item: ExpandableInfo,
#[prop(into)] on_view_click: Callback,
) -> impl IntoView {
// Store the item's subject.
let subject = StoredValue::new(item.subject);
let scholarship_name = Memo::new(move |_| {
item.data
.get("name")
.map(|name| name.as_string().ok().flatten())
.flatten()
.unwrap_or("".to_string())
});
let on_click = move |_| {
on_view_click.run(subject.get_value());
};
view! {
{scholarship_name}
"View Applicants"
}
}
#[component]
pub fn AdminApplicantsStudentList() -> impl IntoView {
use crate::pages::api::get_scholarship_info;
use crate::pages::api::students::admin_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!(
admin_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! {
"Eligible Students"
"Click on a student's name to view their information and download any requested files."
"If your scholarship requires a transcript, please send an email to Sara Smith at "
"ssmith@region15.org"
"."
}
.into_any()
}
Err(e) => {
view! {
"Error while getting eligible students: "{e.to_string()}
}
.into_any()
}
}
})
}}
}
}
#[component]
fn AdminApplicantsStudentListView(
#[prop(into)] access_token: Signal