more refactor wip
This commit is contained in:
		| @@ -3,9 +3,11 @@ pub mod simple_resident_wizard; | ||||
| pub mod simple_step_info; | ||||
| pub mod residence_card; | ||||
| pub mod refactored_resident_wizard; | ||||
| pub mod multi_step_resident_wizard; | ||||
|  | ||||
| pub use step_payment_stripe::*; | ||||
| pub use simple_resident_wizard::*; | ||||
| pub use simple_step_info::*; | ||||
| 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 crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan}; | ||||
| 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}; | ||||
|  | ||||
| #[wasm_bindgen] | ||||
| @@ -217,7 +220,7 @@ impl Component for SimpleResidentWizard { | ||||
|         let (step_title, step_description, step_icon) = self.get_step_info(); | ||||
|  | ||||
|         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"> | ||||
|                     {self.render_current_step(ctx)} | ||||
|                 </form> | ||||
| @@ -233,6 +236,21 @@ impl Component for SimpleResidentWizard { | ||||
|                 } else { | ||||
|                     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> | ||||
|         } | ||||
|     } | ||||
| @@ -295,40 +313,16 @@ impl SimpleResidentWizard { | ||||
|                         }} | ||||
|                     </div> | ||||
|  | ||||
|                     // Step indicator (center) | ||||
|                     // Step indicator (center) - Using our generic ProgressIndicator | ||||
|                     <div class="d-flex align-items-center"> | ||||
|                         {for (1..=2).map(|step| { | ||||
|                             let is_current = step == self.current_step; | ||||
|                             let is_completed = step < self.current_step; | ||||
|                             let step_class = if is_current { | ||||
|                                 "bg-primary text-white" | ||||
|                             } else if is_completed { | ||||
|                                 "bg-success text-white" | ||||
|                             } 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> | ||||
|                             } | ||||
|                         })} | ||||
|                         <ProgressIndicator | ||||
|                             current_step={self.current_step as usize - 1} // Convert to 0-based index | ||||
|                             total_steps={2} | ||||
|                             variant={ProgressVariant::Dots} | ||||
|                             color={ProgressColor::Primary} | ||||
|                             size={ProgressSize::Small} | ||||
|                             show_step_numbers={true} | ||||
|                         /> | ||||
|                     </div> | ||||
|  | ||||
|                     // Next/Register button (right) | ||||
| @@ -362,29 +356,13 @@ impl SimpleResidentWizard { | ||||
|         let close_toast = link.callback(|_| SimpleResidentWizardMsg::HideValidationToast); | ||||
|  | ||||
|         html! { | ||||
|             <div class="position-fixed bottom-0 start-50 translate-middle-x mb-3" style="z-index: 1055; max-width: 500px;"> | ||||
|                 <div class="toast show" role="alert" aria-live="assertive" aria-atomic="true"> | ||||
|                     <div class="toast-header bg-warning text-dark"> | ||||
|                         <i class="bi bi-exclamation-triangle me-2"></i> | ||||
|                         <strong class="me-auto">{"Required Fields Missing"}</strong> | ||||
|                         <button type="button" class="btn-close" onclick={close_toast} aria-label="Close"></button> | ||||
|                     </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> | ||||
|             <ValidationToast | ||||
|                 toast_type={ToastType::Warning} | ||||
|                 title={"Required Fields Missing"} | ||||
|                 messages={self.validation_errors.clone()} | ||||
|                 show={self.show_validation_toast} | ||||
|                 on_close={close_toast} | ||||
|             /> | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -4,6 +4,7 @@ use wasm_bindgen_futures::spawn_local; | ||||
| use web_sys::{window, console, js_sys}; | ||||
| use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan}; | ||||
| use crate::services::ResidentService; | ||||
| use crate::components::common::ui::loading_spinner::LoadingSpinner; | ||||
| use super::ResidenceCard; | ||||
|  | ||||
| #[wasm_bindgen] | ||||
| @@ -175,19 +176,15 @@ impl Component for StepPaymentStripe { | ||||
|                                         {if ctx.props().processing_payment { | ||||
|                                             html! { | ||||
|                                                 <div class="text-center py-4"> | ||||
|                                                     <div class="spinner-border text-secondary mb-3" role="status" style="width: 1.5rem; height: 1.5rem;"> | ||||
|                                                         <span class="visually-hidden">{"Loading..."}</span> | ||||
|                                                     </div> | ||||
|                                                     <p class="text-muted" style="font-size: 0.85rem;">{"Processing payment..."}</p> | ||||
|                                                     <LoadingSpinner /> | ||||
|                                                     <p class="text-muted mt-3" style="font-size: 0.85rem;">{"Processing payment..."}</p> | ||||
|                                                 </div> | ||||
|                                             } | ||||
|                                         } else if !has_client_secret { | ||||
|                                             html! { | ||||
|                                                 <div class="text-center py-4"> | ||||
|                                                     <div class="spinner-border text-secondary mb-3" role="status" style="width: 1.5rem; height: 1.5rem;"> | ||||
|                                                         <span class="visually-hidden">{"Loading..."}</span> | ||||
|                                                     </div> | ||||
|                                                     <p class="text-muted" style="font-size: 0.85rem;">{"Preparing payment form..."}</p> | ||||
|                                                     <LoadingSpinner /> | ||||
|                                                     <p class="text-muted mt-3" style="font-size: 0.85rem;">{"Preparing payment form..."}</p> | ||||
|                                                 </div> | ||||
|                                             } | ||||
|                                         } else { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user