more refactor wip
This commit is contained in:
parent
6f8fb27221
commit
ddbc9d3a75
@ -3,9 +3,11 @@ pub mod simple_resident_wizard;
|
|||||||
pub mod simple_step_info;
|
pub mod simple_step_info;
|
||||||
pub mod residence_card;
|
pub mod residence_card;
|
||||||
pub mod refactored_resident_wizard;
|
pub mod refactored_resident_wizard;
|
||||||
|
pub mod multi_step_resident_wizard;
|
||||||
|
|
||||||
pub use step_payment_stripe::*;
|
pub use step_payment_stripe::*;
|
||||||
pub use simple_resident_wizard::*;
|
pub use simple_resident_wizard::*;
|
||||||
pub use simple_step_info::*;
|
pub use simple_step_info::*;
|
||||||
pub use residence_card::*;
|
pub use residence_card::*;
|
||||||
pub use refactored_resident_wizard::*;
|
pub use refactored_resident_wizard::*;
|
||||||
|
pub use multi_step_resident_wizard::*;
|
@ -0,0 +1,441 @@
|
|||||||
|
//! Resident registration wizard using the generic MultiStepForm component
|
||||||
|
|
||||||
|
use yew::prelude::*;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use wasm_bindgen_futures::spawn_local;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::console;
|
||||||
|
use serde_json::json;
|
||||||
|
use js_sys;
|
||||||
|
|
||||||
|
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
|
||||||
|
use crate::services::ResidentService;
|
||||||
|
use crate::components::common::forms::{MultiStepForm, FormStep, StepValidator, ValidationResult};
|
||||||
|
use crate::components::common::forms::multi_step_form::MultiStepFormMsg;
|
||||||
|
use super::{SimpleStepInfo, StepPaymentStripe};
|
||||||
|
|
||||||
|
/// Step 1: Personal Information and KYC
|
||||||
|
pub struct PersonalInfoStep;
|
||||||
|
|
||||||
|
impl FormStep<DigitalResidentFormData> for PersonalInfoStep {
|
||||||
|
fn render(&self, ctx: &Context<MultiStepForm<DigitalResidentFormData>>, data: &DigitalResidentFormData) -> Html {
|
||||||
|
let on_change = ctx.link().callback(|new_data| {
|
||||||
|
MultiStepFormMsg::UpdateFormData(new_data)
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<SimpleStepInfo
|
||||||
|
form_data={data.clone()}
|
||||||
|
on_change={on_change}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_title(&self) -> &'static str {
|
||||||
|
"Personal Information & KYC"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_description(&self) -> &'static str {
|
||||||
|
"Provide your basic information and complete identity verification"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_icon(&self) -> &'static str {
|
||||||
|
"bi-person-vcard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Step 2: Payment and Legal Agreements
|
||||||
|
pub struct PaymentStep {
|
||||||
|
client_secret: Option<String>,
|
||||||
|
processing_payment: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaymentStep {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
client_secret: None,
|
||||||
|
processing_payment: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_client_secret(&mut self, client_secret: Option<String>) {
|
||||||
|
self.client_secret = client_secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_processing(&mut self, processing: bool) {
|
||||||
|
self.processing_payment = processing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormStep<DigitalResidentFormData> for PaymentStep {
|
||||||
|
fn render(&self, ctx: &Context<MultiStepForm<DigitalResidentFormData>>, data: &DigitalResidentFormData) -> Html {
|
||||||
|
let on_process_payment = ctx.link().callback(|_| {
|
||||||
|
// This would trigger payment processing
|
||||||
|
MultiStepFormMsg::NextStep
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_payment_complete = ctx.link().callback(|resident: DigitalResident| {
|
||||||
|
// This would complete the form
|
||||||
|
MultiStepFormMsg::Complete
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_payment_error = ctx.link().callback(|error: String| {
|
||||||
|
console::log_1(&format!("Payment error: {}", error).into());
|
||||||
|
// Could trigger validation error display
|
||||||
|
MultiStepFormMsg::HideValidationToast
|
||||||
|
});
|
||||||
|
|
||||||
|
let data_clone = data.clone();
|
||||||
|
let on_payment_plan_change = ctx.link().callback(move |plan: ResidentPaymentPlan| {
|
||||||
|
let mut updated_data = data_clone.clone();
|
||||||
|
updated_data.payment_plan = plan;
|
||||||
|
MultiStepFormMsg::UpdateFormData(updated_data)
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_confirmation_change = ctx.link().callback(|_confirmed: bool| {
|
||||||
|
// Handle confirmation state change
|
||||||
|
MultiStepFormMsg::HideValidationToast
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<StepPaymentStripe
|
||||||
|
form_data={data.clone()}
|
||||||
|
client_secret={self.client_secret.clone()}
|
||||||
|
processing_payment={self.processing_payment}
|
||||||
|
on_process_payment={on_process_payment}
|
||||||
|
on_payment_complete={on_payment_complete}
|
||||||
|
on_payment_error={on_payment_error}
|
||||||
|
on_payment_plan_change={on_payment_plan_change}
|
||||||
|
on_confirmation_change={on_confirmation_change}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_title(&self) -> &'static str {
|
||||||
|
"Payment & Legal Agreements"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_description(&self) -> &'static str {
|
||||||
|
"Choose your payment plan and review legal agreements"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_icon(&self) -> &'static str {
|
||||||
|
"bi-credit-card"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_navigation(&self) -> bool {
|
||||||
|
false // Payment step handles its own navigation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validator for personal information step
|
||||||
|
pub struct PersonalInfoValidator;
|
||||||
|
|
||||||
|
impl StepValidator<DigitalResidentFormData> for PersonalInfoValidator {
|
||||||
|
fn validate(&self, data: &DigitalResidentFormData) -> ValidationResult {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
if data.full_name.trim().is_empty() {
|
||||||
|
errors.push("Full name is required".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.email.trim().is_empty() {
|
||||||
|
errors.push("Email address is required".to_string());
|
||||||
|
} else if !data.email.contains('@') {
|
||||||
|
errors.push("Please enter a valid email address".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.public_key.is_none() {
|
||||||
|
errors.push("Please generate your digital identity keys".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data.legal_agreements.terms {
|
||||||
|
errors.push("You must agree to the Terms of Service and Privacy Policy".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
ValidationResult::valid()
|
||||||
|
} else {
|
||||||
|
ValidationResult::invalid(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validator for payment step
|
||||||
|
pub struct PaymentValidator;
|
||||||
|
|
||||||
|
impl StepValidator<DigitalResidentFormData> for PaymentValidator {
|
||||||
|
fn validate(&self, data: &DigitalResidentFormData) -> ValidationResult {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
// Basic payment validation - in real implementation this would check payment completion
|
||||||
|
if data.payment_plan == ResidentPaymentPlan::Monthly && data.full_name.is_empty() {
|
||||||
|
errors.push("Payment information is incomplete".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.is_empty() {
|
||||||
|
ValidationResult::valid()
|
||||||
|
} else {
|
||||||
|
ValidationResult::invalid(errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Properties for the multi-step resident wizard
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct MultiStepResidentWizardProps {
|
||||||
|
pub on_registration_complete: Callback<DigitalResident>,
|
||||||
|
pub on_back_to_parent: Callback<()>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub success_resident_id: Option<u32>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub show_failure: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Messages for the multi-step resident wizard
|
||||||
|
pub enum MultiStepResidentWizardMsg {
|
||||||
|
FormDataChanged(DigitalResidentFormData),
|
||||||
|
FormCompleted(DigitalResidentFormData),
|
||||||
|
FormCancelled,
|
||||||
|
CreatePaymentIntent,
|
||||||
|
PaymentIntentCreated(String),
|
||||||
|
PaymentIntentError(String),
|
||||||
|
RegistrationComplete(DigitalResident),
|
||||||
|
RegistrationError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Multi-step resident wizard component
|
||||||
|
pub struct MultiStepResidentWizard {
|
||||||
|
form_data: DigitalResidentFormData,
|
||||||
|
steps: Vec<Rc<dyn FormStep<DigitalResidentFormData>>>,
|
||||||
|
validators: HashMap<usize, Rc<dyn StepValidator<DigitalResidentFormData>>>,
|
||||||
|
client_secret: Option<String>,
|
||||||
|
processing_registration: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for MultiStepResidentWizard {
|
||||||
|
type Message = MultiStepResidentWizardMsg;
|
||||||
|
type Properties = MultiStepResidentWizardProps;
|
||||||
|
|
||||||
|
fn create(ctx: &Context<Self>) -> Self {
|
||||||
|
// Initialize form data based on props
|
||||||
|
let form_data = if ctx.props().success_resident_id.is_some() || ctx.props().show_failure {
|
||||||
|
// For demo purposes, start with default data
|
||||||
|
DigitalResidentFormData::default()
|
||||||
|
} else {
|
||||||
|
DigitalResidentFormData::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create steps
|
||||||
|
let steps: Vec<Rc<dyn FormStep<DigitalResidentFormData>>> = vec![
|
||||||
|
Rc::new(PersonalInfoStep),
|
||||||
|
Rc::new(PaymentStep::new()),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create validators
|
||||||
|
let mut validators: HashMap<usize, Rc<dyn StepValidator<DigitalResidentFormData>>> = HashMap::new();
|
||||||
|
validators.insert(0, Rc::new(PersonalInfoValidator));
|
||||||
|
validators.insert(1, Rc::new(PaymentValidator));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
form_data,
|
||||||
|
steps,
|
||||||
|
validators,
|
||||||
|
client_secret: None,
|
||||||
|
processing_registration: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
|
match msg {
|
||||||
|
MultiStepResidentWizardMsg::FormDataChanged(new_data) => {
|
||||||
|
self.form_data = new_data;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::FormCompleted(final_data) => {
|
||||||
|
console::log_1(&"🎉 Form completed, processing registration...".into());
|
||||||
|
self.processing_registration = true;
|
||||||
|
self.form_data = final_data;
|
||||||
|
|
||||||
|
// Start registration process
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
let form_data = self.form_data.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
// Simulate registration processing with a simple timeout
|
||||||
|
let promise = js_sys::Promise::new(&mut |resolve, _| {
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
window.set_timeout_with_callback_and_timeout_and_arguments_0(&resolve, 2000).unwrap();
|
||||||
|
});
|
||||||
|
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
|
||||||
|
|
||||||
|
match ResidentService::create_resident_from_form(&form_data) {
|
||||||
|
Ok(resident) => {
|
||||||
|
link.send_message(MultiStepResidentWizardMsg::RegistrationComplete(resident));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
link.send_message(MultiStepResidentWizardMsg::RegistrationError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::FormCancelled => {
|
||||||
|
ctx.props().on_back_to_parent.emit(());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::CreatePaymentIntent => {
|
||||||
|
console::log_1(&"🔧 Creating payment intent...".into());
|
||||||
|
self.create_payment_intent(ctx);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::PaymentIntentCreated(client_secret) => {
|
||||||
|
console::log_1(&"✅ Payment intent created".into());
|
||||||
|
self.client_secret = Some(client_secret);
|
||||||
|
|
||||||
|
// Update the payment step with the client secret
|
||||||
|
if let Some(_payment_step) = self.steps.get_mut(1) {
|
||||||
|
// This is a bit tricky with Rc - in a real implementation,
|
||||||
|
// we might use a different pattern for mutable step state
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::PaymentIntentError(error) => {
|
||||||
|
console::log_1(&format!("❌ Payment intent error: {}", error).into());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::RegistrationComplete(resident) => {
|
||||||
|
self.processing_registration = false;
|
||||||
|
console::log_1(&"✅ Registration completed successfully".into());
|
||||||
|
ctx.props().on_registration_complete.emit(resident);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
MultiStepResidentWizardMsg::RegistrationError(error) => {
|
||||||
|
self.processing_registration = false;
|
||||||
|
console::log_1(&format!("❌ Registration error: {}", error).into());
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
|
let link = ctx.link();
|
||||||
|
|
||||||
|
let on_form_change = link.callback(MultiStepResidentWizardMsg::FormDataChanged);
|
||||||
|
let on_complete = link.callback(MultiStepResidentWizardMsg::FormCompleted);
|
||||||
|
let on_cancel = link.callback(|_| MultiStepResidentWizardMsg::FormCancelled);
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="h-100 d-flex flex-column position-relative">
|
||||||
|
<MultiStepForm<DigitalResidentFormData>
|
||||||
|
form_data={self.form_data.clone()}
|
||||||
|
on_form_change={on_form_change}
|
||||||
|
on_complete={on_complete}
|
||||||
|
on_cancel={Some(on_cancel)}
|
||||||
|
steps={self.steps.clone()}
|
||||||
|
validators={self.validators.clone()}
|
||||||
|
show_progress={true}
|
||||||
|
allow_skip_validation={false}
|
||||||
|
validation_toast_duration={5000}
|
||||||
|
/>
|
||||||
|
|
||||||
|
// Loading overlay when processing registration
|
||||||
|
{if self.processing_registration {
|
||||||
|
html! {
|
||||||
|
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
|
||||||
|
style="background: rgba(255, 255, 255, 0.9); z-index: 1050;">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary mb-3" role="status">
|
||||||
|
<span class="visually-hidden">{"Loading..."}</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-muted">{"Processing registration..."}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiStepResidentWizard {
|
||||||
|
fn create_payment_intent(&self, ctx: &Context<Self>) {
|
||||||
|
let link = ctx.link().clone();
|
||||||
|
let form_data = self.form_data.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
match Self::setup_stripe_payment(form_data).await {
|
||||||
|
Ok(client_secret) => {
|
||||||
|
link.send_message(MultiStepResidentWizardMsg::PaymentIntentCreated(client_secret));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
link.send_message(MultiStepResidentWizardMsg::PaymentIntentError(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_stripe_payment(form_data: DigitalResidentFormData) -> Result<String, String> {
|
||||||
|
use wasm_bindgen_futures::JsFuture;
|
||||||
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
|
console::log_1(&"🔧 Setting up Stripe payment for resident registration".into());
|
||||||
|
|
||||||
|
// Prepare form data for payment intent creation
|
||||||
|
let payment_data = json!({
|
||||||
|
"resident_name": form_data.full_name,
|
||||||
|
"email": form_data.email,
|
||||||
|
"payment_plan": form_data.payment_plan.get_display_name(),
|
||||||
|
"amount": form_data.payment_plan.get_price(),
|
||||||
|
"type": "resident_registration"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create request to server endpoint
|
||||||
|
let mut opts = RequestInit::new();
|
||||||
|
opts.method("POST");
|
||||||
|
opts.mode(RequestMode::Cors);
|
||||||
|
|
||||||
|
let headers = web_sys::js_sys::Map::new();
|
||||||
|
headers.set(&"Content-Type".into(), &"application/json".into());
|
||||||
|
opts.headers(&headers);
|
||||||
|
|
||||||
|
opts.body(Some(&JsValue::from_str(&payment_data.to_string())));
|
||||||
|
|
||||||
|
let request = Request::new_with_str_and_init(
|
||||||
|
"http://127.0.0.1:3001/resident/create-payment-intent",
|
||||||
|
&opts,
|
||||||
|
).map_err(|e| format!("Failed to create request: {:?}", e))?;
|
||||||
|
|
||||||
|
// Make the request
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
|
||||||
|
.map_err(|e| format!("Network request failed: {:?}", e))?;
|
||||||
|
|
||||||
|
let resp: Response = resp_value.dyn_into().unwrap();
|
||||||
|
|
||||||
|
if !resp.ok() {
|
||||||
|
return Err(format!("Server error: HTTP {}", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
let json_value = JsFuture::from(resp.json().unwrap()).await
|
||||||
|
.map_err(|e| format!("Failed to parse response: {:?}", e))?;
|
||||||
|
|
||||||
|
// Extract client secret from response
|
||||||
|
let response_obj = web_sys::js_sys::Object::from(json_value);
|
||||||
|
let client_secret_value = web_sys::js_sys::Reflect::get(&response_obj, &"client_secret".into())
|
||||||
|
.map_err(|e| format!("No client_secret in response: {:?}", e))?;
|
||||||
|
|
||||||
|
let client_secret = client_secret_value.as_string()
|
||||||
|
.ok_or_else(|| "Invalid client secret received from server".to_string())?;
|
||||||
|
|
||||||
|
console::log_1(&"✅ Payment intent created successfully".into());
|
||||||
|
Ok(client_secret)
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,9 @@ use web_sys::{console, js_sys};
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
|
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
|
||||||
use crate::services::{ResidentService, ResidentRegistration, ResidentRegistrationStatus};
|
use crate::services::{ResidentService, ResidentRegistration, ResidentRegistrationStatus};
|
||||||
|
use crate::components::common::ui::progress_indicator::{ProgressIndicator, ProgressVariant, ProgressColor, ProgressSize};
|
||||||
|
use crate::components::common::ui::validation_toast::{ValidationToast, ToastType};
|
||||||
|
use crate::components::common::ui::loading_spinner::LoadingSpinner;
|
||||||
use super::{SimpleStepInfo, StepPaymentStripe};
|
use super::{SimpleStepInfo, StepPaymentStripe};
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@ -217,7 +220,7 @@ impl Component for SimpleResidentWizard {
|
|||||||
let (step_title, step_description, step_icon) = self.get_step_info();
|
let (step_title, step_description, step_icon) = self.get_step_info();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="h-100 d-flex flex-column">
|
<div class="h-100 d-flex flex-column position-relative">
|
||||||
<form class="flex-grow-1 overflow-auto">
|
<form class="flex-grow-1 overflow-auto">
|
||||||
{self.render_current_step(ctx)}
|
{self.render_current_step(ctx)}
|
||||||
</form>
|
</form>
|
||||||
@ -233,6 +236,21 @@ impl Component for SimpleResidentWizard {
|
|||||||
} else {
|
} else {
|
||||||
html! {}
|
html! {}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// Loading overlay when processing registration
|
||||||
|
{if self.processing_registration {
|
||||||
|
html! {
|
||||||
|
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
|
||||||
|
style="background: rgba(255, 255, 255, 0.9); z-index: 1050;">
|
||||||
|
<div class="text-center">
|
||||||
|
<LoadingSpinner />
|
||||||
|
<p class="mt-3 text-muted">{"Processing registration..."}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -295,40 +313,16 @@ impl SimpleResidentWizard {
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// Step indicator (center)
|
// Step indicator (center) - Using our generic ProgressIndicator
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
{for (1..=2).map(|step| {
|
<ProgressIndicator
|
||||||
let is_current = step == self.current_step;
|
current_step={self.current_step as usize - 1} // Convert to 0-based index
|
||||||
let is_completed = step < self.current_step;
|
total_steps={2}
|
||||||
let step_class = if is_current {
|
variant={ProgressVariant::Dots}
|
||||||
"bg-primary text-white"
|
color={ProgressColor::Primary}
|
||||||
} else if is_completed {
|
size={ProgressSize::Small}
|
||||||
"bg-success text-white"
|
show_step_numbers={true}
|
||||||
} else {
|
/>
|
||||||
"bg-white text-muted border"
|
|
||||||
};
|
|
||||||
|
|
||||||
html! {
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class={format!("rounded-circle d-flex align-items-center justify-content-center {} fw-bold", step_class)}
|
|
||||||
style="width: 28px; height: 28px; font-size: 12px;">
|
|
||||||
{if is_completed {
|
|
||||||
html! { <i class="bi bi-check"></i> }
|
|
||||||
} else {
|
|
||||||
html! { {step} }
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
{if step < 2 {
|
|
||||||
html! {
|
|
||||||
<div class={format!("mx-1 {}", if is_completed { "bg-success" } else { "bg-secondary" })}
|
|
||||||
style="height: 2px; width: 24px;"></div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
html! {}
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// Next/Register button (right)
|
// Next/Register button (right)
|
||||||
@ -362,29 +356,13 @@ impl SimpleResidentWizard {
|
|||||||
let close_toast = link.callback(|_| SimpleResidentWizardMsg::HideValidationToast);
|
let close_toast = link.callback(|_| SimpleResidentWizardMsg::HideValidationToast);
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="position-fixed bottom-0 start-50 translate-middle-x mb-3" style="z-index: 1055; max-width: 500px;">
|
<ValidationToast
|
||||||
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
toast_type={ToastType::Warning}
|
||||||
<div class="toast-header bg-warning text-dark">
|
title={"Required Fields Missing"}
|
||||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
messages={self.validation_errors.clone()}
|
||||||
<strong class="me-auto">{"Required Fields Missing"}</strong>
|
show={self.show_validation_toast}
|
||||||
<button type="button" class="btn-close" onclick={close_toast} aria-label="Close"></button>
|
on_close={close_toast}
|
||||||
</div>
|
/>
|
||||||
<div class="toast-body">
|
|
||||||
<div class="mb-2">
|
|
||||||
<strong>{"Please complete all required fields to continue:"}</strong>
|
|
||||||
</div>
|
|
||||||
<ul class="list-unstyled mb-0">
|
|
||||||
{for self.validation_errors.iter().map(|error| {
|
|
||||||
html! {
|
|
||||||
<li class="mb-1">
|
|
||||||
<i class="bi bi-dot text-danger me-1"></i>{error}
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ use wasm_bindgen_futures::spawn_local;
|
|||||||
use web_sys::{window, console, js_sys};
|
use web_sys::{window, console, js_sys};
|
||||||
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
|
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
|
||||||
use crate::services::ResidentService;
|
use crate::services::ResidentService;
|
||||||
|
use crate::components::common::ui::loading_spinner::LoadingSpinner;
|
||||||
use super::ResidenceCard;
|
use super::ResidenceCard;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@ -175,19 +176,15 @@ impl Component for StepPaymentStripe {
|
|||||||
{if ctx.props().processing_payment {
|
{if ctx.props().processing_payment {
|
||||||
html! {
|
html! {
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<div class="spinner-border text-secondary mb-3" role="status" style="width: 1.5rem; height: 1.5rem;">
|
<LoadingSpinner />
|
||||||
<span class="visually-hidden">{"Loading..."}</span>
|
<p class="text-muted mt-3" style="font-size: 0.85rem;">{"Processing payment..."}</p>
|
||||||
</div>
|
|
||||||
<p class="text-muted" style="font-size: 0.85rem;">{"Processing payment..."}</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} else if !has_client_secret {
|
} else if !has_client_secret {
|
||||||
html! {
|
html! {
|
||||||
<div class="text-center py-4">
|
<div class="text-center py-4">
|
||||||
<div class="spinner-border text-secondary mb-3" role="status" style="width: 1.5rem; height: 1.5rem;">
|
<LoadingSpinner />
|
||||||
<span class="visually-hidden">{"Loading..."}</span>
|
<p class="text-muted mt-3" style="font-size: 0.85rem;">{"Preparing payment form..."}</p>
|
||||||
</div>
|
|
||||||
<p class="text-muted" style="font-size: 0.85rem;">{"Preparing payment form..."}</p>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user