475 lines
19 KiB
Rust
475 lines
19 KiB
Rust
use yew::prelude::*;
|
|
use web_sys::MouseEvent;
|
|
use wasm_bindgen::JsCast;
|
|
use crate::routing::{AppView, ViewContext, HistoryManager};
|
|
use crate::components::{Header, Sidebar, Footer, ToastContainer, ToastMessage, create_success_toast, ResidentLandingOverlay};
|
|
use crate::views::{
|
|
HomeView, AdministrationView, PersonAdministrationView, BusinessView, AccountingView, ContractsView,
|
|
GovernanceView, TreasuryView, ResidenceView, EntitiesView, ResidentRegistrationView
|
|
};
|
|
use crate::models::company::DigitalResident;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum Msg {
|
|
SwitchView(AppView),
|
|
SwitchContext(ViewContext),
|
|
ToggleSidebar,
|
|
PopStateChanged,
|
|
ShowToast(ToastMessage),
|
|
DismissToast(u32),
|
|
Login,
|
|
Logout,
|
|
ToggleTheme,
|
|
ShowResidentLanding,
|
|
HideResidentLanding,
|
|
ResidentSignIn(String, String), // email, password
|
|
ResidentRegistrationComplete,
|
|
}
|
|
|
|
pub struct App {
|
|
current_view: AppView,
|
|
current_context: ViewContext,
|
|
sidebar_visible: bool,
|
|
toasts: Vec<ToastMessage>,
|
|
next_toast_id: u32,
|
|
is_logged_in: bool,
|
|
user_name: Option<String>,
|
|
is_dark_mode: bool,
|
|
show_resident_landing: bool,
|
|
}
|
|
|
|
impl Component for App {
|
|
type Message = Msg;
|
|
type Properties = ();
|
|
|
|
fn create(_ctx: &Context<Self>) -> Self {
|
|
wasm_logger::init(wasm_logger::Config::default());
|
|
log::info!("Starting Zanzibar Digital Freezone WASM app");
|
|
|
|
// Determine initial view based on URL, default to Home
|
|
let current_path = HistoryManager::get_current_path();
|
|
let current_view = AppView::from_path(¤t_path);
|
|
|
|
// Load context from localStorage, default to Business
|
|
let current_context = if let Some(storage) = web_sys::window()
|
|
.and_then(|w| w.local_storage().ok())
|
|
.flatten()
|
|
{
|
|
match storage.get_item("view_context").ok().flatten().as_deref() {
|
|
Some("person") => ViewContext::Person,
|
|
_ => ViewContext::Business,
|
|
}
|
|
} else {
|
|
ViewContext::Business
|
|
};
|
|
|
|
// Load theme preference from localStorage, default to light mode
|
|
let is_dark_mode = if let Some(storage) = web_sys::window()
|
|
.and_then(|w| w.local_storage().ok())
|
|
.flatten()
|
|
{
|
|
storage.get_item("theme").ok().flatten().as_deref() == Some("dark")
|
|
} else {
|
|
false
|
|
};
|
|
|
|
// Check if we're coming from a payment success URL
|
|
let mut toasts = Vec::new();
|
|
let mut next_toast_id = 1;
|
|
|
|
if current_path.starts_with("/company/payment-success") {
|
|
// Show payment success toast
|
|
let toast = create_success_toast(
|
|
next_toast_id,
|
|
"Payment Successful!",
|
|
"Your company registration payment has been processed successfully. Your company is now pending approval."
|
|
);
|
|
toasts.push(toast);
|
|
next_toast_id += 1;
|
|
|
|
// Update URL to remove payment success parameters
|
|
let _ = HistoryManager::replace_url("/entities");
|
|
}
|
|
|
|
// Set up popstate event listener for browser back/forward navigation
|
|
let link = _ctx.link().clone();
|
|
let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::Event| {
|
|
link.send_message(Msg::PopStateChanged);
|
|
}) as Box<dyn FnMut(_)>);
|
|
|
|
if let Some(window) = web_sys::window() {
|
|
let _ = window.add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref());
|
|
}
|
|
closure.forget(); // Keep the closure alive
|
|
|
|
Self {
|
|
current_view,
|
|
current_context,
|
|
sidebar_visible: false,
|
|
toasts,
|
|
next_toast_id,
|
|
is_logged_in: false,
|
|
user_name: None,
|
|
is_dark_mode,
|
|
show_resident_landing: false,
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
match msg {
|
|
Msg::SwitchView(view) => {
|
|
self.current_view = view;
|
|
// Update URL
|
|
let path = self.current_view.to_path();
|
|
if let Err(e) = HistoryManager::push_url(&path) {
|
|
log::error!("Failed to update URL: {:?}", e);
|
|
}
|
|
self.sidebar_visible = false; // Close sidebar on mobile after navigation
|
|
true
|
|
}
|
|
Msg::SwitchContext(context) => {
|
|
self.current_context = context;
|
|
// Store context in localStorage for persistence
|
|
if let Some(storage) = web_sys::window()
|
|
.and_then(|w| w.local_storage().ok())
|
|
.flatten()
|
|
{
|
|
let context_str = match self.current_context {
|
|
ViewContext::Business => "business",
|
|
ViewContext::Person => "person",
|
|
};
|
|
let _ = storage.set_item("view_context", context_str);
|
|
}
|
|
true
|
|
}
|
|
Msg::ToggleSidebar => {
|
|
self.sidebar_visible = !self.sidebar_visible;
|
|
true
|
|
}
|
|
Msg::PopStateChanged => {
|
|
// Handle browser back/forward navigation
|
|
let current_path = HistoryManager::get_current_path();
|
|
let new_view = AppView::from_path(¤t_path);
|
|
|
|
if self.current_view != new_view {
|
|
self.current_view = new_view;
|
|
log::info!("PopState: Updated to view {:?}", self.current_view);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
Msg::ShowToast(toast) => {
|
|
self.toasts.push(toast);
|
|
true
|
|
}
|
|
Msg::DismissToast(toast_id) => {
|
|
self.toasts.retain(|t| t.id != toast_id);
|
|
true
|
|
}
|
|
Msg::Login => {
|
|
// For dev purposes, automatically log in
|
|
self.is_logged_in = true;
|
|
self.user_name = Some("John Doe".to_string());
|
|
true
|
|
}
|
|
Msg::Logout => {
|
|
self.is_logged_in = false;
|
|
self.user_name = None;
|
|
true
|
|
}
|
|
Msg::ToggleTheme => {
|
|
self.is_dark_mode = !self.is_dark_mode;
|
|
// Store theme preference in localStorage
|
|
if let Some(storage) = web_sys::window()
|
|
.and_then(|w| w.local_storage().ok())
|
|
.flatten()
|
|
{
|
|
let theme_str = if self.is_dark_mode { "dark" } else { "light" };
|
|
let _ = storage.set_item("theme", theme_str);
|
|
}
|
|
|
|
// Apply theme to document body immediately
|
|
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
|
|
if let Some(body) = document.body() {
|
|
let theme_attr = if self.is_dark_mode { "dark" } else { "light" };
|
|
let _ = body.set_attribute("data-bs-theme", theme_attr);
|
|
}
|
|
}
|
|
true
|
|
}
|
|
Msg::ShowResidentLanding => {
|
|
self.show_resident_landing = true;
|
|
true
|
|
}
|
|
Msg::HideResidentLanding => {
|
|
self.show_resident_landing = false;
|
|
true
|
|
}
|
|
Msg::ResidentSignIn(email, password) => {
|
|
// Handle resident sign in - for now just log them in
|
|
log::info!("Resident sign in attempt: {}", email);
|
|
self.is_logged_in = true;
|
|
self.user_name = Some(email);
|
|
self.show_resident_landing = false;
|
|
true
|
|
}
|
|
Msg::ResidentRegistrationComplete => {
|
|
// Handle successful resident registration
|
|
self.show_resident_landing = false;
|
|
self.is_logged_in = true;
|
|
self.user_name = Some("New Resident".to_string());
|
|
// Navigate to home or success page
|
|
self.current_view = AppView::Home;
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
let link = ctx.link();
|
|
|
|
// Apply theme to document body
|
|
if let Some(document) = web_sys::window().and_then(|w| w.document()) {
|
|
if let Some(body) = document.body() {
|
|
let theme_attr = if self.is_dark_mode { "dark" } else { "light" };
|
|
let _ = body.set_attribute("data-bs-theme", theme_attr);
|
|
}
|
|
}
|
|
|
|
// Show resident landing overlay if:
|
|
// 1. User is not logged in AND visiting resident registration
|
|
// 2. Or explicitly requested to show the overlay
|
|
let should_show_overlay = self.show_resident_landing ||
|
|
(!self.is_logged_in && matches!(self.current_view, AppView::ResidentRegister));
|
|
|
|
if should_show_overlay {
|
|
return html! {
|
|
<ResidentLandingOverlay
|
|
on_registration_complete={link.callback(|_| Msg::ResidentRegistrationComplete)}
|
|
on_sign_in={link.callback(|(email, password)| Msg::ResidentSignIn(email, password))}
|
|
on_close={Some(link.callback(|_| Msg::HideResidentLanding))}
|
|
/>
|
|
};
|
|
}
|
|
|
|
// Determine theme classes
|
|
let theme_class = if self.is_dark_mode { "bg-dark text-light" } else { "bg-light text-dark" };
|
|
let theme_attr = if self.is_dark_mode { "dark" } else { "light" };
|
|
|
|
// Main application layout - show full layout for logged in users
|
|
html! {
|
|
<div class={format!("d-flex flex-column min-vh-100 {}", theme_class)} data-bs-theme={theme_attr}>
|
|
<Header
|
|
user_name={if self.is_logged_in { self.user_name.clone() } else { None }}
|
|
entity_name={if self.is_logged_in { Some("TechCorp Solutions".to_string()) } else { None }}
|
|
current_context={self.current_context.clone()}
|
|
is_dark_mode={self.is_dark_mode}
|
|
on_sidebar_toggle={link.callback(|_: MouseEvent| Msg::ToggleSidebar)}
|
|
on_login={link.callback(|_: MouseEvent| Msg::Login)}
|
|
on_logout={link.callback(|_: MouseEvent| Msg::Logout)}
|
|
on_context_change={link.callback(Msg::SwitchContext)}
|
|
on_navigate={link.callback(Msg::SwitchView)}
|
|
on_theme_toggle={link.callback(|_: MouseEvent| Msg::ToggleTheme)}
|
|
/>
|
|
|
|
<div class="d-flex flex-grow-1">
|
|
<Sidebar
|
|
current_view={self.current_view.clone()}
|
|
current_context={self.current_context.clone()}
|
|
is_visible={self.sidebar_visible}
|
|
on_view_change={link.callback(Msg::SwitchView)}
|
|
/>
|
|
|
|
<div class="main-content flex-grow-1">
|
|
<main class="py-3 w-100 d-block">
|
|
<div class="container-fluid">
|
|
{self.render_current_view(ctx)}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<Footer />
|
|
|
|
// Toast notifications
|
|
<ToastContainer
|
|
toasts={self.toasts.clone()}
|
|
on_dismiss={link.callback(Msg::DismissToast)}
|
|
/>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
impl App {
|
|
fn render_current_view(&self, ctx: &Context<Self>) -> Html {
|
|
match &self.current_view {
|
|
AppView::Login => {
|
|
// Login is not used in this app, redirect to home
|
|
html! { <HomeView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Home => {
|
|
html! { <HomeView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Administration => {
|
|
html! { <AdministrationView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::PersonAdministration => {
|
|
html! { <PersonAdministrationView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Business => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<BusinessView
|
|
context={self.current_context.clone()}
|
|
company_id={Some(1)} // Show the first company by default
|
|
on_navigate={Some(link.callback(Msg::SwitchView))}
|
|
/>
|
|
}
|
|
}
|
|
AppView::Accounting => {
|
|
html! { <AccountingView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Contracts => {
|
|
html! { <ContractsView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Governance => {
|
|
html! { <GovernanceView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Treasury => {
|
|
html! { <TreasuryView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Residence => {
|
|
html! { <ResidenceView context={self.current_context.clone()} /> }
|
|
}
|
|
AppView::Entities => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<EntitiesView
|
|
on_navigate={Some(link.callback(Msg::SwitchView))}
|
|
/>
|
|
}
|
|
}
|
|
AppView::EntitiesRegister => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<EntitiesView
|
|
on_navigate={Some(link.callback(Msg::SwitchView))}
|
|
show_registration={true}
|
|
/>
|
|
}
|
|
}
|
|
AppView::EntitiesRegisterSuccess(company_id) => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<EntitiesView
|
|
on_navigate={Some(link.callback(Msg::SwitchView))}
|
|
show_registration={true}
|
|
registration_success={Some(*company_id)}
|
|
/>
|
|
}
|
|
}
|
|
AppView::EntitiesRegisterFailure => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<EntitiesView
|
|
on_navigate={Some(link.callback(Msg::SwitchView))}
|
|
show_registration={true}
|
|
registration_failure={true}
|
|
/>
|
|
}
|
|
}
|
|
AppView::CompanyView(company_id) => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<BusinessView
|
|
context={self.current_context.clone()}
|
|
company_id={Some(*company_id)}
|
|
on_navigate={Some(link.callback(Msg::SwitchView))}
|
|
/>
|
|
}
|
|
}
|
|
AppView::ResidentRegister => {
|
|
let link = ctx.link();
|
|
html! {
|
|
<ResidentRegistrationView
|
|
on_registration_complete={link.callback(|_resident: DigitalResident| {
|
|
Msg::SwitchView(AppView::ResidentRegisterSuccess)
|
|
})}
|
|
on_navigate={link.callback(Msg::SwitchView)}
|
|
/>
|
|
}
|
|
}
|
|
AppView::ResidentRegisterSuccess => {
|
|
html! {
|
|
<div class="container-fluid">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="bi bi-check-circle text-success" style="font-size: 4rem;"></i>
|
|
<h2 class="mt-3 mb-3">{"Registration Successful!"}</h2>
|
|
<p class="lead text-muted mb-4">
|
|
{"Your digital resident registration has been completed successfully. Welcome to the community!"}
|
|
</p>
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick={ctx.link().callback(|_| Msg::SwitchView(AppView::Home))}
|
|
>
|
|
{"Continue to Dashboard"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
AppView::ResidentRegisterFailure => {
|
|
html! {
|
|
<div class="container-fluid">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8">
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="bi bi-x-circle text-danger" style="font-size: 4rem;"></i>
|
|
<h2 class="mt-3 mb-3">{"Registration Failed"}</h2>
|
|
<p class="lead text-muted mb-4">
|
|
{"There was an issue with your digital resident registration. Please try again."}
|
|
</p>
|
|
<div class="d-flex gap-2 justify-content-center">
|
|
<button
|
|
class="btn btn-primary"
|
|
onclick={ctx.link().callback(|_| Msg::SwitchView(AppView::ResidentRegister))}
|
|
>
|
|
{"Try Again"}
|
|
</button>
|
|
<button
|
|
class="btn btn-outline-secondary"
|
|
onclick={ctx.link().callback(|_| Msg::SwitchView(AppView::Home))}
|
|
>
|
|
{"Back to Home"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
AppView::ResidentLanding => {
|
|
// This should never be reached since ResidentLanding is handled by the overlay
|
|
// But we need this match arm to satisfy the compiler
|
|
html! {
|
|
<div class="container-fluid">
|
|
<div class="text-center py-5">
|
|
<p>{"Loading..."}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |