use std::collections::HashMap;
use leptos::either::Either;
use leptos::html::Dialog;
use leptos::logging::debug_log;
use leptos::prelude::*;
use leptos_oidc::{AuthLoaded, Authenticated};
use leptos_router::hooks::{use_navigate, use_params};
use crate::common::{ComparisonData, ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
use crate::components::{ActionButton, Banner, ChipsList, Header, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
use super::UnauthenticatedPage;
use crate::utils::get_user_claims;
use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized};
/// # Scholarship Info Page
/// This page will handle creating or editing scholarship information given a specific
/// scholarship ID and scholarship provider subject number. The form itself will only
/// track a single scholarship (for now) and so selection of what scholarship will be edited
/// must be handled *before* navigation to this page.
#[component]
pub fn ScholarshipInfoPage() -> impl IntoView {
// Changes to this signal update the scholarship list.
let list_refresh = RwSignal::new(0);
let url_params = use_params::();
let scholarship_id = Memo::new(move |_| {
url_params.read()
.as_ref()
.ok()
.and_then(|params| params.id.clone())
});
// let create_test_comps = ServerAction::::new();
let on_delete = move || {
let navigate = use_navigate();
navigate("/providers/scholarships", Default::default());
};
// Remember that we only want to show the page after we've got authentication information
// and if there's data for us to use.
view! {
}
}
/// # Scholarship List Component
///
/// This component should always be wrapped in an Authenticated component. Assuming that a provider
/// is authenticated, this component will get all the scholarships that this provider owns and
/// will display them in a list, giving them the option to edit, delete, or create new scholarships.
///
/// Example usage:
/// ```
/// let refresh = RwSignal::new(0);
///
/// view! {
///
///
///
///
///
/// }
/// ```
#[component]
fn ScholarshipList(
/// A signal that refreshes the list when changed. Most times, we don't need to worry about the
/// user reloading our page 2 billion times.
#[prop()] refresh_token: RwSignal,
/// A callback that is run on successful server-side deletion of an item.
#[prop(into)] on_delete: Callback<()>
) -> impl IntoView {
// List state
let pending_delete = RwSignal::new(None::);
let delete_dialog_ref = NodeRef::::new();
// Get scholarship provider information from authentication. This element will not display
// unless that information is available.
let user_claims = get_user_claims();
let provider_id = Memo::new(move |_|
user_claims.get()
.as_ref()
.map(|info| info.claims.subject.clone())
);
// Register scholarship server resource
let scholarships = Resource::new(
move || {
(refresh_token.get(), provider_id.get())
},
|(_, provider_id)| async move {
debug_log!("Fetching from scholarship API.");
let Some(provider_id) = provider_id else {
return Ok(Vec::new());
};
// Get the scholarships
get_provider_scholarships(provider_id).await
}
);
// Register scholarship creation/deletion actions
let create_action = ServerAction::::new();
let delete_action = ServerAction::::new();
// Button handlers
let create_on_click = move |_| {
create_action.dispatch(RegisterScholarship {
provider_id: provider_id.get().unwrap()
});
};
let on_item_delete = move |s| {
pending_delete.set(Some(s));
};
// Register scholarship creation effect
Effect::new(move || {
// We want to wait for the action to complete first, but then find whether the request
// succeeded.
if let Some(Ok(_)) = create_action.value().get() {
// Refetch and clear. We could alter this slightly to get the new element and
// update the list on the client side only (no refetch). We also want to set the name
// of the new scholarship before it's created on the server.
scholarships.refetch();
create_action.clear();
}
});
// Register scholarship deletion effects
let modal_disabled = Signal::derive(move || delete_action.pending().get());
Effect::new(move || {
if let Some(Ok(_)) = delete_action.value().get() {
scholarships.refetch();
pending_delete.set(None);
delete_action.clear();
on_delete.run(());
}
});
// Register scholarship pending delete effects
Effect::new(move || {
if let Some(dialog) = delete_dialog_ref.get() {
if pending_delete.get().is_some() {
// Find the modal dialog and show it. Ask for the user's confirmation. Afterward,
// take the correct action.
dialog.show_modal()
.expect("Couldn't show dialog. Is it already open?");
} else {
dialog.close();
}
}
});
let modal_on_click_continue = move |_| {
let subject = pending_delete
.get()
.map(|s| s.subject.clone())
.unwrap_or_default();
debug_log!("Deleting scholarship with subject {}", subject);
delete_action
.dispatch(DeleteProviderScholarship {
scholarship_id: subject,
provider_id: provider_id.get().unwrap_or_default(),
});
};
view! {
"Confirm Deletion"
"Are you sure you want to delete "
{move || {
pending_delete
.get()
.map(|s| {
s.data
.get("name")
.unwrap_or(&ValueType::String(None))
.as_string()
.unwrap_or_default()
.unwrap_or("this unnamed scholarship".to_string())
})
}} "?"
"Scholarships"
{move || {
scholarships
.get()
.map(|result| {
match result {
Ok(list) => {
let views = list
.iter()
.map(|entry| {
view! {
}
});
Either::Left(views.collect_view())
}
Err(err) => {
Either::Right(
view! {
"Failed to load scholarship list: "{err.to_string()}
},
)
}
}
})
.collect_view()
}}
}
}
/// # Scholarship List Entry Component
///
/// Displays a single scholarship within the [`ScholarshipList`] component.
///
/// Example usage (within the [`ScholarshipList`]):
/// ```
/// view! {
///
/// }
/// ```
#[component]
fn ScholarshipListEntry(
/// The scholarship information. This element will attempt to get the name of the scholarship
/// using its `name` field, and also utilizes the `subject` field.
#[prop()] scholarship: ExpandableInfo,
/// A [`Callback`] that runs upon successful deletion of the scholarship.
#[prop(into)] on_delete: Callback,
) -> impl IntoView {
let navigate = use_navigate();
let user_claims = get_user_claims();
let _user_id = Memo::new(move |_| {
user_claims.get()
.map(|info| info.claims.subject.clone())
});
// Get the values that we need out of the info. We just need the subject and the name of
// the scholarship.
let name = scholarship.data.get("name")
.unwrap_or(&ValueType::String(None))
.as_string().unwrap_or_default()
.unwrap_or("Unnamed Scholarship".to_string());
// Button click handlers
let edit_click = {
let subject = scholarship.subject.clone();
move |_| {
let path = format!("/providers/scholarships/{}", subject);
navigate(path.as_str(), Default::default());
}
};
view! {
}
}
/// # Scholarship Edit Form Component
///
/// Displays all information about a scholarship and allows the provider to edit that information.
/// All information is only saved when the submit button is clicked.
///
/// The component will call the `on_submit` callback when the API returns an `Ok` result. In the
/// event that the API call fails, it will not call anything. This is mainly used to allow updates
/// to neighboring components from the parent.
///
/// Example usage:
/// ```
/// view! {
///
/// }
/// ```
#[component]
fn ScholarshipForm(
/// The ID of the scholarship to edit. The form will handle getting all information about the
/// scholarship and submitting new information.
#[prop(into)] scholarship_id: Signal>,
/// A callback that is run when the form successfully submits new data. In the event that the
/// submission fails, this function is not run.
#[prop(into)] on_submit: Callback<()>
) -> impl IntoView {
//#region Form Data Setup
let scholarship_info = Resource::new(
move || scholarship_id.get(),
async move |opt_id| {
if let Some(id) = opt_id {
get_scholarship_info(id).await
} else {
Err(ServerFnError::new("Cannot get from server with no ID."))
}
}
);
let form_data = RwSignal::new(HashMap::new());
let chips_data = RwSignal::new(HashMap::new());
Effect::new(move || {
if let Some(Ok(scholarship)) = scholarship_info.get() {
// Get the default chips data.
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
debug_log!("Found requirements map: {:?}", map);
map.clone()
} else {
debug_log!("Couldn't find requirements map in scholarship info. Using empty map.");
HashMap::new()
};
chips_data.set(chips_default);
form_data.set(scholarship.data);
}
});
//#endregion
//#region Comparison Logic
let comparison_lists = Resource::new(
move || scholarship_id.get().is_some(),
async move |_| {
get_comparisons_categorized().await
}
);
let comparison_lists_sorted = Signal::derive(move || {
if let Some(Ok(map)) = comparison_lists.get() {
let mut map = map.clone();
map
.into_iter()
.map(|(cat, mut list)| {
list.sort_by_key(|comp| comp.display_text.clone());
(cat, list)
})
.collect::>>()
} else {
HashMap::new()
}
});
let get_comp_category = move |category: &str| {
comparison_lists_sorted.get()
.get(category)
.cloned()
.unwrap_or(Vec::new())
};
let get_comp_ids = move |category: &str| {
get_comp_category(category)
.into_iter()
.map(|comp| comp.id)
.collect::>()
};
let get_comp_text = move |category: &str| {
get_comp_category(category)
.into_iter()
.map(|comp| comp.display_text)
.collect::>()
};
// Get comparison lists.
let sports_ids = Signal::derive(move || get_comp_ids("Sports Participation"));
let sports_text = Signal::derive(move || get_comp_text("Sports Participation"));
let misc_ids = Signal::derive(move || get_comp_ids("Additional Eligibility Factors"));
let misc_text = Signal::derive(move || get_comp_text("Additional Eligibility Factors"));
let comm_ids = Signal::derive(move || get_comp_ids("Community Service"));
let comm_text = Signal::derive(move || get_comp_text("Community Service"));
let res_ids = Signal::derive(move || get_comp_ids("Residency"));
let res_text = Signal::derive(move || get_comp_text("Residency"));
let gpa_ids = Signal::derive(move || get_comp_ids("GPA Limits"));
let gpa_text = Signal::derive(move || get_comp_text("GPA Limits"));
let major_ids = Signal::derive(move || get_comp_ids("Majors"));
let major_text = Signal::derive(move || get_comp_text("Majors"));
//#endregion
//#region Submission Logic
let submit_action = ServerAction::::new();
let submit_status = RwSignal::new(SubmitStatus::Idle);
let elements_disabled = Signal::derive(move || {
matches!(submit_action.pending().get(), true)
});
Effect::new(move || {
if submit_action.pending().get() {
submit_status.set(SubmitStatus::Sending);
return;
}
if let Some(result) = submit_action.value().get() {
match result {
Ok(()) => {
submit_status.set(SubmitStatus::Success);
on_submit.run(());
},
Err(_err) => submit_status.set(SubmitStatus::Error(()))
}
}
});
let mut toasts = expect_context::();
let on_submit = move |_| {
let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default());
info.data = form_data.get();
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
debug_log!("Map values: {:?}", info.data);
submit_action.dispatch(CreateScholarshipInfo {
info
});
};
Effect::watch(
move || submit_action.value().get(),
move |value, _, _| {
debug_log!("Running submit_action effect");
let Some(result) = value else {
return;
};
let toast = match result {
Ok(_) => Toast::new()
.id(uuid::Uuid::new_v4())
.header("Submission Successful")
.msg("You can go back or continue editing your responses."),
Err(err) => Toast::new()
.id(uuid::Uuid::new_v4())
.header("Submission Failed")
.msg(err.to_string())
};
untrack(move || {
submit_action.clear();
toasts.toast(toast);
});
},
false
);
//#endregion
view! {
"Choose a scholarship from the right, or create a new one."
}
}
>
{move || {
scholarship_info
.get()
.map(|res_scholarship| {
match res_scholarship {
Ok(_) => {
Either::Left(
view! {
},
)
}
Err(err) => {
Either::Right(
view! {
"Error while getting scholarship: "{err.to_string()}
},
)
}
}
})
.collect_view()
}}
}.into_any()
}