freezone/platform/src/app.rs
2025-06-27 04:13:31 +02:00

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(&current_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(&current_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>
}
}
}
}
}