initial commit

This commit is contained in:
Timur Gordon 2025-06-27 04:13:31 +02:00
commit b2ee21999f
134 changed files with 35580 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
target
.env
dist

1
defi/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

7
defi/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "defi"
version = "0.1.0"

6
defi/Cargo.toml Normal file
View File

@ -0,0 +1,6 @@
[package]
name = "defi"
version = "0.1.0"
edition = "2024"
[dependencies]

14
defi/src/lib.rs Normal file
View File

@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

1
marketplace/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1319
marketplace/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

37
marketplace/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "marketplace"
version = "0.1.0"
edition = "2021"
[workspace]
[lib]
crate-type = ["cdylib"]
[dependencies]
yew = { version = "0.21", features = ["csr"] }
web-sys = { version = "0.3", features = [
"console",
"Document",
"Element",
"HtmlElement",
"HtmlInputElement",
"HtmlSelectElement",
"Location",
"Window",
"History",
"MouseEvent",
"Event",
"EventTarget",
"Storage",
"UrlSearchParams"
] }
wasm-bindgen = "0.2"
log = "0.4"
wasm-logger = "0.2"
gloo = { version = "0.10", features = ["storage", "timers", "events"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
wasm-bindgen-test = "0.3"

11
marketplace/Trunk.toml Normal file
View File

@ -0,0 +1,11 @@
[build]
target = "index.html"
dist = "dist"
[watch]
watch = ["src", "index.html", "static"]
[serve]
address = "127.0.0.1"
port = 8080
open = false

50
marketplace/index.html Normal file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zanzibar Digital Freezone Marketplace</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link rel="stylesheet" href="/static/style.css">
<link data-trunk rel="css" href="static/css/main.css">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>💎</text></svg>">
</head>
<body>
<div id="app"></div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- Theme Toggle Script -->
<script>
// Theme toggle functionality
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
}
// Load saved theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
document.documentElement.setAttribute('data-bs-theme', savedTheme);
}
</script>
</body>
</html>

77
marketplace/src/app.rs Normal file
View File

@ -0,0 +1,77 @@
use yew::prelude::*;
use crate::routing::{AppView, HistoryManager};
use crate::components::{Header, Footer};
use crate::views::{CreateListingView, EditListingView, HomeView, ListingDetailView, MyListingsView};
#[derive(Clone, Debug)]
pub enum Msg {
SwitchView(AppView),
PopStateChanged,
}
pub struct App {
history_manager: HistoryManager,
current_view: AppView,
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(ctx: &Context<Self>) -> Self {
let history_manager = HistoryManager::new(ctx.link().callback(|_| Msg::PopStateChanged));
let current_view = history_manager.current_view();
Self {
history_manager,
current_view,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::SwitchView(view) => {
if self.current_view != view {
self.history_manager.push_state(&view);
self.current_view = view;
return true;
}
false
}
Msg::PopStateChanged => {
let new_view = self.history_manager.current_view();
if self.current_view != new_view {
self.current_view = new_view;
return true;
}
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<div class="d-flex flex-column min-vh-100">
<Header on_view_change={ctx.link().callback(Msg::SwitchView)} />
<main class="flex-grow-1">
{ self.render_current_view(ctx) }
</main>
<Footer />
</div>
}
}
}
impl App {
fn render_current_view(&self, ctx: &Context<Self>) -> Html {
match &self.current_view {
AppView::Home | AppView::Browse => html! { <HomeView on_view_change={ctx.link().callback(Msg::SwitchView)} /> },
AppView::AssetDetail(id) => html! { <ListingDetailView id={id.clone()} /> },
AppView::MyListings => html! { <MyListingsView /> },
AppView::Purchases => html! { <MyListingsView /> }, // Placeholder for now
AppView::CreateListing => html! { <CreateListingView /> },
AppView::EditListing(id) => html! { <EditListingView id={id.clone()} /> },
_ => html!{ <HomeView on_view_change={ctx.link().callback(Msg::SwitchView)} /> } // Default to home
}
}
}

View File

@ -0,0 +1,34 @@
use yew::prelude::*;
#[function_component(Footer)]
pub fn footer() -> Html {
html! {
<footer class="footer mt-auto py-4 border-top">
<div class="container-fluid px-4">
<div class="row align-items-center">
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="bi bi-gem me-2 text-primary fs-5"></i>
<span class="fw-semibold text-primary">{ "Zanzibar Digital Freezone" }</span>
</div>
<p class="text-muted mb-0 mt-1">{ "Trade tokenized real-world assets, NFTs, and crypto on blockchain" }</p>
</div>
<div class="col-md-6 text-md-end mt-3 mt-md-0">
<div class="d-flex justify-content-md-end justify-content-start align-items-center">
<a href="#" class="text-muted me-3 text-decoration-none">
<i class="bi bi-shield-check me-1"></i>{ "Security" }
</a>
<a href="#" class="text-muted me-3 text-decoration-none">
<i class="bi bi-question-circle me-1"></i>{ "Help" }
</a>
<a href="#" class="text-muted text-decoration-none">
<i class="bi bi-file-text me-1"></i>{ "Terms" }
</a>
</div>
<p class="text-muted mb-0 mt-2 small">{ "© 2025 Zanzibar Digital Marketplace. All rights reserved." }</p>
</div>
</div>
</div>
</footer>
}
}

View File

@ -0,0 +1,98 @@
use yew::prelude::*;
use crate::routing::AppView;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn toggleTheme();
}
#[derive(Properties, PartialEq)]
pub struct HeaderProps {
pub on_view_change: Callback<AppView>,
}
#[function_component(Header)]
pub fn header(props: &HeaderProps) -> Html {
let on_nav_click = |view: AppView| {
let on_view_change = props.on_view_change.clone();
Callback::from(move |_| on_view_change.emit(view.clone()))
};
html! {
<header class="navbar navbar-expand-lg shadow-sm py-3">
<div class="container-fluid px-4">
<a class="navbar-brand fw-bold d-flex align-items-center" href="#" onclick={on_nav_click(AppView::Home)}>
<i class="bi bi-gem me-2 fs-3 text-primary"></i>
<span class="text-primary">{ "Zanzibar Digital Freezone" }</span>
</a>
<button class="navbar-toggler border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active fw-semibold" aria-current="page" href="#" onclick={on_nav_click(AppView::Home)}>
<i class="bi bi-shop me-1"></i>
{ "Marketplace" }
</a>
</li>
<li class="nav-item">
<a class="nav-link fw-semibold" href="#" onclick={on_nav_click(AppView::Browse)}>
<i class="bi bi-grid-3x3-gap me-1"></i>
{ "Browse" }
</a>
</li>
</ul>
<ul class="navbar-nav ms-auto d-flex align-items-center">
<li class="nav-item me-3">
<button class="btn btn-outline-secondary btn-sm rounded-pill"
onclick={Callback::from(|_| toggleTheme())}
title="Toggle theme">
<i class="bi bi-moon-stars"></i>
</button>
</li>
<li class="nav-item me-3">
<a class="nav-link position-relative" href="#" onclick={on_nav_click(AppView::Purchases)} title="My Cart">
<i class="bi bi-cart3 fs-5"></i>
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger" style="font-size: 0.6rem;">
{ "2" }
</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center fw-semibold" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="rounded-circle bg-primary d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="bi bi-person-fill text-white"></i>
</div>
{ "Profile" }
</a>
<ul class="dropdown-menu dropdown-menu-end shadow-lg border-0" aria-labelledby="navbarDropdown" style="border-radius: 12px; min-width: 200px;">
<li><a class="dropdown-item py-2" href="#" onclick={on_nav_click(AppView::MyListings)}>
<i class="bi bi-list-ul me-2"></i>{ "My Listings" }
</a></li>
<li><a class="dropdown-item py-2" href="#" onclick={on_nav_click(AppView::CreateListing)}>
<i class="bi bi-plus-circle me-2"></i>{ "Create Listing" }
</a></li>
<li><a class="dropdown-item py-2" href="#" onclick={on_nav_click(AppView::Purchases)}>
<i class="bi bi-bag-check me-2"></i>{ "My Purchases" }
</a></li>
<li><hr class="dropdown-divider my-2" /></li>
<li><a class="dropdown-item py-2" href="#">
<i class="bi bi-gear me-2"></i>{ "Settings" }
</a></li>
<li><a class="dropdown-item py-2 text-danger" href="#">
<i class="bi bi-box-arrow-right me-2"></i>{ "Logout" }
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</header>
}
}

View File

@ -0,0 +1,7 @@
pub mod footer;
pub mod header;
pub mod sidebar;
pub use footer::Footer;
pub use header::Header;

View File

@ -0,0 +1,74 @@
use yew::prelude::*;
use crate::routing::AppView;
#[derive(Properties, PartialEq)]
pub struct SidebarProps {
pub current_view: AppView,
pub is_visible: bool,
pub on_view_change: Callback<AppView>,
}
#[function_component(Sidebar)]
pub fn sidebar(props: &SidebarProps) -> Html {
let class = if props.is_visible { "sidebar-visible" } else { "" };
let on_view_change = props.on_view_change.clone();
let view_changer = move |view: AppView| {
let on_view_change = on_view_change.clone();
Callback::from(move |_| {
on_view_change.emit(view.clone());
})
};
let home_active = if props.current_view == AppView::Home { "active" } else { "" };
let browse_active = if matches!(props.current_view, AppView::Browse | AppView::AssetDetail(_)) { "active" } else { "" };
let listings_active = if props.current_view == AppView::MyListings { "active" } else { "" };
let purchases_active = if props.current_view == AppView::Purchases { "active" } else { "" };
let watchlist_active = if props.current_view == AppView::Watchlist { "active" } else { "" };
let create_listing_active = if props.current_view == AppView::CreateListing { "active" } else { "" };
html! {
<nav id="sidebarMenu" class={classes!("col-md-3", "col-lg-2", "d-md-block", "bg-dark", "sidebar", "collapse", class)}>
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class={classes!("nav-link", home_active)}
href="#" onclick={view_changer(AppView::Home)}>
{ "Home" }
</a>
</li>
<li class="nav-item">
<a class={classes!("nav-link", browse_active)}
href="#" onclick={view_changer(AppView::Browse)}>
{ "Browse Marketplace" }
</a>
</li>
<li class="nav-item">
<a class={classes!("nav-link", listings_active)}
href="#" onclick={view_changer(AppView::MyListings)}>
{ "My Listings" }
</a>
</li>
<li class="nav-item">
<a class={classes!("nav-link", create_listing_active)}
href="#" onclick={view_changer(AppView::CreateListing)}>
{ "Create Listing" }
</a>
</li>
<li class="nav-item">
<a class={classes!("nav-link", purchases_active)}
href="#" onclick={view_changer(AppView::Purchases)}>
{ "My Purchases" }
</a>
</li>
<li class="nav-item">
<a class={classes!("nav-link", watchlist_active)}
href="#" onclick={view_changer(AppView::Watchlist)}>
{ "Watchlist" }
</a>
</li>
</ul>
</div>
</nav>
}
}

13
marketplace/src/lib.rs Normal file
View File

@ -0,0 +1,13 @@
use wasm_bindgen::prelude::*;
mod app;
mod components;
mod routing;
mod views;
use app::App;
#[wasm_bindgen(start)]
pub fn run_app() {
yew::Renderer::<App>::new().render();
}

View File

@ -0,0 +1,86 @@
use yew::Callback;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
#[derive(Clone, PartialEq, Debug)]
pub enum AppView {
Home,
Browse,
AssetDetail(String),
MyListings,
Purchases,
Watchlist,
CreateListing,
EditListing(String),
}
impl AppView {
pub fn to_path(&self) -> String {
match self {
AppView::Home => "/".to_string(),
AppView::Browse => "/browse".to_string(),
AppView::AssetDetail(id) => format!("/asset/{}", id),
AppView::MyListings => "/my-listings".to_string(),
AppView::Purchases => "/my-purchases".to_string(),
AppView::Watchlist => "/my-watchlist".to_string(),
AppView::CreateListing => "/create-listing".to_string(),
AppView::EditListing(id) => format!("/edit-listing/{}", id),
}
}
pub fn from_path(path: &str) -> Self {
if path.starts_with("/asset/") {
let id = path.trim_start_matches("/asset/").to_string();
return AppView::AssetDetail(id);
} else if path.starts_with("/edit-listing/") {
let id = path.trim_start_matches("/edit-listing/").to_string();
return AppView::EditListing(id);
}
match path {
"/" => AppView::Home,
"/browse" => AppView::Browse,
"/my-listings" => AppView::MyListings,
"/my-purchases" => AppView::Purchases,
"/my-watchlist" => AppView::Watchlist,
"/create-listing" => AppView::CreateListing,
_ => AppView::Home, // Default to Home for unknown paths
}
}
}
pub struct HistoryManager {
_closure: Closure<dyn FnMut(web_sys::Event)>,
}
impl HistoryManager {
pub fn new(on_popstate: Callback<()>) -> Self {
let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
on_popstate.emit(());
}) as Box<dyn FnMut(_)>);
if let Some(window) = web_sys::window() {
let _ = window.add_event_listener_with_callback("popstate", closure.as_ref().unchecked_ref());
}
HistoryManager { _closure: closure }
}
pub fn push_state(&self, view: &AppView) {
let path = view.to_path();
if let Some(window) = web_sys::window() {
if let Ok(history) = window.history() {
if history.push_state_with_url(&JsValue::NULL, "", Some(&path)).is_err() {
log::error!("Could not push state for path: {}", path);
}
}
}
}
pub fn current_view(&self) -> AppView {
let path = web_sys::window()
.and_then(|w| w.location().pathname().ok())
.unwrap_or_else(|| "/".to_string());
AppView::from_path(&path)
}
}

View File

@ -0,0 +1,386 @@
use yew::prelude::*;
use web_sys::{HtmlInputElement, HtmlSelectElement, HtmlTextAreaElement};
#[derive(Clone, PartialEq, Debug, Default)]
struct ListingFormData {
title: String,
asset_id: String,
description: String,
price: String,
currency: String,
listing_type: String,
duration_days: u32,
tags: String,
terms_agreed: bool,
}
#[derive(Clone, PartialEq)]
struct Asset {
id: u32,
name: String,
asset_type: String,
image_url: String,
}
#[derive(Clone, PartialEq)]
struct SelectedAssetPreview {
name: String,
asset_type: String,
image_url: String,
}
pub enum Msg {
UpdateString(String, String),
UpdateU32(String, u32),
UpdateBool(String, bool),
SelectAsset(String),
Submit,
}
pub struct CreateListingView {
form_data: ListingFormData,
assets: Vec<Asset>,
listing_types: Vec<String>,
selected_asset: Option<SelectedAssetPreview>,
}
impl Component for CreateListingView {
type Message = Msg;
type Properties = ();
fn create(_ctx: &Context<Self>) -> Self {
Self {
form_data: ListingFormData {
duration_days: 30,
currency: "USD".to_string(),
listing_type: "Sale".to_string(),
..Default::default()
},
assets: vec![
Asset { id: 1, name: "Coastal Reforestation Project".to_string(), asset_type: "Carbon Credits".to_string(), image_url: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.0.3".to_string() },
Asset { id: 2, name: "Community Wind Farm Share".to_string(), asset_type: "Energy Cooperative".to_string(), image_url: "https://images.unsplash.com/photo-1466611653911-95081537e5b7?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.0.3".to_string() },
Asset { id: 3, name: "Organic Farm Collective".to_string(), asset_type: "Tokenized Land".to_string(), image_url: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=400&auto=format&fit=crop&ixlib=rb-4.0.3".to_string() },
],
listing_types: vec!["Sale".to_string(), "Auction".to_string(), "Offer".to_string()],
selected_asset: None,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::UpdateString(name, value) => match name.as_str() {
"title" => self.form_data.title = value,
"description" => self.form_data.description = value,
"price" => self.form_data.price = value,
"currency" => self.form_data.currency = value,
"listing_type" => self.form_data.listing_type = value,
"tags" => self.form_data.tags = value,
_ => (),
},
Msg::UpdateU32(name, value) => {
if name == "duration_days" {
self.form_data.duration_days = value;
}
}
Msg::UpdateBool(name, value) => {
if name == "terms" {
self.form_data.terms_agreed = value;
}
}
Msg::SelectAsset(asset_id) => {
self.form_data.asset_id = asset_id.clone();
self.selected_asset = self.assets.iter().find(|a| a.id.to_string() == asset_id).map(|asset| SelectedAssetPreview {
name: asset.name.clone(),
asset_type: asset.asset_type.clone(),
image_url: asset.image_url.clone(),
});
}
Msg::Submit => {
log::info!("Submitting form: {:?}", self.form_data);
return false; // Prevent re-render on submit for now
}
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let on_input = |name: String| link.callback(move |e: InputEvent| {
let target = e.target_unchecked_into::<HtmlInputElement>();
Msg::UpdateString(name.clone(), target.value())
});
let on_textarea_input = |name: String| link.callback(move |e: InputEvent| {
let target = e.target_unchecked_into::<HtmlTextAreaElement>();
Msg::UpdateString(name.clone(), target.value())
});
let on_select_change = |name: String| link.callback(move |e: Event| {
let target = e.target_unchecked_into::<HtmlSelectElement>();
Msg::UpdateString(name.clone(), target.value())
});
let on_asset_select = link.callback(|e: Event| {
let target = e.target_unchecked_into::<HtmlSelectElement>();
Msg::SelectAsset(target.value())
});
let on_checkbox_change = |name: String| link.callback(move |e: Event| {
let target = e.target_unchecked_into::<HtmlInputElement>();
Msg::UpdateBool(name.clone(), target.checked())
});
let _on_number_input = |name: String| link.callback(move |e: InputEvent| {
let target = e.target_unchecked_into::<HtmlInputElement>();
Msg::UpdateU32(name.clone(), target.value_as_number() as u32)
});
let on_submit = link.callback(|e: SubmitEvent| {
e.prevent_default();
Msg::Submit
});
html! {
<div class="fade-in">
<div class="container-fluid px-4 py-4">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
<li class="breadcrumb-item"><a href="/browse">{ "Marketplace" }</a></li>
<li class="breadcrumb-item active" aria-current="page">{ "Create Listing" }</li>
</ol>
</nav>
<div class="mb-4">
<h1 class="h2 fw-bold mb-2">{ "Create New Listing" }</h1>
<p class="text-muted">{ "List your digital asset for sale or auction" }</p>
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card border-0 shadow-lg">
<div class="card-header bg-transparent border-0 p-4">
<h5 class="fw-bold mb-0">
<i class="bi bi-plus-circle me-2 text-primary"></i>
{ "Listing Details" }
</h5>
</div>
<div class="card-body p-4 pt-0">
<form onsubmit={on_submit}>
<div class="mb-4">
<label for="title" class="form-label">{ "Listing Title" }</label>
<input type="text" class="form-control" id="title" required=true
placeholder="Enter a compelling title for your asset"
oninput={on_input("title".to_string())} />
<div class="form-text">{ "Make it descriptive and eye-catching" }</div>
</div>
<div class="mb-4">
<label for="asset_id" class="form-label">{ "Select Asset" }</label>
<select class="form-select" id="asset_id" required=true onchange={on_asset_select}>
<option value="" selected=true disabled=true>{ "Choose an asset to list" }</option>
{ for self.assets.iter().map(|asset| html! {
<option value={asset.id.to_string()}>
{ format!("{} ({})", asset.name, asset.asset_type) }
</option>
}) }
</select>
<div class="form-text">{ "Select from your owned digital assets" }</div>
</div>
<div class="mb-4">
<label for="description" class="form-label">{ "Description" }</label>
<textarea class="form-control" id="description" rows="5" required=true
placeholder="Describe your asset in detail. Include its features, rarity, and any special attributes..."
oninput={on_textarea_input("description".to_string())}></textarea>
<div class="form-text">{ "Provide detailed information to attract buyers" }</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="price" class="form-label">{ "Starting Price" }</label>
<div class="input-group">
<span class="input-group-text bg-primary text-white">{ "$" }</span>
<input type="number" class="form-control" id="price" required=true
step="0.01" min="0.01" placeholder="0.00"
oninput={on_input("price".to_string())} />
</div>
</div>
<div class="col-md-6">
<label for="currency" class="form-label">{ "Currency" }</label>
<select class="form-select" id="currency" required=true onchange={on_select_change("currency".to_string())}>
<option value="USD" selected={self.form_data.currency == "USD"}>{ "USD ($)" }</option>
<option value="EUR" selected={self.form_data.currency == "EUR"}>{ "EUR (€)" }</option>
</select>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="listing_type" class="form-label">{ "Listing Type" }</label>
<select class="form-select" id="listing_type" required=true onchange={on_select_change("listing_type".to_string())}>
{ for self.listing_types.iter().map(|l_type| html! {
<option value={l_type.clone()} selected={self.form_data.listing_type == *l_type}>
{ match l_type.as_str() {
"Sale" => "Fixed Price Sale",
"Auction" => "Auction (Bidding)",
"Offer" => "Accept Offers",
_ => l_type
}}
</option>
}) }
</select>
</div>
<div class="col-md-6">
<label for="duration_days" class="form-label">{ "Duration (Days)" }</label>
<select class="form-select" id="duration_days" onchange={on_select_change("duration_days".to_string())}>
<option value="7" selected={self.form_data.duration_days == 7}>{ "7 days" }</option>
<option value="14" selected={self.form_data.duration_days == 14}>{ "14 days" }</option>
<option value="30" selected={self.form_data.duration_days == 30}>{ "30 days" }</option>
<option value="60" selected={self.form_data.duration_days == 60}>{ "60 days" }</option>
<option value="90" selected={self.form_data.duration_days == 90}>{ "90 days" }</option>
</select>
</div>
</div>
<div class="mb-4">
<label for="tags" class="form-label">{ "Tags" }</label>
<input type="text" class="form-control" id="tags"
placeholder="rare, collectible, gaming, art (separate with commas)"
oninput={on_input("tags".to_string())} />
<div class="form-text">{ "Add relevant tags to help buyers find your asset" }</div>
</div>
<div class="mb-4">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="terms" required=true
onchange={on_checkbox_change("terms".to_string())} />
<label class="form-check-label" for="terms">
{ "I agree to the " }
<a href="#" class="text-primary text-decoration-none">{ "marketplace terms and conditions" }</a>
{ " and confirm that I own this digital asset" }
</label>
</div>
</div>
<div class="d-flex gap-3 justify-content-end">
<a href="/browse" class="btn btn-outline-secondary px-4">
<i class="bi bi-x-circle me-2"></i>{ "Cancel" }
</a>
<button type="submit" class="btn btn-marketplace px-4">
<i class="bi bi-plus-circle me-2"></i>{ "Create Listing" }
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-lg mb-4">
<div class="card-header bg-transparent border-0 p-4">
<h6 class="fw-bold mb-0">
<i class="bi bi-eye me-2 text-primary"></i>
{ "Asset Preview" }
</h6>
</div>
<div class="card-body text-center p-4 pt-0">
{ self.view_asset_preview() }
</div>
</div>
<div class="card border-0 shadow-lg mb-4">
<div class="card-header bg-transparent border-0 p-4">
<h6 class="fw-bold mb-0">
<i class="bi bi-lightbulb me-2 text-primary"></i>
{ "Listing Tips" }
</h6>
</div>
<div class="card-body p-4 pt-0">
<div class="d-flex flex-column gap-3">
<div class="d-flex align-items-start">
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
<div>
<h6 class="mb-1">{ "Clear Title" }</h6>
<small class="text-muted">{ "Use descriptive, searchable keywords" }</small>
</div>
</div>
<div class="d-flex align-items-start">
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
<div>
<h6 class="mb-1">{ "Detailed Description" }</h6>
<small class="text-muted">{ "Include features, rarity, and provenance" }</small>
</div>
</div>
<div class="d-flex align-items-start">
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
<div>
<h6 class="mb-1">{ "Competitive Pricing" }</h6>
<small class="text-muted">{ "Research similar assets for fair pricing" }</small>
</div>
</div>
<div class="d-flex align-items-start">
<i class="bi bi-check-circle text-success me-3 mt-1"></i>
<div>
<h6 class="mb-1">{ "Relevant Tags" }</h6>
<small class="text-muted">{ "Help buyers discover your listing" }</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
}
impl CreateListingView {
fn view_asset_preview(&self) -> Html {
match &self.selected_asset {
Some(asset) => html! {
<div class="slide-up">
<div class="position-relative mb-3">
{ if !asset.image_url.is_empty() {
html! {
<img src={asset.image_url.clone()}
class="img-fluid rounded-3 shadow-sm"
alt={asset.name.clone()}
style="max-height: 250px; width: 100%; object-fit: cover;" />
}
} else {
html! {
<div class="bg-primary-subtle d-flex align-items-center justify-content-center rounded-3 shadow-sm"
style="height: 250px;">
<i class="bi bi-collection text-primary" style="font-size: 4rem;"></i>
</div>
}
}}
<div class="position-absolute top-0 end-0 m-2">
<span class="badge bg-primary-subtle text-primary px-3 py-2">{ &asset.asset_type }</span>
</div>
</div>
<h5 class="fw-bold mb-2">{ &asset.name }</h5>
<p class="text-muted small mb-3">{ "This is how your asset will appear to potential buyers in the marketplace." }</p>
<div class="d-flex justify-content-between align-items-center p-3 bg-light rounded-3">
<span class="text-muted small">{ "Preview Mode" }</span>
<i class="bi bi-eye text-primary"></i>
</div>
</div>
},
None => html! {
<div class="text-center py-5">
<div class="bg-light d-flex align-items-center justify-content-center rounded-3 mb-4"
style="height: 200px;">
<i class="bi bi-image text-muted" style="font-size: 4rem;"></i>
</div>
<h6 class="text-muted mb-2">{ "No Asset Selected" }</h6>
<p class="text-muted small mb-0">{ "Choose an asset from the dropdown above to see how it will appear to buyers." }</p>
</div>
},
}
}
}

View File

@ -0,0 +1,82 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct EditListingProps {
pub id: String,
}
#[function_component(EditListingView)]
pub fn edit_listing_view(props: &EditListingProps) -> Html {
// Mock data for an existing listing
let listing_title = "Rare Digital Sword of the Ancients";
let listing_price = "150.00";
let asset_preview_url = "https://via.placeholder.com/300/007bff/fff?text=Digital+Sword";
html! {
<div class="container-fluid px-4">
<h1 class="mt-4">{ format!("Edit Listing #{}", &props.id) }</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
<li class="breadcrumb-item"><a href="/my-listings">{ "My Listings" }</a></li>
<li class="breadcrumb-item active">{ "Edit Listing" }</li>
</ol>
<div class="card mb-4 shadow-sm">
<div class="card-header">
<i class="bi bi-pencil-square me-2"></i>{ "Listing Details" }
</div>
<div class="card-body">
<form>
<div class="row">
<div class="col-md-8">
// Form Fields
<div class="mb-3">
<label for="listing-title" class="form-label">{ "Listing Title" }</label>
<input type="text" class="form-control" id="listing-title" value={listing_title} />
</div>
<div class="mb-3">
<label for="listing-description" class="form-label">{ "Description" }</label>
<textarea class="form-control" id="listing-description" rows="4">{ listing_description }</textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="listing-price" class="form-label">{ "Price (USD)" }</label>
<input type="number" class="form-control" id="listing-price" value={listing_price} />
</div>
<div class="col-md-6 mb-3">
<label for="listing-type" class="form-label">{ "Listing Type" }</label>
<select id="listing-type" class="form-select">
<option>{ "Sale" }</option>
<option selected=true>{ "Auction" }</option>
</select>
</div>
</div>
</div>
<div class="col-md-4">
// Asset Preview
<div class="mb-3">
<label class="form-label">{ "Asset Preview" }</label>
<div class="card text-center">
<div class="card-body">
<img src={asset_preview_url} class="img-fluid rounded mb-3" alt="Asset Preview" />
<h5>{ "Digital Sword" }</h5>
<p class="text-muted">{ "Game Item" }</p>
</div>
<div class="card-footer">
<button type="button" class="btn btn-secondary btn-sm">{ "Change Asset" }</button>
</div>
</div>
</div>
</div>
</div>
<hr class="my-4" />
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-secondary me-2">{ "Cancel" }</button>
<button type="submit" class="btn btn-primary">{ "Save Changes" }</button>
</div>
</form>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,254 @@
use yew::prelude::*;
use crate::routing::AppView;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
#[derive(Properties, PartialEq)]
pub struct HomeViewProps {
pub on_view_change: Callback<AppView>,
}
#[derive(Clone, PartialEq)]
struct Listing {
id: String,
title: String,
description: String,
image_url: String,
price: f64,
listing_type: String,
asset_type: String,
seller_name: String,
}
#[function_component(HomeView)]
pub fn home_view(props: &HomeViewProps) -> Html {
let listings = use_memo((), |_| vec![
Listing {
id: "1".to_string(),
title: "Permaculture Farm Token - Kilifi Coast".to_string(),
description: "Tokenized ownership of 50 hectares of regenerative permaculture farmland on Kenya's coast. Includes carbon sequestration rights and sustainable agriculture revenue sharing.".to_string(),
image_url: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 12500.00,
listing_type: "Sale".to_string(),
asset_type: "Tokenized Land".to_string(),
seller_name: "EcoFarms Collective".to_string(),
},
Listing {
id: "2".to_string(),
title: "Community Solar Cooperative Share".to_string(),
description: "Digital ownership certificate for a community-driven solar energy cooperative. Earn dividends from clean energy production while supporting local energy independence.".to_string(),
image_url: "https://images.unsplash.com/photo-1509391366360-2e959784a276?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 850.00,
listing_type: "Auction".to_string(),
asset_type: "Energy Cooperative".to_string(),
seller_name: "SolarCommunity DAO".to_string(),
},
Listing {
id: "3".to_string(),
title: "Biodiversity Credits - Coastal Mangroves".to_string(),
description: "Verified biodiversity conservation credits from protected mangrove restoration project. Each credit represents 1 hectare of preserved coastal ecosystem.".to_string(),
image_url: "https://images.unsplash.com/photo-1559827260-dc66d52bef19?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 450.00,
listing_type: "Sale".to_string(),
asset_type: "Biodiversity Credits".to_string(),
seller_name: "Ocean Guardians".to_string(),
},
Listing {
id: "4".to_string(),
title: "Carbon Offset Portfolio - Reforestation".to_string(),
description: "Premium carbon offset credits from verified reforestation projects across East Africa. Each token represents 1 ton of CO2 sequestered through native tree planting.".to_string(),
image_url: "https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 125.00,
listing_type: "Sale".to_string(),
asset_type: "Carbon Credits".to_string(),
seller_name: "TreeFuture Initiative".to_string(),
},
Listing {
id: "5".to_string(),
title: "Rare CryptoPunk #7804".to_string(),
description: "One of the most sought-after CryptoPunks featuring the rare alien type with cap and small shades. This iconic NFT represents early blockchain art history and digital ownership.".to_string(),
image_url: "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 85000.00,
listing_type: "Sale".to_string(),
asset_type: "NFT".to_string(),
seller_name: "CryptoCollector".to_string(),
},
Listing {
id: "6".to_string(),
title: "DeFi Yield Farm LP Tokens".to_string(),
description: "High-yield liquidity provider tokens from a verified DeFi protocol offering 12% APY. Backed by blue-chip crypto assets with automated compounding rewards.".to_string(),
image_url: "https://images.unsplash.com/photo-1639762681485-074b7f938ba0?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 5000.00,
listing_type: "Sale".to_string(),
asset_type: "DeFi Token".to_string(),
seller_name: "YieldMaster DAO".to_string(),
},
]);
let is_scrolled = use_state(|| false);
{
let is_scrolled = is_scrolled.clone();
use_effect_with((), move |_| {
let closure = Closure::wrap(Box::new(move || {
if let Some(window) = web_sys::window() {
is_scrolled.set(window.scroll_y().unwrap_or(0.0) > 50.0);
}
}) as Box<dyn FnMut()>);
if let Some(window) = web_sys::window() {
window
.add_event_listener_with_callback("scroll", closure.as_ref().unchecked_ref())
.unwrap();
}
move || drop(closure)
});
}
let on_card_click = {
let on_view_change = props.on_view_change.clone();
Callback::from(move |id: String| {
on_view_change.emit(AppView::AssetDetail(id));
})
};
let featured_listing = listings[0].clone();
let render_listing_card = |listing: &Listing, is_featured: bool| {
let on_view_change = on_card_click.clone();
let listing_id = listing.id.clone();
let onclick = Callback::from(move |_| on_view_change.emit(listing_id.clone()));
html! {
<div class={if is_featured { "col-lg-6" } else { "col" }} onclick={onclick}>
<div class="card h-100 marketplace-card fade-in" style="cursor: pointer;">
<div class="position-relative overflow-hidden">
<img src={listing.image_url.clone()} class="card-img-top" alt={listing.title.clone()} style="height: 250px; object-fit: cover;" />
<div class="position-absolute top-0 end-0 m-3">
<span class="badge bg-primary-subtle text-primary px-3 py-2">{ &listing.asset_type }</span>
</div>
</div>
<div class="card-body d-flex flex-column p-4">
<div class="d-flex justify-content-end align-items-start mb-2">
<small class="text-muted">{ format!("by {}", listing.seller_name) }</small>
</div>
<h5 class="card-title fw-bold mb-2">{ &listing.title }</h5>
<p class="card-text text-muted flex-grow-1 mb-3 small">{ &listing.description }</p>
<div class="d-flex justify-content-between align-items-center">
<div class="price-badge">{ format!("${:.2}", listing.price) }</div>
<i class="bi bi-arrow-right text-primary fs-5"></i>
</div>
</div>
</div>
</div>
}
};
let _sticky_class = if *is_scrolled { "sticky-top" } else { "" };
html! {
<div class="fade-in">
// Hero Section
<div class="hero-section py-5">
<div class="container-fluid px-4">
<div class="row align-items-center min-vh-50">
<div class="col-lg-6 mb-4 mb-lg-0">
<div class="slide-up">
<h1 class="display-3 fw-bold mb-4 hero-title">
{ "Trade " }
<span class="text-warning">{ "Digital Assets" }</span>
{ " & Crypto" }
</h1>
<p class="fs-5 mb-4 pe-lg-5 hero-subtitle">
{ "The premier marketplace for tokenized real-world assets and digital collectibles. Trade everything from sustainable land tokens and carbon credits to NFTs, crypto assets, and blockchain-based investments." }
</p>
<div class="d-flex flex-wrap gap-3">
<button class="btn btn-marketplace btn-lg">
<i class="bi bi-search me-2"></i>{ "Explore Marketplace" }
</button>
<button class="btn btn-secondary btn-lg">
<i class="bi bi-plus-circle me-2"></i>{ "List Your Asset" }
</button>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="slide-up" style="animation-delay: 0.2s;">
{ render_listing_card(&featured_listing, true) }
</div>
</div>
</div>
</div>
</div>
// Search and Filter Section
<div class="py-5 bg-light">
<div class="container-fluid px-4">
<div class="search-container">
<div class="row align-items-center mb-4">
<div class="col">
<h2 class="h4 fw-bold mb-0">{ "Browse Digital Assets" }</h2>
<p class="text-muted mb-0">{ "Find exactly what you're looking for" }</p>
</div>
<div class="col-auto">
<span class="badge bg-primary-subtle text-primary px-3 py-2">
{ format!("{} items available", listings.len()) }
</span>
</div>
</div>
<div class="row g-3">
<div class="col-lg-4 col-md-6">
<div class="position-relative">
<i class="bi bi-search position-absolute top-50 start-0 translate-middle-y ms-3 text-muted"></i>
<input type="text" class="form-control ps-5" placeholder="Search by name, category, or seller..." />
</div>
</div>
<div class="col-lg-3 col-md-6">
<select class="form-select">
<option selected={true}>{ "All Categories" }</option>
<option value="land">{ "Tokenized Real Estate" }</option>
<option value="carbon">{ "Carbon & Environmental Credits" }</option>
<option value="energy">{ "Energy & Infrastructure" }</option>
<option value="nft">{ "NFTs & Digital Art" }</option>
<option value="defi">{ "DeFi & Yield Tokens" }</option>
<option value="gaming">{ "Gaming Assets" }</option>
<option value="collectibles">{ "Digital Collectibles" }</option>
</select>
</div>
<div class="col-lg-3 col-md-6">
<select class="form-select">
<option selected={true}>{ "Sort by: Newest" }</option>
<option value="price_low">{ "Price: Low to High" }</option>
<option value="price_high">{ "Price: High to Low" }</option>
<option value="popular">{ "Most Popular" }</option>
<option value="ending">{ "Ending Soon" }</option>
</select>
</div>
<div class="col-lg-2 col-md-6">
<button class="btn btn-primary w-100">
<i class="bi bi-funnel me-2"></i>{ "Apply Filters" }
</button>
</div>
</div>
</div>
</div>
</div>
// Listings Grid
<div class="py-5">
<div class="container-fluid px-4">
<div class="row g-4">
{ for listings.iter().skip(1).map(|l| render_listing_card(l, false)) }
</div>
// Load More Section
<div class="text-center mt-5">
<button class="btn btn-outline-primary btn-lg">
<i class="bi bi-arrow-down-circle me-2"></i>{ "Load More Assets" }
</button>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,325 @@
use yew::prelude::*;
#[derive(Clone, PartialEq)]
struct Listing {
id: String,
title: String,
description: String,
image_url: String,
price: f64,
currency: String,
listing_type: String,
status: String,
seller_name: String,
asset_name: String,
asset_type: String,
asset_id: String,
expires_at: String,
created_at: String,
tags: Vec<String>,
bids: Vec<Bid>,
// Blockchain/Crypto fields
blockchain: String,
contract_address: String,
token_id: String,
token_standard: String,
total_supply: u64,
current_supply: u64,
}
#[derive(Clone, PartialEq)]
struct Bid {
bidder_name: String,
amount: f64,
created_at: String,
}
#[derive(Properties, PartialEq)]
pub struct ListingDetailProps {
pub id: String,
}
#[function_component(ListingDetailView)]
pub fn listing_detail_view(props: &ListingDetailProps) -> Html {
// Mock data - in a real app, this would be fetched based on props.id
let listing = Listing {
id: props.id.clone(),
title: "Permaculture Farm Token - Kilifi Coast".to_string(),
description: "Tokenized ownership of 50 hectares of regenerative permaculture farmland on Kenya's coast. This sustainable agriculture project includes carbon sequestration rights, biodiversity conservation benefits, and revenue sharing from organic crop production. The farm employs local communities and uses traditional ecological knowledge combined with modern permaculture techniques.".to_string(),
image_url: "https://images.unsplash.com/photo-1500382017468-9049fed747ef?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.0.3".to_string(),
price: 12500.00,
currency: "USD".to_string(),
listing_type: "Auction".to_string(),
status: "Active".to_string(),
seller_name: "EcoFarms Collective".to_string(),
asset_name: "Kilifi Permaculture Farm".to_string(),
asset_type: "Tokenized Land".to_string(),
asset_id: "KPF-2025-001".to_string(),
expires_at: "2025-07-15T23:59:59Z".to_string(),
created_at: "2025-06-26T12:00:00Z".to_string(),
tags: vec!["permaculture".to_string(), "sustainable".to_string(), "carbon-credits".to_string(), "community".to_string(), "agriculture".to_string()],
bids: vec![
Bid { bidder_name: "GreenInvestor".to_string(), amount: 13250.00, created_at: "2025-06-26T15:30:00Z".to_string() },
Bid { bidder_name: "SustainableFunds".to_string(), amount: 12800.00, created_at: "2025-06-26T14:00:00Z".to_string() },
],
// Blockchain/Crypto fields
blockchain: "Polygon".to_string(),
contract_address: "0x742d35Cc6634C0532925a3b8D4C9db96c4b4d8e9".to_string(),
token_id: "1001".to_string(),
token_standard: "ERC-721".to_string(),
total_supply: 100,
current_supply: 100,
};
html! {
<div class="fade-in">
<div class="container-fluid px-4 py-4">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
<li class="breadcrumb-item"><a href="/browse">{ "Marketplace" }</a></li>
<li class="breadcrumb-item active" aria-current="page">{ &listing.title }</li>
</ol>
</nav>
<div class="row g-4" style="height: calc(100vh - 200px);">
// Left Column: Image and Actions
<div class="col-lg-4">
<div class="card border-0 shadow-lg">
<div class="card-body p-3">
<div class="position-relative mb-3">
<img src={listing.image_url.clone()} alt={listing.title.clone()}
class="asset-detail-image w-100" style="height: 250px; object-fit: cover;" />
<div class="position-absolute top-0 end-0 m-2">
<span class="badge bg-primary-subtle text-primary px-2 py-1">{ &listing.asset_type }</span>
</div>
</div>
<div class="d-grid gap-2">
{ if listing.listing_type == "Auction" {
html! {
<button type="button" class="btn btn-bid btn-sm">
<i class="bi bi-hammer me-1"></i>{ "Place Bid" }
</button>
}
} else {
html! {
<button type="button" class="btn btn-marketplace btn-sm">
<i class="bi bi-cart-plus me-1"></i>{ "Buy Now" }
</button>
}
}}
<button type="button" class="btn btn-outline-primary btn-sm">
<i class="bi bi-heart me-1"></i>{ "Watchlist" }
</button>
<button type="button" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-share me-1"></i>{ "Share" }
</button>
</div>
</div>
</div>
// Blockchain Details Card
<div class="card border-0 shadow-lg mt-3">
<div class="card-header bg-transparent border-0 p-3">
<h6 class="fw-bold mb-0">
<i class="bi bi-link-45deg me-2 text-primary"></i>
{ "Blockchain Details" }
</h6>
</div>
<div class="card-body p-3 pt-0" style="max-height: 200px; overflow-y: auto;">
<div class="row g-2">
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-diagram-3 me-2 text-success"></i>
<div>
<small class="text-muted d-block">{ "Blockchain" }</small>
<span class="fw-semibold small">{ &listing.blockchain }</span>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-shield-check me-2 text-info"></i>
<div>
<small class="text-muted d-block">{ "Standard" }</small>
<span class="fw-semibold small">{ &listing.token_standard }</span>
</div>
</div>
</div>
<div class="col-12">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-file-earmark-code me-2 text-warning"></i>
<div class="flex-grow-1">
<small class="text-muted d-block">{ "Contract" }</small>
<code class="small bg-light px-2 py-1 rounded d-block text-truncate">{ &listing.contract_address }</code>
</div>
</div>
</div>
<div class="col-4">
<small class="text-muted d-block">{ "Token ID" }</small>
<span class="fw-semibold small">{ &listing.token_id }</span>
</div>
<div class="col-4">
<small class="text-muted d-block">{ "Supply" }</small>
<span class="fw-semibold small">{ listing.total_supply }</span>
</div>
<div class="col-4">
<small class="text-muted d-block">{ "Available" }</small>
<span class="fw-semibold small">{ listing.current_supply }</span>
</div>
</div>
<div class="mt-2 pt-2 border-top">
<div class="d-flex gap-1">
<a href="#" class="btn btn-sm btn-outline-primary flex-fill">
<i class="bi bi-eye me-1"></i>{ "Explorer" }
</a>
<a href="#" class="btn btn-sm btn-outline-secondary flex-fill">
<i class="bi bi-file-text me-1"></i>{ "Contract" }
</a>
</div>
</div>
</div>
</div>
</div>
// Right Column: Details
<div class="col-lg-8">
<div class="d-flex flex-column h-100">
// Header with title and price
<div class="card border-0 shadow-lg mb-3">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="flex-grow-1">
<h1 class="h4 fw-bold mb-1">{ &listing.title }</h1>
<p class="text-muted mb-0 small">{ format!("Asset ID: {}", listing.asset_id) }</p>
</div>
<span class="badge bg-success px-2 py-1">{ &listing.status }</span>
</div>
<div class="price-badge d-inline-block">{ format!("${:.2}", listing.price) }</div>
</div>
</div>
// Scrollable content area
<div class="flex-grow-1" style="overflow-y: auto;">
// Description Card
<div class="card border-0 shadow-lg mb-3">
<div class="card-header bg-transparent border-0 p-3 pb-0">
<h6 class="fw-bold mb-0">{ "Description" }</h6>
</div>
<div class="card-body p-3 pt-2" style="max-height: 150px; overflow-y: auto;">
<p class="mb-0">{ &listing.description }</p>
</div>
</div>
// Details Card
<div class="card border-0 shadow-lg mb-3">
<div class="card-header bg-transparent border-0 p-3 pb-0">
<h6 class="fw-bold mb-0">{ "Details" }</h6>
</div>
<div class="card-body p-3 pt-2">
<div class="row g-2">
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-person-circle me-2 text-primary"></i>
<div>
<small class="text-muted d-block">{ "Seller" }</small>
<a href="#" class="fw-semibold text-decoration-none small">{ &listing.seller_name }</a>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-calendar me-2 text-primary"></i>
<div>
<small class="text-muted d-block">{ "Listed" }</small>
<span class="fw-semibold small">{ &listing.created_at }</span>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-clock me-2 text-primary"></i>
<div>
<small class="text-muted d-block">{ "Expires" }</small>
<span class="fw-semibold small">{ &listing.expires_at }</span>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-tag me-2 text-primary"></i>
<div>
<small class="text-muted d-block">{ "Category" }</small>
<span class="fw-semibold small">{ &listing.asset_type }</span>
</div>
</div>
</div>
</div>
<div class="mt-3 pt-2 border-top">
<h6 class="fw-semibold mb-2 small">{ "Tags" }</h6>
<div class="d-flex flex-wrap gap-1">
{ for listing.tags.iter().map(|tag| html! {
<span class="badge bg-primary-subtle text-primary px-2 py-1 small">{ tag }</span>
}) }
</div>
</div>
</div>
</div>
// Bids Section
<div class="card border-0 shadow-lg">
<div class="card-header bg-transparent border-0 p-3 pb-0">
<h6 class="fw-bold mb-0">
<i class="bi bi-list-ol me-2 text-primary"></i>{ "Bidding History" }
</h6>
</div>
<div class="card-body p-3 pt-2" style="max-height: 200px; overflow-y: auto;">
{ if listing.bids.is_empty() {
html! {
<div class="text-center py-3">
<i class="bi bi-hammer text-muted fs-4"></i>
<p class="text-muted mb-0 mt-2 small">{ "No bids yet. Be the first!" }</p>
</div>
}
} else {
html! {
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead>
<tr>
<th class="border-0 small">{ "Bidder" }</th>
<th class="border-0 small">{ "Amount" }</th>
<th class="border-0 small">{ "Time" }</th>
</tr>
</thead>
<tbody>
{ for listing.bids.iter().enumerate().map(|(i, bid)| html! {
<tr class={if i == 0 { "table-success" } else { "" }}>
<td class="fw-semibold small">
{ if i == 0 {
html! { <><i class="bi bi-trophy-fill text-warning me-1"></i>{ &bid.bidder_name }</> }
} else {
html! { <span>{ &bid.bidder_name }</span> }
}}
</td>
<td class="fw-bold text-success small">{ format!("${:.2}", bid.amount) }</td>
<td class="text-muted small">{ &bid.created_at }</td>
</tr>
}) }
</tbody>
</table>
</div>
}
}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,27 @@
use yew::prelude::*;
#[derive(Properties, PartialEq, Clone)]
pub struct MarketplaceViewProps {
#[prop_or_default]
pub asset_id: Option<String>,
#[prop_or_default]
pub tab: Option<String>,
}
#[function_component(MarketplaceView)]
pub fn marketplace_view(props: &MarketplaceViewProps) -> Html {
if let Some(id) = &props.asset_id {
return html! { <div>{ format!("Viewing asset detail for ID: {}", id) }</div> };
}
if let Some(tab) = &props.tab {
return html! { <div>{ format!("Viewing tab: {}", tab) }</div> };
}
html! {
<div>
<h1>{ "Browse Marketplace" }</h1>
<p>{ "Here you can find all the digital assets." }</p>
</div>
}
}

View File

@ -0,0 +1,13 @@
pub mod home;
pub mod marketplace;
pub mod create_listing;
pub mod edit_listing;
pub mod my_listings;
pub mod listing_detail;
pub use home::HomeView;
pub use create_listing::CreateListingView;
pub use edit_listing::EditListingView;
pub use my_listings::MyListingsView;
pub use listing_detail::ListingDetailView;

View File

@ -0,0 +1,237 @@
use yew::prelude::*;
#[derive(Clone, PartialEq)]
struct Listing {
id: String,
asset_name: String,
title: String,
price: f64,
listing_type: String,
status: String,
created_at: String,
expires_at: String,
}
#[function_component(MyListingsView)]
pub fn my_listings_view() -> Html {
let listings = vec![
Listing {
id: "1".to_string(),
asset_name: "Kilifi Permaculture Farm".to_string(),
title: "Permaculture Farm Token - Kilifi Coast".to_string(),
price: 12500.00,
listing_type: "Auction".to_string(),
status: "Active".to_string(),
created_at: "2025-06-26".to_string(),
expires_at: "2025-07-26".to_string(),
},
Listing {
id: "2".to_string(),
asset_name: "Solar Cooperative Share".to_string(),
title: "Community Solar Cooperative Share".to_string(),
price: 850.00,
listing_type: "Sale".to_string(),
status: "Sold".to_string(),
created_at: "2025-06-15".to_string(),
expires_at: "N/A".to_string(),
},
Listing {
id: "3".to_string(),
asset_name: "Mangrove Biodiversity Credits".to_string(),
title: "Biodiversity Credits - Coastal Mangroves".to_string(),
price: 450.00,
listing_type: "Sale".to_string(),
status: "Expired".to_string(),
created_at: "2025-05-20".to_string(),
expires_at: "2025-06-20".to_string(),
},
];
let render_status_badge = |status: &str| {
let badge_class = match status {
"Active" => "bg-success",
"Sold" => "bg-info",
"Expired" => "bg-warning text-dark",
"Cancelled" => "bg-danger",
_ => "bg-secondary",
};
html! { <span class={classes!("badge", badge_class)}>{ status }</span> }
};
html! {
<div class="fade-in">
<div class="container-fluid px-4 py-4">
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{ "Home" }</a></li>
<li class="breadcrumb-item"><a href="/browse">{ "Marketplace" }</a></li>
<li class="breadcrumb-item active" aria-current="page">{ "My Listings" }</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h2 fw-bold mb-2">{ "My Listings" }</h1>
<p class="text-muted mb-0">{ "Manage your digital asset listings" }</p>
</div>
<a href="/create-listing" class="btn btn-marketplace">
<i class="bi bi-plus-circle me-2"></i>{ "Create New Listing" }
</a>
</div>
// Stats Cards
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body text-center p-4">
<i class="bi bi-list-ul text-primary fs-1 mb-3"></i>
<h3 class="fw-bold mb-1">{ listings.len() }</h3>
<p class="text-muted mb-0">{ "Total Listings" }</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body text-center p-4">
<i class="bi bi-check-circle text-success fs-1 mb-3"></i>
<h3 class="fw-bold mb-1">{ listings.iter().filter(|l| l.status == "Active").count() }</h3>
<p class="text-muted mb-0">{ "Active" }</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body text-center p-4">
<i class="bi bi-bag-check text-info fs-1 mb-3"></i>
<h3 class="fw-bold mb-1">{ listings.iter().filter(|l| l.status == "Sold").count() }</h3>
<p class="text-muted mb-0">{ "Sold" }</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-0 shadow-sm">
<div class="card-body text-center p-4">
<i class="bi bi-currency-dollar text-warning fs-1 mb-3"></i>
<h3 class="fw-bold mb-1">{ format!("${:.0}", listings.iter().filter(|l| l.status == "Sold").map(|l| l.price).sum::<f64>()) }</h3>
<p class="text-muted mb-0">{ "Total Earned" }</p>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-lg">
<div class="card-header bg-transparent border-0 p-4">
<div class="d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0">
<i class="bi bi-list-ul me-2 text-primary"></i>
{ "All Listings" }
</h5>
<div class="d-flex gap-2">
<select class="form-select form-select-sm" style="width: auto;">
<option>{ "All Status" }</option>
<option>{ "Active" }</option>
<option>{ "Sold" }</option>
<option>{ "Expired" }</option>
</select>
<select class="form-select form-select-sm" style="width: auto;">
<option>{ "All Types" }</option>
<option>{ "Sale" }</option>
<option>{ "Auction" }</option>
</select>
</div>
</div>
</div>
<div class="card-body p-0">
{ if listings.is_empty() {
html! {
<div class="empty-state">
<i class="bi bi-plus-circle"></i>
<h3>{ "No listings yet" }</h3>
<p>{ "Create your first listing to start selling digital assets" }</p>
<a href="/create-listing" class="btn btn-marketplace">
<i class="bi bi-plus-circle me-2"></i>{ "Create First Listing" }
</a>
</div>
}
} else {
html! {
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="border-0 ps-4">{ "Asset" }</th>
<th class="border-0">{ "Title" }</th>
<th class="border-0">{ "Price" }</th>
<th class="border-0">{ "Type" }</th>
<th class="border-0">{ "Status" }</th>
<th class="border-0">{ "Created" }</th>
<th class="border-0">{ "Expires" }</th>
<th class="border-0 pe-4">{ "Actions" }</th>
</tr>
</thead>
<tbody>
{ for listings.iter().map(|listing| html! {
<tr>
<td class="ps-4">
<div class="d-flex align-items-center">
<div class="rounded bg-primary-subtle d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px;">
<i class="bi bi-image text-primary"></i>
</div>
<span class="fw-semibold">{ &listing.asset_name }</span>
</div>
</td>
<td>
<a href={format!("/asset/{}", listing.id)} class="text-decoration-none fw-semibold">
{ &listing.title }
</a>
</td>
<td class="fw-bold">{ format!("${:.2}", listing.price) }</td>
<td>
<span class="badge bg-primary-subtle text-primary px-3 py-2">
{ &listing.listing_type }
</span>
</td>
<td>{ render_status_badge(&listing.status) }</td>
<td class="text-muted">{ &listing.created_at }</td>
<td class="text-muted">{ &listing.expires_at }</td>
<td class="pe-4">
<div class="d-flex gap-2">
<a href={format!("/asset/{}", listing.id)}
class="btn btn-sm btn-outline-primary"
title="View Details">
<i class="bi bi-eye"></i>
</a>
{ if listing.status == "Active" {
html! {
<>
<button type="button"
class="btn btn-sm btn-outline-secondary"
title="Edit Listing">
<i class="bi bi-pencil"></i>
</button>
<button type="button"
class="btn btn-sm btn-outline-danger"
title="Cancel Listing">
<i class="bi bi-x-circle"></i>
</button>
</>
}
} else {
html! {}
}}
</div>
</td>
</tr>
}) }
</tbody>
</table>
</div>
}
}}
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,466 @@
/* Marketplace App Custom Styles */
/* Layout */
.main-content {
min-height: calc(100vh - 120px);
}
.sidebar {
width: 250px;
min-height: calc(100vh - 56px);
transition: margin-left 0.3s ease-in-out;
}
.sidebar.show {
margin-left: 0;
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
top: 56px;
left: 0;
z-index: 1000;
margin-left: -250px;
background-color: white !important;
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
}
.sidebar.show {
margin-left: 0;
}
.main-content {
margin-left: 0 !important;
}
}
/* Hero Section */
.hero-section {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white !important;
position: relative;
overflow: hidden;
min-height: 60vh;
}
.hero-section::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="50" cy="50" r="1" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.1;
}
.hero-section .container-fluid {
position: relative;
z-index: 1;
}
.hero-section .text-warning {
color: #ffc107 !important;
}
/* Dark theme hero text */
[data-bs-theme="dark"] .hero-section .hero-title,
[data-bs-theme="dark"] .hero-section .hero-subtitle {
color: white !important;
}
/* Marketplace specific styles */
.marketplace-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 8px;
overflow: hidden;
background: var(--bg-primary);
border: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
}
.marketplace-card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: var(--shadow-xl);
border-color: var(--primary-color);
}
.marketplace-card .card-img-top {
height: 250px;
object-fit: cover;
transition: transform 0.4s ease;
}
.marketplace-card:hover .card-img-top {
transform: scale(1.1);
}
.marketplace-card .card-body {
padding: 1.5rem;
}
.marketplace-card .card-footer {
background: var(--bg-tertiary);
border-top: 1px solid var(--border-light);
padding: 1rem 1.5rem;
}
.asset-image {
height: 250px;
object-fit: cover;
transition: transform 0.3s ease;
}
.marketplace-card:hover .asset-image {
transform: scale(1.05);
}
/* Price and Badge Styling */
.price-badge {
background-color: var(--primary-color);
color: white;
padding: 0.5rem 1rem;
border-radius: 6px;
font-weight: 700;
font-size: 1.1rem;
box-shadow: var(--shadow-sm);
}
.auction-badge {
background: linear-gradient(135deg, #ec4899 0%, #f59e0b 100%);
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
animation: pulse-glow 2s infinite;
box-shadow: 0 0 20px rgba(236, 72, 153, 0.3);
}
@keyframes pulse-glow {
0%, 100% {
opacity: 1;
box-shadow: 0 0 20px rgba(236, 72, 153, 0.3);
}
50% {
opacity: 0.8;
box-shadow: 0 0 30px rgba(236, 72, 153, 0.5);
}
}
.category-filter {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 1rem;
margin-bottom: 2rem;
}
/* Filter Bar */
.filter-bar {
background: var(--bg-primary);
border-bottom: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
backdrop-filter: blur(10px);
}
.filter-bar.sticky-top {
z-index: 1020;
}
/* Search Container */
.search-container {
background: var(--bg-primary);
backdrop-filter: blur(10px);
border-radius: 8px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-light);
}
.asset-detail-image {
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item + .breadcrumb-item::before {
content: "";
font-size: 1.2rem;
color: #6c757d;
}
/* Button styles */
.btn-marketplace {
background-color: var(--primary-color);
border: 1px solid var(--primary-color);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-marketplace:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
color: white;
}
.btn-bid {
background-color: #ec4899;
border: 1px solid #ec4899;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 6px;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-bid:hover {
background-color: #db2777;
border-color: #db2777;
transform: translateY(-1px);
box-shadow: var(--shadow-md);
color: white;
}
/* Search and filters */
.search-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 5px 20px rgba(0,0,0,0.05);
}
.form-control:focus, .form-select:focus {
border-color: #0099FF;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
/* Empty state */
.empty-state {
padding: 4rem 2rem;
text-align: center;
}
.empty-state i {
font-size: 4rem;
color: #dee2e6;
margin-bottom: 1.5rem;
}
/* Responsive adjustments */
@media (max-width: 576px) {
.marketplace-card {
margin-bottom: 1.5rem;
}
.search-container {
padding: 1rem;
}
.asset-detail-image {
margin-bottom: 2rem;
}
}
/* Loading States */
.loading-skeleton {
background: linear-gradient(90deg, var(--bg-tertiary) 25%, var(--border-light) 50%, var(--bg-tertiary) 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 8px;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-tertiary);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--gradient-primary);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary-hover);
}
/* Responsive Design */
@media (max-width: 576px) {
.marketplace-card {
margin-bottom: 1.5rem;
}
.search-container {
padding: 1rem;
border-radius: 12px;
}
.asset-detail-image {
margin-bottom: 2rem;
}
.hero-section {
padding: 2rem 0 !important;
}
.hero-section h1 {
font-size: 2rem !important;
}
.btn-marketplace,
.btn-bid {
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
}
.price-badge {
padding: 0.5rem 1rem;
font-size: 1rem;
}
}
@media (max-width: 768px) {
.filter-bar .row > div {
margin-bottom: 0.75rem;
}
.filter-bar .row > div:last-child {
margin-bottom: 0;
}
}
/* Animation Utilities */
.fade-in {
animation: fadeIn 0.6s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.slide-up {
animation: slideUp 0.4s ease-out;
}
@keyframes slideUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* Utility Classes */
.text-gradient {
background: var(--gradient-primary);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 700;
}
.border-gradient {
border: 2px solid transparent;
background: linear-gradient(var(--bg-primary), var(--bg-primary)) padding-box,
var(--gradient-primary) border-box;
border-radius: 12px;
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Dark theme adjustments */
[data-bs-theme="dark"] .hero-section {
background: linear-gradient(135deg, #1e293b 0%, #334155 100%) !important;
}
[data-bs-theme="dark"] .marketplace-card {
background: var(--bg-primary) !important;
border-color: var(--border-light) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .marketplace-card .card-body {
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .marketplace-card .text-muted {
color: var(--text-muted) !important;
}
[data-bs-theme="dark"] .filter-bar {
background: var(--bg-primary) !important;
border-bottom-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .search-container {
background: var(--bg-primary) !important;
border-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .table {
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .table thead th {
background: var(--bg-tertiary) !important;
color: var(--text-primary) !important;
border-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .table tbody td {
color: var(--text-primary) !important;
border-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .table tbody tr:hover {
background-color: var(--bg-tertiary) !important;
}
[data-bs-theme="dark"] .empty-state {
background: var(--bg-primary) !important;
border-color: var(--border-medium) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .empty-state h3,
[data-bs-theme="dark"] .empty-state h6 {
color: var(--text-secondary) !important;
}
[data-bs-theme="dark"] .empty-state p {
color: var(--text-muted) !important;
}
[data-bs-theme="dark"] .glass-effect {
background: rgba(0, 0, 0, 0.2) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
[data-bs-theme="dark"] .bg-light {
background-color: var(--bg-tertiary) !important;
}

View File

@ -0,0 +1,301 @@
:root {
/* Modern marketplace color palette */
--primary-color: #6366f1;
--primary-hover: #5b5bd6;
--secondary-color: #f59e0b;
--success-color: #10b981;
--danger-color: #ef4444;
--warning-color: #f59e0b;
--info-color: #3b82f6;
/* Background colors */
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--bg-tertiary: #f1f5f9;
--bg-dark: #1e293b;
--bg-darker: #0f172a;
/* Text colors */
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--text-light: #ffffff;
/* Border colors */
--border-light: #e2e8f0;
--border-medium: #cbd5e1;
--border-dark: #475569;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
/* Gradients */
--gradient-primary: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
--gradient-success: linear-gradient(135deg, var(--success-color) 0%, #059669 100%);
--gradient-info: linear-gradient(135deg, var(--info-color) 0%, #2563eb 100%);
}
/* Light theme (default) */
body {
background-color: var(--bg-secondary);
color: var(--text-primary);
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
}
/* Card styling */
.card {
background-color: var(--bg-primary);
border: 1px solid var(--border-light);
border-radius: 12px;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease-in-out;
}
.card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-1px);
}
.card-header {
background-color: var(--bg-tertiary);
border-bottom: 1px solid var(--border-light);
border-radius: 12px 12px 0 0 !important;
font-weight: 600;
color: var(--text-primary);
}
/* Navigation styling */
.navbar {
background: var(--bg-primary) !important;
border-bottom: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
}
.navbar-brand {
font-weight: 700;
color: var(--primary-color) !important;
font-size: 1.5rem;
}
.nav-link {
color: var(--text-secondary) !important;
font-weight: 500;
transition: color 0.2s ease;
}
.nav-link:hover {
color: var(--primary-color) !important;
}
.nav-link.active {
color: var(--primary-color) !important;
font-weight: 600;
}
/* Sidebar styling */
.sidebar {
background-color: var(--bg-primary) !important;
border-right: 1px solid var(--border-light);
box-shadow: var(--shadow-sm);
}
/* Button styling */
.btn-primary {
background-color: var(--primary-color);
border: 1px solid var(--primary-color);
border-radius: 6px;
font-weight: 600;
padding: 0.75rem 1.5rem;
transition: all 0.2s ease;
color: white;
}
.btn-primary:hover {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
color: white;
}
.btn-secondary {
background-color: var(--bg-tertiary);
border: 1px solid var(--border-medium);
color: var(--text-primary);
border-radius: 6px;
font-weight: 500;
}
.btn-secondary:hover {
background-color: var(--border-light);
border-color: var(--border-dark);
}
/* Form styling */
.form-control, .form-select {
border: 1px solid var(--border-medium);
border-radius: 8px;
padding: 0.75rem 1rem;
transition: all 0.2s ease;
}
.form-control:focus, .form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgb(99 102 241 / 0.1);
}
/* Badge styling */
.badge {
border-radius: 6px;
font-weight: 500;
padding: 0.5rem 0.75rem;
}
.bg-success {
background: var(--gradient-success) !important;
}
.bg-info {
background: var(--gradient-info) !important;
}
.bg-warning {
background-color: var(--warning-color) !important;
}
.bg-danger {
background-color: var(--danger-color) !important;
}
/* Dark theme support */
[data-bs-theme="dark"] {
--bg-primary: #1e293b;
--bg-secondary: #0f172a;
--bg-tertiary: #334155;
--text-primary: #f8fafc;
--text-secondary: #cbd5e1;
--text-muted: #94a3b8;
--border-light: #475569;
--border-medium: #64748b;
}
[data-bs-theme="dark"] body {
background-color: var(--bg-secondary) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .navbar {
background: var(--bg-primary) !important;
border-bottom-color: var(--border-light);
}
[data-bs-theme="dark"] .card {
background-color: var(--bg-primary) !important;
border-color: var(--border-light) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .card-body,
[data-bs-theme="dark"] .card-header,
[data-bs-theme="dark"] .card-footer {
background-color: transparent !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .card-header {
background-color: var(--bg-tertiary) !important;
border-bottom-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .text-muted {
color: var(--text-muted) !important;
}
[data-bs-theme="dark"] .sidebar {
background-color: var(--bg-primary) !important;
border-right-color: var(--border-light);
}
[data-bs-theme="dark"] .form-control,
[data-bs-theme="dark"] .form-select {
background-color: var(--bg-tertiary) !important;
border-color: var(--border-medium) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .form-control:focus,
[data-bs-theme="dark"] .form-select:focus {
background-color: var(--bg-tertiary) !important;
border-color: var(--primary-color) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .btn-secondary {
background-color: var(--bg-tertiary) !important;
border-color: var(--border-medium) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .btn-outline-secondary {
border-color: var(--border-medium) !important;
color: var(--text-secondary) !important;
}
[data-bs-theme="dark"] .btn-outline-secondary:hover {
background-color: var(--bg-tertiary) !important;
border-color: var(--border-light) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .bg-light {
background-color: var(--bg-tertiary) !important;
}
[data-bs-theme="dark"] .border-top {
border-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .dropdown-menu {
background-color: var(--bg-primary) !important;
border-color: var(--border-light) !important;
}
[data-bs-theme="dark"] .dropdown-item {
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .dropdown-item:hover {
background-color: var(--bg-tertiary) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .breadcrumb-item a {
color: var(--primary-color) !important;
}
[data-bs-theme="dark"] .breadcrumb-item.active {
color: var(--text-muted) !important;
}
[data-bs-theme="dark"] .table {
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .table thead th {
background-color: var(--bg-tertiary) !important;
border-color: var(--border-light) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .table tbody td {
border-color: var(--border-light) !important;
color: var(--text-primary) !important;
}
[data-bs-theme="dark"] .table tbody tr:hover {
background-color: var(--bg-tertiary) !important;
}

16
platform/.env.example Normal file
View File

@ -0,0 +1,16 @@
# Stripe Configuration
STRIPE_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
STRIPE_SECRET_KEY=sk_test_your_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
# Server Configuration
PORT=8080
HOST=127.0.0.1
RUST_LOG=info
# Database (if needed)
DATABASE_URL=sqlite:./data/app.db
# Security
JWT_SECRET=your_jwt_secret_here
CORS_ORIGIN=http://127.0.0.1:8080

2724
platform/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

74
platform/Cargo.toml Normal file
View File

@ -0,0 +1,74 @@
[package]
name = "zanzibar-freezone-app"
version = "0.1.0"
edition = "2021"
[workspace]
[lib]
crate-type = ["cdylib"]
# Binary for the server (only built with server feature)
[[bin]]
name = "server"
path = "src/bin/server.rs"
required-features = ["server"]
[dependencies]
# Frontend (WASM) dependencies
yew = { version = "0.21", features = ["csr"] }
web-sys = { version = "0.3", features = [
"console",
"Document",
"Element",
"HtmlElement",
"HtmlInputElement",
"HtmlSelectElement",
"HtmlTextAreaElement",
"HtmlFormElement",
"Location",
"Window",
"History",
"MouseEvent",
"Event",
"EventTarget",
"Storage",
"UrlSearchParams",
"Blob",
"File",
"FileList",
"FormData",
"Crypto",
"SubtleCrypto",
"CryptoKey"
] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
log = "0.4"
wasm-logger = "0.2"
gloo = { version = "0.10", features = ["storage", "timers", "events"] }
gloo-utils = "0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
base64 = "0.21"
uuid = { version = "1.0", features = ["v4", "js"] }
chrono = { version = "0.4", features = ["serde", "wasm-bindgen"] }
# Backend server dependencies (optional)
tokio = { version = "1.0", features = ["full"], optional = true }
axum = { version = "0.7", optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["cors", "fs"], optional = true }
reqwest = { version = "0.11", features = ["json"], optional = true }
dotenv = { version = "0.15", optional = true }
anyhow = { version = "1.0", optional = true }
tracing = { version = "0.1", optional = true }
tracing-subscriber = { version = "0.3", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3"
[features]
default = []
server = ["tokio", "axum", "tower", "tower-http", "reqwest", "dotenv", "anyhow", "tracing", "tracing-subscriber"]

View File

@ -0,0 +1,255 @@
# 🚀 Production Setup Guide
## Complete Stripe Integration Implementation
This guide covers the complete production setup for the Stripe Elements integration, including secure key storage, backend server, webhook handling, and comprehensive error handling.
## 📋 What's Been Implemented
### ✅ 1. Frontend Integration
- **Manual credit card form completely removed** from step_four.rs
- **Real Stripe Elements integration** with proper JavaScript interop
- **Automatic fallback to demo mode** when server is not available
- **Comprehensive error handling** and user guidance
### ✅ 2. Backend Server (`src/bin/server.rs`)
- **Payment intent creation endpoint**: `/company/create-payment-intent`
- **Webhook handling**: `/webhooks/stripe`
- **Payment success page**: `/company/payment-success`
- **Health check**: `/api/health`
- **Static file serving** for WASM, HTML, CSS, JS
- **CORS configuration** for development
### ✅ 3. Environment Configuration
- **Secure key storage** in `.env` file
- **Environment variable validation**
- **Development and production configurations**
### ✅ 4. Pricing Logic
- **Automatic pricing calculation** based on company type and payment plan
- **Discount handling** (20% yearly, 40% two-year)
- **ZDFZ Twin fee inclusion** ($2/month)
### ✅ 5. Error Handling
- **Comprehensive error responses**
- **Stripe API error handling**
- **Network failure fallbacks**
- **User-friendly error messages**
## 🔧 Setup Instructions
### Step 1: Get Stripe API Keys
1. **Create Stripe Account**: [https://stripe.com](https://stripe.com)
2. **Access Dashboard**: [https://dashboard.stripe.com](https://dashboard.stripe.com)
3. **Get API Keys**: Developers → API keys
- Copy **Publishable key** (starts with `pk_test_`)
- Copy **Secret key** (starts with `sk_test_`)
### Step 2: Configure Environment
1. **Copy environment template**:
```bash
cp .env.example .env
```
2. **Edit `.env` file**:
```bash
# Stripe Configuration
STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_ACTUAL_KEY_HERE
STRIPE_SECRET_KEY=sk_test_YOUR_ACTUAL_SECRET_KEY_HERE
STRIPE_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET_HERE
# Server Configuration
PORT=8080
HOST=127.0.0.1
RUST_LOG=info
```
3. **Update frontend key** in `index.html`:
```javascript
const STRIPE_PUBLISHABLE_KEY = 'pk_test_YOUR_ACTUAL_KEY_HERE';
```
### Step 3: Run the Server
1. **Install dependencies**:
```bash
cargo build --features server
```
2. **Start the server**:
```bash
cargo run --bin server --features server
```
3. **Verify server is running**:
```bash
curl http://127.0.0.1:8080/api/health
```
### Step 4: Set Up Webhooks (Production)
1. **In Stripe Dashboard**: Developers → Webhooks
2. **Add endpoint**: `https://yourdomain.com/webhooks/stripe`
3. **Select events**:
- `payment_intent.succeeded`
- `payment_intent.payment_failed`
4. **Copy webhook secret** to `.env` file
## 🧪 Testing
### Test with Demo Mode (No Server)
```javascript
window.testStripeIntegration()
```
### Test with Real Server
1. Start server: `cargo run --bin server --features server`
2. Navigate to entities page
3. Complete registration form
4. Use test cards:
- **Success**: 4242 4242 4242 4242
- **Declined**: 4000 0000 0000 0002
- **3D Secure**: 4000 0025 0000 3155
## 📊 Pricing Structure
| Company Type | Setup Fee | Monthly Fee | Total Monthly |
|--------------|-----------|-------------|---------------|
| Single FZC | $20 | $20 + $2 | $22 |
| Startup FZC | $50 | $50 + $2 | $52 |
| Growth FZC | $1000 | $100 + $2 | $102 |
| Global FZC | $2000 | $200 + $2 | $202 |
| Cooperative FZC | $2000 | $200 + $2 | $202 |
**Payment Plans:**
- **Monthly**: Setup + Monthly fee
- **Yearly**: Setup + (Monthly × 12 × 0.8) - 20% discount
- **Two Year**: Setup + (Monthly × 24 × 0.6) - 40% discount
## 🔒 Security Best Practices
### Environment Variables
- ✅ **Never commit `.env` to version control**
- ✅ **Use different keys for development/production**
- ✅ **Rotate keys regularly**
- ✅ **Restrict API key permissions in Stripe Dashboard**
### Server Security
- ✅ **Webhook signature verification** (implemented)
- ✅ **CORS configuration** for allowed origins
- ✅ **Input validation** on all endpoints
- ✅ **Error message sanitization**
### Frontend Security
- ✅ **Publishable keys only** (safe for frontend)
- ✅ **No sensitive data in client code**
- ✅ **Secure payment form** via Stripe Elements
## 🔄 Webhook Events Handled
### `payment_intent.succeeded`
- Company registration completion
- Database updates
- Confirmation emails
- Account activation
### `payment_intent.payment_failed`
- Failed payment logging
- User notification
- Retry mechanisms
## 📁 File Structure
```
freezone/platform/
├── .env # Environment variables (DO NOT COMMIT)
├── .env.example # Environment template
├── Cargo.toml # Dependencies with server feature
├── index.html # Frontend with Stripe integration
├── src/
│ ├── bin/
│ │ └── server.rs # Backend server with payment endpoints
│ ├── components/
│ │ └── entities/
│ │ └── company_registration/
│ │ └── step_four.rs # Updated payment step (no manual form)
│ └── models/
│ └── company.rs # Data models
└── static/
└── js/
└── stripe-integration.js # Stripe JavaScript (if needed)
```
## 🚀 Deployment
### Development
```bash
# Start WASM dev server
trunk serve
# Start backend server (separate terminal)
cargo run --bin server --features server
```
### Production
```bash
# Build WASM
trunk build --release
# Build and run server
cargo build --release --features server
./target/release/server
```
## 🐛 Troubleshooting
### Common Issues
1. **"Invalid API Key"**
- Check `.env` file has correct Stripe keys
- Verify keys are for correct environment (test/live)
2. **"Payment form not loading"**
- Check browser console for errors
- Verify Stripe publishable key in `index.html`
- Check network tab for failed requests
3. **"Server not available"**
- Ensure server is running: `cargo run --bin server --features server`
- Check server logs for errors
- Verify port 8080 is available
4. **"CORS errors"**
- Check server CORS configuration
- Ensure frontend and backend on same origin for development
### Debug Commands
```bash
# Check server health
curl http://127.0.0.1:8080/api/health
# Test payment intent creation
curl -X POST http://127.0.0.1:8080/company/create-payment-intent \
-H "Content-Type: application/json" \
-d '{"company_name":"Test","company_type":"Single FZC","payment_plan":"monthly","final_agreement":true,"agreements":["terms"]}'
# Check server logs
RUST_LOG=debug cargo run --bin server --features server
```
## ✅ Success Criteria
When everything is working correctly:
1. ✅ **Manual credit card form is gone** from step 4
2. ✅ **Real Stripe Elements widget appears** when payment is ready
3. ✅ **Server creates payment intents** successfully
4. ✅ **Webhooks process payment events** correctly
5. ✅ **Test payments complete** end-to-end
6. ✅ **Error handling works** gracefully
7. ✅ **Demo mode works** when server is unavailable
The integration is now production-ready with secure key storage, comprehensive error handling, and real Stripe payment processing!

226
platform/README.md Normal file
View File

@ -0,0 +1,226 @@
# Zanzibar Digital Freezone - Yew WASM App
A modern web application built with Yew and WebAssembly, porting the Zanzibar Digital Freezone platform from Actix MVC to a client-side WASM application.
## 🎯 Project Overview
**Motto**: "Convenience, Safety and Privacy"
This project is a UI-first port of the Zanzibar Digital Freezone platform, focusing on replicating the exact visual design and user experience of the original Actix MVC application while leveraging modern WASM technology.
## ✨ Features
### Core Platform Features
- **Home Dashboard** - 5 feature cards showcasing platform benefits
- **Governance** - Proposal management and voting system
- **Flows** - Business process management
- **Contracts** - Digital contract management and signatures
- **Digital Assets** - Asset tokenization and management
- **DeFi Platform** - 7-tab DeFi interface (Overview, Providing/Receiving, Liquidity, Staking, Swap, Collateral, Lending/Borrowing)
- **Companies** - Entity management and registration
- **Marketplace** - Asset trading platform
- **Calendar** - Event management system
### Technical Features
- **Responsive Design** - Mobile-first Bootstrap 5.3.3 layout
- **Authentication** - Session-based login system
- **Client-side Routing** - Browser history integration
- **Local Storage** - Persistent authentication state
- **Component Architecture** - Modular Yew components
## 🏗️ Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Header │ │ Sidebar │ │ Main Content │
│ - Logo │ │ - Navigation │ │ - Views │
│ - User Menu │ │ - Active State │ │ - Components │
└─────────────────┘ └──────────────────┘ └─────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Footer │
│ "Convenience, Safety and Privacy" │
└─────────────────────────────────────────────────────────────────┘
```
## 🚀 Quick Start
### Prerequisites
- Rust (latest stable)
- Trunk (WASM build tool)
- Modern web browser
### Installation
1. **Install Rust and WASM target**:
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknown
```
2. **Install Trunk**:
```bash
cargo install trunk wasm-bindgen-cli
```
3. **Clone and run**:
```bash
cd zanzibar_freezone_app
trunk serve
```
4. **Open browser**:
Navigate to `http://localhost:8080`
### Demo Login
- **Email**: `admin@zanzibar.tf`
- **Password**: `admin123`
## 📁 Project Structure
```
zanzibar_freezone_app/
├── src/
│ ├── app.rs # Main application component
│ ├── lib.rs # WASM entry point
│ ├── components/ # Reusable UI components
│ │ ├── layout/ # Header, Sidebar, Footer
│ │ ├── forms/ # Login and other forms
│ │ └── cards/ # Feature cards
│ ├── views/ # Main application views
│ │ ├── home_view.rs # Dashboard with 5 cards
│ │ ├── login_view.rs # Authentication
│ │ └── placeholder_view.rs # Placeholder for other sections
│ └── routing/ # Client-side routing
├── static/
│ └── css/
│ └── main.css # Custom styles + Bootstrap
├── index.html # HTML template
├── Cargo.toml # Rust dependencies
└── Trunk.toml # Build configuration
```
## 🎨 UI Components
### Layout Components
- **Header**: Fixed top navigation with user menu
- **Sidebar**: Collapsible navigation with active states
- **Footer**: Three-column layout with links
### Feature Cards (Home Page)
1. 🤝 **Frictionless Collaboration** (Primary Blue)
2. 💱 **Frictionless Banking** (Success Green)
3. 📈 **Tax Efficiency** (Info Blue)
4. 🌍 **Global Ecommerce** (Warning Yellow)
5. 🛡️ **Clear Regulations** (Danger Red)
### Navigation Items
- 🏠 Home
- 👥 Governance
- 📊 Flows
- 📄 Contracts
- 🪙 Digital Assets
- 🏦 DeFi Platform
- 🏢 Companies
- 🛒 Marketplace
- 📅 Calendar
## 🔧 Development
### Build Commands
```bash
# Development server with hot reload
trunk serve
# Production build
trunk build --release
# Clean build artifacts
trunk clean
```
### Code Organization
- **Components**: Reusable UI elements following Yew patterns
- **Views**: Page-level components for each section
- **Routing**: Client-side navigation with browser history
- **Styling**: Bootstrap 5.3.3 + custom CSS for exact visual fidelity
## 📱 Responsive Design
### Desktop (≥768px)
- Fixed sidebar (240px width)
- Full header with navigation links
- Three-column footer layout
### Mobile (<768px)
- Collapsible sidebar with slide animation
- Hamburger menu in header
- Stacked footer layout
- Touch-friendly navigation
## 🔐 Authentication
### Current Implementation
- Simple mock authentication for demo
- Session persistence via LocalStorage
- Automatic redirect to login when not authenticated
### Future Enhancements
- Integration with backend authentication API
- JWT token management
- Role-based access control
- Multi-factor authentication
## 🎯 Implementation Status
### ✅ Completed (Phase 1)
- [x] Project structure and build system
- [x] Bootstrap 5.3.3 integration
- [x] Responsive layout components (Header, Sidebar, Footer)
- [x] Home view with 5 feature cards
- [x] Login form and authentication UI
- [x] Client-side routing
- [x] Mobile responsive design
- [x] Navigation state management
### 🚧 In Progress (Phase 2)
- [ ] Business logic for each section
- [ ] API integration
- [ ] Data models and services
- [ ] Form validation and error handling
- [ ] Advanced state management
### 📋 Planned (Phase 3)
- [ ] Real backend integration
- [ ] Database connectivity
- [ ] File upload and management
- [ ] Real-time updates
- [ ] Advanced DeFi functionality
## 🌐 Browser Support
- Chrome/Chromium 80+
- Firefox 74+
- Safari 13.1+
- Edge 80+
## 📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## 📞 Support
For questions or support, please contact the development team or visit our documentation at [info.ourworld.tf/zdfz](https://info.ourworld.tf/zdfz).
---
**Zanzibar Digital Freezone** - Convenience, Safety and Privacy

View File

@ -0,0 +1,243 @@
# 🏦 Treasury Dashboard - DeFi-Style Interface
## Overview
A comprehensive, modern treasury management dashboard designed with a sleek DeFi (Decentralized Finance) aesthetic for the Zanzibar Digital Freezone platform. This dashboard provides enterprise-grade treasury management capabilities with a user-friendly interface.
## 🎯 Features
### Four Main Tabs
#### 1. **Overview Tab** 📊
- **Portfolio Summary Cards**: Total balance, active wallets, digital assets, and 24h volume
- **Recent Transactions**: Latest 5 transactions with status indicators
- **Top Assets**: Asset portfolio with price changes and values
- **Real-time Metrics**: Live data with percentage changes and growth indicators
#### 2. **Wallets Tab** 💳
- **Wallet Management**: Create, import, and manage multiple wallet types
- **Wallet Types**: Company, Multi-Sig, Personal, and Hardware wallets
- **Address Management**: Copy addresses, view balances, send/receive functions
- **Default Wallet**: Designation and management of primary treasury wallet
#### 3. **Assets Tab** 💎
- **Digital Asset Portfolio**: Complete overview of all digital assets
- **Real-time Pricing**: Live price data with 24h change indicators
- **Asset Actions**: Send, swap, and manage individual assets
- **Multi-network Support**: Assets across different blockchain networks
#### 4. **Transactions Tab** 📋
- **Complete Transaction History**: All treasury transactions with detailed information
- **Transaction Types**: Send, Receive, Swap, Stake, Unstake operations
- **Search & Filter**: Advanced search and filtering capabilities
- **Export Functionality**: Export transaction data for reporting
## 🎨 Design Features
### Modern DeFi Aesthetic
- **Gradient Cards**: Professional blue gradient backgrounds
- **Hover Effects**: Smooth animations and transitions
- **Color-coded Elements**: Semantic colors for different transaction types and statuses
- **Shadow Effects**: Soft shadows for depth and modern appearance
### Visual Elements
- **Status Badges**: Color-coded status indicators (Pending, Confirmed, Failed)
- **Transaction Icons**: Intuitive icons for different transaction types
- **Progress Indicators**: Visual feedback for various states
- **Responsive Design**: Optimized for all screen sizes
## 🔧 Technical Implementation
### Data Models
#### Wallet Structure
```rust
pub struct Wallet {
pub id: String,
pub name: String,
pub address: String,
pub balance_usd: f64,
pub wallet_type: WalletType,
pub is_default: bool,
}
pub enum WalletType {
Company, // Primary business wallet
MultiSig, // Multi-signature safe
Personal, // Personal wallet
Hardware, // Hardware wallet
}
```
#### Asset Structure
```rust
pub struct Asset {
pub symbol: String,
pub name: String,
pub balance: f64,
pub value_usd: f64,
pub price_change_24h: f64,
pub icon: String,
}
```
#### Transaction Structure
```rust
pub struct Transaction {
pub id: String,
pub transaction_type: TransactionType,
pub amount: f64,
pub asset: String,
pub from_address: String,
pub to_address: String,
pub timestamp: String,
pub status: TransactionStatus,
pub hash: String,
}
pub enum TransactionType {
Send, // Outgoing transaction
Receive, // Incoming transaction
Swap, // Asset exchange
Stake, // Staking operation
Unstake, // Unstaking operation
}
pub enum TransactionStatus {
Pending, // Transaction pending
Confirmed, // Transaction confirmed
Failed, // Transaction failed
}
```
### Component Architecture
- **Modular Design**: Each tab is a separate component for maintainability
- **Reusable Elements**: Common UI patterns abstracted into reusable components
- **State Management**: Efficient state handling with Yew hooks
- **Type Safety**: Strong typing throughout the application
## 💰 Mock Data Examples
### Sample Wallets
1. **Company Treasury** - $125,430.50 (Default)
2. **Multi-Sig Safe** - $89,250.75
3. **Hardware Wallet** - $45,680.25
### Sample Assets
1. **Ethereum (ETH)** - 45.67 ETH ($89,250.75) +2.45%
2. **Bitcoin (BTC)** - 1.234 BTC ($52,340.80) -1.23%
3. **USD Coin (USDC)** - 25,000 USDC ($25,000.00) +0.01%
4. **Chainlink (LINK)** - 1,250.50 LINK ($18,750.75) +5.67%
### Sample Transactions
1. **Receive** - 2.5 ETH (Confirmed)
2. **Send** - 1,000 USDC (Confirmed)
3. **Swap** - 0.5 ETH (Pending)
## 🎯 User Experience Features
### Interactive Elements
- **Hover Effects**: Cards lift and scale on hover
- **Click Actions**: Responsive button interactions
- **Dropdown Menus**: Context menus for additional actions
- **Copy Functions**: One-click address copying
### Visual Feedback
- **Loading States**: Smooth loading animations
- **Success Indicators**: Green badges and icons for positive actions
- **Error States**: Red indicators for failed transactions
- **Progress Tracking**: Visual progress for pending operations
### Accessibility
- **Keyboard Navigation**: Full keyboard support
- **Screen Reader Support**: Proper ARIA labels
- **Color Contrast**: WCAG compliant color combinations
- **Focus Indicators**: Clear focus states for all interactive elements
## 🔒 Security Features
### Address Display
- **Truncated Addresses**: Show first 6 and last 4 characters for security
- **Copy Protection**: Secure clipboard operations
- **Address Validation**: Input validation for all address fields
### Transaction Security
- **Status Verification**: Clear transaction status indicators
- **Hash Display**: Transaction hash for verification
- **Explorer Links**: Direct links to blockchain explorers
## 📱 Responsive Design
### Desktop (≥1024px)
- **Full Layout**: All elements visible with optimal spacing
- **Hover Effects**: Desktop-specific interactions
- **Multi-column Layout**: Efficient use of screen real estate
### Tablet (768px - 1023px)
- **Adapted Layout**: Responsive grid adjustments
- **Touch-friendly**: Larger touch targets
- **Optimized Spacing**: Adjusted padding and margins
### Mobile (<768px)
- **Stacked Layout**: Single-column design
- **Touch Optimized**: Large buttons and touch areas
- **Simplified Navigation**: Mobile-first navigation patterns
## 🚀 Performance Optimizations
### Rendering Efficiency
- **Virtual Scrolling**: For large transaction lists
- **Lazy Loading**: Load data as needed
- **Memoization**: Prevent unnecessary re-renders
- **Efficient Updates**: Targeted DOM updates
### Data Management
- **Caching**: Smart data caching strategies
- **Pagination**: Efficient data loading
- **Real-time Updates**: WebSocket integration for live data
- **Offline Support**: Basic offline functionality
## 🔮 Future Enhancements
### Planned Features
1. **Real API Integration**: Connect to actual blockchain APIs
2. **Advanced Analytics**: Charts and detailed portfolio analytics
3. **Multi-chain Support**: Support for multiple blockchain networks
4. **DeFi Integrations**: Direct integration with DeFi protocols
5. **Advanced Security**: Hardware wallet integration and multi-sig support
### Technical Improvements
1. **WebSocket Integration**: Real-time price and transaction updates
2. **Advanced Filtering**: Complex transaction filtering and search
3. **Export Options**: Multiple export formats (CSV, PDF, Excel)
4. **Notification System**: Real-time alerts for transactions and price changes
5. **Mobile App**: Native mobile application
## 📊 Metrics & Analytics
### Key Performance Indicators
- **Total Portfolio Value**: Real-time portfolio valuation
- **24h Volume**: Daily transaction volume
- **Asset Allocation**: Portfolio distribution across assets
- **Transaction Success Rate**: Percentage of successful transactions
### Reporting Features
- **Portfolio Reports**: Detailed portfolio analysis
- **Transaction Reports**: Comprehensive transaction history
- **Tax Reports**: Tax-ready transaction exports
- **Performance Analytics**: Portfolio performance over time
## 🛠️ Development Guidelines
### Code Standards
- **TypeScript**: Strong typing throughout
- **Component Patterns**: Consistent component architecture
- **Error Handling**: Comprehensive error management
- **Testing**: Unit and integration tests
### Styling Guidelines
- **CSS Variables**: Consistent color and spacing
- **Component Styling**: Scoped component styles
- **Responsive Patterns**: Mobile-first responsive design
- **Animation Standards**: Consistent animation timing and easing
This treasury dashboard represents a modern, professional approach to digital asset management, combining the best practices of DeFi interfaces with enterprise-grade functionality and security.

2
platform/Trunk.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
target = "index.html"

483
platform/index.html Normal file
View File

@ -0,0 +1,483 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Digital Freezone Platform</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<style>
.step-indicator {
text-align: center;
position: relative;
opacity: 0.5;
}
.step-indicator.active {
opacity: 1;
font-weight: bold;
}
.step-indicator::after {
content: '';
position: absolute;
top: 50%;
right: -50%;
width: 100%;
height: 2px;
background-color: #dee2e6;
z-index: -1;
}
.step-indicator:last-child::after {
display: none;
}
/* Stripe Elements styling */
#payment-element {
min-height: 40px;
padding: 10px;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
background-color: #ffffff;
}
.payment-ready {
border-color: #198754 !important;
border-width: 2px !important;
box-shadow: 0 0 0 0.2rem rgba(25, 135, 84, 0.25) !important;
}
/* Loading state for payment form */
.payment-loading {
opacity: 0.7;
pointer-events: none;
}
/* Error display styling */
#payment-errors {
margin-top: 1rem;
margin-bottom: 1rem;
display: none;
}
/* Persistent error styling */
#payment-errors[data-persistent-error="true"] {
position: sticky;
top: 20px;
z-index: 1050;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* Enhanced alert styling */
.alert-dismissible .btn-close {
position: absolute;
top: 0.5rem;
right: 0.5rem;
}
/* Ad blocker guidance specific styling */
.alert-warning .bi-shield-exclamation {
color: #856404;
}
/* Payment card error state */
.border-danger {
animation: pulse-border-danger 2s infinite;
}
.border-warning {
animation: pulse-border-warning 2s infinite;
}
@keyframes pulse-border-danger {
0% {
border-color: #dc3545;
}
50% {
border-color: #f8d7da;
}
100% {
border-color: #dc3545;
}
}
@keyframes pulse-border-warning {
0% {
border-color: #ffc107;
}
50% {
border-color: #fff3cd;
}
100% {
border-color: #ffc107;
}
}
@keyframes shake {
0%,
100% {
transform: translateX(0);
}
10%,
30%,
50%,
70%,
90% {
transform: translateX(-5px);
}
20%,
40%,
60%,
80% {
transform: translateX(5px);
}
}
</style>
</head>
<body>
<div id="app"></div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Stripe JavaScript SDK -->
<script src="https://js.stripe.com/v3/"></script>
<!-- Custom Stripe Integration -->
<script>
// Stripe Integration for Company Registration
let stripe;
let elements;
let paymentElement;
// Stripe publishable key - replace with your actual key from Stripe Dashboard
const STRIPE_PUBLISHABLE_KEY = 'pk_test_51MCkZTC7LG8OeRdIcqmmoDkRwDObXSwYdChprMHJYoD2VRO8OCDBV5KtegLI0tLFXJo9yyvEXi7jzk1NAB5owj8i00DkYSaV9y'; // Replace with your real key
// Initialize Stripe when the script loads
document.addEventListener('DOMContentLoaded', function() {
console.log('🔧 Stripe integration script loaded');
// Initialize Stripe
if (window.Stripe) {
stripe = Stripe(STRIPE_PUBLISHABLE_KEY);
console.log('✅ Stripe initialized');
} else {
console.error('❌ Stripe.js not loaded');
}
});
// Create payment intent on server
window.createPaymentIntent = async function(formDataJson) {
console.log('💳 Creating payment intent for company registration...');
console.log('🔧 Server endpoint: /company/create-payment-intent');
try {
// Parse the JSON string from Rust
let formData;
if (typeof formDataJson === 'string') {
formData = JSON.parse(formDataJson);
} else {
formData = formDataJson;
}
console.log('📋 Form data being sent:', {
company_name: formData.company_name,
company_type: formData.company_type,
payment_plan: formData.payment_plan,
final_agreement: formData.final_agreement
});
const response = await fetch('http://127.0.0.1:3001/company/create-payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
console.log('📡 Server response status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('❌ Payment intent creation failed:', errorText);
let errorData;
try {
errorData = JSON.parse(errorText);
} catch (e) {
errorData = { error: errorText };
}
// Show user-friendly error message
const errorMsg = errorData.error || 'Failed to create payment intent';
console.error('💥 Error details:', errorData);
throw new Error(errorMsg);
}
const responseData = await response.json();
console.log('✅ Payment intent created successfully');
console.log('🔑 Client secret received:', responseData.client_secret ? 'Yes' : 'No');
console.log('🆔 Payment intent ID:', responseData.payment_intent_id);
const { client_secret } = responseData;
if (!client_secret) {
throw new Error('No client secret received from server');
}
return client_secret;
} catch (error) {
console.error('❌ Payment intent creation error:', error.message);
console.error('🔧 Troubleshooting:');
console.error(' 1. Check if server is running on port 3001');
console.error(' 2. Verify Stripe API keys in .env file');
console.error(' 3. Check server logs for detailed error info');
throw error;
}
};
// Initialize Stripe Elements with client secret
window.initializeStripeElements = async function(clientSecret) {
console.log('🔧 Initializing Stripe Elements for company registration payment...');
console.log('🔑 Client secret format check:', clientSecret ? 'Valid' : 'Missing');
try {
if (!stripe) {
throw new Error('Stripe not initialized - check your publishable key');
}
// Create Elements instance with client secret
elements = stripe.elements({
clientSecret: clientSecret,
appearance: {
theme: 'stripe',
variables: {
colorPrimary: '#198754',
colorBackground: '#ffffff',
colorText: '#30313d',
colorDanger: '#df1b41',
fontFamily: 'system-ui, sans-serif',
spacingUnit: '4px',
borderRadius: '6px',
}
}
});
// Clear the payment element container first
const paymentElementDiv = document.getElementById('payment-element');
if (!paymentElementDiv) {
throw new Error('Payment element container not found');
}
paymentElementDiv.innerHTML = '';
// Create and mount the Payment Element
paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
// Handle real-time validation errors from the Payment Element
paymentElement.on('change', (event) => {
const displayError = document.getElementById('payment-errors');
if (event.error) {
displayError.textContent = event.error.message;
displayError.style.display = 'block';
displayError.classList.remove('alert-success');
displayError.classList.add('alert-danger');
} else {
displayError.style.display = 'none';
}
});
// Handle when the Payment Element is ready
paymentElement.on('ready', () => {
console.log('✅ Stripe Elements ready for payment');
// Add a subtle success indicator
const paymentCard = paymentElementDiv.closest('.card');
if (paymentCard) {
paymentCard.style.borderColor = '#198754';
paymentCard.style.borderWidth = '2px';
}
// Update button text to show payment is ready
const submitButton = document.getElementById('submit-payment');
const submitText = document.getElementById('submit-text');
if (submitButton && submitText) {
submitButton.disabled = false;
submitText.textContent = 'Complete Payment';
submitButton.classList.remove('btn-secondary');
submitButton.classList.add('btn-success');
}
});
console.log('✅ Stripe Elements initialized successfully');
return true;
} catch (error) {
console.error('❌ Error initializing Stripe Elements:', error);
// Show helpful error message
const errorElement = document.getElementById('payment-errors');
if (errorElement) {
errorElement.innerHTML = `
<div class="alert alert-warning alert-dismissible" role="alert">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Stripe Setup Required:</strong> ${error.message || 'Failed to load payment form'}<br><br>
<strong>Next Steps:</strong><br>
1. Get your Stripe API keys from <a href="https://dashboard.stripe.com/apikeys" target="_blank">Stripe Dashboard</a><br>
2. Replace the placeholder publishable key in the code<br>
3. Set up a server to create payment intents<br><br>
<small>The integration is complete - you just need real Stripe credentials!</small>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
errorElement.style.display = 'block';
}
throw error;
}
};
// Confirm payment with Stripe
window.confirmStripePayment = async function(clientSecret) {
console.log('🔄 Confirming company registration payment...');
console.log('🔑 Using client secret for payment confirmation');
try {
// Ensure elements are ready before submitting
if (!elements) {
console.error('❌ Payment elements not initialized');
throw new Error('Payment form not ready. Please wait a moment and try again.');
}
console.log('🔄 Step 1: Submitting payment elements...');
// Step 1: Submit the payment elements first (required by new Stripe API)
const { error: submitError } = await elements.submit();
if (submitError) {
console.error('❌ Elements submit failed:', submitError);
throw new Error(submitError.message || 'Payment form validation failed.');
}
console.log('✅ Step 1 complete: Elements submitted successfully');
console.log('🔄 Step 2: Confirming payment with Stripe...');
// Step 2: Confirm payment with Stripe
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
clientSecret: clientSecret,
confirmParams: {
return_url: `${window.location.origin}/company/payment-success`,
},
redirect: 'if_required'
});
if (error) {
console.error('❌ Payment confirmation failed:', error);
console.error('🔧 Error details:', {
type: error.type,
code: error.code,
message: error.message
});
throw new Error(error.message);
}
if (paymentIntent && paymentIntent.status === 'succeeded') {
console.log('✅ Payment completed successfully!');
console.log('🆔 Payment Intent ID:', paymentIntent.id);
console.log('💰 Amount paid:', paymentIntent.amount_received / 100, paymentIntent.currency.toUpperCase());
// Clear saved form data since registration is complete
localStorage.removeItem('freezone_company_registration');
console.log('🗑️ Cleared saved registration data');
// Redirect to success page
const successUrl = `${window.location.origin}/company/payment-success?payment_intent=${paymentIntent.id}&payment_intent_client_secret=${clientSecret}`;
console.log('🔄 Redirecting to success page:', successUrl);
window.location.href = successUrl;
return true;
} else {
console.error('❌ Unexpected payment status:', paymentIntent?.status);
console.error('🔧 Payment Intent details:', paymentIntent);
throw new Error('Payment processing failed. Please try again.');
}
} catch (error) {
console.error('❌ Payment confirmation error:', error.message);
console.error('🔧 Full error details:', error);
throw error;
}
};
console.log('✅ Stripe integration ready for company registration payments');
console.log('🔧 Server endpoint: /company/create-payment-intent');
console.log('💡 Navigate to Entities → Register Company → Step 4 to process payments');
// Add a test function for manual payment testing
window.testPaymentFlow = async function() {
console.log('🧪 Testing payment flow manually...');
const mockFormData = {
company_name: "Test Company Ltd",
company_type: "Single FZC",
payment_plan: "monthly",
company_email: "test@example.com",
company_phone: "+1234567890",
company_website: "https://test.com",
company_address: "123 Test Street",
company_industry: "Technology",
company_purpose: "Software Development",
fiscal_year_end: "December",
shareholders: "[]",
agreements: ["terms", "privacy", "compliance", "articles"],
final_agreement: true
};
try {
console.log('📋 Using test form data:', mockFormData);
const clientSecret = await window.createPaymentIntent(JSON.stringify(mockFormData));
console.log('✅ Payment intent created, initializing Stripe Elements...');
await window.initializeStripeElements(clientSecret);
console.log('🎉 Payment form should now be visible!');
console.log('💡 Check the payment section in the UI');
} catch (error) {
console.error('❌ Test failed:', error);
}
};
console.log('💡 You can test the payment flow manually with: window.testPaymentFlow()');
</script>
<!-- WASM Application -->
<script type="module">
async function run() {
try {
// Load the WASM module for the Yew application
const init = await import('./pkg/freezone_platform.js');
await init.default();
console.log('✅ Freezone Platform WASM application initialized');
console.log('🏢 Company registration system ready');
} catch (error) {
console.error('❌ Failed to initialize WASM application:', error);
console.error('🔧 Make sure to build the WASM module with: trunk build');
}
}
run();
</script>
</body>
</html>

55
platform/run-server.sh Executable file
View File

@ -0,0 +1,55 @@
#!/bin/bash
# Freezone Platform Server Runner
# This script sets up and runs the complete Stripe-integrated server
echo "🚀 Starting Freezone Platform with Stripe Integration"
echo "=================================================="
# Check if .env file exists
if [ ! -f ".env" ]; then
echo "❌ .env file not found!"
echo "📋 Please copy .env.example to .env and add your Stripe keys:"
echo " cp .env.example .env"
echo " # Then edit .env with your actual Stripe keys"
exit 1
fi
# Check if Stripe keys are configured
if grep -q "YOUR_ACTUAL" .env; then
echo "⚠️ Warning: .env file contains placeholder values"
echo "📋 Please update .env with your real Stripe API keys from:"
echo " https://dashboard.stripe.com/apikeys"
echo ""
echo "🎭 Running in demo mode (server will still start)..."
echo ""
fi
# Load environment variables
source .env
echo "🔧 Building server with Stripe integration..."
cargo build --bin server --features server
if [ $? -ne 0 ]; then
echo "❌ Build failed! Please check the error messages above."
exit 1
fi
echo "✅ Build successful!"
echo ""
echo "🌐 Starting server on http://${HOST:-127.0.0.1}:${PORT:-8080}"
echo "📊 Health check: http://${HOST:-127.0.0.1}:${PORT:-8080}/api/health"
echo "💳 Payment endpoint: http://${HOST:-127.0.0.1}:${PORT:-8080}/company/create-payment-intent"
echo ""
echo "🧪 To test the integration:"
echo " 1. Open http://${HOST:-127.0.0.1}:${PORT:-8080} in your browser"
echo " 2. Navigate to the entities page"
echo " 3. Go through the company registration steps"
echo " 4. In step 4, you'll see Stripe Elements instead of manual form"
echo ""
echo "🔄 Press Ctrl+C to stop the server"
echo "=================================================="
# Run the server
cargo run --bin server --features server

475
platform/src/app.rs Normal file
View File

@ -0,0 +1,475 @@
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>
}
}
}
}
}

526
platform/src/bin/server.rs Normal file
View File

@ -0,0 +1,526 @@
use axum::{
extract::{Json, Query},
http::{HeaderMap, StatusCode},
response::Json as ResponseJson,
routing::{get, post},
Router,
};
use dotenv::dotenv;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, env};
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
services::ServeDir,
};
use tracing::{info, warn, error};
#[derive(Debug, Deserialize)]
struct CreatePaymentIntentRequest {
company_name: String,
company_type: String,
company_email: Option<String>,
company_phone: Option<String>,
company_website: Option<String>,
company_address: Option<String>,
company_industry: Option<String>,
company_purpose: Option<String>,
fiscal_year_end: Option<String>,
shareholders: Option<String>,
payment_plan: String,
agreements: Vec<String>,
final_agreement: bool,
}
#[derive(Debug, Deserialize)]
struct CreateResidentPaymentIntentRequest {
resident_name: String,
email: String,
phone: Option<String>,
date_of_birth: Option<String>,
nationality: Option<String>,
passport_number: Option<String>,
address: Option<String>,
payment_plan: String,
amount: f64,
#[serde(rename = "type")]
request_type: String,
}
#[derive(Debug, Serialize)]
struct CreatePaymentIntentResponse {
client_secret: String,
payment_intent_id: String,
}
#[derive(Debug, Serialize)]
struct ErrorResponse {
error: String,
details: Option<String>,
}
#[derive(Debug, Deserialize)]
struct WebhookQuery {
#[serde(rename = "payment_intent")]
payment_intent_id: Option<String>,
#[serde(rename = "payment_intent_client_secret")]
client_secret: Option<String>,
}
// Calculate pricing based on company type and payment plan
fn calculate_amount(company_type: &str, payment_plan: &str) -> Result<i64, String> {
let base_amounts = match company_type {
"Single FZC" => (20, 20), // (setup, monthly)
"Startup FZC" => (50, 50),
"Growth FZC" => (1000, 100),
"Global FZC" => (2000, 200),
"Cooperative FZC" => (2000, 200),
_ => return Err("Invalid company type".to_string()),
};
let (setup_fee, monthly_fee) = base_amounts;
let twin_fee = 2; // ZDFZ Twin fee
let total_monthly = monthly_fee + twin_fee;
let amount_cents = match payment_plan {
"monthly" => (setup_fee + total_monthly) * 100,
"yearly" => (setup_fee + (total_monthly * 12 * 80 / 100)) * 100, // 20% discount
"two_year" => (setup_fee + (total_monthly * 24 * 60 / 100)) * 100, // 40% discount
_ => return Err("Invalid payment plan".to_string()),
};
Ok(amount_cents as i64)
}
// Create payment intent with Stripe
async fn create_payment_intent(
Json(payload): Json<CreatePaymentIntentRequest>,
) -> Result<ResponseJson<CreatePaymentIntentResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
info!("Creating payment intent for company: {}", payload.company_name);
// Validate required fields
if !payload.final_agreement {
return Err((
StatusCode::BAD_REQUEST,
ResponseJson(ErrorResponse {
error: "Final agreement must be accepted".to_string(),
details: None,
}),
));
}
// Calculate amount based on company type and payment plan
let amount = match calculate_amount(&payload.company_type, &payload.payment_plan) {
Ok(amount) => amount,
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
ResponseJson(ErrorResponse {
error: e,
details: None,
}),
));
}
};
// Get Stripe secret key from environment
let stripe_secret_key = env::var("STRIPE_SECRET_KEY").map_err(|_| {
error!("STRIPE_SECRET_KEY not found in environment");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Server configuration error".to_string(),
details: Some("Stripe not configured".to_string()),
}),
)
})?;
// Create Stripe client
let client = reqwest::Client::new();
// Prepare payment intent data
let mut form_data = HashMap::new();
form_data.insert("amount", amount.to_string());
form_data.insert("currency", "usd".to_string());
form_data.insert("automatic_payment_methods[enabled]", "true".to_string());
// Add metadata
form_data.insert("metadata[company_name]", payload.company_name.clone());
form_data.insert("metadata[company_type]", payload.company_type.clone());
form_data.insert("metadata[payment_plan]", payload.payment_plan.clone());
if let Some(email) = &payload.company_email {
form_data.insert("metadata[company_email]", email.clone());
}
// Add description
let description = format!(
"Company Registration: {} ({})",
payload.company_name, payload.company_type
);
form_data.insert("description", description);
// Call Stripe API
let response = client
.post("https://api.stripe.com/v1/payment_intents")
.header("Authorization", format!("Bearer {}", stripe_secret_key))
.form(&form_data)
.send()
.await
.map_err(|e| {
error!("Failed to call Stripe API: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Failed to create payment intent".to_string(),
details: Some(e.to_string()),
}),
)
})?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
error!("Stripe API error: {}", error_text);
return Err((
StatusCode::BAD_REQUEST,
ResponseJson(ErrorResponse {
error: "Stripe payment intent creation failed".to_string(),
details: Some(error_text),
}),
));
}
let stripe_response: serde_json::Value = response.json().await.map_err(|e| {
error!("Failed to parse Stripe response: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Invalid response from payment processor".to_string(),
details: Some(e.to_string()),
}),
)
})?;
let client_secret = stripe_response["client_secret"]
.as_str()
.ok_or_else(|| {
error!("No client_secret in Stripe response");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Invalid payment intent response".to_string(),
details: None,
}),
)
})?;
let payment_intent_id = stripe_response["id"]
.as_str()
.ok_or_else(|| {
error!("No id in Stripe response");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Invalid payment intent response".to_string(),
details: None,
}),
)
})?;
info!("Payment intent created successfully: {}", payment_intent_id);
Ok(ResponseJson(CreatePaymentIntentResponse {
client_secret: client_secret.to_string(),
payment_intent_id: payment_intent_id.to_string(),
}))
}
// Create payment intent for resident registration
async fn create_resident_payment_intent(
Json(payload): Json<CreateResidentPaymentIntentRequest>,
) -> Result<ResponseJson<CreatePaymentIntentResponse>, (StatusCode, ResponseJson<ErrorResponse>)> {
info!("Creating payment intent for resident: {}", payload.resident_name);
// Convert amount from dollars to cents
let amount_cents = (payload.amount * 100.0) as i64;
// Get Stripe secret key from environment
let stripe_secret_key = env::var("STRIPE_SECRET_KEY").map_err(|_| {
error!("STRIPE_SECRET_KEY not found in environment");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Server configuration error".to_string(),
details: Some("Stripe not configured".to_string()),
}),
)
})?;
// Create Stripe client
let client = reqwest::Client::new();
// Prepare payment intent data
let mut form_data = HashMap::new();
form_data.insert("amount", amount_cents.to_string());
form_data.insert("currency", "usd".to_string());
form_data.insert("automatic_payment_methods[enabled]", "true".to_string());
// Add metadata
form_data.insert("metadata[resident_name]", payload.resident_name.clone());
form_data.insert("metadata[email]", payload.email.clone());
form_data.insert("metadata[payment_plan]", payload.payment_plan.clone());
form_data.insert("metadata[type]", payload.request_type.clone());
if let Some(phone) = &payload.phone {
form_data.insert("metadata[phone]", phone.clone());
}
if let Some(nationality) = &payload.nationality {
form_data.insert("metadata[nationality]", nationality.clone());
}
// Add description
let description = format!(
"Resident Registration: {} ({})",
payload.resident_name, payload.payment_plan
);
form_data.insert("description", description);
// Call Stripe API
let response = client
.post("https://api.stripe.com/v1/payment_intents")
.header("Authorization", format!("Bearer {}", stripe_secret_key))
.form(&form_data)
.send()
.await
.map_err(|e| {
error!("Failed to call Stripe API: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Failed to create payment intent".to_string(),
details: Some(e.to_string()),
}),
)
})?;
if !response.status().is_success() {
let error_text = response.text().await.unwrap_or_default();
error!("Stripe API error: {}", error_text);
return Err((
StatusCode::BAD_REQUEST,
ResponseJson(ErrorResponse {
error: "Stripe payment intent creation failed".to_string(),
details: Some(error_text),
}),
));
}
let stripe_response: serde_json::Value = response.json().await.map_err(|e| {
error!("Failed to parse Stripe response: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Invalid response from payment processor".to_string(),
details: Some(e.to_string()),
}),
)
})?;
let client_secret = stripe_response["client_secret"]
.as_str()
.ok_or_else(|| {
error!("No client_secret in Stripe response");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Invalid payment intent response".to_string(),
details: None,
}),
)
})?;
let payment_intent_id = stripe_response["id"]
.as_str()
.ok_or_else(|| {
error!("No id in Stripe response");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Invalid payment intent response".to_string(),
details: None,
}),
)
})?;
info!("Resident payment intent created successfully: {}", payment_intent_id);
Ok(ResponseJson(CreatePaymentIntentResponse {
client_secret: client_secret.to_string(),
payment_intent_id: payment_intent_id.to_string(),
}))
}
// Handle Stripe webhooks
async fn handle_webhook(
headers: HeaderMap,
body: String,
) -> Result<StatusCode, (StatusCode, ResponseJson<ErrorResponse>)> {
let stripe_signature = headers
.get("stripe-signature")
.and_then(|v| v.to_str().ok())
.ok_or_else(|| {
warn!("Missing Stripe signature header");
(
StatusCode::BAD_REQUEST,
ResponseJson(ErrorResponse {
error: "Missing signature".to_string(),
details: None,
}),
)
})?;
let _webhook_secret = env::var("STRIPE_WEBHOOK_SECRET").map_err(|_| {
error!("STRIPE_WEBHOOK_SECRET not found in environment");
(
StatusCode::INTERNAL_SERVER_ERROR,
ResponseJson(ErrorResponse {
error: "Webhook not configured".to_string(),
details: None,
}),
)
})?;
// In a real implementation, you would verify the webhook signature here
// For now, we'll just log the event
info!("Received webhook with signature: {}", stripe_signature);
info!("Webhook body: {}", body);
// Parse the webhook event
let event: serde_json::Value = serde_json::from_str(&body).map_err(|e| {
error!("Failed to parse webhook body: {}", e);
(
StatusCode::BAD_REQUEST,
ResponseJson(ErrorResponse {
error: "Invalid webhook body".to_string(),
details: Some(e.to_string()),
}),
)
})?;
let event_type = event["type"].as_str().unwrap_or("unknown");
info!("Processing webhook event: {}", event_type);
match event_type {
"payment_intent.succeeded" => {
let payment_intent = &event["data"]["object"];
let payment_intent_id = payment_intent["id"].as_str().unwrap_or("unknown");
info!("Payment succeeded: {}", payment_intent_id);
// Here you would typically:
// 1. Update your database to mark the company as registered
// 2. Send confirmation emails
// 3. Trigger any post-payment workflows
}
"payment_intent.payment_failed" => {
let payment_intent = &event["data"]["object"];
let payment_intent_id = payment_intent["id"].as_str().unwrap_or("unknown");
warn!("Payment failed: {}", payment_intent_id);
// Handle failed payment
}
_ => {
info!("Unhandled webhook event type: {}", event_type);
}
}
Ok(StatusCode::OK)
}
// Payment success redirect
async fn payment_success(Query(params): Query<WebhookQuery>) -> axum::response::Redirect {
info!("Payment success page accessed");
if let Some(ref payment_intent_id) = params.payment_intent_id {
info!("Payment intent ID: {}", payment_intent_id);
// In a real implementation, you would:
// 1. Verify the payment intent with Stripe
// 2. Get the company ID from your database
// 3. Redirect to the success page with the actual company ID
// For now, we'll use a mock company ID (in real app, get from database)
let company_id = 1; // This should be retrieved from your database based on payment_intent_id
axum::response::Redirect::to(&format!("/entities/register/success/{}", company_id))
} else {
// If no payment intent ID, redirect to entities page
axum::response::Redirect::to("/entities")
}
}
// Payment failure redirect
async fn payment_failure() -> axum::response::Redirect {
info!("Payment failure page accessed");
axum::response::Redirect::to("/entities/register/failure")
}
// Health check endpoint
async fn health_check() -> ResponseJson<serde_json::Value> {
ResponseJson(serde_json::json!({
"status": "healthy",
"timestamp": chrono::Utc::now().to_rfc3339(),
"service": "freezone-platform-server"
}))
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Load environment variables
dotenv().ok();
// Initialize tracing
tracing_subscriber::fmt::init();
// Check required environment variables
let required_vars = ["STRIPE_SECRET_KEY", "STRIPE_PUBLISHABLE_KEY"];
for var in &required_vars {
if env::var(var).is_err() {
warn!("Environment variable {} not set", var);
}
}
// Build the application router
let app = Router::new()
// API routes
.route("/api/health", get(health_check))
.route("/company/create-payment-intent", post(create_payment_intent))
.route("/resident/create-payment-intent", post(create_resident_payment_intent))
.route("/company/payment-success", get(payment_success))
.route("/company/payment-failure", get(payment_failure))
.route("/webhooks/stripe", post(handle_webhook))
// Serve static files (WASM, HTML, CSS, JS)
.nest_service("/", ServeDir::new("."))
// Add middleware
.layer(
ServiceBuilder::new()
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
)
);
// Get server configuration from environment
let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
let addr = format!("{}:{}", host, port);
info!("Starting server on {}", addr);
info!("Health check: http://{}/api/health", addr);
info!("Payment endpoint: http://{}/company/create-payment-intent", addr);
// Start the server
let listener = tokio::net::TcpListener::bind(&addr).await?;
axum::serve(listener, app).await?;
Ok(())
}

View File

@ -0,0 +1,801 @@
use yew::prelude::*;
use wasm_bindgen::JsCast;
use crate::components::accounting::models::*;
use js_sys;
#[derive(Properties, PartialEq)]
pub struct ExpensesTabProps {
pub state: UseStateHandle<AccountingState>,
}
#[function_component(ExpensesTab)]
pub fn expenses_tab(props: &ExpensesTabProps) -> Html {
let state = &props.state;
html! {
<div class="animate-fade-in-up">
// Expense Form Modal
{if state.show_expense_form {
html! {
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Add New Expense"}</h5>
<button type="button" class="btn-close" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_expense_form = false;
state.set(new_state);
})
}></button>
</div>
<div class="modal-body">
<form>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">{"Receipt Number"}</label>
<input type="text" class="form-control" value={state.expense_form.receipt_number.clone()} readonly=true />
</div>
<div class="col-md-6">
<label class="form-label">{"Date"}</label>
<input type="date" class="form-control" value={state.expense_form.date.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.date = input.value();
state.set(new_state);
})
} />
</div>
<div class="col-md-6">
<label class="form-label">{"Vendor Name"}</label>
<input type="text" class="form-control" value={state.expense_form.vendor_name.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.vendor_name = input.value();
state.set(new_state);
})
} />
</div>
<div class="col-md-6">
<label class="form-label">{"Vendor Email"}</label>
<input type="email" class="form-control" value={state.expense_form.vendor_email.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.vendor_email = input.value();
state.set(new_state);
})
} />
</div>
<div class="col-12">
<label class="form-label">{"Description"}</label>
<textarea class="form-control" rows="3" value={state.expense_form.description.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.description = input.value();
state.set(new_state);
})
}></textarea>
</div>
<div class="col-md-6">
<label class="form-label">{"Amount"}</label>
<input type="number" step="0.01" class="form-control" value={state.expense_form.amount.to_string()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.amount = input.value().parse().unwrap_or(0.0);
state.set(new_state);
})
} />
</div>
<div class="col-md-6">
<label class="form-label">{"Tax Amount"}</label>
<input type="number" step="0.01" class="form-control" value={state.expense_form.tax_amount.to_string()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.tax_amount = input.value().parse().unwrap_or(0.0);
state.set(new_state);
})
} />
</div>
<div class="col-md-6">
<label class="form-label">{"Tax Deductible"}</label>
<select class="form-select" onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let select: web_sys::HtmlSelectElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.expense_form.is_deductible = select.value() == "true";
state.set(new_state);
})
}>
<option value="true" selected={state.expense_form.is_deductible}>{"Yes"}</option>
<option value="false" selected={!state.expense_form.is_deductible}>{"No"}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">{"Project Code (Optional)"}</label>
<input type="text" class="form-control" value={state.expense_form.project_code.clone().unwrap_or_default()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
let value = input.value();
new_state.expense_form.project_code = if value.is_empty() { None } else { Some(value) };
state.set(new_state);
})
} />
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_expense_form = false;
state.set(new_state);
})
}>{"Cancel"}</button>
<button type="button" class="btn btn-danger" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
// Calculate total
new_state.expense_form.total_amount = new_state.expense_form.amount + new_state.expense_form.tax_amount;
// Add to entries
new_state.expense_entries.push(new_state.expense_form.clone());
// Reset form
new_state.show_expense_form = false;
new_state.expense_form = AccountingState::default().expense_form;
state.set(new_state);
})
}>{"Add Expense"}</button>
</div>
</div>
</div>
</div>
}
} else {
html! {}
}}
// Expense Detail Modal
{if state.show_expense_detail {
if let Some(expense_id) = &state.selected_expense_id {
if let Some(expense) = state.expense_entries.iter().find(|e| &e.id == expense_id) {
let expense_transactions: Vec<&PaymentTransaction> = state.payment_transactions.iter()
.filter(|t| t.expense_id.as_ref() == Some(expense_id))
.collect();
let total_paid: f64 = expense_transactions.iter().map(|t| t.amount).sum();
let remaining_balance = expense.total_amount - total_paid;
html! {
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{format!("Expense Details - {}", expense.receipt_number)}</h5>
<button type="button" class="btn-close" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_expense_detail = false;
new_state.selected_expense_id = None;
state.set(new_state);
})
}></button>
</div>
<div class="modal-body">
<div class="row g-4">
// Expense Information
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{"Expense Information"}</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-6"><strong>{"Receipt #:"}</strong></div>
<div class="col-6">{&expense.receipt_number}</div>
<div class="col-6"><strong>{"Date:"}</strong></div>
<div class="col-6">{&expense.date}</div>
<div class="col-6"><strong>{"Category:"}</strong></div>
<div class="col-6">{expense.category.to_string()}</div>
<div class="col-6"><strong>{"Status:"}</strong></div>
<div class="col-6">
<span class={format!("badge bg-{}", expense.payment_status.get_color())}>
{expense.payment_status.to_string()}
</span>
</div>
<div class="col-6"><strong>{"Total Amount:"}</strong></div>
<div class="col-6 fw-bold text-danger">{format!("${:.2}", expense.total_amount)}</div>
<div class="col-6"><strong>{"Amount Paid:"}</strong></div>
<div class="col-6 fw-bold text-primary">{format!("${:.2}", total_paid)}</div>
<div class="col-6"><strong>{"Remaining:"}</strong></div>
<div class="col-6 fw-bold text-warning">{format!("${:.2}", remaining_balance)}</div>
</div>
</div>
</div>
</div>
// Vendor Information
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{"Vendor Information"}</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-4"><strong>{"Name:"}</strong></div>
<div class="col-8">{&expense.vendor_name}</div>
<div class="col-4"><strong>{"Email:"}</strong></div>
<div class="col-8">{&expense.vendor_email}</div>
<div class="col-4"><strong>{"Address:"}</strong></div>
<div class="col-8">{&expense.vendor_address}</div>
</div>
</div>
</div>
</div>
// Payment Transactions
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{"Payment Transactions"}</h6>
<button class="btn btn-sm btn-primary" onclick={
let state = state.clone();
let expense_id = expense.id.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = true;
new_state.transaction_form.expense_id = Some(expense_id.clone());
state.set(new_state);
})
}>
<i class="bi bi-plus-circle me-1"></i>{"Record Payment"}
</button>
</div>
<div class="card-body p-0">
{if expense_transactions.is_empty() {
html! {
<div class="text-center py-4 text-muted">
<i class="bi bi-credit-card fs-1 mb-2 d-block"></i>
<p class="mb-0">{"No payments recorded yet"}</p>
</div>
}
} else {
html! {
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 py-3">{"Date"}</th>
<th class="border-0 py-3">{"Amount"}</th>
<th class="border-0 py-3">{"Method"}</th>
<th class="border-0 py-3">{"Reference"}</th>
<th class="border-0 py-3">{"Status"}</th>
<th class="border-0 py-3">{"Notes"}</th>
</tr>
</thead>
<tbody>
{for expense_transactions.iter().map(|transaction| {
html! {
<tr>
<td class="py-3">{&transaction.date}</td>
<td class="py-3 fw-bold text-danger">{format!("${:.2}", transaction.amount)}</td>
<td class="py-3">
<div class="d-flex align-items-center">
<i class={format!("bi bi-{} text-{} me-2", transaction.payment_method.get_icon(), transaction.payment_method.get_color())}></i>
{transaction.payment_method.to_string()}
</div>
</td>
<td class="py-3">
{if let Some(hash) = &transaction.transaction_hash {
html! { <code class="small">{&hash[..12]}{"..."}</code> }
} else if let Some(ref_num) = &transaction.reference_number {
html! { <span>{ref_num}</span> }
} else {
html! { <span class="text-muted">{"-"}</span> }
}}
</td>
<td class="py-3">
<span class={format!("badge bg-{}", transaction.status.get_color())}>
{transaction.status.to_string()}
</span>
</td>
<td class="py-3">{&transaction.notes}</td>
</tr>
}
})}
</tbody>
</table>
</div>
}
}}
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_expense_detail = false;
new_state.selected_expense_id = None;
state.set(new_state);
})
}>{"Close"}</button>
<button type="button" class="btn btn-primary" onclick={
let state = state.clone();
let expense_id = expense.id.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = true;
new_state.transaction_form.expense_id = Some(expense_id.clone());
state.set(new_state);
})
}>
<i class="bi bi-credit-card me-2"></i>{"Record Payment"}
</button>
</div>
</div>
</div>
</div>
}
} else {
html! {}
}
} else {
html! {}
}
} else {
html! {}
}}
// Transaction Form Modal (for expense payments)
{if state.show_transaction_form && state.transaction_form.expense_id.is_some() {
html! {
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Record Expense Payment"}</h5>
<button type="button" class="btn-close" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = false;
state.set(new_state);
})
}></button>
</div>
<div class="modal-body">
<form>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">{"Expense Receipt Number"}</label>
<input type="text" class="form-control" value={state.transaction_form.expense_id.clone().unwrap_or_default()} readonly=true />
</div>
<div class="col-md-6">
<label class="form-label">{"Payment Amount"}</label>
<input type="number" step="0.01" class="form-control" value={state.transaction_form.amount.to_string()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.amount = input.value().parse().unwrap_or(0.0);
state.set(new_state);
})
} />
</div>
<div class="col-12">
<label class="form-label">{"Payment Method"}</label>
<select class="form-select" onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let select: web_sys::HtmlSelectElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.payment_method = match select.value().as_str() {
"BankTransfer" => PaymentMethod::BankTransfer,
"CreditCard" => PaymentMethod::CreditCard,
"CryptoBitcoin" => PaymentMethod::CryptoBitcoin,
"CryptoEthereum" => PaymentMethod::CryptoEthereum,
"CryptoUSDC" => PaymentMethod::CryptoUSDC,
"Cash" => PaymentMethod::Cash,
"Check" => PaymentMethod::Check,
_ => PaymentMethod::BankTransfer,
};
state.set(new_state);
})
}>
<option value="BankTransfer" selected={matches!(state.transaction_form.payment_method, PaymentMethod::BankTransfer)}>{"Bank Transfer"}</option>
<option value="CreditCard" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CreditCard)}>{"Credit Card"}</option>
<option value="CryptoBitcoin" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoBitcoin)}>{"Bitcoin"}</option>
<option value="CryptoEthereum" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoEthereum)}>{"Ethereum"}</option>
<option value="CryptoUSDC" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoUSDC)}>{"USDC"}</option>
<option value="Cash" selected={matches!(state.transaction_form.payment_method, PaymentMethod::Cash)}>{"Cash"}</option>
<option value="Check" selected={matches!(state.transaction_form.payment_method, PaymentMethod::Check)}>{"Check"}</option>
</select>
</div>
{if matches!(state.transaction_form.payment_method, PaymentMethod::CryptoBitcoin | PaymentMethod::CryptoEthereum | PaymentMethod::CryptoUSDC | PaymentMethod::CryptoOther) {
html! {
<div class="col-12">
<label class="form-label">{"Transaction Hash"}</label>
<input type="text" class="form-control" placeholder="0x..." value={state.transaction_form.transaction_hash.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.transaction_hash = input.value();
state.set(new_state);
})
} />
</div>
}
} else {
html! {
<div class="col-12">
<label class="form-label">{"Reference Number"}</label>
<input type="text" class="form-control" placeholder="REF-2024-001" value={state.transaction_form.reference_number.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.reference_number = input.value();
state.set(new_state);
})
} />
</div>
}
}}
<div class="col-12">
<label class="form-label">{"Notes"}</label>
<textarea class="form-control" rows="3" value={state.transaction_form.notes.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.notes = input.value();
state.set(new_state);
})
}></textarea>
</div>
<div class="col-12">
<label class="form-label">{"Attach Files"}</label>
<input type="file" class="form-control" multiple=true accept=".pdf,.jpg,.jpeg,.png" />
<small class="text-muted">{"Upload receipts, confirmations, or other supporting documents"}</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = false;
state.set(new_state);
})
}>{"Cancel"}</button>
<button type="button" class="btn btn-success" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
// Create new transaction
let transaction_count = new_state.payment_transactions.len() + 1;
let new_transaction = PaymentTransaction {
id: format!("TXN-2024-{:03}", transaction_count),
invoice_id: None,
expense_id: new_state.transaction_form.expense_id.clone(),
date: js_sys::Date::new_0().to_iso_string().as_string().unwrap()[..10].to_string(),
amount: new_state.transaction_form.amount,
payment_method: new_state.transaction_form.payment_method.clone(),
transaction_hash: if new_state.transaction_form.transaction_hash.is_empty() { None } else { Some(new_state.transaction_form.transaction_hash.clone()) },
reference_number: if new_state.transaction_form.reference_number.is_empty() { None } else { Some(new_state.transaction_form.reference_number.clone()) },
notes: new_state.transaction_form.notes.clone(),
attached_files: new_state.transaction_form.attached_files.clone(),
status: TransactionStatus::Confirmed,
};
new_state.payment_transactions.push(new_transaction);
new_state.show_transaction_form = false;
new_state.transaction_form = TransactionForm::default();
state.set(new_state);
})
}>{"Record Payment"}</button>
</div>
</div>
</div>
</div>
}
} else {
html! {}
}}
// Expense Actions and Table
<div class="row g-4">
<div class="col-12">
<div class="card shadow-soft border-0">
<div class="card-header bg-white border-bottom-0 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0 fw-bold">{"Expense Entries"}</h5>
<small class="text-muted">{"Click on any row to view details"}</small>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary btn-sm" onclick={
Callback::from(move |_| {
web_sys::window()
.unwrap()
.alert_with_message("Expense filter feature coming soon!")
.unwrap();
})
}>
<i class="bi bi-funnel me-2"></i>{"Filter"}
</button>
<button class="btn btn-outline-secondary btn-sm" onclick={
let expense_entries = state.expense_entries.clone();
Callback::from(move |_| {
// Create CSV content
let mut csv_content = "Receipt Number,Date,Vendor Name,Vendor Email,Description,Amount,Tax Amount,Total Amount,Category,Payment Method,Payment Status,Tax Deductible,Approval Status,Approved By,Notes,Project Code,Currency\n".to_string();
for entry in &expense_entries {
csv_content.push_str(&format!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
entry.receipt_number,
entry.date,
entry.vendor_name,
entry.vendor_email,
entry.description.replace(",", ";"),
entry.amount,
entry.tax_amount,
entry.total_amount,
entry.category.to_string(),
entry.payment_method.to_string(),
entry.payment_status.to_string(),
entry.is_deductible,
entry.approval_status.to_string(),
entry.approved_by.as_ref().unwrap_or(&"".to_string()),
entry.notes.replace(",", ";"),
entry.project_code.as_ref().unwrap_or(&"".to_string()),
entry.currency
));
}
// Create and download file
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let element = document.create_element("a").unwrap();
element.set_attribute("href", &format!("data:text/csv;charset=utf-8,{}", js_sys::encode_uri_component(&csv_content))).unwrap();
element.set_attribute("download", "expenses_export.csv").unwrap();
element.set_attribute("style", "display: none").unwrap();
document.body().unwrap().append_child(&element).unwrap();
let html_element: web_sys::HtmlElement = element.clone().dyn_into().unwrap();
html_element.click();
document.body().unwrap().remove_child(&element).unwrap();
})
}>
<i class="bi bi-download me-2"></i>{"Export"}
</button>
<button class="btn btn-danger btn-sm" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_expense_form = true;
let expense_count = new_state.expense_entries.len() + 1;
new_state.expense_form.receipt_number = format!("EXP-2024-{:03}", expense_count);
new_state.expense_form.id = new_state.expense_form.receipt_number.clone();
state.set(new_state);
})
}>
<i class="bi bi-plus-circle me-2"></i>{"Add Expense"}
</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 py-3 px-4">{"Receipt #"}</th>
<th class="border-0 py-3">{"Vendor"}</th>
<th class="border-0 py-3">{"Description"}</th>
<th class="border-0 py-3">{"Amount"}</th>
<th class="border-0 py-3">{"Payment Method"}</th>
<th class="border-0 py-3">{"Status"}</th>
<th class="border-0 py-3">{"Approval"}</th>
<th class="border-0 py-3">{"Actions"}</th>
</tr>
</thead>
<tbody>
{for state.expense_entries.iter().map(|entry| {
html! {
<tr class="border-bottom">
<td class="py-3 px-4 cursor-pointer" style="cursor: pointer;" onclick={
let state = state.clone();
let expense_id = entry.id.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_expense_detail = true;
new_state.selected_expense_id = Some(expense_id.clone());
state.set(new_state);
})
}>
<div class="fw-bold text-primary">{&entry.receipt_number}</div>
<small class="text-muted">{&entry.date}</small>
</td>
<td class="py-3">
<div class="fw-semibold">{&entry.vendor_name}</div>
<small class="text-muted">{&entry.vendor_email}</small>
</td>
<td class="py-3">
<div class="fw-semibold">{&entry.description}</div>
<small class="text-muted">
<span class={format!("badge bg-{} bg-opacity-10 text-{} me-1", entry.category.get_color(), entry.category.get_color())}>
{entry.category.to_string()}
</span>
{if entry.is_deductible { "• Tax Deductible" } else { "" }}
{if let Some(project) = &entry.project_code {
html! { <span class="ms-1">{format!("{}", project)}</span> }
} else {
html! {}
}}
</small>
</td>
<td class="py-3">
<div class="fw-bold text-danger">{format!("${:.2}", entry.total_amount)}</div>
<small class="text-muted">{format!("${:.2} + ${:.2} tax", entry.amount, entry.tax_amount)}</small>
</td>
<td class="py-3">
<div class="d-flex align-items-center">
<i class={format!("bi bi-{} text-{} me-2", entry.payment_method.get_icon(), entry.payment_method.get_color())}></i>
<span class="small">{entry.payment_method.to_string()}</span>
</div>
</td>
<td class="py-3">
<span class={format!("badge bg-{} bg-opacity-10 text-{}", entry.payment_status.get_color(), entry.payment_status.get_color())}>
{entry.payment_status.to_string()}
</span>
</td>
<td class="py-3">
<span class={format!("badge bg-{} bg-opacity-10 text-{}", entry.approval_status.get_color(), entry.approval_status.get_color())}>
{entry.approval_status.to_string()}
</span>
{
if let Some(approver) = &entry.approved_by {
html! { <small class="d-block text-muted">{format!("by {}", approver)}</small> }
} else {
html! {}
}
}
</td>
<td class="py-3">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick={
let state = state.clone();
let expense_id = entry.id.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
let mut new_state = (*state).clone();
new_state.show_expense_detail = true;
new_state.selected_expense_id = Some(expense_id.clone());
state.set(new_state);
})
}><i class="bi bi-eye me-2"></i>{"View Details"}</a></li>
<li><a class="dropdown-item" href="#" onclick={
let state = state.clone();
let expense_id = entry.id.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
let mut new_state = (*state).clone();
new_state.show_transaction_form = true;
new_state.transaction_form.expense_id = Some(expense_id.clone());
state.set(new_state);
})
}><i class="bi bi-credit-card me-2"></i>{"Record Payment"}</a></li>
<li><a class="dropdown-item" href="#" onclick={
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
web_sys::window()
.unwrap()
.alert_with_message("Edit expense feature coming soon!")
.unwrap();
})
}><i class="bi bi-pencil me-2"></i>{"Edit"}</a></li>
<li><a class="dropdown-item" href="#" onclick={
let receipt_url = entry.receipt_url.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
if let Some(url) = &receipt_url {
web_sys::window().unwrap().open_with_url(url).unwrap();
} else {
web_sys::window()
.unwrap()
.alert_with_message("No receipt available for this expense")
.unwrap();
}
})
}><i class="bi bi-file-earmark me-2"></i>{"View Receipt"}</a></li>
<li><a class="dropdown-item" href="#" onclick={
let state = state.clone();
let expense_id = entry.id.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
let mut new_state = (*state).clone();
// Find and update the expense approval status
if let Some(expense) = new_state.expense_entries.iter_mut().find(|e| e.id == expense_id) {
expense.approval_status = ApprovalStatus::Approved;
expense.approved_by = Some("Current User".to_string());
}
state.set(new_state);
})
}><i class="bi bi-check-circle me-2"></i>{"Approve"}</a></li>
<li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="#" onclick={
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
web_sys::window()
.unwrap()
.alert_with_message("Duplicate expense feature coming soon!")
.unwrap();
})
}><i class="bi bi-files me-2"></i>{"Duplicate"}</a></li>
<li><a class="dropdown-item text-danger" href="#" onclick={
let state = state.clone();
let expense_id = entry.id.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
if web_sys::window().unwrap().confirm_with_message("Are you sure you want to delete this expense?").unwrap() {
let mut new_state = (*state).clone();
new_state.expense_entries.retain(|e| e.id != expense_id);
state.set(new_state);
}
})
}><i class="bi bi-trash me-2"></i>{"Delete"}</a></li>
</ul>
</div>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,261 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use wasm_bindgen::JsCast;
use crate::components::accounting::models::*;
#[derive(Properties, PartialEq)]
pub struct FinancialReportsTabProps {
pub state: UseStateHandle<AccountingState>,
}
#[function_component(FinancialReportsTab)]
pub fn financial_reports_tab(props: &FinancialReportsTabProps) -> Html {
let state = &props.state;
let show_report_modal = use_state(|| false);
let report_type = use_state(|| ReportType::ProfitLoss);
let start_date = use_state(|| "".to_string());
let end_date = use_state(|| "".to_string());
let on_generate_report = {
let state = state.clone();
let show_report_modal = show_report_modal.clone();
let report_type = report_type.clone();
let start_date = start_date.clone();
let end_date = end_date.clone();
Callback::from(move |_| {
if start_date.is_empty() || end_date.is_empty() {
web_sys::window()
.unwrap()
.alert_with_message("Please select both start and end dates")
.unwrap();
return;
}
let new_report = FinancialReport {
id: state.financial_reports.len() + 1,
report_type: (*report_type).clone(),
period_start: (*start_date).clone(),
period_end: (*end_date).clone(),
generated_date: js_sys::Date::new_0().to_iso_string().as_string().unwrap()[..10].to_string(),
status: "Generated".to_string(),
};
let mut new_state = (*state).clone();
new_state.financial_reports.push(new_report);
state.set(new_state);
show_report_modal.set(false);
})
};
let on_export_report = {
Callback::from(move |report_id: usize| {
// Create CSV content for the report
let csv_content = format!(
"Financial Report Export\nReport ID: {}\nGenerated: {}\n\nThis is a placeholder for the actual report data.",
report_id,
js_sys::Date::new_0().to_iso_string().as_string().unwrap()
);
// Create and download the file
let blob = web_sys::Blob::new_with_str_sequence(&js_sys::Array::of1(&csv_content.into())).unwrap();
let url = web_sys::Url::create_object_url_with_blob(&blob).unwrap();
let document = web_sys::window().unwrap().document().unwrap();
let a = document.create_element("a").unwrap();
a.set_attribute("href", &url).unwrap();
a.set_attribute("download", &format!("financial_report_{}.csv", report_id)).unwrap();
a.dyn_ref::<web_sys::HtmlElement>().unwrap().click();
web_sys::Url::revoke_object_url(&url).unwrap();
})
};
html! {
<div class="animate-fade-in-up">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">{"Financial Reports"}</h4>
<button
class="btn btn-primary"
onclick={
let show_report_modal = show_report_modal.clone();
Callback::from(move |_| show_report_modal.set(true))
}
>
<i class="bi bi-plus-lg me-2"></i>
{"Generate Report"}
</button>
</div>
<div class="card shadow-soft border-0">
<div class="card-body">
if state.financial_reports.is_empty() {
<div class="text-center py-5">
<i class="bi bi-file-earmark-text display-1 text-muted mb-3"></i>
<h5 class="text-muted">{"No reports generated yet"}</h5>
<p class="text-muted">{"Generate your first financial report to get started"}</p>
</div>
} else {
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>{"Report Type"}</th>
<th>{"Period"}</th>
<th>{"Generated"}</th>
<th>{"Status"}</th>
<th>{"Actions"}</th>
</tr>
</thead>
<tbody>
{for state.financial_reports.iter().map(|report| {
let report_id = report.id;
let on_export = on_export_report.clone();
html! {
<tr>
<td>
<span class="badge bg-primary">{format!("{:?}", report.report_type)}</span>
</td>
<td>{format!("{} to {}", report.period_start, report.period_end)}</td>
<td>{&report.generated_date}</td>
<td>
<span class="badge bg-success">{&report.status}</span>
</td>
<td>
<div class="btn-group btn-group-sm">
<button
class="btn btn-outline-primary"
onclick={
Callback::from(move |_| {
web_sys::window()
.unwrap()
.alert_with_message("Report preview feature coming soon!")
.unwrap();
})
}
>
<i class="bi bi-eye"></i>
</button>
<button
class="btn btn-outline-success"
onclick={move |_| on_export.emit(report_id)}
>
<i class="bi bi-download"></i>
</button>
</div>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
}
</div>
</div>
// Report Generation Modal
if *show_report_modal {
<div class="modal show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Generate Financial Report"}</h5>
<button
type="button"
class="btn-close"
onclick={
let show_report_modal = show_report_modal.clone();
Callback::from(move |_| show_report_modal.set(false))
}
></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">{"Report Type"}</label>
<select
class="form-select"
onchange={
let report_type = report_type.clone();
Callback::from(move |e: Event| {
let target = e.target_dyn_into::<HtmlInputElement>().unwrap();
let value = match target.value().as_str() {
"ProfitLoss" => ReportType::ProfitLoss,
"BalanceSheet" => ReportType::BalanceSheet,
"CashFlow" => ReportType::CashFlow,
"TaxSummary" => ReportType::TaxSummary,
_ => ReportType::ProfitLoss,
};
report_type.set(value);
})
}
>
<option value="ProfitLoss">{"Profit & Loss"}</option>
<option value="BalanceSheet">{"Balance Sheet"}</option>
<option value="CashFlow">{"Cash Flow"}</option>
<option value="TaxSummary">{"Tax Summary"}</option>
</select>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{"Start Date"}</label>
<input
type="date"
class="form-control"
value={(*start_date).clone()}
onchange={
let start_date = start_date.clone();
Callback::from(move |e: Event| {
let target = e.target_dyn_into::<HtmlInputElement>().unwrap();
start_date.set(target.value());
})
}
/>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">{"End Date"}</label>
<input
type="date"
class="form-control"
value={(*end_date).clone()}
onchange={
let end_date = end_date.clone();
Callback::from(move |e: Event| {
let target = e.target_dyn_into::<HtmlInputElement>().unwrap();
end_date.set(target.value());
})
}
/>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
onclick={
let show_report_modal = show_report_modal.clone();
Callback::from(move |_| show_report_modal.set(false))
}
>
{"Cancel"}
</button>
<button
type="button"
class="btn btn-primary"
onclick={on_generate_report}
>
{"Generate Report"}
</button>
</div>
</div>
</div>
</div>
}
</div>
}
}

View File

@ -0,0 +1,13 @@
pub mod models;
pub mod overview_tab;
pub mod revenue_tab;
pub mod expenses_tab;
pub mod tax_tab;
pub mod financial_reports_tab;
pub use models::*;
pub use overview_tab::*;
pub use revenue_tab::*;
pub use expenses_tab::*;
pub use tax_tab::*;
pub use financial_reports_tab::*;

View File

@ -0,0 +1,632 @@
use serde::{Serialize, Deserialize};
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct GeneratedReport {
pub id: String,
pub report_type: ReportType,
pub title: String,
pub date_generated: String,
pub period_start: String,
pub period_end: String,
pub file_url: String,
pub file_size: String,
pub status: ReportStatus,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum ReportType {
ProfitLoss,
BalanceSheet,
CashFlow,
TaxSummary,
ExpenseReport,
RevenueReport,
}
impl ReportType {
pub fn to_string(&self) -> &str {
match self {
ReportType::ProfitLoss => "Profit & Loss",
ReportType::BalanceSheet => "Balance Sheet",
ReportType::CashFlow => "Cash Flow",
ReportType::TaxSummary => "Tax Summary",
ReportType::ExpenseReport => "Expense Report",
ReportType::RevenueReport => "Revenue Report",
}
}
pub fn get_icon(&self) -> &str {
match self {
ReportType::ProfitLoss => "graph-up",
ReportType::BalanceSheet => "pie-chart",
ReportType::CashFlow => "arrow-left-right",
ReportType::TaxSummary => "receipt",
ReportType::ExpenseReport => "graph-down",
ReportType::RevenueReport => "graph-up-arrow",
}
}
pub fn get_color(&self) -> &str {
match self {
ReportType::ProfitLoss => "primary",
ReportType::BalanceSheet => "success",
ReportType::CashFlow => "info",
ReportType::TaxSummary => "warning",
ReportType::ExpenseReport => "danger",
ReportType::RevenueReport => "success",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum ReportStatus {
Generating,
Ready,
Failed,
}
impl ReportStatus {
pub fn to_string(&self) -> &str {
match self {
ReportStatus::Generating => "Generating",
ReportStatus::Ready => "Ready",
ReportStatus::Failed => "Failed",
}
}
pub fn get_color(&self) -> &str {
match self {
ReportStatus::Generating => "warning",
ReportStatus::Ready => "success",
ReportStatus::Failed => "danger",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct FinancialReport {
pub id: usize,
pub report_type: ReportType,
pub period_start: String,
pub period_end: String,
pub generated_date: String,
pub status: String,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct PaymentTransaction {
pub id: String,
pub invoice_id: Option<String>, // For revenue transactions
pub expense_id: Option<String>, // For expense transactions
pub date: String,
pub amount: f64,
pub payment_method: PaymentMethod,
pub transaction_hash: Option<String>, // For crypto payments
pub reference_number: Option<String>, // For bank transfers, checks, etc.
pub notes: String,
pub attached_files: Vec<String>, // File URLs/paths
pub status: TransactionStatus,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum TransactionStatus {
Pending,
Confirmed,
Failed,
Cancelled,
}
impl TransactionStatus {
pub fn to_string(&self) -> &str {
match self {
TransactionStatus::Pending => "Pending",
TransactionStatus::Confirmed => "Confirmed",
TransactionStatus::Failed => "Failed",
TransactionStatus::Cancelled => "Cancelled",
}
}
pub fn get_color(&self) -> &str {
match self {
TransactionStatus::Pending => "warning",
TransactionStatus::Confirmed => "success",
TransactionStatus::Failed => "danger",
TransactionStatus::Cancelled => "secondary",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct RevenueEntry {
pub id: String,
pub date: String,
pub invoice_number: String,
pub client_name: String,
pub client_email: String,
pub client_address: String,
pub description: String,
pub quantity: f64,
pub unit_price: f64,
pub subtotal: f64,
pub tax_rate: f64,
pub tax_amount: f64,
pub total_amount: f64,
pub category: RevenueCategory,
pub payment_method: PaymentMethod,
pub payment_status: PaymentStatus,
pub due_date: String,
pub paid_date: Option<String>,
pub notes: String,
pub recurring: bool,
pub currency: String,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub struct ExpenseEntry {
pub id: String,
pub date: String,
pub receipt_number: String,
pub vendor_name: String,
pub vendor_email: String,
pub vendor_address: String,
pub description: String,
pub amount: f64,
pub tax_amount: f64,
pub total_amount: f64,
pub category: ExpenseCategory,
pub payment_method: PaymentMethod,
pub payment_status: PaymentStatus,
pub is_deductible: bool,
pub receipt_url: Option<String>,
pub approval_status: ApprovalStatus,
pub approved_by: Option<String>,
pub notes: String,
pub project_code: Option<String>,
pub currency: String,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum RevenueCategory {
ProductSales,
ServiceRevenue,
ConsultingFees,
LicensingRoyalties,
SubscriptionRevenue,
InterestIncome,
Other,
}
impl RevenueCategory {
pub fn to_string(&self) -> &str {
match self {
RevenueCategory::ProductSales => "Product Sales",
RevenueCategory::ServiceRevenue => "Service Revenue",
RevenueCategory::ConsultingFees => "Consulting Fees",
RevenueCategory::LicensingRoyalties => "Licensing & Royalties",
RevenueCategory::SubscriptionRevenue => "Subscription Revenue",
RevenueCategory::InterestIncome => "Interest Income",
RevenueCategory::Other => "Other Revenue",
}
}
pub fn get_color(&self) -> &str {
match self {
RevenueCategory::ProductSales => "success",
RevenueCategory::ServiceRevenue => "primary",
RevenueCategory::ConsultingFees => "info",
RevenueCategory::LicensingRoyalties => "warning",
RevenueCategory::SubscriptionRevenue => "secondary",
RevenueCategory::InterestIncome => "dark",
RevenueCategory::Other => "light",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum ExpenseCategory {
OfficeSupplies,
MarketingAdvertising,
TravelExpenses,
SoftwareLicenses,
EquipmentPurchases,
UtilitiesBills,
RentLease,
SalariesWages,
ProfessionalServices,
Insurance,
Telecommunications,
Maintenance,
Training,
Entertainment,
Other,
}
impl ExpenseCategory {
pub fn to_string(&self) -> &str {
match self {
ExpenseCategory::OfficeSupplies => "Office Supplies",
ExpenseCategory::MarketingAdvertising => "Marketing & Advertising",
ExpenseCategory::TravelExpenses => "Travel Expenses",
ExpenseCategory::SoftwareLicenses => "Software Licenses",
ExpenseCategory::EquipmentPurchases => "Equipment Purchases",
ExpenseCategory::UtilitiesBills => "Utilities & Bills",
ExpenseCategory::RentLease => "Rent & Lease",
ExpenseCategory::SalariesWages => "Salaries & Wages",
ExpenseCategory::ProfessionalServices => "Professional Services",
ExpenseCategory::Insurance => "Insurance",
ExpenseCategory::Telecommunications => "Telecommunications",
ExpenseCategory::Maintenance => "Maintenance",
ExpenseCategory::Training => "Training & Development",
ExpenseCategory::Entertainment => "Entertainment",
ExpenseCategory::Other => "Other Expenses",
}
}
pub fn get_color(&self) -> &str {
match self {
ExpenseCategory::OfficeSupplies => "secondary",
ExpenseCategory::MarketingAdvertising => "primary",
ExpenseCategory::TravelExpenses => "info",
ExpenseCategory::SoftwareLicenses => "success",
ExpenseCategory::EquipmentPurchases => "warning",
ExpenseCategory::UtilitiesBills => "dark",
ExpenseCategory::RentLease => "danger",
ExpenseCategory::SalariesWages => "primary",
ExpenseCategory::ProfessionalServices => "info",
ExpenseCategory::Insurance => "secondary",
ExpenseCategory::Telecommunications => "success",
ExpenseCategory::Maintenance => "warning",
ExpenseCategory::Training => "info",
ExpenseCategory::Entertainment => "secondary",
ExpenseCategory::Other => "light",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum PaymentMethod {
BankTransfer,
CreditCard,
DebitCard,
Cash,
Check,
CryptoBitcoin,
CryptoEthereum,
CryptoUSDC,
CryptoOther,
PayPal,
Stripe,
WireTransfer,
Other,
}
impl PaymentMethod {
pub fn to_string(&self) -> &str {
match self {
PaymentMethod::BankTransfer => "Bank Transfer",
PaymentMethod::CreditCard => "Credit Card",
PaymentMethod::DebitCard => "Debit Card",
PaymentMethod::Cash => "Cash",
PaymentMethod::Check => "Check",
PaymentMethod::CryptoBitcoin => "Bitcoin",
PaymentMethod::CryptoEthereum => "Ethereum",
PaymentMethod::CryptoUSDC => "USDC",
PaymentMethod::CryptoOther => "Other Crypto",
PaymentMethod::PayPal => "PayPal",
PaymentMethod::Stripe => "Stripe",
PaymentMethod::WireTransfer => "Wire Transfer",
PaymentMethod::Other => "Other",
}
}
pub fn get_icon(&self) -> &str {
match self {
PaymentMethod::BankTransfer => "bank",
PaymentMethod::CreditCard => "credit-card",
PaymentMethod::DebitCard => "credit-card-2-front",
PaymentMethod::Cash => "cash-stack",
PaymentMethod::Check => "receipt",
PaymentMethod::CryptoBitcoin => "currency-bitcoin",
PaymentMethod::CryptoEthereum => "currency-ethereum",
PaymentMethod::CryptoUSDC => "currency-dollar",
PaymentMethod::CryptoOther => "coin",
PaymentMethod::PayPal => "paypal",
PaymentMethod::Stripe => "stripe",
PaymentMethod::WireTransfer => "arrow-left-right",
PaymentMethod::Other => "question-circle",
}
}
pub fn get_color(&self) -> &str {
match self {
PaymentMethod::BankTransfer => "primary",
PaymentMethod::CreditCard => "success",
PaymentMethod::DebitCard => "info",
PaymentMethod::Cash => "warning",
PaymentMethod::Check => "secondary",
PaymentMethod::CryptoBitcoin => "warning",
PaymentMethod::CryptoEthereum => "info",
PaymentMethod::CryptoUSDC => "success",
PaymentMethod::CryptoOther => "dark",
PaymentMethod::PayPal => "primary",
PaymentMethod::Stripe => "info",
PaymentMethod::WireTransfer => "secondary",
PaymentMethod::Other => "light",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum PaymentStatus {
Pending,
Paid,
Overdue,
PartiallyPaid,
Cancelled,
Refunded,
}
impl PaymentStatus {
pub fn to_string(&self) -> &str {
match self {
PaymentStatus::Pending => "Pending",
PaymentStatus::Paid => "Paid",
PaymentStatus::Overdue => "Overdue",
PaymentStatus::PartiallyPaid => "Partially Paid",
PaymentStatus::Cancelled => "Cancelled",
PaymentStatus::Refunded => "Refunded",
}
}
pub fn get_color(&self) -> &str {
match self {
PaymentStatus::Pending => "warning",
PaymentStatus::Paid => "success",
PaymentStatus::Overdue => "danger",
PaymentStatus::PartiallyPaid => "info",
PaymentStatus::Cancelled => "secondary",
PaymentStatus::Refunded => "dark",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum ApprovalStatus {
Pending,
Approved,
Rejected,
RequiresReview,
}
impl ApprovalStatus {
pub fn to_string(&self) -> &str {
match self {
ApprovalStatus::Pending => "Pending",
ApprovalStatus::Approved => "Approved",
ApprovalStatus::Rejected => "Rejected",
ApprovalStatus::RequiresReview => "Requires Review",
}
}
pub fn get_color(&self) -> &str {
match self {
ApprovalStatus::Pending => "warning",
ApprovalStatus::Approved => "success",
ApprovalStatus::Rejected => "danger",
ApprovalStatus::RequiresReview => "info",
}
}
}
#[derive(Clone, PartialEq)]
pub struct ReportForm {
pub report_type: ReportType,
pub period_start: String,
pub period_end: String,
pub title: String,
}
impl Default for ReportForm {
fn default() -> Self {
Self {
report_type: ReportType::ProfitLoss,
period_start: String::new(),
period_end: String::new(),
title: String::new(),
}
}
}
#[derive(Clone, PartialEq)]
pub struct TransactionForm {
pub invoice_id: Option<String>,
pub expense_id: Option<String>,
pub amount: f64,
pub payment_method: PaymentMethod,
pub transaction_hash: String,
pub reference_number: String,
pub notes: String,
pub attached_files: Vec<String>,
}
impl Default for TransactionForm {
fn default() -> Self {
Self {
invoice_id: None,
expense_id: None,
amount: 0.0,
payment_method: PaymentMethod::BankTransfer,
transaction_hash: String::new(),
reference_number: String::new(),
notes: String::new(),
attached_files: Vec::new(),
}
}
}
#[derive(Clone, PartialEq)]
pub struct AccountingState {
pub revenue_entries: Vec<RevenueEntry>,
pub expense_entries: Vec<ExpenseEntry>,
pub generated_reports: Vec<GeneratedReport>,
pub financial_reports: Vec<FinancialReport>,
pub payment_transactions: Vec<PaymentTransaction>,
pub show_revenue_form: bool,
pub show_expense_form: bool,
pub show_report_form: bool,
pub show_transaction_form: bool,
pub show_invoice_detail: bool,
pub selected_invoice_id: Option<String>,
pub show_expense_detail: bool,
pub selected_expense_id: Option<String>,
pub revenue_form: RevenueEntry,
pub expense_form: ExpenseEntry,
pub report_form: ReportForm,
pub transaction_form: TransactionForm,
pub revenue_filter: String,
pub expense_filter: String,
pub revenue_search: String,
pub expense_search: String,
}
impl Default for AccountingState {
fn default() -> Self {
Self {
revenue_entries: Vec::new(),
expense_entries: Vec::new(),
generated_reports: Vec::new(),
financial_reports: Vec::new(),
payment_transactions: Vec::new(),
show_revenue_form: false,
show_expense_form: false,
show_report_form: false,
show_transaction_form: false,
show_invoice_detail: false,
selected_invoice_id: None,
show_expense_detail: false,
selected_expense_id: None,
revenue_form: RevenueEntry {
id: String::new(),
date: String::new(),
invoice_number: String::new(),
client_name: String::new(),
client_email: String::new(),
client_address: String::new(),
description: String::new(),
quantity: 1.0,
unit_price: 0.0,
subtotal: 0.0,
tax_rate: 0.20,
tax_amount: 0.0,
total_amount: 0.0,
category: RevenueCategory::ServiceRevenue,
payment_method: PaymentMethod::BankTransfer,
payment_status: PaymentStatus::Pending,
due_date: String::new(),
paid_date: None,
notes: String::new(),
recurring: false,
currency: "USD".to_string(),
},
expense_form: ExpenseEntry {
id: String::new(),
date: String::new(),
receipt_number: String::new(),
vendor_name: String::new(),
vendor_email: String::new(),
vendor_address: String::new(),
description: String::new(),
amount: 0.0,
tax_amount: 0.0,
total_amount: 0.0,
category: ExpenseCategory::OfficeSupplies,
payment_method: PaymentMethod::BankTransfer,
payment_status: PaymentStatus::Pending,
is_deductible: true,
receipt_url: None,
approval_status: ApprovalStatus::Pending,
approved_by: None,
notes: String::new(),
project_code: None,
currency: "USD".to_string(),
},
report_form: ReportForm::default(),
transaction_form: TransactionForm::default(),
revenue_filter: String::new(),
expense_filter: String::new(),
revenue_search: String::new(),
expense_search: String::new(),
}
}
}
#[derive(Clone, PartialEq)]
pub enum AccountingMsg {
// Revenue actions
CreateInvoice,
EditRevenue(String),
DeleteRevenue(String),
ViewRevenue(String),
PrintInvoice(String),
SendReminder(String),
DuplicateRevenue(String),
// Transaction actions
RecordTransaction(String), // invoice_id
ShowTransactionForm(String), // invoice_id
HideTransactionForm,
UpdateTransactionForm(String, String), // field, value
SubmitTransactionForm,
ViewTransaction(String),
DeleteTransaction(String),
// Expense actions
AddExpense,
EditExpense(String),
DeleteExpense(String),
ViewExpense(String),
ViewReceipt(String),
ApproveExpense(String),
DuplicateExpense(String),
// Filter and search
FilterRevenue(String),
FilterExpense(String),
SearchRevenue(String),
SearchExpense(String),
// Export actions
ExportRevenue,
ExportExpense,
// Tax actions
GenerateTaxReport,
OpenTaxCalculator,
ExportForAccountant,
// Financial reports
GenerateProfitLoss,
GenerateBalanceSheet,
GenerateCashFlow,
// Report generation actions
ShowReportForm,
HideReportForm,
UpdateReportForm(String, String), // field, value
SubmitReportForm,
DownloadReport(String),
DeleteReport(String),
// Form actions
ShowRevenueForm,
ShowExpenseForm,
HideForm,
UpdateRevenueForm(String, String), // field, value
UpdateExpenseForm(String, String), // field, value
SubmitRevenueForm,
SubmitExpenseForm,
// Invoice detail view
ShowInvoiceDetail(String), // invoice_id
HideInvoiceDetail,
}

View File

@ -0,0 +1,207 @@
use yew::prelude::*;
use crate::components::accounting::models::*;
#[derive(Properties, PartialEq)]
pub struct OverviewTabProps {
pub state: UseStateHandle<AccountingState>,
}
#[function_component(OverviewTab)]
pub fn overview_tab(props: &OverviewTabProps) -> Html {
let state = &props.state;
// Calculate totals
let total_revenue: f64 = state.revenue_entries.iter().map(|r| r.total_amount).sum();
let total_expenses: f64 = state.expense_entries.iter().map(|e| e.total_amount).sum();
let net_profit = total_revenue - total_expenses;
let pending_revenue: f64 = state.revenue_entries.iter()
.filter(|r| r.payment_status == PaymentStatus::Pending)
.map(|r| r.total_amount)
.sum();
let pending_expenses: f64 = state.expense_entries.iter()
.filter(|e| e.payment_status == PaymentStatus::Pending)
.map(|e| e.total_amount)
.sum();
html! {
<div class="animate-fade-in-up">
// Key Statistics Cards
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card border-warning shadow-soft card-hover">
<div class="card-body p-4">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="text-warning mb-1">{"Pending Items"}</h6>
<h3 class="mb-0 fw-bold text-dark">{format!("${:.2}", pending_revenue + pending_expenses)}</h3>
</div>
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
<i class="bi bi-clock text-warning fs-4"></i>
</div>
</div>
<div class="mt-3">
<small class="text-muted">{format!("${:.2} revenue, ${:.2} expenses", pending_revenue, pending_expenses)}</small>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-info shadow-soft card-hover">
<div class="card-body p-4">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="text-info mb-1">{"Avg Invoice Value"}</h6>
<h3 class="mb-0 fw-bold text-dark">{format!("${:.2}", total_revenue / state.revenue_entries.len() as f64)}</h3>
</div>
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="bi bi-receipt text-info fs-4"></i>
</div>
</div>
<div class="mt-3">
<small class="text-muted">{format!("{} invoices total", state.revenue_entries.len())}</small>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-success shadow-soft card-hover">
<div class="card-body p-4">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="text-success mb-1">{"Tax Deductible"}</h6>
<h3 class="mb-0 fw-bold text-dark">{format!("${:.2}", state.expense_entries.iter().filter(|e| e.is_deductible).map(|e| e.total_amount).sum::<f64>())}</h3>
</div>
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="bi bi-receipt-cutoff text-success fs-4"></i>
</div>
</div>
<div class="mt-3">
<small class="text-muted">{"100% of expenses deductible"}</small>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card border-primary shadow-soft card-hover">
<div class="card-body p-4">
<div class="d-flex align-items-center justify-content-between">
<div>
<h6 class="text-primary mb-1">{"Collection Rate"}</h6>
<h3 class="mb-0 fw-bold text-dark">{"85.2%"}</h3>
</div>
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="bi bi-percent text-primary fs-4"></i>
</div>
</div>
<div class="mt-3">
<small class="text-muted">{"Above industry avg"}</small>
</div>
</div>
</div>
</div>
</div>
// Recent Transactions
<div class="row g-4">
<div class="col-12">
<div class="card shadow-soft border-0">
<div class="card-header bg-white border-bottom-0 py-3">
<h5 class="mb-0 fw-bold">{"Recent Transactions"}</h5>
<small class="text-muted">{"Latest payments made and received"}</small>
</div>
<div class="card-body p-0">
{if state.payment_transactions.is_empty() {
html! {
<div class="text-center py-5">
<i class="bi bi-credit-card fs-1 text-muted mb-3 d-block"></i>
<h6 class="text-muted">{"No transactions recorded yet"}</h6>
<p class="text-muted mb-0">{"Transactions will appear here once you record payments"}</p>
</div>
}
} else {
html! {
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 py-3 px-4">{"Date"}</th>
<th class="border-0 py-3">{"Type"}</th>
<th class="border-0 py-3">{"Reference"}</th>
<th class="border-0 py-3">{"Amount"}</th>
<th class="border-0 py-3">{"Method"}</th>
<th class="border-0 py-3">{"Status"}</th>
<th class="border-0 py-3">{"Notes"}</th>
</tr>
</thead>
<tbody>
{for state.payment_transactions.iter().take(10).map(|transaction| {
let (transaction_type, reference, amount_color) = if let Some(invoice_id) = &transaction.invoice_id {
("Revenue", invoice_id.clone(), "text-success")
} else if let Some(expense_id) = &transaction.expense_id {
("Expense", expense_id.clone(), "text-danger")
} else {
("Unknown", "N/A".to_string(), "text-muted")
};
html! {
<tr>
<td class="py-3 px-4">
<div class="fw-semibold">{&transaction.date}</div>
<small class="text-muted">{&transaction.id}</small>
</td>
<td class="py-3">
<span class={format!("badge bg-{} bg-opacity-10 text-{}",
if transaction_type == "Revenue" { "success" } else { "danger" },
if transaction_type == "Revenue" { "success" } else { "danger" }
)}>
{transaction_type}
</span>
</td>
<td class="py-3">
<div class="fw-semibold">{&reference}</div>
{if let Some(hash) = &transaction.transaction_hash {
html! { <small class="text-muted"><code>{&hash[..12]}{"..."}</code></small> }
} else if let Some(ref_num) = &transaction.reference_number {
html! { <small class="text-muted">{ref_num}</small> }
} else {
html! {}
}}
</td>
<td class="py-3">
<div class={format!("fw-bold {}", amount_color)}>
{if transaction_type == "Revenue" { "+" } else { "-" }}
{format!("${:.2}", transaction.amount)}
</div>
</td>
<td class="py-3">
<div class="d-flex align-items-center">
<i class={format!("bi bi-{} text-{} me-2", transaction.payment_method.get_icon(), transaction.payment_method.get_color())}></i>
<span class="small">{transaction.payment_method.to_string()}</span>
</div>
</td>
<td class="py-3">
<span class={format!("badge bg-{}", transaction.status.get_color())}>
{transaction.status.to_string()}
</span>
</td>
<td class="py-3">
<span class="text-muted">{&transaction.notes}</span>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
}
}}
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,724 @@
use yew::prelude::*;
use wasm_bindgen::JsCast;
use crate::components::accounting::models::*;
use js_sys;
#[derive(Properties, PartialEq)]
pub struct RevenueTabProps {
pub state: UseStateHandle<AccountingState>,
}
#[function_component(RevenueTab)]
pub fn revenue_tab(props: &RevenueTabProps) -> Html {
let state = &props.state;
html! {
<div class="animate-fade-in-up">
// Revenue Form Modal
{if state.show_revenue_form {
html! {
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Create New Invoice"}</h5>
<button type="button" class="btn-close" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_revenue_form = false;
state.set(new_state);
})
}></button>
</div>
<div class="modal-body">
<form>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">{"Invoice Number"}</label>
<input type="text" class="form-control" value={state.revenue_form.invoice_number.clone()} readonly=true />
</div>
<div class="col-md-6">
<label class="form-label">{"Date"}</label>
<input type="date" class="form-control" value={state.revenue_form.date.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.date = input.value();
state.set(new_state);
})
} />
</div>
<div class="col-md-6">
<label class="form-label">{"Client Name"}</label>
<input type="text" class="form-control" value={state.revenue_form.client_name.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.client_name = input.value();
state.set(new_state);
})
} />
</div>
<div class="col-md-6">
<label class="form-label">{"Client Email"}</label>
<input type="email" class="form-control" value={state.revenue_form.client_email.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.client_email = input.value();
state.set(new_state);
})
} />
</div>
<div class="col-12">
<label class="form-label">{"Description"}</label>
<textarea class="form-control" rows="3" value={state.revenue_form.description.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.description = input.value();
state.set(new_state);
})
}></textarea>
</div>
<div class="col-md-4">
<label class="form-label">{"Quantity"}</label>
<input type="number" class="form-control" value={state.revenue_form.quantity.to_string()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.quantity = input.value().parse().unwrap_or(1.0);
state.set(new_state);
})
} />
</div>
<div class="col-md-4">
<label class="form-label">{"Unit Price"}</label>
<input type="number" step="0.01" class="form-control" value={state.revenue_form.unit_price.to_string()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.unit_price = input.value().parse().unwrap_or(0.0);
state.set(new_state);
})
} />
</div>
<div class="col-md-4">
<label class="form-label">{"Due Date"}</label>
<input type="date" class="form-control" value={state.revenue_form.due_date.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.revenue_form.due_date = input.value();
state.set(new_state);
})
} />
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_revenue_form = false;
state.set(new_state);
})
}>{"Cancel"}</button>
<button type="button" class="btn btn-success" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
// Calculate totals
new_state.revenue_form.subtotal = new_state.revenue_form.quantity * new_state.revenue_form.unit_price;
new_state.revenue_form.tax_amount = new_state.revenue_form.subtotal * new_state.revenue_form.tax_rate;
new_state.revenue_form.total_amount = new_state.revenue_form.subtotal + new_state.revenue_form.tax_amount;
// Add to entries
new_state.revenue_entries.push(new_state.revenue_form.clone());
// Reset form
new_state.show_revenue_form = false;
new_state.revenue_form = AccountingState::default().revenue_form;
state.set(new_state);
})
}>{"Create Invoice"}</button>
</div>
</div>
</div>
</div>
}
} else {
html! {}
}}
// Transaction Form Modal
{if state.show_transaction_form {
html! {
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Record Payment Transaction"}</h5>
<button type="button" class="btn-close" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = false;
state.set(new_state);
})
}></button>
</div>
<div class="modal-body">
<form>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">{"Invoice Number"}</label>
<input type="text" class="form-control" value={state.transaction_form.invoice_id.clone().unwrap_or_default()} readonly=true />
</div>
<div class="col-md-6">
<label class="form-label">{"Payment Amount"}</label>
<input type="number" step="0.01" class="form-control" value={state.transaction_form.amount.to_string()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.amount = input.value().parse().unwrap_or(0.0);
state.set(new_state);
})
} />
</div>
<div class="col-12">
<label class="form-label">{"Payment Method"}</label>
<select class="form-select" onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let select: web_sys::HtmlSelectElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.payment_method = match select.value().as_str() {
"BankTransfer" => PaymentMethod::BankTransfer,
"CreditCard" => PaymentMethod::CreditCard,
"CryptoBitcoin" => PaymentMethod::CryptoBitcoin,
"CryptoEthereum" => PaymentMethod::CryptoEthereum,
"CryptoUSDC" => PaymentMethod::CryptoUSDC,
"Cash" => PaymentMethod::Cash,
"Check" => PaymentMethod::Check,
_ => PaymentMethod::BankTransfer,
};
state.set(new_state);
})
}>
<option value="BankTransfer" selected={matches!(state.transaction_form.payment_method, PaymentMethod::BankTransfer)}>{"Bank Transfer"}</option>
<option value="CreditCard" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CreditCard)}>{"Credit Card"}</option>
<option value="CryptoBitcoin" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoBitcoin)}>{"Bitcoin"}</option>
<option value="CryptoEthereum" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoEthereum)}>{"Ethereum"}</option>
<option value="CryptoUSDC" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoUSDC)}>{"USDC"}</option>
<option value="Cash" selected={matches!(state.transaction_form.payment_method, PaymentMethod::Cash)}>{"Cash"}</option>
<option value="Check" selected={matches!(state.transaction_form.payment_method, PaymentMethod::Check)}>{"Check"}</option>
</select>
</div>
{if matches!(state.transaction_form.payment_method, PaymentMethod::CryptoBitcoin | PaymentMethod::CryptoEthereum | PaymentMethod::CryptoUSDC | PaymentMethod::CryptoOther) {
html! {
<div class="col-12">
<label class="form-label">{"Transaction Hash"}</label>
<input type="text" class="form-control" placeholder="0x..." value={state.transaction_form.transaction_hash.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.transaction_hash = input.value();
state.set(new_state);
})
} />
</div>
}
} else {
html! {
<div class="col-12">
<label class="form-label">{"Reference Number"}</label>
<input type="text" class="form-control" placeholder="REF-2024-001" value={state.transaction_form.reference_number.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.reference_number = input.value();
state.set(new_state);
})
} />
</div>
}
}}
<div class="col-12">
<label class="form-label">{"Notes"}</label>
<textarea class="form-control" rows="3" value={state.transaction_form.notes.clone()} onchange={
let state = state.clone();
Callback::from(move |e: Event| {
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
let mut new_state = (*state).clone();
new_state.transaction_form.notes = input.value();
state.set(new_state);
})
}></textarea>
</div>
<div class="col-12">
<label class="form-label">{"Attach Files"}</label>
<input type="file" class="form-control" multiple=true accept=".pdf,.jpg,.jpeg,.png" />
<small class="text-muted">{"Upload receipts, confirmations, or other supporting documents"}</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = false;
state.set(new_state);
})
}>{"Cancel"}</button>
<button type="button" class="btn btn-success" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
// Create new transaction
let transaction_count = new_state.payment_transactions.len() + 1;
let new_transaction = PaymentTransaction {
id: format!("TXN-2024-{:03}", transaction_count),
invoice_id: new_state.transaction_form.invoice_id.clone(),
expense_id: None,
date: js_sys::Date::new_0().to_iso_string().as_string().unwrap()[..10].to_string(),
amount: new_state.transaction_form.amount,
payment_method: new_state.transaction_form.payment_method.clone(),
transaction_hash: if new_state.transaction_form.transaction_hash.is_empty() { None } else { Some(new_state.transaction_form.transaction_hash.clone()) },
reference_number: if new_state.transaction_form.reference_number.is_empty() { None } else { Some(new_state.transaction_form.reference_number.clone()) },
notes: new_state.transaction_form.notes.clone(),
attached_files: new_state.transaction_form.attached_files.clone(),
status: TransactionStatus::Confirmed,
};
new_state.payment_transactions.push(new_transaction);
new_state.show_transaction_form = false;
new_state.transaction_form = TransactionForm::default();
state.set(new_state);
})
}>{"Record Transaction"}</button>
</div>
</div>
</div>
</div>
}
} else {
html! {}
}}
// Invoice Detail Modal
{if state.show_invoice_detail {
if let Some(invoice_id) = &state.selected_invoice_id {
if let Some(invoice) = state.revenue_entries.iter().find(|r| &r.id == invoice_id) {
let invoice_transactions: Vec<&PaymentTransaction> = state.payment_transactions.iter()
.filter(|t| t.invoice_id.as_ref() == Some(invoice_id))
.collect();
let total_paid: f64 = invoice_transactions.iter().map(|t| t.amount).sum();
let remaining_balance = invoice.total_amount - total_paid;
html! {
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{format!("Invoice Details - {}", invoice.invoice_number)}</h5>
<button type="button" class="btn-close" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_invoice_detail = false;
new_state.selected_invoice_id = None;
state.set(new_state);
})
}></button>
</div>
<div class="modal-body">
<div class="row g-4">
// Invoice Information
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{"Invoice Information"}</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-6"><strong>{"Invoice #:"}</strong></div>
<div class="col-6">{&invoice.invoice_number}</div>
<div class="col-6"><strong>{"Date:"}</strong></div>
<div class="col-6">{&invoice.date}</div>
<div class="col-6"><strong>{"Due Date:"}</strong></div>
<div class="col-6">{&invoice.due_date}</div>
<div class="col-6"><strong>{"Status:"}</strong></div>
<div class="col-6">
<span class={format!("badge bg-{}", invoice.payment_status.get_color())}>
{invoice.payment_status.to_string()}
</span>
</div>
<div class="col-6"><strong>{"Total Amount:"}</strong></div>
<div class="col-6 fw-bold text-success">{format!("${:.2}", invoice.total_amount)}</div>
<div class="col-6"><strong>{"Amount Paid:"}</strong></div>
<div class="col-6 fw-bold text-primary">{format!("${:.2}", total_paid)}</div>
<div class="col-6"><strong>{"Remaining:"}</strong></div>
<div class="col-6 fw-bold text-danger">{format!("${:.2}", remaining_balance)}</div>
</div>
</div>
</div>
</div>
// Client Information
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h6 class="mb-0">{"Client Information"}</h6>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-4"><strong>{"Name:"}</strong></div>
<div class="col-8">{&invoice.client_name}</div>
<div class="col-4"><strong>{"Email:"}</strong></div>
<div class="col-8">{&invoice.client_email}</div>
<div class="col-4"><strong>{"Address:"}</strong></div>
<div class="col-8">{&invoice.client_address}</div>
</div>
</div>
</div>
</div>
// Payment Transactions
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{"Payment Transactions"}</h6>
<button class="btn btn-sm btn-primary" onclick={
let state = state.clone();
let invoice_id = invoice.id.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = true;
new_state.transaction_form.invoice_id = Some(invoice_id.clone());
state.set(new_state);
})
}>
<i class="bi bi-plus-circle me-1"></i>{"Record Payment"}
</button>
</div>
<div class="card-body p-0">
{if invoice_transactions.is_empty() {
html! {
<div class="text-center py-4 text-muted">
<i class="bi bi-credit-card fs-1 mb-2 d-block"></i>
<p class="mb-0">{"No payments recorded yet"}</p>
</div>
}
} else {
html! {
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 py-3">{"Date"}</th>
<th class="border-0 py-3">{"Amount"}</th>
<th class="border-0 py-3">{"Method"}</th>
<th class="border-0 py-3">{"Reference"}</th>
<th class="border-0 py-3">{"Status"}</th>
<th class="border-0 py-3">{"Notes"}</th>
</tr>
</thead>
<tbody>
{for invoice_transactions.iter().map(|transaction| {
html! {
<tr>
<td class="py-3">{&transaction.date}</td>
<td class="py-3 fw-bold text-success">{format!("${:.2}", transaction.amount)}</td>
<td class="py-3">
<div class="d-flex align-items-center">
<i class={format!("bi bi-{} text-{} me-2", transaction.payment_method.get_icon(), transaction.payment_method.get_color())}></i>
{transaction.payment_method.to_string()}
</div>
</td>
<td class="py-3">
{if let Some(hash) = &transaction.transaction_hash {
html! { <code class="small">{&hash[..12]}{"..."}</code> }
} else if let Some(ref_num) = &transaction.reference_number {
html! { <span>{ref_num}</span> }
} else {
html! { <span class="text-muted">{"-"}</span> }
}}
</td>
<td class="py-3">
<span class={format!("badge bg-{}", transaction.status.get_color())}>
{transaction.status.to_string()}
</span>
</td>
<td class="py-3">{&transaction.notes}</td>
</tr>
}
})}
</tbody>
</table>
</div>
}
}}
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_invoice_detail = false;
new_state.selected_invoice_id = None;
state.set(new_state);
})
}>{"Close"}</button>
<button type="button" class="btn btn-primary" onclick={
let state = state.clone();
let invoice_id = invoice.id.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_transaction_form = true;
new_state.transaction_form.invoice_id = Some(invoice_id.clone());
state.set(new_state);
})
}>
<i class="bi bi-credit-card me-2"></i>{"Record Payment"}
</button>
</div>
</div>
</div>
</div>
}
} else {
html! {}
}
} else {
html! {}
}
} else {
html! {}
}}
// Revenue Actions and Table
<div class="row g-4">
<div class="col-12">
<div class="card shadow-soft border-0">
<div class="card-header bg-white border-bottom-0 py-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0 fw-bold">{"Revenue Entries"}</h5>
<small class="text-muted">{"Click on any row to view details"}</small>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary btn-sm" onclick={
Callback::from(move |_| {
web_sys::window()
.unwrap()
.alert_with_message("Revenue filter feature coming soon!")
.unwrap();
})
}>
<i class="bi bi-funnel me-2"></i>{"Filter"}
</button>
<button class="btn btn-outline-secondary btn-sm" onclick={
let revenue_entries = state.revenue_entries.clone();
Callback::from(move |_| {
// Create CSV content
let mut csv_content = "Invoice Number,Date,Client Name,Client Email,Description,Quantity,Unit Price,Subtotal,Tax Amount,Total Amount,Category,Payment Method,Payment Status,Due Date,Paid Date,Notes,Recurring,Currency\n".to_string();
for entry in &revenue_entries {
csv_content.push_str(&format!(
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
entry.invoice_number,
entry.date,
entry.client_name,
entry.client_email,
entry.description.replace(",", ";"),
entry.quantity,
entry.unit_price,
entry.subtotal,
entry.tax_amount,
entry.total_amount,
entry.category.to_string(),
entry.payment_method.to_string(),
entry.payment_status.to_string(),
entry.due_date,
entry.paid_date.as_ref().unwrap_or(&"".to_string()),
entry.notes.replace(",", ";"),
entry.recurring,
entry.currency
));
}
// Create and download file
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let element = document.create_element("a").unwrap();
element.set_attribute("href", &format!("data:text/csv;charset=utf-8,{}", js_sys::encode_uri_component(&csv_content))).unwrap();
element.set_attribute("download", "revenue_export.csv").unwrap();
element.set_attribute("style", "display: none").unwrap();
document.body().unwrap().append_child(&element).unwrap();
let html_element: web_sys::HtmlElement = element.clone().dyn_into().unwrap();
html_element.click();
document.body().unwrap().remove_child(&element).unwrap();
})
}>
<i class="bi bi-download me-2"></i>{"Export"}
</button>
<button class="btn btn-success btn-sm" onclick={
let state = state.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_revenue_form = true;
let invoice_count = new_state.revenue_entries.len() + 1;
new_state.revenue_form.invoice_number = format!("INV-2024-{:03}", invoice_count);
new_state.revenue_form.id = new_state.revenue_form.invoice_number.clone();
state.set(new_state);
})
}>
<i class="bi bi-plus-circle me-2"></i>{"New Invoice"}
</button>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="bg-light">
<tr>
<th class="border-0 py-3 px-4">{"Invoice #"}</th>
<th class="border-0 py-3">{"Client"}</th>
<th class="border-0 py-3">{"Description"}</th>
<th class="border-0 py-3">{"Amount"}</th>
<th class="border-0 py-3">{"Payment Method"}</th>
<th class="border-0 py-3">{"Status"}</th>
<th class="border-0 py-3">{"Due Date"}</th>
<th class="border-0 py-3">{"Actions"}</th>
</tr>
</thead>
<tbody>
{for state.revenue_entries.iter().map(|entry| {
html! {
<tr class="border-bottom">
<td class="py-3 px-4 cursor-pointer" style="cursor: pointer;" onclick={
let state = state.clone();
let invoice_id = entry.id.clone();
Callback::from(move |_| {
let mut new_state = (*state).clone();
new_state.show_invoice_detail = true;
new_state.selected_invoice_id = Some(invoice_id.clone());
state.set(new_state);
})
}>
<div class="fw-bold text-primary">{&entry.invoice_number}</div>
<small class="text-muted">{&entry.date}</small>
</td>
<td class="py-3">
<div class="fw-semibold">{&entry.client_name}</div>
<small class="text-muted">{&entry.client_email}</small>
</td>
<td class="py-3">
<div class="fw-semibold">{&entry.description}</div>
<small class="text-muted">
<span class={format!("badge bg-{} bg-opacity-10 text-{} me-1", entry.category.get_color(), entry.category.get_color())}>
{entry.category.to_string()}
</span>
{if entry.recurring { "• Recurring" } else { "" }}
</small>
</td>
<td class="py-3">
<div class="fw-bold text-success">{format!("${:.2}", entry.total_amount)}</div>
<small class="text-muted">{format!("${:.2} + ${:.2} tax", entry.subtotal, entry.tax_amount)}</small>
</td>
<td class="py-3">
<div class="d-flex align-items-center">
<i class={format!("bi bi-{} text-{} me-2", entry.payment_method.get_icon(), entry.payment_method.get_color())}></i>
<span class="small">{entry.payment_method.to_string()}</span>
</div>
</td>
<td class="py-3">
<span class={format!("badge bg-{} bg-opacity-10 text-{}", entry.payment_status.get_color(), entry.payment_status.get_color())}>
{entry.payment_status.to_string()}
</span>
</td>
<td class="py-3">
<div class="fw-semibold">{&entry.due_date}</div>
{
if let Some(paid_date) = &entry.paid_date {
html! { <small class="text-success">{format!("Paid: {}", paid_date)}</small> }
} else {
html! {}
}
}
</td>
<td class="py-3">
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick={
let state = state.clone();
let invoice_id = entry.id.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
let mut new_state = (*state).clone();
new_state.show_invoice_detail = true;
new_state.selected_invoice_id = Some(invoice_id.clone());
state.set(new_state);
})
}><i class="bi bi-eye me-2"></i>{"View Details"}</a></li>
<li><a class="dropdown-item" href="#" onclick={
let state = state.clone();
let invoice_id = entry.id.clone();
Callback::from(move |e: web_sys::MouseEvent| {
e.prevent_default();
e.stop_propagation();
let mut new_state = (*state).clone();
new_state.show_transaction_form = true;
new_state.transaction_form.invoice_id = Some(invoice_id.clone());
state.set(new_state);
})
}><i class="bi bi-credit-card me-2"></i>{"Record Transaction"}</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-pencil me-2"></i>{"Edit"}</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-printer me-2"></i>{"Print Invoice"}</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-envelope me-2"></i>{"Send Reminder"}</a></li>
<li><hr class="dropdown-divider" /></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-files me-2"></i>{"Duplicate"}</a></li>
<li><a class="dropdown-item text-danger" href="#"><i class="bi bi-trash me-2"></i>{"Delete"}</a></li>
</ul>
</div>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,111 @@
use yew::prelude::*;
use crate::components::accounting::models::*;
#[derive(Properties, PartialEq)]
pub struct TaxTabProps {
pub state: UseStateHandle<AccountingState>,
}
#[function_component(TaxTab)]
pub fn tax_tab(props: &TaxTabProps) -> Html {
let state = &props.state;
// Calculate totals
let total_revenue: f64 = state.revenue_entries.iter().map(|r| r.total_amount).sum();
let total_expenses: f64 = state.expense_entries.iter().map(|e| e.total_amount).sum();
html! {
<div class="animate-fade-in-up">
<div class="alert alert-info" role="alert">
<i class="bi bi-info-circle me-2"></i>
{"Tax calculations are automatically updated based on your revenue and expense entries. Consult with a tax professional for accurate filing."}
</div>
<div class="row g-4">
<div class="col-lg-8">
<div class="card shadow-soft border-0">
<div class="card-header bg-white border-bottom-0 py-3">
<h5 class="mb-0 fw-bold">{"Tax Summary"}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div class="border rounded p-3">
<h6 class="text-muted mb-3">{"Revenue Summary"}</h6>
<div class="d-flex justify-content-between mb-2">
<span>{"Gross Revenue"}</span>
<span class="fw-bold text-success">{format!("${:.2}", total_revenue)}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>{"VAT Collected"}</span>
<span class="fw-bold">{format!("${:.2}", state.revenue_entries.iter().map(|r| r.tax_amount).sum::<f64>())}</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="border rounded p-3">
<h6 class="text-muted mb-3">{"Expense Summary"}</h6>
<div class="d-flex justify-content-between mb-2">
<span>{"Total Expenses"}</span>
<span class="fw-bold text-danger">{format!("${:.2}", total_expenses)}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>{"VAT Paid"}</span>
<span class="fw-bold">{format!("${:.2}", state.expense_entries.iter().map(|e| e.tax_amount).sum::<f64>())}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card shadow-soft border-0">
<div class="card-header bg-white border-bottom-0 py-3">
<h5 class="mb-0 fw-bold">{"Tax Actions"}</h5>
</div>
<div class="card-body">
<div class="d-grid gap-3">
<button class="btn btn-primary" onclick={
Callback::from(move |_| {
web_sys::window()
.unwrap()
.alert_with_message("Tax report generation feature coming soon!")
.unwrap();
})
}>
<i class="bi bi-file-earmark-pdf me-2"></i>
{"Generate Tax Report"}
</button>
<button class="btn btn-outline-primary" onclick={
Callback::from(move |_| {
web_sys::window()
.unwrap()
.alert_with_message("Tax calculator feature coming soon!")
.unwrap();
})
}>
<i class="bi bi-calculator me-2"></i>
{"Tax Calculator"}
</button>
<button class="btn btn-outline-secondary" onclick={
Callback::from(move |_| {
web_sys::window()
.unwrap()
.alert_with_message("Export for accountant feature coming soon!")
.unwrap();
})
}>
<i class="bi bi-download me-2"></i>
{"Export for Accountant"}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,31 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct FeatureCardProps {
pub title: String,
pub description: String,
pub icon: String,
pub color_variant: String, // "primary", "success", "info", "warning", "danger"
}
#[function_component(FeatureCard)]
pub fn feature_card(props: &FeatureCardProps) -> Html {
let header_class = format!("card-header py-2 bg-{} bg-opacity-10 border-{}",
props.color_variant, props.color_variant);
let title_class = format!("mb-0 text-{}", props.color_variant);
let icon_class = format!("bi {} me-2", props.icon);
html! {
<div class="card shadow mb-3" style={format!("border-color: var(--bs-{});", props.color_variant)}>
<div class={header_class}>
<h6 class={title_class}>
<i class={icon_class}></i>
{&props.title}
</h6>
</div>
<div class="card-body p-2 compact-card">
<p class="card-text small">{&props.description}</p>
</div>
</div>
}
}

View File

@ -0,0 +1,3 @@
pub mod feature_card;
pub use feature_card::*;

View File

@ -0,0 +1,3 @@
pub mod multi_step_form;
pub use multi_step_form::*;

View File

@ -0,0 +1,294 @@
use yew::prelude::*;
use gloo::timers::callback::Timeout;
use web_sys::console;
#[derive(Properties, PartialEq)]
pub struct MultiStepFormProps<T: Clone + PartialEq + 'static> {
pub form_data: T,
pub current_step: u8,
pub total_steps: u8,
pub step_titles: Vec<String>,
pub step_descriptions: Vec<String>,
pub step_icons: Vec<String>,
pub on_form_update: Callback<T>,
pub on_step_change: Callback<u8>,
pub on_validation_request: Callback<(u8, Callback<ValidationResult>)>,
pub on_back_to_parent: Callback<()>,
pub validation_errors: Vec<String>,
pub show_validation_toast: bool,
pub children: Children,
#[prop_or_default]
pub custom_footer: Option<Html>,
#[prop_or_default]
pub disable_navigation: bool,
}
#[derive(Clone, PartialEq)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<String>,
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
}
}
pub fn invalid(errors: Vec<String>) -> Self {
Self {
is_valid: false,
errors,
}
}
}
pub enum MultiStepFormMsg {
NextStep,
PrevStep,
SetStep(u8),
ValidationResult(ValidationResult),
HideValidationToast,
}
pub struct MultiStepForm<T: Clone + PartialEq + 'static> {
_phantom: std::marker::PhantomData<T>,
}
impl<T: Clone + PartialEq + 'static> Component for MultiStepForm<T> {
type Message = MultiStepFormMsg;
type Properties = MultiStepFormProps<T>;
fn create(_ctx: &Context<Self>) -> Self {
Self {
_phantom: std::marker::PhantomData,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
MultiStepFormMsg::NextStep => {
let current_step = ctx.props().current_step;
let total_steps = ctx.props().total_steps;
if current_step < total_steps {
// Request validation for current step
let validation_callback = ctx.link().callback(MultiStepFormMsg::ValidationResult);
ctx.props().on_validation_request.emit((current_step, validation_callback));
}
false
}
MultiStepFormMsg::PrevStep => {
let current_step = ctx.props().current_step;
if current_step > 1 {
ctx.props().on_step_change.emit(current_step - 1);
}
false
}
MultiStepFormMsg::SetStep(step) => {
if step >= 1 && step <= ctx.props().total_steps {
ctx.props().on_step_change.emit(step);
}
false
}
MultiStepFormMsg::ValidationResult(result) => {
if result.is_valid {
let current_step = ctx.props().current_step;
let total_steps = ctx.props().total_steps;
if current_step < total_steps {
ctx.props().on_step_change.emit(current_step + 1);
}
}
false
}
MultiStepFormMsg::HideValidationToast => {
// This will be handled by parent component
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let props = ctx.props();
let current_step = props.current_step;
let total_steps = props.total_steps;
let (step_title, step_description, step_icon) = self.get_step_info(ctx);
html! {
<div class="card" style="height: calc(100vh - 200px); display: flex; flex-direction: column;">
<div class="card-header flex-shrink-0">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h5 class="mb-1">
<i class={format!("bi {} me-2", step_icon)}></i>{step_title}
</h5>
<p class="text-muted mb-0 small">{step_description}</p>
</div>
<button
class="btn btn-outline-secondary btn-sm ms-3"
onclick={props.on_back_to_parent.reform(|_| ())}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
</div>
<div class="card-body flex-grow-1 overflow-auto">
<form>
{for props.children.iter()}
</form>
</div>
{if let Some(custom_footer) = &props.custom_footer {
custom_footer.clone()
} else {
self.render_default_footer(ctx)
}}
{if props.show_validation_toast {
self.render_validation_toast(ctx)
} else {
html! {}
}}
</div>
}
}
}
impl<T: Clone + PartialEq + 'static> MultiStepForm<T> {
fn get_step_info(&self, ctx: &Context<Self>) -> (String, String, String) {
let props = ctx.props();
let current_step = props.current_step as usize;
let title = props.step_titles.get(current_step.saturating_sub(1))
.cloned()
.unwrap_or_else(|| format!("Step {}", current_step));
let description = props.step_descriptions.get(current_step.saturating_sub(1))
.cloned()
.unwrap_or_else(|| "Complete this step to continue.".to_string());
let icon = props.step_icons.get(current_step.saturating_sub(1))
.cloned()
.unwrap_or_else(|| "bi-circle".to_string());
(title, description, icon)
}
fn render_default_footer(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let props = ctx.props();
let current_step = props.current_step;
let total_steps = props.total_steps;
html! {
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
// Previous button (left)
<div style="width: 120px;">
{if current_step > 1 && !props.disable_navigation {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={link.callback(|_| MultiStepFormMsg::PrevStep)}
>
<i class="bi bi-arrow-left me-1"></i>{"Previous"}
</button>
}
} else {
html! {}
}}
</div>
// Step indicator (center)
<div class="d-flex align-items-center">
{for (1..=total_steps).map(|step| {
let is_current = step == current_step;
let is_completed = step < 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 < total_steps {
html! {
<div class={format!("mx-1 {}", if is_completed { "bg-success" } else { "bg-secondary" })}
style="height: 2px; width: 24px;"></div>
}
} else {
html! {}
}}
</div>
}
})}
</div>
// Next button (right)
<div style="width: 120px;" class="text-end">
{if current_step < total_steps && !props.disable_navigation {
html! {
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| MultiStepFormMsg::NextStep)}
>
{"Next"}<i class="bi bi-arrow-right ms-1"></i>
</button>
}
} else {
html! {}
}}
</div>
</div>
</div>
}
}
fn render_validation_toast(&self, ctx: &Context<Self>) -> Html {
let props = ctx.props();
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={ctx.link().callback(|_| MultiStepFormMsg::HideValidationToast)} 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 props.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>
}
}
}

View File

@ -0,0 +1,62 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct EmptyStateProps {
pub icon: String, // Bootstrap icon class (e.g., "bi-building")
pub title: String,
pub description: String,
#[prop_or_default]
pub primary_action: Option<(String, String)>, // (label, href/onclick)
#[prop_or_default]
pub secondary_action: Option<(String, String)>, // (label, href/onclick)
}
#[function_component(EmptyState)]
pub fn empty_state(props: &EmptyStateProps) -> Html {
html! {
<div class="card border-0">
<div class="card-body text-center py-5">
<div class="mb-4">
<i class={classes!("bi", props.icon.clone(), "display-1", "text-muted")}></i>
</div>
<h3 class="text-muted mb-3">{&props.title}</h3>
<p class="lead text-muted mb-4">
{&props.description}
</p>
if props.primary_action.is_some() || props.secondary_action.is_some() {
<div class="row justify-content-center">
<div class="col-md-6">
<div class="row g-3">
if let Some((label, action)) = &props.primary_action {
<div class="col-md-6">
<div class="card h-100 border-primary">
<div class="card-body text-center">
<i class="bi bi-plus-circle text-primary fs-2 mb-2"></i>
<h6 class="card-title">{label}</h6>
<p class="card-text small text-muted">{"Get started with your first item"}</p>
<a href={action.clone()} class="btn btn-primary btn-sm">{"Get Started"}</a>
</div>
</div>
</div>
}
if let Some((label, action)) = &props.secondary_action {
<div class="col-md-6">
<div class="card h-100 border-success">
<div class="card-body text-center">
<i class="bi bi-question-circle text-success fs-2 mb-2"></i>
<h6 class="card-title">{label}</h6>
<p class="card-text small text-muted">{"Learn how to use the system"}</p>
<button class="btn btn-outline-success btn-sm">{"Learn More"}</button>
</div>
</div>
</div>
}
</div>
</div>
</div>
}
</div>
</div>
}
}

View File

@ -0,0 +1,101 @@
use yew::prelude::*;
use crate::models::*;
use crate::services::CompanyService;
#[derive(Properties, PartialEq)]
pub struct CompaniesListProps {
pub companies: Vec<Company>,
pub on_view_company: Callback<u32>,
pub on_switch_to_entity: Callback<u32>,
}
#[function_component(CompaniesList)]
pub fn companies_list(props: &CompaniesListProps) -> Html {
let companies = &props.companies;
if companies.is_empty() {
html! {
<div class="text-center py-5">
<i class="bi bi-building display-4 text-muted mb-3"></i>
<h4 class="text-muted">{"No Companies Found"}</h4>
<p class="text-muted">{"You haven't registered any companies yet. Get started by registering your first company."}</p>
<button class="btn btn-primary" onclick={Callback::from(|_| {
// This will be handled by the parent component to switch tabs
})}>
<i class="bi bi-plus-circle me-1"></i>{"Register Your First Company"}
</button>
</div>
}
} else {
html! {
<div class="card">
<div class="card-header bg-primary text-white">
<i class="bi bi-building me-1"></i>{" Your Companies"}
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>{"Name"}</th>
<th>{"Type"}</th>
<th>{"Status"}</th>
<th>{"Date Registered"}</th>
<th>{"Actions"}</th>
</tr>
</thead>
<tbody>
{for companies.iter().map(|company| {
let company_id = company.id;
let on_view = {
let on_view_company = props.on_view_company.clone();
Callback::from(move |_: MouseEvent| {
on_view_company.emit(company_id);
})
};
let on_switch = {
let on_switch_to_entity = props.on_switch_to_entity.clone();
Callback::from(move |_: MouseEvent| {
on_switch_to_entity.emit(company_id);
})
};
html! {
<tr key={company.id}>
<td>{&company.name}</td>
<td>{company.company_type.to_string()}</td>
<td>
<span class={company.status.get_badge_class()}>
{company.status.to_string()}
</span>
</td>
<td>{&company.incorporation_date}</td>
<td>
<div class="btn-group">
<button
class="btn btn-sm btn-outline-primary"
onclick={on_view}
title="View company details"
>
<i class="bi bi-eye"></i>{" View"}
</button>
<button
class="btn btn-sm btn-primary"
onclick={on_switch}
title="Switch to this entity"
>
<i class="bi bi-box-arrow-in-right"></i>{" Switch to Entity"}
</button>
</div>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
</div>
</div>
}
}
}

View File

@ -0,0 +1,17 @@
pub mod registration_wizard;
pub mod step_one;
pub mod step_two;
pub mod step_two_combined;
pub mod step_three;
pub mod step_four;
pub mod step_five;
pub mod progress_indicator;
pub use registration_wizard::*;
pub use step_one::*;
pub use step_two::*;
pub use step_two_combined::*;
pub use step_three::*;
pub use step_four::*;
pub use step_five::*;
pub use progress_indicator::*;

View File

@ -0,0 +1,58 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct ProgressIndicatorProps {
pub current_step: u8,
pub total_steps: u8,
}
#[function_component(ProgressIndicator)]
pub fn progress_indicator(props: &ProgressIndicatorProps) -> Html {
let percentage = (props.current_step as f32 / props.total_steps as f32) * 100.0;
html! {
<>
// Progress bar
<div class="progress mb-4">
<div
class="progress-bar bg-success"
role="progressbar"
style={format!("width: {}%", percentage)}
aria-valuenow={percentage.to_string()}
aria-valuemin="0"
aria-valuemax="100"
>
{format!("Step {} of {}", props.current_step, props.total_steps)}
</div>
</div>
// Step indicators
<div class="d-flex justify-content-between mb-4">
{for (1..=props.total_steps).map(|step| {
let is_active = step == props.current_step;
let is_completed = step < props.current_step;
let badge_class = if is_completed || is_active {
"badge rounded-pill bg-success"
} else {
"badge rounded-pill bg-secondary"
};
let step_name = match step {
1 => "General Info",
2 => "Company Type",
3 => "Shareholders",
4 => "Payment",
_ => "Step",
};
html! {
<div class={classes!("step-indicator", if is_active { "active" } else { "" })} id={format!("step-indicator-{}", step)}>
<span class={badge_class}>{step}</span>{format!(" {}", step_name)}
</div>
}
})}
</div>
</>
}
}

View File

@ -0,0 +1,839 @@
use yew::prelude::*;
use gloo::timers::callback::Timeout;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{console, js_sys};
use serde_json::json;
use crate::models::*;
use crate::services::{CompanyService, CompanyRegistration, RegistrationStatus};
use super::{ProgressIndicator, StepOne, StepTwoCombined, StepFive};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn createPaymentIntent(form_data: &JsValue) -> js_sys::Promise;
#[wasm_bindgen(js_namespace = window)]
fn initializeStripeElements(client_secret: &str) -> js_sys::Promise;
}
#[derive(Properties, PartialEq)]
pub struct RegistrationWizardProps {
pub on_registration_complete: Callback<Company>,
pub on_back_to_companies: Callback<()>,
#[prop_or_default]
pub success_company_id: Option<u32>,
#[prop_or_default]
pub show_failure: bool,
#[prop_or_default]
pub force_fresh_start: bool,
#[prop_or_default]
pub continue_registration: Option<CompanyFormData>,
#[prop_or_default]
pub continue_step: Option<u8>,
}
pub enum RegistrationMsg {
NextStep,
PrevStep,
UpdateFormData(CompanyFormData),
SetStep(u8),
AutoSave,
LoadSavedData,
ClearSavedData,
CreatePaymentIntent,
PaymentIntentCreated(String),
PaymentIntentError(String),
ProcessPayment,
PaymentComplete(Company),
PaymentError(String),
ShowValidationToast(Vec<String>),
HideValidationToast,
PaymentPlanChanged(PaymentPlan),
RetryPayment,
ConfirmationChanged(bool),
}
pub struct RegistrationWizard {
current_step: u8,
form_data: CompanyFormData,
validation_errors: Vec<String>,
auto_save_timeout: Option<Timeout>,
processing_payment: bool,
show_validation_toast: bool,
client_secret: Option<String>,
confirmation_checked: bool,
current_registration_id: Option<u32>,
}
impl Component for RegistrationWizard {
type Message = RegistrationMsg;
type Properties = RegistrationWizardProps;
fn create(ctx: &Context<Self>) -> Self {
// Determine initial step based on props
let (form_data, current_step) = if ctx.props().success_company_id.is_some() {
// Show success step
(CompanyFormData::default(), 4)
} else if ctx.props().show_failure {
// Show failure, go back to payment step
let (form_data, _) = CompanyService::load_registration_form()
.unwrap_or_else(|| (CompanyFormData::default(), 3));
(form_data, 3)
} else if ctx.props().force_fresh_start {
// Force fresh start - clear any saved data and start from step 1
let _ = CompanyService::clear_registration_form();
(CompanyFormData::default(), 1)
} else if let (Some(continue_form_data), Some(continue_step)) = (&ctx.props().continue_registration, ctx.props().continue_step) {
// Continue existing registration - adjust step numbers for merged steps
let adjusted_step = match continue_step {
1 => 1, // Step 1 remains the same
2 | 3 => 2, // Steps 2 and 3 are now merged into step 2
4 => 3, // Step 4 becomes step 3 (payment)
_ => 1, // Default to step 1 for any other case
};
(continue_form_data.clone(), adjusted_step)
} else {
// Normal flow - try to load saved form data
let (form_data, saved_step) = CompanyService::load_registration_form()
.unwrap_or_else(|| (CompanyFormData::default(), 1));
// Adjust step numbers for merged steps
let adjusted_step = match saved_step {
1 => 1, // Step 1 remains the same
2 | 3 => 2, // Steps 2 and 3 are now merged into step 2
4 => 3, // Step 4 becomes step 3 (payment)
_ => 1, // Default to step 1 for any other case
};
(form_data, adjusted_step)
};
// Auto-save every 2 seconds after changes
let link = ctx.link().clone();
let auto_save_timeout = Some(Timeout::new(2000, move || {
link.send_message(RegistrationMsg::AutoSave);
}));
Self {
current_step,
form_data,
validation_errors: Vec::new(),
auto_save_timeout,
processing_payment: false,
show_validation_toast: false,
client_secret: None,
confirmation_checked: false,
current_registration_id: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
RegistrationMsg::NextStep => {
// Validate current step
let validation_result = CompanyService::validate_step(&self.form_data, self.current_step);
if !validation_result.is_valid {
self.validation_errors = validation_result.errors;
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(RegistrationMsg::HideValidationToast);
}).forget();
return true;
}
if self.current_step < 4 {
self.current_step += 1;
self.auto_save();
// If moving to step 3 (payment), create payment intent
if self.current_step == 3 {
ctx.link().send_message(RegistrationMsg::CreatePaymentIntent);
}
true
} else {
false
}
}
RegistrationMsg::PrevStep => {
if self.current_step > 1 {
self.current_step -= 1;
self.auto_save();
true
} else {
false
}
}
RegistrationMsg::UpdateFormData(new_form_data) => {
self.form_data = new_form_data;
self.schedule_auto_save(ctx);
true
}
RegistrationMsg::SetStep(step) => {
if step >= 1 && step <= 4 {
self.current_step = step;
// If moving to step 3 (payment), create payment intent
if step == 3 && self.client_secret.is_none() {
ctx.link().send_message(RegistrationMsg::CreatePaymentIntent);
}
true
} else {
false
}
}
RegistrationMsg::AutoSave => {
self.auto_save();
false
}
RegistrationMsg::LoadSavedData => {
if let Some((form_data, step)) = CompanyService::load_registration_form() {
self.form_data = form_data;
self.current_step = step;
true
} else {
false
}
}
RegistrationMsg::ClearSavedData => {
let _ = CompanyService::clear_registration_form();
self.form_data = CompanyFormData::default();
self.current_step = 1;
self.client_secret = None;
true
}
RegistrationMsg::CreatePaymentIntent => {
console::log_1(&"🔧 Creating payment intent for step 5...".into());
self.create_payment_intent(ctx);
false
}
RegistrationMsg::PaymentIntentCreated(client_secret) => {
console::log_1(&"✅ Payment intent created, initializing Stripe Elements...".into());
self.client_secret = Some(client_secret.clone());
self.initialize_stripe_elements(&client_secret);
true
}
RegistrationMsg::PaymentIntentError(error) => {
console::log_1(&format!("❌ Payment intent creation failed: {}", error).into());
self.validation_errors = vec![format!("Payment setup failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(RegistrationMsg::HideValidationToast);
}).forget();
true
}
RegistrationMsg::ProcessPayment => {
self.processing_payment = true;
// Simulate payment processing (in real app, this would integrate with Stripe)
let link = ctx.link().clone();
let form_data = self.form_data.clone();
let registration_id = self.current_registration_id;
Timeout::new(2000, move || {
// Create company and update registration status
match CompanyService::create_company_from_form(&form_data) {
Ok(company) => {
// Update registration status to PendingApproval
if let Some(reg_id) = registration_id {
let mut registrations = CompanyService::get_registrations();
if let Some(registration) = registrations.iter_mut().find(|r| r.id == reg_id) {
registration.status = RegistrationStatus::PendingApproval;
let _ = CompanyService::save_registrations(&registrations);
}
} else {
// Create new registration if none exists
let now = js_sys::Date::new_0();
let created_at = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let registration = CompanyRegistration {
id: 0, // Will be set by save_registration
company_name: form_data.company_name.clone(),
company_type: form_data.company_type.clone(),
status: RegistrationStatus::PendingApproval,
created_at,
form_data: form_data.clone(),
current_step: 5, // Completed
};
let _ = CompanyService::save_registration(registration);
}
link.send_message(RegistrationMsg::PaymentComplete(company));
}
Err(error) => {
link.send_message(RegistrationMsg::PaymentError(error));
}
}
}).forget();
true
}
RegistrationMsg::PaymentComplete(company) => {
self.processing_payment = false;
// Move to success step instead of clearing immediately
self.current_step = 4;
// Clear saved form data
let _ = CompanyService::clear_registration_form();
// Notify parent component
ctx.props().on_registration_complete.emit(company);
true
}
RegistrationMsg::PaymentError(error) => {
self.processing_payment = false;
// Stay on payment step and show error
self.validation_errors = vec![format!("Payment failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(RegistrationMsg::HideValidationToast);
}).forget();
true
}
RegistrationMsg::RetryPayment => {
// Clear errors and try payment again
self.validation_errors.clear();
self.show_validation_toast = false;
// Reset client secret to force new payment intent
self.client_secret = None;
ctx.link().send_message(RegistrationMsg::CreatePaymentIntent);
true
}
RegistrationMsg::ShowValidationToast(errors) => {
self.validation_errors = errors;
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(RegistrationMsg::HideValidationToast);
}).forget();
true
}
RegistrationMsg::HideValidationToast => {
self.show_validation_toast = false;
true
}
RegistrationMsg::PaymentPlanChanged(plan) => {
console::log_1(&format!("💳 Payment plan changed to: {}", plan.get_display_name()).into());
// Update form data with new payment plan
self.form_data.payment_plan = plan;
// Clear existing client secret to force new payment intent creation
self.client_secret = None;
// Create new payment intent with updated plan
ctx.link().send_message(RegistrationMsg::CreatePaymentIntent);
// Auto-save the updated form data
self.auto_save();
true
}
RegistrationMsg::ConfirmationChanged(checked) => {
self.confirmation_checked = checked;
console::log_1(&format!("📋 Confirmation state updated: {}", checked).into());
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let (step_title, step_description, step_icon) = self.get_step_info();
html! {
<div class="card" style="height: calc(100vh - 200px); display: flex; flex-direction: column;">
<div class="card-header flex-shrink-0">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h5 class="mb-1">
<i class={format!("bi {} me-2", step_icon)}></i>{step_title}
</h5>
<p class="text-muted mb-0 small">{step_description}</p>
</div>
<button
class="btn btn-outline-secondary btn-sm ms-3"
onclick={ctx.props().on_back_to_companies.reform(|_| ())}
>
<i class="bi bi-arrow-left me-1"></i>{"Back to Companies"}
</button>
</div>
</div>
<div class="card-body flex-grow-1 overflow-auto">
<form id="companyRegistrationForm">
{self.render_current_step(ctx)}
</form>
</div>
{if self.current_step <= 3 {
self.render_footer_navigation(ctx)
} else {
html! {}
}}
{if self.show_validation_toast {
self.render_validation_toast(ctx)
} else {
html! {}
}}
</div>
}
}
}
impl RegistrationWizard {
fn render_current_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let form_data = self.form_data.clone();
let on_form_update = link.callback(RegistrationMsg::UpdateFormData);
match self.current_step {
1 => html! {
<StepOne
form_data={form_data}
on_form_update={on_form_update}
/>
},
2 => html! {
<StepTwoCombined
form_data={form_data}
on_form_update={on_form_update}
/>
},
3 => html! {
<StepFive
form_data={form_data}
client_secret={self.client_secret.clone()}
processing_payment={self.processing_payment}
on_process_payment={link.callback(|_| RegistrationMsg::ProcessPayment)}
on_payment_complete={link.callback(RegistrationMsg::PaymentComplete)}
on_payment_error={link.callback(RegistrationMsg::PaymentError)}
on_payment_plan_change={link.callback(RegistrationMsg::PaymentPlanChanged)}
on_confirmation_change={link.callback(RegistrationMsg::ConfirmationChanged)}
/>
},
4 => {
// Success step
self.render_success_step(ctx)
},
_ => html! { <div>{"Invalid step"}</div> }
}
}
fn render_footer_navigation(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
// Previous button (left)
<div style="width: 120px;">
{if self.current_step > 1 {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={link.callback(|_| RegistrationMsg::PrevStep)}
disabled={self.processing_payment}
>
<i class="bi bi-arrow-left me-1"></i>{"Previous"}
</button>
}
} else {
html! {}
}}
</div>
// Step indicator (center)
<div class="d-flex align-items-center">
{for (1..=3).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 < 3 {
html! {
<div class={format!("mx-1 {}", if is_completed { "bg-success" } else { "bg-secondary" })}
style="height: 2px; width: 24px;"></div>
}
} else {
html! {}
}}
</div>
}
})}
</div>
// Next/Payment button (right)
<div style="width: 150px;" class="text-end">
{if self.current_step < 3 {
html! {
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| RegistrationMsg::NextStep)}
disabled={self.processing_payment}
>
{"Next"}<i class="bi bi-arrow-right ms-1"></i>
</button>
}
} else if self.current_step == 3 {
// Payment button for step 3
let has_client_secret = self.client_secret.is_some();
let can_process_payment = has_client_secret && !self.processing_payment && self.confirmation_checked;
html! {
<button
type="button"
class="btn btn-success text-nowrap"
id="submit-payment"
disabled={!can_process_payment}
onclick={link.callback(|_| RegistrationMsg::ProcessPayment)}
>
{if self.processing_payment {
html! {
<>
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
<span>{"Processing..."}</span>
</>
}
} else if has_client_secret {
html! {
<>
<i class="bi bi-credit-card me-2"></i>
<span>{"Pay Now"}</span>
</>
}
} else {
html! {
<>
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
<span>{"Preparing..."}</span>
</>
}
}}
</button>
}
} else {
html! {}
}}
</div>
</div>
</div>
}
}
fn render_validation_toast(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let close_toast = link.callback(|_| RegistrationMsg::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>
}
}
fn schedule_auto_save(&mut self, ctx: &Context<Self>) {
// Cancel existing timeout
self.auto_save_timeout = None;
// Schedule new auto-save
let link = ctx.link().clone();
self.auto_save_timeout = Some(Timeout::new(2000, move || {
link.send_message(RegistrationMsg::AutoSave);
}));
}
fn auto_save(&mut self) {
// Save form data to localStorage for recovery
let _ = CompanyService::save_registration_form(&self.form_data, self.current_step);
// Also save as a draft registration
let now = js_sys::Date::new_0();
let created_at = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let status = if self.current_step >= 3 {
RegistrationStatus::PendingPayment
} else {
RegistrationStatus::Draft
};
let registration = CompanyRegistration {
id: self.current_registration_id.unwrap_or(0),
company_name: if self.form_data.company_name.is_empty() {
"Draft Registration".to_string()
} else {
self.form_data.company_name.clone()
},
company_type: self.form_data.company_type.clone(),
status,
created_at,
form_data: self.form_data.clone(),
current_step: self.current_step,
};
if let Ok(saved_registration) = CompanyService::save_registration(registration) {
self.current_registration_id = Some(saved_registration.id);
}
}
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(RegistrationMsg::PaymentIntentCreated(client_secret));
}
Err(e) => {
link.send_message(RegistrationMsg::PaymentIntentError(e));
}
}
});
}
async fn setup_stripe_payment(form_data: CompanyFormData) -> Result<String, String> {
use wasm_bindgen_futures::JsFuture;
console::log_1(&"🔧 Setting up Stripe payment for company registration".into());
console::log_1(&format!("📋 Company: {} ({})", form_data.company_name, form_data.company_type.to_string()).into());
console::log_1(&format!("💳 Payment plan: {}", form_data.payment_plan.to_string()).into());
// Prepare form data for payment intent creation
// Note: For payment intent creation, we set final_agreement to true since the actual
// confirmation is now handled by the confirmation checkbox in the UI
let payment_data = json!({
"company_name": form_data.company_name,
"company_type": form_data.company_type.to_string(),
"company_email": form_data.company_email,
"company_phone": form_data.company_phone,
"company_website": form_data.company_website,
"company_address": form_data.company_address,
"company_industry": form_data.company_industry,
"company_purpose": form_data.company_purpose,
"fiscal_year_end": form_data.fiscal_year_end,
"shareholders": serde_json::to_string(&form_data.shareholders).unwrap_or_default(),
"payment_plan": form_data.payment_plan.to_string(),
"agreements": vec!["terms", "privacy", "compliance", "articles"],
"final_agreement": true
});
console::log_1(&"📡 Calling JavaScript createPaymentIntent function".into());
let js_value = JsValue::from_str(&payment_data.to_string());
// Call JavaScript function to create payment intent
let promise = createPaymentIntent(&js_value);
let result = JsFuture::from(promise).await
.map_err(|e| {
let error_msg = format!("Payment intent creation failed: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
// Extract client secret from result
let client_secret = result.as_string()
.ok_or_else(|| {
let error_msg = "Invalid client secret received from server";
console::log_1(&format!("{}", error_msg).into());
error_msg.to_string()
})?;
console::log_1(&"✅ Payment intent created successfully".into());
console::log_1(&format!("🔑 Client secret received: {}", if client_secret.len() > 10 { "Yes" } else { "No" }).into());
Ok(client_secret)
}
fn initialize_stripe_elements(&self, client_secret: &str) {
console::log_1(&"🔧 Initializing Stripe Elements for payment form".into());
console::log_1(&format!("🔑 Client secret length: {}", client_secret.len()).into());
spawn_local({
let client_secret = client_secret.to_string();
async move {
use wasm_bindgen_futures::JsFuture;
// Call JavaScript function to initialize Stripe Elements
let promise = initializeStripeElements(&client_secret);
match JsFuture::from(promise).await {
Ok(_) => {
console::log_1(&"✅ Stripe Elements initialized successfully".into());
console::log_1(&"💳 Payment form should now be visible in the UI".into());
}
Err(e) => {
console::log_1(&format!("❌ Stripe Elements initialization failed: {:?}", e).into());
}
}
}
});
}
fn get_step_info(&self) -> (&'static str, &'static str, &'static str) {
match self.current_step {
1 => (
"Company Information & Type",
"Provide basic company information and select your company type and structure.",
"bi-building"
),
2 => (
"Shareholders & Documents",
"Add shareholders, select bylaw template, and review generated legal documents.",
"bi-people-fill"
),
3 => (
"Payment Plan & Processing",
"Select your payment plan and complete the payment to finalize your company registration.",
"bi-credit-card"
),
4 => (
"Registration Complete",
"Your company registration has been successfully completed.",
"bi-check-circle-fill"
),
_ => (
"Company Registration",
"Complete the registration process for your new company.",
"bi-file-earmark-plus"
)
}
}
fn render_success_step(&self, ctx: &Context<Self>) -> Html {
let company_id = ctx.props().success_company_id.unwrap_or(1); // Default to 1 if not provided
html! {
<div class="text-center py-5">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h2 class="text-success mb-3">{"Registration Successful!"}</h2>
<p class="lead mb-4">
{"Your company has been successfully registered and is now pending approval."}
</p>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-success">
<div class="card-body">
<h5 class="card-title text-success">
<i class="bi bi-info-circle me-2"></i>{"What happens next?"}
</h5>
<div class="text-start">
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-success rounded-pill">{"1"}</span>
</div>
<div>
<strong>{"Document Review"}</strong>
<p class="mb-0 text-muted">{"Our team will review your submitted documents and information."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-primary rounded-pill">{"2"}</span>
</div>
<div>
<strong>{"Compliance Check"}</strong>
<p class="mb-0 text-muted">{"We'll verify compliance with local regulations and requirements."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-info rounded-pill">{"3"}</span>
</div>
<div>
<strong>{"Approval & Activation"}</strong>
<p class="mb-0 text-muted">{"Once approved, your company will be activated and you'll receive your certificate."}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="d-flex justify-content-center">
<button
class="btn btn-success btn-lg"
onclick={ctx.props().on_back_to_companies.reform(|_| ())}
>
<i class="bi bi-list me-2"></i>{"Back to Companies"}
</button>
</div>
</div>
<div class="mt-4">
<div class="alert alert-info">
<i class="bi bi-envelope me-2"></i>
{"You will receive email updates about your registration status. The approval process typically takes 1-3 business days."}
</div>
</div>
</div>
}
}
}

View File

@ -0,0 +1,486 @@
use yew::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{window, console, js_sys};
use crate::models::*;
use crate::services::CompanyService;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn confirmStripePayment(client_secret: &str) -> js_sys::Promise;
#[wasm_bindgen(js_namespace = window)]
fn initializeStripeElements(client_secret: &str);
}
#[derive(Properties, PartialEq)]
pub struct StepFiveProps {
pub form_data: CompanyFormData,
pub client_secret: Option<String>,
pub processing_payment: bool,
pub on_process_payment: Callback<()>,
pub on_payment_complete: Callback<Company>,
pub on_payment_error: Callback<String>,
pub on_payment_plan_change: Callback<PaymentPlan>,
pub on_confirmation_change: Callback<bool>,
}
pub enum StepFiveMsg {
ProcessPayment,
PaymentComplete,
PaymentError(String),
PaymentPlanChanged(PaymentPlan),
ToggleConfirmation,
}
pub struct StepFive {
form_data: CompanyFormData,
payment_error: Option<String>,
selected_payment_plan: PaymentPlan,
confirmation_checked: bool,
}
impl Component for StepFive {
type Message = StepFiveMsg;
type Properties = StepFiveProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
form_data: ctx.props().form_data.clone(),
payment_error: None,
selected_payment_plan: ctx.props().form_data.payment_plan.clone(),
confirmation_checked: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepFiveMsg::ProcessPayment => {
if let Some(client_secret) = &ctx.props().client_secret {
console::log_1(&"🔄 User clicked 'Complete Payment' - processing with Stripe".into());
self.process_stripe_payment(ctx, client_secret.clone());
} else {
console::log_1(&"❌ No client secret available for payment".into());
self.payment_error = Some("Payment not ready. Please try again.".to_string());
}
return false;
}
StepFiveMsg::PaymentComplete => {
console::log_1(&"✅ Payment completed successfully".into());
// Create company from form data with current payment plan
let mut updated_form_data = self.form_data.clone();
updated_form_data.payment_plan = self.selected_payment_plan.clone();
match crate::services::CompanyService::create_company_from_form(&updated_form_data) {
Ok(company) => {
ctx.props().on_payment_complete.emit(company);
}
Err(e) => {
console::log_1(&format!("❌ Failed to create company: {}", e).into());
ctx.props().on_payment_error.emit(format!("Failed to create company: {}", e));
}
}
return false;
}
StepFiveMsg::PaymentError(error) => {
console::log_1(&format!("❌ Payment failed: {}", error).into());
self.payment_error = Some(error.clone());
ctx.props().on_payment_error.emit(error);
}
StepFiveMsg::PaymentPlanChanged(plan) => {
console::log_1(&format!("💳 Payment plan changed to: {}", plan.get_display_name()).into());
self.selected_payment_plan = plan.clone();
self.payment_error = None; // Clear any previous errors
// Notify parent to create new payment intent
ctx.props().on_payment_plan_change.emit(plan);
return true;
}
StepFiveMsg::ToggleConfirmation => {
self.confirmation_checked = !self.confirmation_checked;
console::log_1(&format!("📋 Confirmation checkbox toggled: {}", self.confirmation_checked).into());
// Notify parent of confirmation state change
ctx.props().on_confirmation_change.emit(self.confirmation_checked);
}
}
true
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
// Update selected payment plan if it changed from parent
if self.selected_payment_plan != ctx.props().form_data.payment_plan {
self.selected_payment_plan = ctx.props().form_data.payment_plan.clone();
}
// Initialize Stripe Elements if client secret became available
if old_props.client_secret.is_none() && ctx.props().client_secret.is_some() {
if let Some(client_secret) = &ctx.props().client_secret {
initializeStripeElements(client_secret);
}
}
true
}
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
// Initialize Stripe Elements if client secret is available
if let Some(client_secret) = &ctx.props().client_secret {
initializeStripeElements(client_secret);
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let has_client_secret = ctx.props().client_secret.is_some();
let can_process_payment = has_client_secret && !ctx.props().processing_payment && self.confirmation_checked;
let total_amount = CompanyService::calculate_payment_amount(&self.form_data.company_type, &self.selected_payment_plan);
html! {
<div class="step-content">
// Compact Registration Summary
<div class="row mb-3">
<div class="col-12">
<h6 class="text-secondary mb-3">
<i class="bi bi-receipt me-2"></i>{"Registration Summary"}
</h6>
<div class="card border-0">
<div class="card-body py-3">
<div class="row g-2 small">
// Row 1: Company basics
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-building text-primary me-2"></i>
<div>
<div class="fw-bold">{&self.form_data.company_name}</div>
<div class="text-muted">{self.form_data.company_type.to_string()}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-envelope text-primary me-2"></i>
<div>
<div class="fw-bold">{&self.form_data.company_email}</div>
<div class="text-muted">{"Email"}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-briefcase text-primary me-2"></i>
<div>
<div class="fw-bold">{
self.form_data.company_industry.as_ref().unwrap_or(&"Not specified".to_string())
}</div>
<div class="text-muted">{"Industry"}</div>
</div>
</div>
</div>
// Row 2: Additional details
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-people text-primary me-2"></i>
<div>
<div class="fw-bold">{format!("{} shareholders", self.form_data.shareholders.len())}</div>
<div class="text-muted">{
match self.form_data.shareholder_structure {
ShareholderStructure::Equal => "Equal ownership",
ShareholderStructure::Custom => "Custom ownership",
}
}</div>
</div>
</div>
</div>
{if let Some(purpose) = &self.form_data.company_purpose {
if !purpose.is_empty() {
html! {
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-bullseye text-primary me-2"></i>
<div>
<div class="fw-bold">{purpose}</div>
<div class="text-muted">{"Purpose"}</div>
</div>
</div>
</div>
}
} else {
html! {}
}
} else {
html! {}
}}
{if let Some(fiscal_year) = &self.form_data.fiscal_year_end {
if !fiscal_year.is_empty() {
html! {
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-calendar-event text-primary me-2"></i>
<div>
<div class="fw-bold">{fiscal_year}</div>
<div class="text-muted">{"Fiscal Year End"}</div>
</div>
</div>
</div>
}
} else {
html! {}
}
} else {
html! {}
}}
</div>
// Shareholders details (if more than 1)
{if self.form_data.shareholders.len() > 1 {
html! {
<div class="mt-2 pt-2 border-top">
<div class="small text-muted mb-1">{"Shareholders:"}</div>
<div class="row g-1">
{for self.form_data.shareholders.iter().map(|shareholder| {
html! {
<div class="col-md-6">
<span class="fw-bold">{&shareholder.name}</span>
<span class="text-muted ms-1">{format!("({}%)", shareholder.percentage)}</span>
</div>
}
})}
</div>
</div>
}
} else {
html! {}
}}
</div>
</div>
</div>
</div>
// Compact Confirmation Checkbox
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning py-2 mb-0">
<div class="form-check mb-0">
<input
class="form-check-input"
type="checkbox"
id="registrationConfirmation"
checked={self.confirmation_checked}
onchange={link.callback(|_| StepFiveMsg::ToggleConfirmation)}
/>
<label class="form-check-label small" for="registrationConfirmation">
<strong>{"I confirm the accuracy of all information and authorize company registration with the selected payment plan."}</strong>
</label>
</div>
</div>
</div>
</div>
// Payment Plans (Left) and Payment Form (Right)
<div class="row mb-4">
// Payment Plan Selection - Left
<div class="col-lg-6 mb-4">
<h5 class="text-secondary mb-3">
{"Choose Your Payment Plan"} <span class="text-danger">{"*"}</span>
</h5>
<div class="row">
{self.render_payment_plan_option(ctx, PaymentPlan::Monthly, "Monthly Plan", "Pay monthly with flexibility", "bi-calendar-month")}
{self.render_payment_plan_option(ctx, PaymentPlan::Yearly, "Yearly Plan", "Save 20% with annual payments", "bi-calendar-check")}
{self.render_payment_plan_option(ctx, PaymentPlan::TwoYear, "2-Year Plan", "Save 40% with 2-year commitment", "bi-calendar2-range")}
</div>
</div>
// Payment Form - Right
<div class="col-lg-6">
<h5 class="text-secondary mb-3">
{"Payment Information"} <span class="text-danger">{"*"}</span>
</h5>
<div class="card" id="payment-information-section">
<div class="card-header bg-success text-white">
<h6 class="mb-0">
<i class="bi bi-shield-check me-2"></i>{"Secure Payment Processing"}
</h6>
</div>
<div class="card-body">
// Stripe Elements will be mounted here
<div id="payment-element" style="min-height: 40px; padding: 10px; border: 1px solid #dee2e6; border-radius: 0.375rem; background-color: #ffffff;">
{if ctx.props().processing_payment {
html! {
<div class="text-center py-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Processing payment..."}</p>
</div>
}
} else if !has_client_secret {
html! {
<div class="text-center py-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Preparing payment form..."}</p>
</div>
}
} else {
html! {}
}}
</div>
// Payment button
{if has_client_secret && !ctx.props().processing_payment {
html! {
<div class="d-grid mt-3">
<button
type="button"
class="btn btn-primary btn-lg"
disabled={!can_process_payment}
onclick={link.callback(|_| StepFiveMsg::ProcessPayment)}
>
{if self.confirmation_checked {
html! {
<>
<i class="bi bi-credit-card me-2"></i>
{format!("Complete Payment - ${:.0}", total_amount)}
</>
}
} else {
html! {
<>
<i class="bi bi-exclamation-triangle me-2"></i>
{"Please confirm registration details"}
</>
}
}}
</button>
</div>
}
} else {
html! {}
}}
{if let Some(error) = &self.payment_error {
html! {
<div id="payment-errors" class="alert alert-danger mt-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>{"Payment Error: "}</strong>{error}
</div>
}
} else {
html! {
<div id="payment-errors" class="alert alert-danger mt-3" style="display: none;"></div>
}
}}
// Payment info text
<div class="text-center mt-3">
<small class="text-muted">
{"Payment plan: "}{self.selected_payment_plan.get_display_name()}
{" with "}{(self.selected_payment_plan.get_discount() * 100.0) as i32}{"% discount"}
</small>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
}
impl StepFive {
fn render_payment_plan_option(&self, ctx: &Context<Self>, plan: PaymentPlan, title: &str, description: &str, icon: &str) -> Html {
let link = ctx.link();
let is_selected = self.selected_payment_plan == plan;
let card_class = if is_selected {
"card border-success mb-3"
} else {
"card border-secondary mb-3"
};
let on_select = link.callback(move |_| StepFiveMsg::PaymentPlanChanged(plan.clone()));
// Calculate pricing for this plan
let total_amount = CompanyService::calculate_payment_amount(&self.form_data.company_type, &plan);
let discount_percent = ((1.0 - plan.get_discount()) * 100.0) as i32;
html! {
<div class="col-12">
<div class={card_class} style="cursor: pointer;" onclick={on_select}>
<div class="card-body">
<div class="d-flex align-items-center">
<i class={format!("bi {} fs-3 text-primary me-3", icon)}></i>
<div class="flex-grow-1">
<h6 class="card-title mb-1">{title}</h6>
<p class="card-text text-muted mb-0 small">{description}</p>
<div class="mt-1">
<span class="fw-bold text-success">{format!("${:.0}", total_amount)}</span>
{if discount_percent > 0 {
html! {
<span class="badge bg-success ms-2 small">
{format!("{}% OFF", discount_percent)}
</span>
}
} else {
html! {}
}}
</div>
</div>
<div class="text-end">
{if is_selected {
html! {
<i class="bi bi-check-circle-fill text-success fs-4"></i>
}
} else {
html! {
<i class="bi bi-circle text-muted fs-4"></i>
}
}}
</div>
</div>
</div>
</div>
</div>
}
}
fn process_stripe_payment(&mut self, ctx: &Context<Self>, client_secret: String) {
let link = ctx.link().clone();
// Trigger parent to show processing state
ctx.props().on_process_payment.emit(());
spawn_local(async move {
match Self::confirm_payment(&client_secret).await {
Ok(_) => {
link.send_message(StepFiveMsg::PaymentComplete);
}
Err(e) => {
link.send_message(StepFiveMsg::PaymentError(e));
}
}
});
}
async fn confirm_payment(client_secret: &str) -> Result<(), String> {
use wasm_bindgen_futures::JsFuture;
console::log_1(&"🔄 Confirming payment with Stripe...".into());
// Call JavaScript function to confirm payment
let promise = confirmStripePayment(client_secret);
JsFuture::from(promise).await
.map_err(|e| format!("Payment confirmation failed: {:?}", e))?;
console::log_1(&"✅ Payment confirmed successfully".into());
Ok(())
}
}

View File

@ -0,0 +1,219 @@
use yew::prelude::*;
use crate::models::*;
#[derive(Properties, PartialEq)]
pub struct StepFourProps {
pub form_data: CompanyFormData,
pub on_form_update: Callback<CompanyFormData>,
}
pub enum StepFourMsg {
ToggleTermsAccepted,
TogglePrivacyAccepted,
ToggleComplianceAccepted,
ToggleArticlesAccepted,
ToggleFinalAgreementAccepted,
}
pub struct StepFour {
form_data: CompanyFormData,
}
impl Component for StepFour {
type Message = StepFourMsg;
type Properties = StepFourProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
form_data: ctx.props().form_data.clone(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepFourMsg::ToggleTermsAccepted => {
self.form_data.legal_agreements.terms = !self.form_data.legal_agreements.terms;
}
StepFourMsg::TogglePrivacyAccepted => {
self.form_data.legal_agreements.privacy = !self.form_data.legal_agreements.privacy;
}
StepFourMsg::ToggleComplianceAccepted => {
self.form_data.legal_agreements.compliance = !self.form_data.legal_agreements.compliance;
}
StepFourMsg::ToggleArticlesAccepted => {
self.form_data.legal_agreements.articles = !self.form_data.legal_agreements.articles;
}
StepFourMsg::ToggleFinalAgreementAccepted => {
self.form_data.legal_agreements.final_agreement = !self.form_data.legal_agreements.final_agreement;
}
}
// Notify parent of form data changes
ctx.props().on_form_update.emit(self.form_data.clone());
true
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
// Document Upload Section
<div class="row mb-4">
<div class="col-12">
<h5 class="text-secondary mb-3">
{"Required Documents"} <span class="text-danger">{"*"}</span>
</h5>
<div class="card">
<div class="card-body">
<div class="alert alert-info mb-3">
<i class="bi bi-info-circle me-2"></i>
<strong>{"Document Requirements"}</strong><br/>
{"Please prepare the following documents for upload. All documents must be in PDF format and clearly legible."}
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
{"Passport/ID Copy"} <span class="text-danger">{"*"}</span>
</label>
<input type="file" class="form-control" accept=".pdf" />
<small class="text-muted">{"Government-issued photo ID"}</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
{"Proof of Address"} <span class="text-danger">{"*"}</span>
</label>
<input type="file" class="form-control" accept=".pdf" />
<small class="text-muted">{"Utility bill or bank statement (last 3 months)"}</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
{"Business Plan"} <span class="text-muted">{"(Optional)"}</span>
</label>
<input type="file" class="form-control" accept=".pdf" />
<small class="text-muted">{"Detailed business plan and projections"}</small>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
{"Financial Statements"} <span class="text-muted">{"(If applicable)"}</span>
</label>
<input type="file" class="form-control" accept=".pdf" />
<small class="text-muted">{"Previous company financial records"}</small>
</div>
</div>
</div>
</div>
</div>
</div>
// Legal Agreements Section
<div class="row mb-4">
<div class="col-12">
<h5 class="text-secondary mb-3">
{"Legal Agreements"} <span class="text-danger">{"*"}</span>
</h5>
<div class="card">
<div class="card-body">
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="termsAccepted"
checked={self.form_data.legal_agreements.terms}
onchange={link.callback(|_| StepFourMsg::ToggleTermsAccepted)}
required=true
/>
<label class="form-check-label" for="termsAccepted">
{"I agree to the "}
<a href="#" class="text-primary">{"Terms of Service"}</a>
{" "} <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="privacyAccepted"
checked={self.form_data.legal_agreements.privacy}
onchange={link.callback(|_| StepFourMsg::TogglePrivacyAccepted)}
required=true
/>
<label class="form-check-label" for="privacyAccepted">
{"I acknowledge that I have read and agree to the "}
<a href="#" class="text-primary">{"Privacy Policy"}</a>
{" "} <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="complianceAccepted"
checked={self.form_data.legal_agreements.compliance}
onchange={link.callback(|_| StepFourMsg::ToggleComplianceAccepted)}
required=true
/>
<label class="form-check-label" for="complianceAccepted">
{"I agree to comply with all applicable laws and regulations"}
{" "} <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="articlesAccepted"
checked={self.form_data.legal_agreements.articles}
onchange={link.callback(|_| StepFourMsg::ToggleArticlesAccepted)}
required=true
/>
<label class="form-check-label" for="articlesAccepted">
{"I agree to the "}
<a href="#" class="text-primary">{"Articles of Incorporation"}</a>
{" "} <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="finalAgreementAccepted"
checked={self.form_data.legal_agreements.final_agreement}
onchange={link.callback(|_| StepFourMsg::ToggleFinalAgreementAccepted)}
required=true
/>
<label class="form-check-label" for="finalAgreementAccepted">
{"I agree to the "}
<a href="#" class="text-primary">{"Final Registration Agreement"}</a>
{" "} <span class="text-danger">{"*"}</span>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
}
impl StepFour {
// Step 4 is now focused on documents and legal agreements only
}

View File

@ -0,0 +1,277 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::*;
#[derive(Properties, PartialEq)]
pub struct StepOneProps {
pub form_data: CompanyFormData,
pub on_form_update: Callback<CompanyFormData>,
}
pub enum StepOneMsg {
UpdateCompanyName(String),
UpdateDescription(String),
UpdateEmail(String),
UpdateIndustry(String),
SelectCompanyType(CompanyType),
}
pub struct StepOne {
form_data: CompanyFormData,
}
impl Component for StepOne {
type Message = StepOneMsg;
type Properties = StepOneProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
form_data: ctx.props().form_data.clone(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepOneMsg::UpdateCompanyName(value) => {
self.form_data.company_name = value;
}
StepOneMsg::UpdateDescription(value) => {
self.form_data.company_purpose = Some(value);
}
StepOneMsg::UpdateEmail(value) => {
self.form_data.company_email = value;
}
StepOneMsg::UpdateIndustry(value) => {
self.form_data.company_industry = if value.is_empty() { None } else { Some(value) };
}
StepOneMsg::SelectCompanyType(company_type) => {
self.form_data.company_type = company_type;
}
}
// Notify parent of form data changes
ctx.props().on_form_update.emit(self.form_data.clone());
true
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
<div class="row">
<div class="col-md-6">
<div class="row mb-3">
<label for="companyName" class="col-sm-4 col-form-label" data-bs-toggle="tooltip" data-bs-placement="top" title="The official name of your company or legal entity">
{"Company Name"} <span class="text-danger">{"*"}</span>
</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="companyName"
placeholder="Enter company name"
value={self.form_data.company_name.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepOneMsg::UpdateCompanyName(input.value())
})}
required=true
data-bs-toggle="tooltip"
data-bs-placement="top"
title="The official name of your company or legal entity"
/>
</div>
</div>
<div class="row mb-3">
<label for="email" class="col-sm-4 col-form-label" data-bs-toggle="tooltip" data-bs-placement="top" title="Primary contact email for the company">
{"Email Address"} <span class="text-danger">{"*"}</span>
</label>
<div class="col-sm-8">
<input
type="email"
class="form-control"
id="email"
placeholder="company@example.com"
value={self.form_data.company_email.clone()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepOneMsg::UpdateEmail(input.value())
})}
required=true
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Primary contact email for the company"
/>
</div>
</div>
<div class="row mb-3">
<label for="industry" class="col-sm-4 col-form-label" data-bs-toggle="tooltip" data-bs-placement="top" title="Primary industry sector (optional)">
{"Industry"}
</label>
<div class="col-sm-8">
<select
class="form-select"
id="industry"
value={self.form_data.company_industry.clone().unwrap_or_default()}
onchange={link.callback(|e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
StepOneMsg::UpdateIndustry(input.value())
})}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Primary industry sector (optional)"
>
<option value="">{"Select industry"}</option>
<option value="Technology">{"Technology"}</option>
<option value="Finance">{"Finance"}</option>
<option value="Healthcare">{"Healthcare"}</option>
<option value="Education">{"Education"}</option>
<option value="Retail">{"Retail"}</option>
<option value="Manufacturing">{"Manufacturing"}</option>
<option value="Real Estate">{"Real Estate"}</option>
<option value="Consulting">{"Consulting"}</option>
<option value="Media">{"Media"}</option>
<option value="Transportation">{"Transportation"}</option>
<option value="Energy">{"Energy"}</option>
<option value="Agriculture">{"Agriculture"}</option>
<option value="Other">{"Other"}</option>
</select>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<textarea
class="form-control"
id="description"
rows="5"
placeholder="Describe your company's business activities and purpose..."
value={self.form_data.company_purpose.clone().unwrap_or_default()}
oninput={link.callback(|e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepOneMsg::UpdateDescription(input.value())
})}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Brief description of your company's business activities and purpose"
></textarea>
</div>
</div>
</div>
// Company Type Selection
<div class="row mt-4">
<div class="col-12">
<div class="row">
{self.render_company_type_option(ctx, CompanyType::SingleFZC,
"Single FZC",
"Perfect for individual entrepreneurs and solo ventures. Simple structure with one shareholder.",
vec!["1 shareholder only", "Cannot issue digital assets", "Can hold external shares", "Connect to bank", "Participate in ecosystem"],
"$20 setup + $20/month")}
{self.render_company_type_option(ctx, CompanyType::StartupFZC,
"Startup FZC",
"Ideal for small teams and early-stage startups. Allows multiple shareholders and digital asset issuance.",
vec!["Up to 5 shareholders", "Can issue digital assets", "Hold external shares", "Connect to bank", "Full ecosystem access"],
"$50 setup + $50/month")}
{self.render_company_type_option(ctx, CompanyType::GrowthFZC,
"Growth FZC",
"Designed for growing businesses that need more flexibility and can hold physical assets.",
vec!["Up to 20 shareholders", "Can issue digital assets", "Hold external shares", "Connect to bank", "Hold physical assets"],
"$100 setup + $100/month")}
{self.render_company_type_option(ctx, CompanyType::GlobalFZC,
"Global FZC",
"Enterprise-level structure for large organizations with unlimited shareholders and full capabilities.",
vec!["Unlimited shareholders", "Can issue digital assets", "Hold external shares", "Connect to bank", "Hold physical assets"],
"$2000 setup + $200/month")}
{self.render_company_type_option(ctx, CompanyType::CooperativeFZC,
"Cooperative FZC",
"Democratic organization structure with collective decision-making and equitable distribution.",
vec!["Unlimited members", "Democratic governance", "Collective decision-making", "Equitable distribution", "Full capabilities"],
"$2000 setup + $200/month")}
</div>
</div>
</div>
</div>
}
}
}
impl StepOne {
fn render_company_type_option(
&self,
ctx: &Context<Self>,
company_type: CompanyType,
title: &str,
description: &str,
benefits: Vec<&str>,
price: &str,
) -> Html {
let link = ctx.link();
let is_selected = self.form_data.company_type == company_type;
let card_class = if is_selected {
"card border-success mb-3 shadow-sm"
} else {
"card border-light mb-3"
};
html! {
<div class="col-xl col-lg-4 col-md-6 mb-3" style="min-width: 220px; max-width: 280px;">
<div class={card_class} style="cursor: pointer;" onclick={link.callback(move |_| StepOneMsg::SelectCompanyType(company_type.clone()))}>
<div class="card-header">
<div class="d-flex align-items-center">
<input
type="radio"
class="form-check-input me-2"
checked={is_selected}
onchange={link.callback(move |_| StepOneMsg::SelectCompanyType(company_type.clone()))}
/>
<h6 class="mb-0">{title}</h6>
</div>
</div>
<div class="card-body">
<p class="card-text text-muted mb-2">{description}</p>
<div class="text-left mb-3">
<span class="badge bg-primary">{price}</span>
</div>
<div class="row">
<div class="col-12">
<h6 class="text-success mb-2">{"Key Features:"}</h6>
<ul class="list-unstyled mb-0">
{for benefits.iter().map(|benefit| {
html! {
<li class="mb-1">
<i class="bi bi-check-circle text-success me-2"></i>{benefit}
</li>
}
})}
</ul>
</div>
</div>
</div>
{if is_selected {
html! {
<div class="card-footer bg-success text-white">
<i class="bi bi-check-circle me-2"></i>{"Selected"}
</div>
}
} else {
html! {}
}}
</div>
</div>
}
}
}

View File

@ -0,0 +1,293 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::*;
#[derive(Properties, PartialEq)]
pub struct StepThreeProps {
pub form_data: CompanyFormData,
pub on_form_update: Callback<CompanyFormData>,
}
pub enum StepThreeMsg {
AddShareholder,
RemoveShareholder(usize),
UpdateShareholderName(usize, String),
UpdateShareholderPercentage(usize, String),
UpdateShareholderStructure(ShareholderStructure),
}
pub struct StepThree {
form_data: CompanyFormData,
}
impl Component for StepThree {
type Message = StepThreeMsg;
type Properties = StepThreeProps;
fn create(ctx: &Context<Self>) -> Self {
let mut form_data = ctx.props().form_data.clone();
// Ensure at least one shareholder exists
if form_data.shareholders.is_empty() {
form_data.shareholders.push(Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 100.0,
});
}
Self { form_data }
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepThreeMsg::AddShareholder => {
self.form_data.shareholders.push(Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 0.0,
});
}
StepThreeMsg::RemoveShareholder(index) => {
if self.form_data.shareholders.len() > 1 && index < self.form_data.shareholders.len() {
self.form_data.shareholders.remove(index);
}
}
StepThreeMsg::UpdateShareholderName(index, value) => {
if let Some(shareholder) = self.form_data.shareholders.get_mut(index) {
shareholder.name = value;
}
}
StepThreeMsg::UpdateShareholderPercentage(index, value) => {
if let Some(shareholder) = self.form_data.shareholders.get_mut(index) {
shareholder.percentage = value.parse().unwrap_or(0.0);
}
}
StepThreeMsg::UpdateShareholderStructure(structure) => {
self.form_data.shareholder_structure = structure;
// If switching to equal, redistribute percentages equally
if matches!(self.form_data.shareholder_structure, ShareholderStructure::Equal) {
let count = self.form_data.shareholders.len() as f64;
let equal_percentage = 100.0 / count;
for shareholder in &mut self.form_data.shareholders {
shareholder.percentage = equal_percentage;
}
}
}
}
// Notify parent of form data changes
ctx.props().on_form_update.emit(self.form_data.clone());
true
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
// Ensure at least one shareholder exists
if self.form_data.shareholders.is_empty() {
self.form_data.shareholders.push(Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 100.0,
});
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let total_percentage: f64 = self.form_data.shareholders.iter()
.map(|s| s.percentage)
.sum();
html! {
<div class="step-content">
<div class="row mb-3">
<div class="col-12">
<h5 class="text-secondary mb-3">{"Ownership Structure"}</h5>
<div class="row">
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="shareholderStructure"
id="equalStructure"
checked={matches!(self.form_data.shareholder_structure, ShareholderStructure::Equal)}
onchange={link.callback(|_| StepThreeMsg::UpdateShareholderStructure(ShareholderStructure::Equal))}
/>
<label class="form-check-label" for="equalStructure">
<strong>{"Equal Ownership"}</strong>
<div class="text-muted small">{"All shareholders have equal ownership percentages"}</div>
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="shareholderStructure"
id="customStructure"
checked={matches!(self.form_data.shareholder_structure, ShareholderStructure::Custom)}
onchange={link.callback(|_| StepThreeMsg::UpdateShareholderStructure(ShareholderStructure::Custom))}
/>
<label class="form-check-label" for="customStructure">
<strong>{"Custom Ownership"}</strong>
<div class="text-muted small">{"Specify individual ownership percentages"}</div>
</label>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="text-secondary mb-0">
{"Shareholders"} <span class="text-danger">{"*"}</span>
</h5>
<button
type="button"
class="btn btn-outline-success btn-sm"
onclick={link.callback(|_| StepThreeMsg::AddShareholder)}
>
<i class="bi bi-plus-circle me-1"></i>{"Add Shareholder"}
</button>
</div>
{if total_percentage != 100.0 && total_percentage > 0.0 {
html! {
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
{"Total ownership percentage is "}{total_percentage}{"%"}
{" - it should equal 100% for proper ownership distribution."}
</div>
}
} else if total_percentage == 100.0 {
html! {
<div class="alert alert-success">
<i class="bi bi-check-circle me-2"></i>
{"Ownership percentages total 100% ✓"}
</div>
}
} else {
html! {}
}}
</div>
</div>
{for self.form_data.shareholders.iter().enumerate().map(|(index, shareholder)| {
self.render_shareholder_form(ctx, index, shareholder)
})}
<div class="alert alert-info mt-4">
<div class="d-flex align-items-start">
<i class="bi bi-info-circle me-3 mt-1"></i>
<div>
<strong>{"Important Notes:"}</strong>
<ul class="mb-0 mt-2">
<li>{"All shareholders must be at least 18 years old"}</li>
<li>{"Total ownership percentages must equal 100%"}</li>
<li>{"Each shareholder will receive official documentation"}</li>
<li>{"Shareholder information is used for legal filings and compliance"}</li>
</ul>
</div>
</div>
</div>
</div>
}
}
}
impl StepThree {
fn render_shareholder_form(&self, ctx: &Context<Self>, index: usize, shareholder: &Shareholder) -> Html {
let link = ctx.link();
let can_remove = self.form_data.shareholders.len() > 1;
let is_equal_structure = matches!(self.form_data.shareholder_structure, ShareholderStructure::Equal);
html! {
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">
<i class="bi bi-person me-2"></i>
{"Shareholder "}{index + 1}
</h6>
{if can_remove {
html! {
<button
type="button"
class="btn btn-outline-danger btn-sm"
onclick={link.callback(move |_| StepThreeMsg::RemoveShareholder(index))}
title="Remove this shareholder"
>
<i class="bi bi-trash"></i>
</button>
}
} else {
html! {}
}}
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8 mb-3">
<label for={format!("shareholderName{}", index)} class="form-label">
{"Full Name"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id={format!("shareholderName{}", index)}
placeholder="Enter full legal name"
value={shareholder.name.clone()}
oninput={link.callback(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepThreeMsg::UpdateShareholderName(index, input.value())
})}
required=true
/>
</div>
<div class="col-md-4 mb-3">
<label for={format!("shareholderPercentage{}", index)} class="form-label">
{"Ownership %"} <span class="text-danger">{"*"}</span>
</label>
<div class="input-group">
<input
type="number"
class="form-control"
id={format!("shareholderPercentage{}", index)}
placeholder="0"
min="0"
max="100"
step="0.01"
value={shareholder.percentage.to_string()}
oninput={link.callback(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepThreeMsg::UpdateShareholderPercentage(index, input.value())
})}
disabled={is_equal_structure}
required=true
/>
<span class="input-group-text">{"%"}</span>
</div>
{if is_equal_structure {
html! {
<div class="form-text text-info">
{"Automatically calculated for equal ownership"}
</div>
}
} else {
html! {}
}}
</div>
</div>
</div>
</div>
}
}
}

View File

@ -0,0 +1,159 @@
use yew::prelude::*;
use crate::models::*;
#[derive(Properties, PartialEq)]
pub struct StepTwoProps {
pub form_data: CompanyFormData,
pub on_form_update: Callback<CompanyFormData>,
}
pub enum StepTwoMsg {
SelectCompanyType(CompanyType),
}
pub struct StepTwo {
form_data: CompanyFormData,
}
impl Component for StepTwo {
type Message = StepTwoMsg;
type Properties = StepTwoProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
form_data: ctx.props().form_data.clone(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepTwoMsg::SelectCompanyType(company_type) => {
self.form_data.company_type = company_type;
}
}
// Notify parent of form data changes
ctx.props().on_form_update.emit(self.form_data.clone());
true
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="step-content">
<div class="row">
{self.render_company_type_option(ctx, CompanyType::SingleFZC,
"Single FZC",
"Perfect for individual entrepreneurs and solo ventures. Simple structure with one shareholder.",
vec!["1 shareholder only", "Cannot issue digital assets", "Can hold external shares", "Connect to bank", "Participate in ecosystem"],
"$20 setup + $20/month")}
{self.render_company_type_option(ctx, CompanyType::StartupFZC,
"Startup FZC",
"Ideal for small teams and early-stage startups. Allows multiple shareholders and digital asset issuance.",
vec!["Up to 5 shareholders", "Can issue digital assets", "Hold external shares", "Connect to bank", "Full ecosystem access"],
"$50 setup + $50/month")}
{self.render_company_type_option(ctx, CompanyType::GrowthFZC,
"Growth FZC",
"Designed for growing businesses that need more flexibility and can hold physical assets.",
vec!["Up to 20 shareholders", "Can issue digital assets", "Hold external shares", "Connect to bank", "Hold physical assets"],
"$100 setup + $100/month")}
{self.render_company_type_option(ctx, CompanyType::GlobalFZC,
"Global FZC",
"Enterprise-level structure for large organizations with unlimited shareholders and full capabilities.",
vec!["Unlimited shareholders", "Can issue digital assets", "Hold external shares", "Connect to bank", "Hold physical assets"],
"$2000 setup + $200/month")}
{self.render_company_type_option(ctx, CompanyType::CooperativeFZC,
"Cooperative FZC",
"Democratic organization structure with collective decision-making and equitable distribution.",
vec!["Unlimited members", "Democratic governance", "Collective decision-making", "Equitable distribution", "Full capabilities"],
"$2000 setup + $200/month")}
</div>
<div class="alert alert-info mt-4">
<div class="d-flex align-items-start">
<i class="bi bi-lightbulb me-3 mt-1"></i>
<div>
<strong>{"Need help choosing?"}</strong> {" The choice of entity type affects your capabilities, costs, and governance structure. "}
{"Consider your current needs and future growth plans when selecting your FZC type."}
</div>
</div>
</div>
</div>
}
}
}
impl StepTwo {
fn render_company_type_option(
&self,
ctx: &Context<Self>,
company_type: CompanyType,
title: &str,
description: &str,
benefits: Vec<&str>,
price: &str,
) -> Html {
let link = ctx.link();
let is_selected = self.form_data.company_type == company_type;
let card_class = if is_selected {
"card border-success mb-3 shadow-sm"
} else {
"card border-light mb-3"
};
html! {
<div class="col-lg-6 mb-3">
<div class={card_class} style="cursor: pointer;" onclick={link.callback(move |_| StepTwoMsg::SelectCompanyType(company_type.clone()))}>
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<input
type="radio"
class="form-check-input me-2"
checked={is_selected}
onchange={link.callback(move |_| StepTwoMsg::SelectCompanyType(company_type.clone()))}
/>
<h6 class="mb-0">{title}</h6>
</div>
<span class="badge bg-primary">{price}</span>
</div>
<div class="card-body">
<p class="card-text text-muted mb-3">{description}</p>
<div class="row">
<div class="col-12">
<h6 class="text-success mb-2">{"Key Features:"}</h6>
<ul class="list-unstyled mb-0">
{for benefits.iter().map(|benefit| {
html! {
<li class="mb-1">
<i class="bi bi-check-circle text-success me-2"></i>{benefit}
</li>
}
})}
</ul>
</div>
</div>
</div>
{if is_selected {
html! {
<div class="card-footer bg-success text-white">
<i class="bi bi-check-circle me-2"></i>{"Selected"}
</div>
}
} else {
html! {}
}}
</div>
</div>
}
}
}

View File

@ -0,0 +1,676 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use gloo::timers::callback::Timeout;
use crate::models::*;
#[derive(Properties, PartialEq)]
pub struct StepTwoCombinedProps {
pub form_data: CompanyFormData,
pub on_form_update: Callback<CompanyFormData>,
}
#[derive(Clone, Copy, PartialEq)]
pub enum BylawTemplate {
Standard,
Startup,
Enterprise,
Cooperative,
}
impl BylawTemplate {
fn get_display_name(&self) -> &'static str {
match self {
BylawTemplate::Standard => "Standard Bylaws",
BylawTemplate::Startup => "Startup-Friendly Bylaws",
BylawTemplate::Enterprise => "Enterprise Bylaws",
BylawTemplate::Cooperative => "Cooperative Bylaws",
}
}
fn get_description(&self) -> &'static str {
match self {
BylawTemplate::Standard => "Basic corporate governance structure suitable for most companies",
BylawTemplate::Startup => "Flexible structure with provisions for equity incentives and rapid growth",
BylawTemplate::Enterprise => "Comprehensive governance framework for larger organizations",
BylawTemplate::Cooperative => "Democratic governance structure for cooperative organizations",
}
}
}
pub enum StepTwoCombinedMsg {
// Shareholder messages
AddShareholder,
RemoveShareholder(usize),
UpdateShareholderName(usize, String),
UpdateShareholderResidentId(usize, String),
UpdateShareholderPercentage(usize, String),
UpdateShareholderStructure(ShareholderStructure),
// Bylaw template messages
SelectBylawTemplate(BylawTemplate),
// Document actions
ViewDocument(String),
SignDocument(String),
CloseDocumentModal,
DocumentGenerationComplete,
}
pub struct StepTwoCombined {
form_data: CompanyFormData,
selected_bylaw_template: Option<BylawTemplate>,
documents_generated: bool,
documents_generating: bool,
show_document_modal: bool,
current_document: Option<String>,
signed_documents: std::collections::HashSet<String>,
}
impl Component for StepTwoCombined {
type Message = StepTwoCombinedMsg;
type Properties = StepTwoCombinedProps;
fn create(ctx: &Context<Self>) -> Self {
let mut form_data = ctx.props().form_data.clone();
// Ensure at least one shareholder exists
if form_data.shareholders.is_empty() {
form_data.shareholders.push(Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 100.0,
});
}
Self {
form_data,
selected_bylaw_template: None,
documents_generated: false,
documents_generating: false,
show_document_modal: false,
current_document: None,
signed_documents: std::collections::HashSet::new(),
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
// Shareholder handling
StepTwoCombinedMsg::AddShareholder => {
self.form_data.shareholders.push(Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 0.0,
});
// Mark documents as generating due to shareholder change
if self.documents_generated {
self.documents_generating = true;
self.schedule_document_generation_completion(ctx);
}
}
StepTwoCombinedMsg::RemoveShareholder(index) => {
if self.form_data.shareholders.len() > 1 && index < self.form_data.shareholders.len() {
self.form_data.shareholders.remove(index);
// Mark documents as generating due to shareholder change
if self.documents_generated {
self.documents_generating = true;
self.schedule_document_generation_completion(ctx);
}
}
}
StepTwoCombinedMsg::UpdateShareholderName(index, value) => {
if let Some(shareholder) = self.form_data.shareholders.get_mut(index) {
shareholder.name = value;
// Mark documents as generating due to shareholder change
if self.documents_generated {
self.documents_generating = true;
self.schedule_document_generation_completion(ctx);
}
}
}
StepTwoCombinedMsg::UpdateShareholderResidentId(index, value) => {
if let Some(shareholder) = self.form_data.shareholders.get_mut(index) {
shareholder.resident_id = value;
// Mark documents as generating due to shareholder change
if self.documents_generated {
self.documents_generating = true;
self.schedule_document_generation_completion(ctx);
}
}
}
StepTwoCombinedMsg::UpdateShareholderPercentage(index, value) => {
if let Some(shareholder) = self.form_data.shareholders.get_mut(index) {
shareholder.percentage = value.parse().unwrap_or(0.0);
// Mark documents as generating due to shareholder change
if self.documents_generated {
self.documents_generating = true;
self.schedule_document_generation_completion(ctx);
}
}
}
StepTwoCombinedMsg::UpdateShareholderStructure(structure) => {
self.form_data.shareholder_structure = structure;
// If switching to equal, redistribute percentages equally
if matches!(self.form_data.shareholder_structure, ShareholderStructure::Equal) {
let count = self.form_data.shareholders.len() as f64;
let equal_percentage = 100.0 / count;
for shareholder in &mut self.form_data.shareholders {
shareholder.percentage = equal_percentage;
}
}
// Mark documents as generating due to shareholder structure change
if self.documents_generated {
self.documents_generating = true;
self.schedule_document_generation_completion(ctx);
}
}
// Bylaw template handling
StepTwoCombinedMsg::SelectBylawTemplate(template) => {
self.selected_bylaw_template = Some(template);
self.documents_generated = true;
self.documents_generating = false; // Documents are now ready
}
// Document actions
StepTwoCombinedMsg::ViewDocument(document_name) => {
self.current_document = Some(document_name);
self.show_document_modal = true;
}
StepTwoCombinedMsg::SignDocument(document_name) => {
self.signed_documents.insert(document_name);
web_sys::console::log_1(&format!("Document signed: {:?}", self.signed_documents).into());
}
StepTwoCombinedMsg::CloseDocumentModal => {
self.show_document_modal = false;
self.current_document = None;
}
StepTwoCombinedMsg::DocumentGenerationComplete => {
self.documents_generating = false;
}
}
// Notify parent of form data changes
ctx.props().on_form_update.emit(self.form_data.clone());
true
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
// Ensure at least one shareholder exists
if self.form_data.shareholders.is_empty() {
self.form_data.shareholders.push(Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 100.0,
});
}
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let total_percentage: f64 = self.form_data.shareholders.iter()
.map(|s| s.percentage)
.sum();
let is_equal_structure = matches!(self.form_data.shareholder_structure, ShareholderStructure::Equal);
html! {
<>
<div class="step-content">
// Shareholders Section
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="text-secondary mb-0">
{"Shareholders"} <span class="text-danger">{"*"}</span>
</h5>
<div class="d-flex align-items-center gap-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="equalOwnership"
checked={is_equal_structure}
onchange={link.callback(move |_| {
if is_equal_structure {
StepTwoCombinedMsg::UpdateShareholderStructure(ShareholderStructure::Custom)
} else {
StepTwoCombinedMsg::UpdateShareholderStructure(ShareholderStructure::Equal)
}
})}
/>
<label class="form-check-label" for="equalOwnership">
{"Equal Ownership"}
</label>
</div>
<button
type="button"
class="btn btn-outline-success btn-sm"
onclick={link.callback(|_| StepTwoCombinedMsg::AddShareholder)}
>
<i class="bi bi-plus-circle me-1"></i>{"Add Shareholder"}
</button>
</div>
</div>
{if total_percentage != 100.0 && total_percentage > 0.0 {
html! {
<div class="alert alert-warning alert-sm mb-3">
<i class="bi bi-exclamation-triangle me-2"></i>
{"Total: "}{total_percentage}{"% (should be 100%)"}
</div>
}
} else if total_percentage == 100.0 {
html! {
<div class="alert alert-success alert-sm mb-3">
<i class="bi bi-check-circle me-2"></i>
{"Total: 100% ✓"}
</div>
}
} else {
html! {}
}}
// Shareholder headers
<div class="row mb-2 text-muted small fw-bold">
<div class="col-1">{"#"}</div>
<div class="col-4">{"Full Legal Name"}</div>
<div class="col-3">{"Resident ID"}</div>
<div class="col-3">{"Ownership %"}</div>
<div class="col-1"></div>
</div>
{for self.form_data.shareholders.iter().enumerate().map(|(index, shareholder)| {
self.render_compact_shareholder_form(ctx, index, shareholder)
})}
</div>
</div>
// Compact Bylaw Template Selection
<div class="row mb-4">
<div class="col-12">
<div class="row align-items-center">
<div class="col-md-3">
<label for="bylawTemplate" class="form-label mb-0">
{"Bylaw Template"} <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="col-md-9">
<select
class="form-select"
id="bylawTemplate"
value={match &self.selected_bylaw_template {
Some(BylawTemplate::Standard) => "standard",
Some(BylawTemplate::Startup) => "startup",
Some(BylawTemplate::Enterprise) => "enterprise",
Some(BylawTemplate::Cooperative) => "cooperative",
None => "",
}}
onchange={link.callback(|e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
match input.value().as_str() {
"standard" => StepTwoCombinedMsg::SelectBylawTemplate(BylawTemplate::Standard),
"startup" => StepTwoCombinedMsg::SelectBylawTemplate(BylawTemplate::Startup),
"enterprise" => StepTwoCombinedMsg::SelectBylawTemplate(BylawTemplate::Enterprise),
"cooperative" => StepTwoCombinedMsg::SelectBylawTemplate(BylawTemplate::Cooperative),
_ => StepTwoCombinedMsg::SelectBylawTemplate(BylawTemplate::Standard),
}
})}
>
<option value="">{"Select bylaw template..."}</option>
<option value="standard">{"Standard Bylaws - Basic corporate governance structure"}</option>
<option value="startup">{"Startup-Friendly Bylaws - Flexible structure with equity incentives"}</option>
<option value="enterprise">{"Enterprise Bylaws - Comprehensive governance framework"}</option>
<option value="cooperative">{"Cooperative Bylaws - Democratic governance structure"}</option>
</select>
</div>
</div>
</div>
</div>
// Generated Documents Section - Always visible
{self.render_generated_documents(ctx)}
</div>
// Document Modal
{
if self.show_document_modal {
html! {
<div class="modal fade show" style="display: block;" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
{ self.current_document.as_ref().unwrap_or(&"Document".to_string()) }
</h5>
<button type="button" class="btn-close" onclick={ctx.link().callback(|_| StepTwoCombinedMsg::CloseDocumentModal)}></button>
</div>
<div class="modal-body">
<div class="bg-light p-3 rounded">
{
if let Some(doc_name) = &self.current_document {
match doc_name.as_str() {
"articles" => html! {
<pre class="mb-0">
{ "# Articles of Formation\n\n" }
{ format!("**Company Name:** {}\n", self.form_data.company_name) }
{ format!("**Company Type:** {:?}\n", self.form_data.company_type) }
{ "**Purpose:** General business purposes\n\n" }
{ "## Shareholders\n" }
{
for self.form_data.shareholders.iter().enumerate().map(|(i, shareholder)| {
html! {
<div>
{ format!("{}. {} (ID: {}) - {}%\n",
i + 1,
shareholder.name,
shareholder.resident_id,
shareholder.percentage
) }
</div>
}
})
}
</pre>
},
"bylaws" => html! {
<pre class="mb-0">
{ "# Company Bylaws\n\n" }
{ format!("**Company:** {}\n", self.form_data.company_name) }
{ format!("**Template:** {}\n\n",
if let Some(template) = &self.selected_bylaw_template {
template.get_display_name()
} else {
"Standard"
}
) }
{ "## Article I - Corporate Offices\n" }
{ "The registered office shall be located as specified in the Articles of Formation.\n\n" }
{ "## Article II - Shareholders\n" }
{ "The corporation is authorized to issue shares as detailed in the Articles of Formation.\n\n" }
{ "## Article III - Board of Directors\n" }
{ "The business and affairs of the corporation shall be managed by the board of directors.\n\n" }
{ "## Article IV - Officers\n" }
{ "The officers of the corporation shall consist of a President, Secretary, and Treasurer.\n" }
</pre>
},
_ => html! { <p>{ "Document content not available" }</p> }
}
} else {
html! { <p>{ "No document selected" }</p> }
}
}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={ctx.link().callback(|_| StepTwoCombinedMsg::CloseDocumentModal)}>
{ "Close" }
</button>
{
if let Some(doc_name) = &self.current_document {
if !self.signed_documents.contains(doc_name) {
let doc_name_clone = doc_name.clone();
html! {
<button type="button" class="btn btn-primary"
onclick={ctx.link().callback(move |_| StepTwoCombinedMsg::SignDocument(doc_name_clone.clone()))}>
{ "Sign Document" }
</button>
}
} else {
html! {
<span class="badge bg-success">{ "✓ Signed" }</span>
}
}
} else {
html! {}
}
}
</div>
</div>
</div>
<div class="modal-backdrop fade show"></div>
</div>
}
} else {
html! {}
}
}
</>
}
}
}
impl StepTwoCombined {
fn render_compact_shareholder_form(&self, ctx: &Context<Self>, index: usize, shareholder: &Shareholder) -> Html {
let link = ctx.link();
let can_remove = self.form_data.shareholders.len() > 1;
let is_equal_structure = matches!(self.form_data.shareholder_structure, ShareholderStructure::Equal);
html! {
<div class="row mb-2 align-items-center">
<div class="col-1">
<span class="badge bg-secondary">{index + 1}</span>
</div>
<div class="col-4">
<input
type="text"
class="form-control form-control-sm"
placeholder="Full legal name"
value={shareholder.name.clone()}
oninput={link.callback(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepTwoCombinedMsg::UpdateShareholderName(index, input.value())
})}
required=true
/>
</div>
<div class="col-3">
<input
type="text"
class="form-control form-control-sm"
placeholder="Resident ID"
value={shareholder.resident_id.clone()}
oninput={link.callback(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepTwoCombinedMsg::UpdateShareholderResidentId(index, input.value())
})}
required=true
/>
</div>
<div class="col-3">
<div class="input-group input-group-sm">
<input
type="number"
class="form-control"
placeholder="0"
min="0"
max="100"
step="0.01"
value={shareholder.percentage.to_string()}
oninput={link.callback(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
StepTwoCombinedMsg::UpdateShareholderPercentage(index, input.value())
})}
disabled={is_equal_structure}
required=true
/>
<span class="input-group-text">{"%"}</span>
</div>
</div>
<div class="col-1">
{if can_remove {
html! {
<button
type="button"
class="btn btn-outline-danger btn-sm"
onclick={link.callback(move |_| StepTwoCombinedMsg::RemoveShareholder(index))}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Remove shareholder"
>
<i class="bi bi-trash"></i>
</button>
}
} else {
html! {}
}}
</div>
</div>
}
}
fn render_generated_documents(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// Determine document status
let (status_icon, status_text, description_text) = if !self.documents_generated {
("", "Pending", "Documents will be generated once you select a bylaw template.")
} else if self.documents_generating {
("🔄", "Generating", "Documents are being regenerated based on your recent changes.")
} else {
("", "Ready", "Based on your selections, the following documents have been generated and are ready for review and signing.")
};
html! {
<div class="row">
<div class="col-12">
<h5 class="text-secondary mb-3">
{"Generated Documents"} <span class="text-success">{status_icon}</span>
</h5>
<p class="text-muted mb-3">{description_text}</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>{"Document"}</th>
<th>{"Description"}</th>
<th>{"Status"}</th>
<th class="text-end">{"Actions"}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<i class="bi bi-file-earmark-text me-2 text-primary"></i>
<strong>{"Articles of Formation"}</strong>
</td>
<td class="text-muted">{"Legal document establishing your company's existence and basic structure"}</td>
<td>
{if !self.documents_generated {
html! { <span class="badge bg-secondary">{"Pending"}</span> }
} else if self.documents_generating {
html! { <span class="badge bg-info">{"Generating..."}</span> }
} else {
html! { <span class="badge bg-warning">{"Ready for Review"}</span> }
}}
</td>
<td class="text-end">
{if self.documents_generated && !self.documents_generating {
html! {
<div class="btn-group btn-group-sm">
<button
type="button"
class="btn btn-outline-primary"
onclick={link.callback(|_| StepTwoCombinedMsg::ViewDocument("articles".to_string()))}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Review document carefully before signing"
>
<i class="bi bi-eye me-1"></i>{"View"}
</button>
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| StepTwoCombinedMsg::SignDocument("articles".to_string()))}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Documents are legally binding once signed"
>
<i class="bi bi-pen me-1"></i>{"Sign"}
</button>
</div>
}
} else {
html! {
<span class="text-muted small">{"Not available"}</span>
}
}}
</td>
</tr>
<tr>
<td>
<i class="bi bi-file-earmark-ruled me-2 text-info"></i>
<strong>{"Company Bylaws"}</strong>
</td>
<td class="text-muted">
{"Internal governance rules based on "}
{if let Some(template) = &self.selected_bylaw_template {
template.get_display_name()
} else {
"selected template"
}}
</td>
<td>
{if !self.documents_generated {
html! { <span class="badge bg-secondary">{"Pending"}</span> }
} else if self.documents_generating {
html! { <span class="badge bg-info">{"Generating..."}</span> }
} else {
html! { <span class="badge bg-warning">{"Ready for Review"}</span> }
}}
</td>
<td class="text-end">
{if self.documents_generated && !self.documents_generating {
html! {
<div class="btn-group btn-group-sm">
<button
type="button"
class="btn btn-outline-primary"
onclick={link.callback(|_| StepTwoCombinedMsg::ViewDocument("bylaws".to_string()))}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="Review document carefully before signing"
>
<i class="bi bi-eye me-1"></i>{"View"}
</button>
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| StepTwoCombinedMsg::SignDocument("bylaws".to_string()))}
data-bs-toggle="tooltip"
data-bs-placement="top"
title="You can download copies after signing"
>
<i class="bi bi-pen me-1"></i>{"Sign"}
</button>
</div>
}
} else {
html! {
<span class="text-muted small">{"Not available"}</span>
}
}}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
}
}
fn schedule_document_generation_completion(&self, ctx: &Context<Self>) {
let link = ctx.link().clone();
Timeout::new(2000, move || {
link.send_message(StepTwoCombinedMsg::DocumentGenerationComplete);
}).forget();
}
}

View File

@ -0,0 +1,68 @@
use yew::prelude::*;
use crate::models::*;
#[derive(Properties, PartialEq)]
pub struct EntitiesTabsProps {
pub active_tab: ActiveTab,
pub on_tab_change: Callback<ActiveTab>,
}
#[function_component(EntitiesTabs)]
pub fn entities_tabs(props: &EntitiesTabsProps) -> Html {
let on_companies_click = {
let on_tab_change = props.on_tab_change.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
on_tab_change.emit(ActiveTab::Companies);
})
};
let on_register_click = {
let on_tab_change = props.on_tab_change.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
on_tab_change.emit(ActiveTab::RegisterCompany);
})
};
html! {
<div class="mb-4">
<div class="card-body">
<ul class="nav nav-tabs" id="companyTabs" role="tablist">
<li class="nav-item" role="presentation">
<button
class={classes!(
"nav-link",
if props.active_tab == ActiveTab::Companies { "active" } else { "" }
)}
id="manage-tab"
type="button"
role="tab"
aria-controls="manage"
aria-selected={if props.active_tab == ActiveTab::Companies { "true" } else { "false" }}
onclick={on_companies_click}
>
<i class="bi bi-building me-1"></i>{" Manage Companies"}
</button>
</li>
<li class="nav-item" role="presentation">
<button
class={classes!(
"nav-link",
if props.active_tab == ActiveTab::RegisterCompany { "active" } else { "" }
)}
id="register-tab"
type="button"
role="tab"
aria-controls="register"
aria-selected={if props.active_tab == ActiveTab::RegisterCompany { "true" } else { "false" }}
onclick={on_register_click}
>
<i class="bi bi-file-earmark-plus me-1"></i>{" Register New Company"}
</button>
</li>
</ul>
</div>
</div>
}
}

View File

@ -0,0 +1,9 @@
pub mod entities_tabs;
pub mod companies_list;
pub mod company_registration;
pub mod resident_registration;
pub use entities_tabs::*;
pub use companies_list::*;
pub use company_registration::*;
pub use resident_registration::*;

View File

@ -0,0 +1,23 @@
pub mod step_one;
pub mod step_two;
pub mod step_three;
pub mod step_four;
pub mod step_five;
pub mod resident_wizard;
pub mod step_info_kyc;
pub mod step_payment;
pub mod step_payment_stripe;
pub mod simple_resident_wizard;
pub mod simple_step_info;
pub use step_one::*;
pub use step_two::*;
pub use step_three::*;
pub use step_four::*;
pub use step_five::*;
pub use resident_wizard::*;
pub use step_info_kyc::*;
pub use step_payment::*;
pub use step_payment_stripe::*;
pub use simple_resident_wizard::*;
pub use simple_step_info::*;

View File

@ -0,0 +1,689 @@
use yew::prelude::*;
use gloo::timers::callback::Timeout;
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentStatus};
use super::{
step_one::StepOne,
step_two::StepTwo,
step_three::StepThree,
step_four::StepFour,
step_five::StepFive,
};
#[derive(Properties, PartialEq)]
pub struct ResidentWizardProps {
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,
}
pub enum ResidentWizardMsg {
NextStep,
PrevStep,
UpdateFormData(DigitalResidentFormData),
ProcessRegistration,
RegistrationComplete(DigitalResident),
RegistrationError(String),
ShowValidationToast(Vec<String>),
HideValidationToast,
}
pub struct ResidentWizard {
current_step: u8,
form_data: DigitalResidentFormData,
validation_errors: Vec<String>,
processing_registration: bool,
show_validation_toast: bool,
}
impl Component for ResidentWizard {
type Message = ResidentWizardMsg;
type Properties = ResidentWizardProps;
fn create(ctx: &Context<Self>) -> Self {
// Determine initial step based on props
let current_step = if ctx.props().success_resident_id.is_some() {
// Show success step
6
} else if ctx.props().show_failure {
// Show failure, go back to payment step
5
} else {
// Normal flow - start from step 1
1
};
Self {
current_step,
form_data: DigitalResidentFormData::default(),
validation_errors: Vec::new(),
processing_registration: false,
show_validation_toast: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ResidentWizardMsg::NextStep => {
// Validate current step
let validation_result = self.validate_current_step();
if !validation_result.is_valid {
self.validation_errors = validation_result.errors;
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(ResidentWizardMsg::HideValidationToast);
}).forget();
return true;
}
if self.current_step < 6 {
if self.current_step == 5 {
// Process registration on final step
ctx.link().send_message(ResidentWizardMsg::ProcessRegistration);
} else {
self.current_step += 1;
}
true
} else {
false
}
}
ResidentWizardMsg::PrevStep => {
if self.current_step > 1 {
self.current_step -= 1;
true
} else {
false
}
}
ResidentWizardMsg::UpdateFormData(new_form_data) => {
self.form_data = new_form_data;
true
}
ResidentWizardMsg::ProcessRegistration => {
self.processing_registration = true;
// Simulate registration processing
let link = ctx.link().clone();
let form_data = self.form_data.clone();
Timeout::new(2000, move || {
// Legacy wizard - create a minimal resident for compatibility
let resident = DigitalResident {
id: 1,
full_name: form_data.full_name,
email: form_data.email,
phone: form_data.phone,
date_of_birth: form_data.date_of_birth,
nationality: form_data.nationality,
passport_number: form_data.passport_number,
passport_expiry: form_data.passport_expiry,
current_address: form_data.current_address,
city: form_data.city,
country: form_data.country,
postal_code: form_data.postal_code,
occupation: form_data.occupation,
employer: form_data.employer,
annual_income: form_data.annual_income,
education_level: form_data.education_level,
selected_services: form_data.requested_services,
payment_plan: form_data.payment_plan,
registration_date: "2025-01-01".to_string(),
status: crate::models::company::ResidentStatus::Pending,
kyc_documents_uploaded: false,
kyc_status: crate::models::company::KycStatus::NotStarted,
public_key: form_data.public_key,
};
link.send_message(ResidentWizardMsg::RegistrationComplete(resident));
}).forget();
true
}
ResidentWizardMsg::RegistrationComplete(resident) => {
self.processing_registration = false;
// Move to success step
self.current_step = 6;
// Notify parent component
ctx.props().on_registration_complete.emit(resident);
true
}
ResidentWizardMsg::RegistrationError(error) => {
self.processing_registration = false;
// Stay on payment step and show error
self.validation_errors = vec![format!("Registration failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(ResidentWizardMsg::HideValidationToast);
}).forget();
true
}
ResidentWizardMsg::ShowValidationToast(errors) => {
self.validation_errors = errors;
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(ResidentWizardMsg::HideValidationToast);
}).forget();
true
}
ResidentWizardMsg::HideValidationToast => {
self.show_validation_toast = false;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let (step_title, step_description, step_icon) = self.get_step_info();
html! {
<div class="card" style="height: calc(100vh - 200px); display: flex; flex-direction: column;">
<div class="card-header flex-shrink-0">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h5 class="mb-1">
<i class={format!("bi {} me-2", step_icon)}></i>{step_title}
</h5>
<p class="text-muted mb-0 small">{step_description}</p>
</div>
<button
class="btn btn-outline-secondary btn-sm ms-3"
onclick={ctx.props().on_back_to_parent.reform(|_| ())}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
</div>
<div class="card-body flex-grow-1 overflow-auto">
<form>
{self.render_current_step(ctx)}
</form>
</div>
{if self.current_step <= 5 {
self.render_footer_navigation(ctx)
} else {
html! {}
}}
{if self.show_validation_toast {
self.render_validation_toast(ctx)
} else {
html! {}
}}
</div>
}
}
}
impl ResidentWizard {
fn render_current_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let form_data = self.form_data.clone();
let on_form_update = link.callback(ResidentWizardMsg::UpdateFormData);
match self.current_step {
1 => html! {
<StepOne
form_data={form_data}
on_change={on_form_update}
/>
},
2 => html! {
<StepTwo
form_data={form_data}
on_change={on_form_update}
/>
},
3 => html! {
<StepThree
form_data={form_data}
on_change={on_form_update}
/>
},
4 => html! {
<StepFour
form_data={form_data}
on_change={on_form_update}
/>
},
5 => html! {
<StepFive
form_data={form_data}
on_change={on_form_update}
/>
},
6 => {
// Success step
self.render_success_step(ctx)
},
_ => html! { <div>{"Invalid step"}</div> }
}
}
fn render_footer_navigation(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
// Previous button (left)
<div style="width: 120px;">
{if self.current_step > 1 {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={link.callback(|_| ResidentWizardMsg::PrevStep)}
disabled={self.processing_registration}
>
<i class="bi bi-arrow-left me-1"></i>{"Previous"}
</button>
}
} else {
html! {}
}}
</div>
// Step indicator (center)
<div class="d-flex align-items-center">
{for (1..=5).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 < 5 {
html! {
<div class={format!("mx-1 {}", if is_completed { "bg-success" } else { "bg-secondary" })}
style="height: 2px; width: 24px;"></div>
}
} else {
html! {}
}}
</div>
}
})}
</div>
// Next/Register button (right)
<div style="width: 150px;" class="text-end">
{if self.current_step < 5 {
html! {
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| ResidentWizardMsg::NextStep)}
disabled={self.processing_registration}
>
{"Next"}<i class="bi bi-arrow-right ms-1"></i>
</button>
}
} else if self.current_step == 5 {
// Registration button for step 5
let can_register = self.form_data.legal_agreements.all_agreed() && !self.processing_registration;
html! {
<button
type="button"
class="btn btn-success text-nowrap"
disabled={!can_register}
onclick={link.callback(|_| ResidentWizardMsg::NextStep)}
>
{if self.processing_registration {
html! {
<>
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
<span>{"Processing..."}</span>
</>
}
} else {
html! {
<>
<i class="bi bi-person-plus me-2"></i>
<span>{"Complete Registration"}</span>
</>
}
}}
</button>
}
} else {
html! {}
}}
</div>
</div>
</div>
}
}
fn render_validation_toast(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let close_toast = link.callback(|_| ResidentWizardMsg::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>
}
}
fn get_step_info(&self) -> (&'static str, &'static str, &'static str) {
match self.current_step {
1 => (
"Personal Information",
"Provide your basic personal details for digital resident registration.",
"bi-person"
),
2 => (
"Address Information",
"Enter your current and permanent address information.",
"bi-house"
),
3 => (
"Professional Information",
"Share your professional background and qualifications.",
"bi-briefcase"
),
4 => (
"Digital Services & Preferences",
"Select the digital services you'd like access to and set your preferences.",
"bi-gear"
),
5 => (
"Payment Plan & Legal Agreements",
"Choose your payment plan and review the legal agreements.",
"bi-credit-card"
),
6 => (
"Registration Complete",
"Your digital resident registration has been successfully completed.",
"bi-check-circle-fill"
),
_ => (
"Digital Resident Registration",
"Complete the registration process to become a digital resident.",
"bi-person-plus"
)
}
}
fn render_success_step(&self, ctx: &Context<Self>) -> Html {
let resident_id = ctx.props().success_resident_id.unwrap_or(1);
html! {
<div class="text-center py-5">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h2 class="text-success mb-3">{"Registration Successful!"}</h2>
<p class="lead mb-4">
{"Your digital resident registration has been successfully submitted and is now pending approval."}
</p>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-success">
<div class="card-body">
<h5 class="card-title text-success">
<i class="bi bi-info-circle me-2"></i>{"What happens next?"}
</h5>
<div class="text-start">
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-success rounded-pill">{"1"}</span>
</div>
<div>
<strong>{"Identity Verification"}</strong>
<p class="mb-0 text-muted">{"Our team will verify your identity and submitted documents."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-primary rounded-pill">{"2"}</span>
</div>
<div>
<strong>{"Background Check"}</strong>
<p class="mb-0 text-muted">{"We'll conduct necessary background checks and compliance verification."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-info rounded-pill">{"3"}</span>
</div>
<div>
<strong>{"Approval & Activation"}</strong>
<p class="mb-0 text-muted">{"Once approved, your digital resident status will be activated and you'll gain access to selected services."}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="d-flex justify-content-center">
<button
class="btn btn-success btn-lg"
onclick={ctx.props().on_back_to_parent.reform(|_| ())}
>
<i class="bi bi-list me-2"></i>{"View My Registrations"}
</button>
</div>
</div>
<div class="mt-4">
<div class="alert alert-info">
<i class="bi bi-envelope me-2"></i>
{"You will receive email updates about your registration status. The approval process typically takes 3-5 business days."}
</div>
</div>
</div>
}
}
fn validate_current_step(&self) -> ValidationResult {
match self.current_step {
1 => validate_step_one(&self.form_data),
2 => validate_step_two(&self.form_data),
3 => validate_step_three(&self.form_data),
4 => validate_step_four(&self.form_data),
5 => validate_step_five(&self.form_data),
_ => ValidationResult::valid(),
}
}
}
#[derive(Clone, PartialEq)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<String>,
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
}
}
pub fn invalid(errors: Vec<String>) -> Self {
Self {
is_valid: false,
errors,
}
}
}
// Validation functions for each step
fn validate_step_one(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.phone.trim().is_empty() {
errors.push("Phone number is required".to_string());
}
if data.date_of_birth.trim().is_empty() {
errors.push("Date of birth is required".to_string());
}
if data.nationality.trim().is_empty() {
errors.push("Nationality is required".to_string());
}
if data.passport_number.trim().is_empty() {
errors.push("Passport number is required".to_string());
}
if data.passport_expiry.trim().is_empty() {
errors.push("Passport expiry date is required".to_string());
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
fn validate_step_two(data: &DigitalResidentFormData) -> ValidationResult {
let mut errors = Vec::new();
if data.current_address.trim().is_empty() {
errors.push("Current address is required".to_string());
}
if data.city.trim().is_empty() {
errors.push("City is required".to_string());
}
if data.country.trim().is_empty() {
errors.push("Country is required".to_string());
}
if data.postal_code.trim().is_empty() {
errors.push("Postal code is required".to_string());
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
fn validate_step_three(data: &DigitalResidentFormData) -> ValidationResult {
let mut errors = Vec::new();
if data.occupation.trim().is_empty() {
errors.push("Occupation is required".to_string());
}
if data.education_level.trim().is_empty() {
errors.push("Education level is required".to_string());
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
fn validate_step_four(data: &DigitalResidentFormData) -> ValidationResult {
let mut errors = Vec::new();
if data.requested_services.is_empty() {
errors.push("Please select at least one digital service".to_string());
}
if data.preferred_language.trim().is_empty() {
errors.push("Preferred language is required".to_string());
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
fn validate_step_five(data: &DigitalResidentFormData) -> ValidationResult {
let mut errors = Vec::new();
if !data.legal_agreements.all_agreed() {
let missing = data.legal_agreements.missing_agreements();
errors.push(format!("Please accept all required agreements: {}", missing.join(", ")));
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}

View File

@ -0,0 +1,710 @@
use yew::prelude::*;
use gloo::timers::callback::Timeout;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{console, js_sys};
use serde_json::json;
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::{ResidentService, ResidentRegistration, ResidentRegistrationStatus};
use super::{SimpleStepInfo, StepPaymentStripe};
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn createPaymentIntent(form_data: &JsValue) -> js_sys::Promise;
}
#[derive(Properties, PartialEq)]
pub struct SimpleResidentWizardProps {
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,
}
pub enum SimpleResidentWizardMsg {
NextStep,
PrevStep,
UpdateFormData(DigitalResidentFormData),
ProcessRegistration,
RegistrationComplete(DigitalResident),
RegistrationError(String),
HideValidationToast,
ProcessPayment,
PaymentPlanChanged(ResidentPaymentPlan),
ConfirmationChanged(bool),
CreatePaymentIntent,
PaymentIntentCreated(String),
PaymentIntentError(String),
}
pub struct SimpleResidentWizard {
current_step: u8,
form_data: DigitalResidentFormData,
validation_errors: Vec<String>,
processing_registration: bool,
show_validation_toast: bool,
current_registration_id: Option<u32>,
client_secret: Option<String>,
processing_payment: bool,
confirmation_checked: bool,
}
impl Component for SimpleResidentWizard {
type Message = SimpleResidentWizardMsg;
type Properties = SimpleResidentWizardProps;
fn create(ctx: &Context<Self>) -> Self {
// Determine initial step based on props
let (form_data, current_step) = if ctx.props().success_resident_id.is_some() {
// Show success step
(DigitalResidentFormData::default(), 3)
} else if ctx.props().show_failure {
// Show failure, go back to payment step
let (form_data, _) = ResidentService::load_resident_registration_form()
.unwrap_or_else(|| (DigitalResidentFormData::default(), 2));
(form_data, 2)
} else {
// Normal flow - try to load saved form data
let (form_data, saved_step) = ResidentService::load_resident_registration_form()
.unwrap_or_else(|| (DigitalResidentFormData::default(), 1));
// Ensure step is within valid range for 2-step form
let adjusted_step = if saved_step > 2 { 2 } else { saved_step };
(form_data, adjusted_step)
};
Self {
current_step,
form_data,
validation_errors: Vec::new(),
processing_registration: false,
show_validation_toast: false,
current_registration_id: None,
client_secret: None,
processing_payment: false,
confirmation_checked: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
SimpleResidentWizardMsg::NextStep => {
// Validate current step
let validation_result = ResidentService::validate_resident_step(&self.form_data, self.current_step);
if !validation_result.is_valid {
self.validation_errors = validation_result.errors;
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(SimpleResidentWizardMsg::HideValidationToast);
}).forget();
return true;
}
if self.current_step < 3 {
if self.current_step == 2 {
// Process registration on final step
ctx.link().send_message(SimpleResidentWizardMsg::ProcessRegistration);
} else {
self.current_step += 1;
// If moving to payment step, create payment intent
if self.current_step == 2 {
ctx.link().send_message(SimpleResidentWizardMsg::CreatePaymentIntent);
}
self.auto_save();
}
true
} else {
false
}
}
SimpleResidentWizardMsg::PrevStep => {
if self.current_step > 1 {
self.current_step -= 1;
self.auto_save();
true
} else {
false
}
}
SimpleResidentWizardMsg::UpdateFormData(new_form_data) => {
self.form_data = new_form_data;
self.schedule_auto_save(ctx);
true
}
SimpleResidentWizardMsg::ProcessRegistration => {
self.processing_registration = true;
// Simulate registration processing
let link = ctx.link().clone();
let form_data = self.form_data.clone();
let registration_id = self.current_registration_id;
Timeout::new(2000, move || {
// Create resident and update registration status
match ResidentService::create_resident_from_form(&form_data) {
Ok(resident) => {
// Update registration status to PendingApproval
if let Some(reg_id) = registration_id {
let mut registrations = ResidentService::get_resident_registrations();
if let Some(registration) = registrations.iter_mut().find(|r| r.id == reg_id) {
registration.status = ResidentRegistrationStatus::PendingApproval;
let _ = ResidentService::save_resident_registrations(&registrations);
}
} else {
// Create new registration if none exists
let now = js_sys::Date::new_0();
let created_at = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let registration = ResidentRegistration {
id: 0, // Will be set by save_resident_registration
full_name: form_data.full_name.clone(),
email: form_data.email.clone(),
status: ResidentRegistrationStatus::PendingApproval,
created_at,
form_data: form_data.clone(),
current_step: 3, // Completed
};
let _ = ResidentService::save_resident_registration(registration);
}
// Clear saved form data
let _ = ResidentService::clear_resident_registration_form();
link.send_message(SimpleResidentWizardMsg::RegistrationComplete(resident));
}
Err(error) => {
link.send_message(SimpleResidentWizardMsg::RegistrationError(error));
}
}
}).forget();
true
}
SimpleResidentWizardMsg::RegistrationComplete(resident) => {
self.processing_registration = false;
// Move to success step
self.current_step = 3;
// Notify parent component
ctx.props().on_registration_complete.emit(resident);
true
}
SimpleResidentWizardMsg::RegistrationError(error) => {
self.processing_registration = false;
// Stay on payment step and show error
self.validation_errors = vec![format!("Registration failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(SimpleResidentWizardMsg::HideValidationToast);
}).forget();
true
}
SimpleResidentWizardMsg::HideValidationToast => {
self.show_validation_toast = false;
true
}
SimpleResidentWizardMsg::ProcessPayment => {
self.processing_payment = true;
true
}
SimpleResidentWizardMsg::PaymentPlanChanged(plan) => {
self.form_data.payment_plan = plan;
self.client_secret = None; // Reset client secret when plan changes
ctx.link().send_message(SimpleResidentWizardMsg::CreatePaymentIntent);
true
}
SimpleResidentWizardMsg::ConfirmationChanged(checked) => {
self.confirmation_checked = checked;
true
}
SimpleResidentWizardMsg::CreatePaymentIntent => {
console::log_1(&"🔧 Creating payment intent for resident registration...".into());
self.create_payment_intent(ctx);
false
}
SimpleResidentWizardMsg::PaymentIntentCreated(client_secret) => {
self.client_secret = Some(client_secret);
true
}
SimpleResidentWizardMsg::PaymentIntentError(error) => {
self.validation_errors = vec![format!("Payment setup failed: {}", error)];
self.show_validation_toast = true;
// Auto-hide toast after 5 seconds
let link = ctx.link().clone();
Timeout::new(5000, move || {
link.send_message(SimpleResidentWizardMsg::HideValidationToast);
}).forget();
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let (step_title, step_description, step_icon) = self.get_step_info();
html! {
<div class="card" style="height: calc(100vh - 200px); display: flex; flex-direction: column;">
<div class="card-header flex-shrink-0">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h5 class="mb-1">
<i class={format!("bi {} me-2", step_icon)}></i>{step_title}
</h5>
<p class="text-muted mb-0 small">{step_description}</p>
</div>
<button
class="btn btn-outline-secondary btn-sm ms-3"
onclick={ctx.props().on_back_to_parent.reform(|_| ())}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
</div>
<div class="card-body flex-grow-1 overflow-auto">
<form>
{self.render_current_step(ctx)}
</form>
</div>
{if self.current_step <= 2 {
self.render_footer_navigation(ctx)
} else {
html! {}
}}
{if self.show_validation_toast {
self.render_validation_toast(ctx)
} else {
html! {}
}}
</div>
}
}
}
impl SimpleResidentWizard {
fn render_current_step(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let form_data = self.form_data.clone();
let on_form_update = link.callback(SimpleResidentWizardMsg::UpdateFormData);
match self.current_step {
1 => html! {
<SimpleStepInfo
form_data={form_data}
on_change={on_form_update}
/>
},
2 => html! {
<StepPaymentStripe
form_data={form_data}
client_secret={self.client_secret.clone()}
processing_payment={self.processing_payment}
on_process_payment={link.callback(|_| SimpleResidentWizardMsg::ProcessPayment)}
on_payment_complete={link.callback(SimpleResidentWizardMsg::RegistrationComplete)}
on_payment_error={link.callback(SimpleResidentWizardMsg::RegistrationError)}
on_payment_plan_change={link.callback(SimpleResidentWizardMsg::PaymentPlanChanged)}
on_confirmation_change={link.callback(SimpleResidentWizardMsg::ConfirmationChanged)}
/>
},
3 => {
// Success step
self.render_success_step(ctx)
},
_ => html! { <div>{"Invalid step"}</div> }
}
}
fn render_footer_navigation(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center">
// Previous button (left)
<div style="width: 120px;">
{if self.current_step > 1 {
html! {
<button
type="button"
class="btn btn-outline-secondary"
onclick={link.callback(|_| SimpleResidentWizardMsg::PrevStep)}
disabled={self.processing_registration}
>
<i class="bi bi-arrow-left me-1"></i>{"Previous"}
</button>
}
} else {
html! {}
}}
</div>
// Step indicator (center)
<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>
}
})}
</div>
// Next/Register button (right)
<div style="width: 150px;" class="text-end">
{if self.current_step < 2 {
html! {
<button
type="button"
class="btn btn-success"
onclick={link.callback(|_| SimpleResidentWizardMsg::NextStep)}
disabled={self.processing_registration}
>
{"Next"}<i class="bi bi-arrow-right ms-1"></i>
</button>
}
} else if self.current_step == 2 {
// Payment is handled by the StepPaymentStripe component itself
// No button needed here as the payment component has its own payment button
html! {}
} else {
html! {}
}}
</div>
</div>
</div>
}
}
fn render_validation_toast(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
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>
}
}
fn get_step_info(&self) -> (&'static str, &'static str, &'static str) {
match self.current_step {
1 => (
"Personal Information & KYC",
"Provide your basic information and complete identity verification.",
"bi-person-vcard"
),
2 => (
"Payment Plan & Legal Agreements",
"Choose your payment plan and review the legal agreements.",
"bi-credit-card"
),
3 => (
"Registration Complete",
"Your digital resident registration has been successfully completed.",
"bi-check-circle-fill"
),
_ => (
"Digital Resident Registration",
"Complete the registration process to become a digital resident.",
"bi-person-plus"
)
}
}
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(SimpleResidentWizardMsg::PaymentIntentCreated(client_secret));
}
Err(e) => {
link.send_message(SimpleResidentWizardMsg::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};
console::log_1(&"🔧 Setting up Stripe payment for resident registration".into());
console::log_1(&format!("📋 Resident: {}", form_data.full_name).into());
console::log_1(&format!("💳 Payment plan: {}", form_data.payment_plan.get_display_name()).into());
// Prepare form data for payment intent creation
let payment_data = json!({
"resident_name": form_data.full_name,
"email": form_data.email,
"phone": form_data.phone,
"date_of_birth": form_data.date_of_birth,
"nationality": form_data.nationality,
"passport_number": form_data.passport_number,
"address": form_data.current_address,
"payment_plan": form_data.payment_plan.get_display_name(),
"amount": form_data.payment_plan.get_price(),
"type": "resident_registration"
});
console::log_1(&"📡 Calling server endpoint for resident payment intent creation".into());
// Create request to server endpoint
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::Cors);
let headers = 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| {
let error_msg = format!("Failed to create request: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
// Make the request
let window = web_sys::window().unwrap();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
.map_err(|e| {
let error_msg = format!("Network request failed: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
let resp: Response = resp_value.dyn_into().unwrap();
if !resp.ok() {
let status = resp.status();
let error_msg = format!("Server error: HTTP {}", status);
console::log_1(&format!("{}", error_msg).into());
return Err(error_msg);
}
// Parse response
let json_value = JsFuture::from(resp.json().unwrap()).await
.map_err(|e| {
let error_msg = format!("Failed to parse response: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
// Extract client secret from response
let response_obj = js_sys::Object::from(json_value);
let client_secret_value = js_sys::Reflect::get(&response_obj, &"client_secret".into())
.map_err(|e| {
let error_msg = format!("No client_secret in response: {:?}", e);
console::log_1(&format!("{}", error_msg).into());
error_msg
})?;
let client_secret = client_secret_value.as_string()
.ok_or_else(|| {
let error_msg = "Invalid client secret received from server";
console::log_1(&format!("{}", error_msg).into());
error_msg.to_string()
})?;
console::log_1(&"✅ Payment intent created successfully".into());
console::log_1(&format!("🔑 Client secret received: {}", if client_secret.len() > 10 { "Yes" } else { "No" }).into());
Ok(client_secret)
}
fn render_success_step(&self, ctx: &Context<Self>) -> Html {
let resident_id = ctx.props().success_resident_id.unwrap_or(1);
html! {
<div class="text-center py-5">
<div class="mb-4">
<i class="bi bi-check-circle-fill text-success" style="font-size: 4rem;"></i>
</div>
<h2 class="text-success mb-3">{"Registration Successful!"}</h2>
<p class="lead mb-4">
{"Your digital resident registration has been successfully submitted and is now pending approval."}
</p>
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-success">
<div class="card-body">
<h5 class="card-title text-success">
<i class="bi bi-info-circle me-2"></i>{"What happens next?"}
</h5>
<div class="text-start">
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-success rounded-pill">{"1"}</span>
</div>
<div>
<strong>{"Identity Verification"}</strong>
<p class="mb-0 text-muted">{"Our team will verify your identity and submitted documents."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-primary rounded-pill">{"2"}</span>
</div>
<div>
<strong>{"Background Check"}</strong>
<p class="mb-0 text-muted">{"We'll conduct necessary background checks and compliance verification."}</p>
</div>
</div>
<div class="d-flex align-items-start mb-3">
<div class="me-3">
<span class="badge bg-info rounded-pill">{"3"}</span>
</div>
<div>
<strong>{"Approval & Activation"}</strong>
<p class="mb-0 text-muted">{"Once approved, your digital resident status will be activated and you'll gain access to selected services."}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mt-4">
<div class="d-flex justify-content-center">
<button
class="btn btn-success btn-lg"
onclick={ctx.props().on_back_to_parent.reform(|_| ())}
>
<i class="bi bi-list me-2"></i>{"View My Registrations"}
</button>
</div>
</div>
<div class="mt-4">
<div class="alert alert-info">
<i class="bi bi-envelope me-2"></i>
{"You will receive email updates about your registration status. The approval process typically takes 3-5 business days."}
</div>
</div>
</div>
}
}
fn schedule_auto_save(&mut self, ctx: &Context<Self>) {
// Auto-save after 2 seconds of inactivity
let link = ctx.link().clone();
Timeout::new(2000, move || {
// Auto-save will be handled by the auto_save method
}).forget();
self.auto_save();
}
fn auto_save(&mut self) {
// Save form data to localStorage for recovery
let _ = ResidentService::save_resident_registration_form(&self.form_data, self.current_step);
// Also save as a draft registration
let now = js_sys::Date::new_0();
let created_at = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let status = if self.current_step >= 2 {
ResidentRegistrationStatus::PendingPayment
} else {
ResidentRegistrationStatus::Draft
};
let registration = ResidentRegistration {
id: self.current_registration_id.unwrap_or(0),
full_name: if self.form_data.full_name.is_empty() {
"Draft Registration".to_string()
} else {
self.form_data.full_name.clone()
},
email: self.form_data.email.clone(),
status,
created_at,
form_data: self.form_data.clone(),
current_step: self.current_step,
};
if let Ok(saved_registration) = ResidentService::save_resident_registration(registration) {
self.current_registration_id = Some(saved_registration.id);
}
}
}

View File

@ -0,0 +1,307 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::DigitalResidentFormData;
#[derive(Properties, PartialEq)]
pub struct SimpleStepInfoProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(SimpleStepInfo)]
pub fn simple_step_info(props: &SimpleStepInfoProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let show_private_key = use_state(|| false);
let on_input = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let field_name = input.name();
let value = input.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"full_name" => updated_data.full_name = value,
"email" => updated_data.email = value,
_ => {}
}
on_change.emit(updated_data);
})
};
let on_terms_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |_: Event| {
let mut updated_data = form_data.clone();
updated_data.legal_agreements.terms = !updated_data.legal_agreements.terms;
on_change.emit(updated_data);
})
};
let on_kyc_click = {
Callback::from(move |_: MouseEvent| {
// TODO: Redirect to KYC provider
web_sys::window()
.unwrap()
.alert_with_message("KYC verification will be implemented - redirecting to identity verification provider")
.unwrap();
})
};
let on_generate_keys = {
let form_data = form_data.clone();
let on_change = on_change.clone();
let show_private_key = show_private_key.clone();
Callback::from(move |_: MouseEvent| {
// Generate secp256k1 keypair (simplified for demo)
let private_key = generate_private_key();
let public_key = generate_public_key(&private_key);
let mut updated_data = form_data.clone();
updated_data.public_key = Some(public_key);
updated_data.private_key = Some(private_key);
updated_data.private_key_shown = true;
show_private_key.set(true);
on_change.emit(updated_data);
})
};
let copy_private_key = {
let private_key = form_data.private_key.clone();
Callback::from(move |_: MouseEvent| {
if let Some(key) = &private_key {
// Copy to clipboard using a simple approach
web_sys::window()
.unwrap()
.alert_with_message(&format!("Private key copied! Please save it: {}", key))
.unwrap();
}
})
};
html! {
<>
<div class="row h-100">
// Left side - Form inputs
<div class="col-md-6">
<div class="mb-4">
<label for="full_name" class="form-label">{"Full Name"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control form-control-lg"
id="full_name"
name="full_name"
value={form_data.full_name.clone()}
oninput={on_input.clone()}
placeholder="Enter your full legal name"
title="As it appears on your government-issued ID"
/>
</div>
<div class="mb-4">
<label for="email" class="form-label">{"Email Address"} <span class="text-danger">{"*"}</span></label>
<input
type="email"
class="form-control form-control-lg"
id="email"
name="email"
value={form_data.email.clone()}
oninput={on_input.clone()}
placeholder="your.email@example.com"
title="We'll use this to send you updates about your application"
/>
</div>
<div class="mb-4">
<label class="form-label">{"Identity Verification"} <span class="text-danger">{"*"}</span></label>
<div class="d-grid">
<button
type="button"
class="btn btn-outline-primary btn-lg"
onclick={on_kyc_click}
>
<i class="bi bi-shield-check me-2"></i>
{"Complete KYC Verification"}
</button>
</div>
</div>
<div class="mb-4">
<label class="form-label">{"Digital Identity Keys"}</label>
{if form_data.public_key.is_none() {
html! {
<div class="d-grid">
<button
type="button"
class="btn btn-success btn-lg"
onclick={on_generate_keys}
>
<i class="bi bi-key me-2"></i>
{"Generate Keys"}
</button>
</div>
}
} else {
html! {
<div>
{if *show_private_key && form_data.private_key.is_some() {
html! {
<div class="mb-3 p-3 bg-warning bg-opacity-10 border border-warning rounded">
<strong class="text-warning">{"Private Key (save securely!):"}</strong>
<div class="mt-2 p-2 border rounded" style="font-family: monospace; font-size: 0.9rem; word-break: break-all;">
{form_data.private_key.as_ref().unwrap_or(&"".to_string())}
</div>
</div>
}
} else {
html! {}
}}
<div class="mb-3">
<label class="form-label small">{"Public Key"}</label>
<div class="form-control" style="font-family: monospace; font-size: 0.8rem; word-break: break-all;">
{form_data.public_key.as_ref().unwrap_or(&"".to_string())}
</div>
</div>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
onclick={on_generate_keys}
>
<i class="bi bi-arrow-clockwise me-1"></i>
{"Generate New Keys"}
</button>
</div>
}
}}
</div>
<div class="mb-4">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="terms_agreement"
checked={form_data.legal_agreements.terms}
onchange={on_terms_change}
/>
<label class="form-check-label" for="terms_agreement">
{"I agree to the "}<a href="#" class="text-primary">{"Terms of Service"}</a>{" and "}<a href="#" class="text-primary">{"Privacy Policy"}</a> <span class="text-danger">{"*"}</span>
</label>
</div>
</div>
</div>
// Right side - Residence card preview
<div class="col-md-6">
<div class="d-flex align-items-center justify-content-center h-100">
<div class="residence-card">
<div class="card border-0 shadow-lg" style="width: 350px; background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%); color: white; border-radius: 15px;">
<div class="card-body p-4">
<div class="mb-4">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="mb-0 text-white-50">{"DIGITAL RESIDENT"}</h6>
<small class="text-white-50">{"Zanzibar Digital Freezone"}</small>
</div>
<i class="bi bi-shield-check-fill" style="font-size: 1.5rem; opacity: 0.8;"></i>
</div>
<div class="mb-3">
<div class="text-white-50 small">{"FULL NAME"}</div>
<div class="h5 mb-0 text-white">
{if form_data.full_name.is_empty() {
"Your Name Here"
} else {
&form_data.full_name
}}
</div>
</div>
<div class="mb-3">
<div class="text-white-50 small">{"EMAIL"}</div>
<div class="text-white" style="font-size: 0.9rem;">
{if form_data.email.is_empty() {
"your.email@example.com"
} else {
&form_data.email
}}
</div>
</div>
{if let Some(public_key) = &form_data.public_key {
html! {
<div class="mb-3">
<div class="text-white-50 small">
<i class="bi bi-key me-1"></i>
{"PUBLIC KEY"}
</div>
<div class="text-white" style="font-size: 0.7rem; font-family: monospace; word-break: break-all;">
{&public_key[..std::cmp::min(24, public_key.len())]}{"..."}
</div>
</div>
}
} else {
html! {}
}}
</div>
<div class="d-flex justify-content-between align-items-end mb-3">
<div>
<div class="text-white-50 small">{"RESIDENT ID"}</div>
<div class="text-white">{"ZDF-2025-****"}</div>
</div>
<div class="text-end">
<div class="text-white-50 small">{"STATUS"}</div>
<div class="badge bg-warning text-dark">{"PENDING"}</div>
</div>
</div>
// QR Code at bottom
<div class="text-center border-top border-white border-opacity-25 pt-3">
<div class="d-inline-block p-2 rounded">
<div style="width: 60px; height: 60px; background: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBmaWxsPSJ3aGl0ZSIvPgo8cmVjdCB4PSI0IiB5PSI0IiB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjIwIiB5PSI0IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSIyOCIgeT0iNCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDQiIHk9IjQiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iOCIgeT0iOCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iNDgiIHk9IjgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjIwIiB5PSIxMiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMzYiIHk9IjEyIiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI0IiB5PSIyMCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMTIiIHk9IjIwIiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSIyMCIgeT0iMjAiIHdpZHRoPSI4IiBoZWlnaHQ9IjgiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjM2IiB5PSIyMCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDQiIHk9IjIwIiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI1MiIgeT0iMjAiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjI0IiB5PSIyNCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0id2hpdGUiLz4KPHJlY3QgeD0iNCIgeT0iMjgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjEyIiB5PSIyOCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMzYiIHk9IjI4IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI0NCIgeT0iMjgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjUyIiB5PSIyOCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNCIgeT0iMzYiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjEyIiB5PSIzNiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMjAiIHk9IjM2IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSIyOCIgeT0iMzYiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjM2IiB5PSIzNiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDQiIHk9IjM2IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI1MiIgeT0iMzYiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjQiIHk9IjQ0IiB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIGZpbGw9ImJsYWNrIi8+CjxyZWN0IHg9IjIwIiB5PSI0NCIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iMjgiIHk9IjQ0IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJibGFjayIvPgo8cmVjdCB4PSI0NCIgeT0iNDQiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iOCIgeT0iNDgiIHdpZHRoPSI0IiBoZWlnaHQ9IjQiIGZpbGw9IndoaXRlIi8+CjxyZWN0IHg9IjIwIiB5PSI1MiIgd2lkdGg9IjQiIGhlaWdodD0iNCIgZmlsbD0iYmxhY2siLz4KPHJlY3QgeD0iNDgiIHk9IjQ4IiB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K') no-repeat center; background-size: contain;"></div>
</div>
<div class="text-white-50 small mt-2">{"Scan to verify"}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</>
}
}
// Simplified key generation functions (for demo purposes)
fn generate_private_key() -> String {
// In a real implementation, this would use proper secp256k1 key generation
// For demo purposes, we'll generate a hex string
use js_sys::Math;
let mut key = String::new();
for _ in 0..64 {
let digit = (Math::random() * 16.0) as u8;
key.push_str(&format!("{:x}", digit));
}
key
}
fn generate_public_key(private_key: &str) -> String {
// In a real implementation, this would derive the public key from the private key
// For demo purposes, we'll generate a different hex string
use js_sys::Math;
let mut key = String::from("04"); // Uncompressed public key prefix
for _ in 0..128 {
let digit = (Math::random() * 16.0) as u8;
key.push_str(&format!("{:x}", digit));
}
key
}

View File

@ -0,0 +1,287 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::{DigitalResidentFormData, ResidentPaymentPlan, LegalAgreements};
#[derive(Properties, PartialEq)]
pub struct StepFiveProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepFive)]
pub fn step_five(props: &StepFiveProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let payment_plans = vec![
ResidentPaymentPlan::Monthly,
ResidentPaymentPlan::Yearly,
ResidentPaymentPlan::Lifetime,
];
let select_payment_plan = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |plan: ResidentPaymentPlan| {
let mut updated_data = form_data.clone();
updated_data.payment_plan = plan;
on_change.emit(updated_data);
})
};
let toggle_agreement = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |agreement_type: String| {
let mut updated_data = form_data.clone();
let mut agreements = updated_data.legal_agreements.clone();
match agreement_type.as_str() {
"terms" => agreements.terms = !agreements.terms,
"privacy" => agreements.privacy = !agreements.privacy,
"compliance" => agreements.compliance = !agreements.compliance,
"articles" => agreements.articles = !agreements.articles,
"final_agreement" => agreements.final_agreement = !agreements.final_agreement,
_ => {}
}
updated_data.legal_agreements = agreements;
on_change.emit(updated_data);
})
};
let calculate_savings = |plan: &ResidentPaymentPlan| -> Option<String> {
match plan {
ResidentPaymentPlan::Monthly => None,
ResidentPaymentPlan::Yearly => {
let monthly_total = ResidentPaymentPlan::Monthly.get_price() * 12.0;
let yearly_price = plan.get_price();
let savings = monthly_total - yearly_price;
Some(format!("Save ${:.2}", savings))
},
ResidentPaymentPlan::Lifetime => {
let monthly_total = ResidentPaymentPlan::Monthly.get_price() * 36.0; // 3 years
let lifetime_price = plan.get_price();
let savings = monthly_total - lifetime_price;
Some(format!("Save ${:.2} over 3 years", savings))
},
}
};
html! {
<div class="step-content">
<div class="step-header mb-4">
<h3 class="step-title">{"Payment Plan & Legal Agreements"}</h3>
<p class="step-description text-muted">
{"Choose your payment plan and review the legal agreements to complete your registration."}
</p>
</div>
<div class="mb-5">
<h5 class="mb-3">{"Select Payment Plan"}</h5>
<div class="row">
{for payment_plans.iter().map(|plan| {
let plan_clone = *plan;
let is_selected = form_data.payment_plan == *plan;
let select_callback = {
let select_payment_plan = select_payment_plan.clone();
Callback::from(move |_: MouseEvent| {
select_payment_plan.emit(plan_clone);
})
};
html! {
<div class="col-md-4 mb-3">
<div class={classes!(
"card", "h-100", "payment-plan-card",
if is_selected { "border-primary" } else { "" }
)} style="cursor: pointer;" onclick={select_callback}>
<div class="card-body text-center">
<div class="form-check d-flex justify-content-center mb-3">
<input
class="form-check-input"
type="radio"
name="payment_plan"
checked={is_selected}
readonly=true
/>
</div>
<h5 class="card-title">{plan.get_display_name()}</h5>
<div class="price mb-2">
<span class="h4 text-primary">{format!("${:.2}", plan.get_price())}</span>
{match plan {
ResidentPaymentPlan::Monthly => html! { <span class="text-muted">{"/month"}</span> },
ResidentPaymentPlan::Yearly => html! { <span class="text-muted">{"/year"}</span> },
ResidentPaymentPlan::Lifetime => html! { <span class="text-muted">{" once"}</span> },
}}
</div>
{if let Some(savings) = calculate_savings(plan) {
html! {
<div class="badge bg-success mb-2">{savings}</div>
}
} else {
html! {}
}}
<p class="card-text small text-muted">
{plan.get_description()}
</p>
</div>
</div>
</div>
}
})}
</div>
</div>
<div class="mb-4">
<h5 class="mb-3">{"Legal Agreements"}</h5>
<p class="text-muted mb-3">
{"Please review and accept the following agreements to proceed:"}
</p>
<div class="agreements-section">
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="terms"
checked={form_data.legal_agreements.terms}
onclick={{
let toggle = toggle_agreement.clone();
Callback::from(move |_| toggle.emit("terms".to_string()))
}}
/>
<label class="form-check-label" for="terms">
{"I agree to the "}
<a href="#" class="text-primary">{"Terms of Service"}</a>
{" and "}
<a href="#" class="text-primary">{"User Agreement"}</a>
<span class="text-danger">{" *"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="privacy"
checked={form_data.legal_agreements.privacy}
onclick={{
let toggle = toggle_agreement.clone();
Callback::from(move |_| toggle.emit("privacy".to_string()))
}}
/>
<label class="form-check-label" for="privacy">
{"I acknowledge the "}
<a href="#" class="text-primary">{"Privacy Policy"}</a>
{" and consent to data processing"}
<span class="text-danger">{" *"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="compliance"
checked={form_data.legal_agreements.compliance}
onclick={{
let toggle = toggle_agreement.clone();
Callback::from(move |_| toggle.emit("compliance".to_string()))
}}
/>
<label class="form-check-label" for="compliance">
{"I agree to comply with "}
<a href="#" class="text-primary">{"Digital Resident Regulations"}</a>
{" and applicable laws"}
<span class="text-danger">{" *"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="articles"
checked={form_data.legal_agreements.articles}
onclick={{
let toggle = toggle_agreement.clone();
Callback::from(move |_| toggle.emit("articles".to_string()))
}}
/>
<label class="form-check-label" for="articles">
{"I accept the "}
<a href="#" class="text-primary">{"Digital Resident Charter"}</a>
{" and community guidelines"}
<span class="text-danger">{" *"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="final_agreement"
checked={form_data.legal_agreements.final_agreement}
onclick={{
let toggle = toggle_agreement.clone();
Callback::from(move |_| toggle.emit("final_agreement".to_string()))
}}
/>
<label class="form-check-label" for="final_agreement">
{"I confirm that all information provided is accurate and complete"}
<span class="text-danger">{" *"}</span>
</label>
</div>
</div>
</div>
<div class="payment-summary p-4 rounded mb-4">
<h6 class="mb-3">{"Payment Summary"}</h6>
<div class="d-flex justify-content-between mb-2">
<span>{"Digital Resident Registration"}</span>
<span>{format!("${:.2}", form_data.payment_plan.get_price())}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>{"Selected Services"}</span>
<span>{format!("{} services", form_data.requested_services.len())}</span>
</div>
<hr />
<div class="d-flex justify-content-between fw-bold">
<span>{"Total"}</span>
<span class="text-primary">{format!("${:.2}", form_data.payment_plan.get_price())}</span>
</div>
{if form_data.payment_plan != ResidentPaymentPlan::Monthly {
html! {
<div class="text-success small mt-2">
{calculate_savings(&form_data.payment_plan).unwrap_or_default()}
</div>
}
} else {
html! {}
}}
</div>
{if !form_data.legal_agreements.all_agreed() {
html! {
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
{"Please accept all required agreements to proceed with registration."}
</div>
}
} else {
html! {
<div class="alert alert-success">
<i class="bi bi-check-circle me-2"></i>
{"All requirements met! You can now proceed to payment."}
</div>
}
}}
</div>
}
}

View File

@ -0,0 +1,265 @@
use yew::prelude::*;
use web_sys::{HtmlInputElement, HtmlSelectElement};
use crate::models::company::{DigitalResidentFormData, DigitalService, CommunicationPreferences};
#[derive(Properties, PartialEq)]
pub struct StepFourProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepFour)]
pub fn step_four(props: &StepFourProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let available_services = vec![
DigitalService::BankingAccess,
DigitalService::TaxFiling,
DigitalService::HealthcareAccess,
DigitalService::EducationServices,
DigitalService::BusinessLicensing,
DigitalService::PropertyServices,
DigitalService::LegalServices,
DigitalService::DigitalIdentity,
];
let toggle_service = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |service: DigitalService| {
let mut updated_data = form_data.clone();
if updated_data.requested_services.contains(&service) {
updated_data.requested_services.retain(|s| s != &service);
} else {
updated_data.requested_services.push(service);
}
on_change.emit(updated_data);
})
};
let on_language_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let select: HtmlSelectElement = e.target_unchecked_into();
let mut updated_data = form_data.clone();
updated_data.preferred_language = select.value();
on_change.emit(updated_data);
})
};
let toggle_communication_pref = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |pref_type: String| {
let mut updated_data = form_data.clone();
let mut prefs = updated_data.communication_preferences.clone();
match pref_type.as_str() {
"email" => prefs.email_notifications = !prefs.email_notifications,
"sms" => prefs.sms_notifications = !prefs.sms_notifications,
"push" => prefs.push_notifications = !prefs.push_notifications,
"newsletter" => prefs.newsletter = !prefs.newsletter,
_ => {}
}
updated_data.communication_preferences = prefs;
on_change.emit(updated_data);
})
};
html! {
<div class="step-content">
<div class="step-header mb-4">
<h3 class="step-title">{"Digital Services & Preferences"}</h3>
<p class="step-description text-muted">
{"Select the digital services you're interested in and set your communication preferences."}
</p>
</div>
<div class="mb-5">
<h5 class="mb-3">{"Requested Digital Services"}</h5>
<p class="text-muted mb-3">
{"Choose the services you'd like access to as a digital resident:"}
</p>
<div class="row">
{for available_services.iter().map(|service| {
let service_clone = service.clone();
let is_selected = form_data.requested_services.contains(service);
let toggle_callback = {
let toggle_service = toggle_service.clone();
let service = service.clone();
Callback::from(move |_: MouseEvent| {
toggle_service.emit(service.clone());
})
};
html! {
<div class="col-md-6 mb-3">
<div class={classes!(
"card", "h-100", "service-card",
if is_selected { "border-primary" } else { "" }
)} style="cursor: pointer;" onclick={toggle_callback}>
<div class="card-body">
<div class="d-flex align-items-start">
<div class="form-check me-3">
<input
class="form-check-input"
type="checkbox"
checked={is_selected}
readonly=true
/>
</div>
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<i class={classes!("bi", service.get_icon(), "me-2", "text-primary")}></i>
<h6 class="card-title mb-0">{service.get_display_name()}</h6>
</div>
<p class="card-text small text-muted mb-0">
{service.get_description()}
</p>
</div>
</div>
</div>
</div>
</div>
}
})}
</div>
</div>
<div class="mb-4">
<h5 class="mb-3">{"Language Preference"}</h5>
<div class="col-md-6">
<label for="preferred_language" class="form-label">
{"Preferred Language"} <span class="text-danger">{"*"}</span>
</label>
<select
class="form-select"
id="preferred_language"
value={form_data.preferred_language.clone()}
onchange={on_language_change}
required=true
>
<option value="English">{"English"}</option>
<option value="Spanish">{"Spanish"}</option>
<option value="French">{"French"}</option>
<option value="German">{"German"}</option>
<option value="Italian">{"Italian"}</option>
<option value="Portuguese">{"Portuguese"}</option>
<option value="Dutch">{"Dutch"}</option>
<option value="Arabic">{"Arabic"}</option>
<option value="Chinese">{"Chinese"}</option>
<option value="Japanese">{"Japanese"}</option>
<option value="Korean">{"Korean"}</option>
<option value="Russian">{"Russian"}</option>
</select>
</div>
</div>
<div class="mb-4">
<h5 class="mb-3">{"Communication Preferences"}</h5>
<p class="text-muted mb-3">
{"Choose how you'd like to receive updates and notifications:"}
</p>
<div class="row">
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="email_notifications"
checked={form_data.communication_preferences.email_notifications}
onclick={{
let toggle = toggle_communication_pref.clone();
Callback::from(move |_| toggle.emit("email".to_string()))
}}
/>
<label class="form-check-label" for="email_notifications">
<i class="bi bi-envelope me-2"></i>
{"Email Notifications"}
</label>
<div class="form-text">
{"Important updates and service notifications"}
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="sms_notifications"
checked={form_data.communication_preferences.sms_notifications}
onclick={{
let toggle = toggle_communication_pref.clone();
Callback::from(move |_| toggle.emit("sms".to_string()))
}}
/>
<label class="form-check-label" for="sms_notifications">
<i class="bi bi-phone me-2"></i>
{"SMS Notifications"}
</label>
<div class="form-text">
{"Urgent alerts and security notifications"}
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="push_notifications"
checked={form_data.communication_preferences.push_notifications}
onclick={{
let toggle = toggle_communication_pref.clone();
Callback::from(move |_| toggle.emit("push".to_string()))
}}
/>
<label class="form-check-label" for="push_notifications">
<i class="bi bi-bell me-2"></i>
{"Push Notifications"}
</label>
<div class="form-text">
{"Real-time updates in your browser"}
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="newsletter"
checked={form_data.communication_preferences.newsletter}
onclick={{
let toggle = toggle_communication_pref.clone();
Callback::from(move |_| toggle.emit("newsletter".to_string()))
}}
/>
<label class="form-check-label" for="newsletter">
<i class="bi bi-newspaper me-2"></i>
{"Newsletter"}
</label>
<div class="form-text">
{"Monthly updates and community news"}
</div>
</div>
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{"You can modify your service selections and communication preferences at any time from your account settings."}
</div>
</div>
}
}

View File

@ -0,0 +1,438 @@
use yew::prelude::*;
use web_sys::{HtmlInputElement, HtmlSelectElement};
use crate::models::company::{DigitalResidentFormData, DigitalService};
#[derive(Properties, PartialEq)]
pub struct StepInfoKycProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepInfoKyc)]
pub fn step_info_kyc(props: &StepInfoKycProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let update_field = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |field: String| {
let mut updated_data = form_data.clone();
// This will be called by individual field updates
on_change.emit(updated_data);
})
};
let on_input = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let field_name = input.name();
let value = input.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"full_name" => updated_data.full_name = value,
"email" => updated_data.email = value,
"phone" => updated_data.phone = value,
"date_of_birth" => updated_data.date_of_birth = value,
"nationality" => updated_data.nationality = value,
"passport_number" => updated_data.passport_number = value,
"passport_expiry" => updated_data.passport_expiry = value,
"current_address" => updated_data.current_address = value,
"city" => updated_data.city = value,
"country" => updated_data.country = value,
"postal_code" => updated_data.postal_code = value,
"occupation" => updated_data.occupation = value,
"employer" => updated_data.employer = Some(value),
"annual_income" => updated_data.annual_income = Some(value),
_ => {}
}
on_change.emit(updated_data);
})
};
let on_select = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let select: HtmlSelectElement = e.target_unchecked_into();
let field_name = select.name();
let value = select.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"education_level" => updated_data.education_level = value,
_ => {}
}
on_change.emit(updated_data);
})
};
let on_service_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
let service_name = input.value();
let is_checked = input.checked();
let mut updated_data = form_data.clone();
// Parse the service
let service = match service_name.as_str() {
"BankingAccess" => DigitalService::BankingAccess,
"TaxFiling" => DigitalService::TaxFiling,
"HealthcareAccess" => DigitalService::HealthcareAccess,
"EducationServices" => DigitalService::EducationServices,
"BusinessLicensing" => DigitalService::BusinessLicensing,
"PropertyServices" => DigitalService::PropertyServices,
"LegalServices" => DigitalService::LegalServices,
"DigitalIdentity" => DigitalService::DigitalIdentity,
_ => return,
};
if is_checked {
if !updated_data.requested_services.contains(&service) {
updated_data.requested_services.push(service);
}
} else {
updated_data.requested_services.retain(|s| s != &service);
}
on_change.emit(updated_data);
})
};
html! {
<div class="row">
<div class="col-12">
<h4 class="mb-4">{"Personal Information & KYC"}</h4>
// Personal Information Section
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Personal Details"}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="full_name" class="form-label">{"Full Name"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="full_name"
name="full_name"
value={form_data.full_name.clone()}
oninput={on_input.clone()}
placeholder="Enter your full legal name"
/>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">{"Email Address"} <span class="text-danger">{"*"}</span></label>
<input
type="email"
class="form-control"
id="email"
name="email"
value={form_data.email.clone()}
oninput={on_input.clone()}
placeholder="your.email@example.com"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">{"Phone Number"} <span class="text-danger">{"*"}</span></label>
<input
type="tel"
class="form-control"
id="phone"
name="phone"
value={form_data.phone.clone()}
oninput={on_input.clone()}
placeholder="+1 (555) 123-4567"
/>
</div>
<div class="col-md-6 mb-3">
<label for="date_of_birth" class="form-label">{"Date of Birth"} <span class="text-danger">{"*"}</span></label>
<input
type="date"
class="form-control"
id="date_of_birth"
name="date_of_birth"
value={form_data.date_of_birth.clone()}
oninput={on_input.clone()}
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="nationality" class="form-label">{"Nationality"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="nationality"
name="nationality"
value={form_data.nationality.clone()}
oninput={on_input.clone()}
placeholder="e.g., American, British, etc."
/>
</div>
<div class="col-md-6 mb-3">
<label for="passport_number" class="form-label">{"Passport Number"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="passport_number"
name="passport_number"
value={form_data.passport_number.clone()}
oninput={on_input.clone()}
placeholder="Enter passport number"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="passport_expiry" class="form-label">{"Passport Expiry Date"} <span class="text-danger">{"*"}</span></label>
<input
type="date"
class="form-control"
id="passport_expiry"
name="passport_expiry"
value={form_data.passport_expiry.clone()}
oninput={on_input.clone()}
/>
</div>
</div>
</div>
</div>
// Address Information Section
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Address Information"}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<label for="current_address" class="form-label">{"Current Address"} <span class="text-danger">{"*"}</span></label>
<textarea
class="form-control"
id="current_address"
name="current_address"
rows="3"
value={form_data.current_address.clone()}
oninput={on_input.clone()}
placeholder="Enter your full current address"
></textarea>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="city" class="form-label">{"City"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="city"
name="city"
value={form_data.city.clone()}
oninput={on_input.clone()}
placeholder="City"
/>
</div>
<div class="col-md-4 mb-3">
<label for="country" class="form-label">{"Country"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="country"
name="country"
value={form_data.country.clone()}
oninput={on_input.clone()}
placeholder="Country"
/>
</div>
<div class="col-md-4 mb-3">
<label for="postal_code" class="form-label">{"Postal Code"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="postal_code"
name="postal_code"
value={form_data.postal_code.clone()}
oninput={on_input.clone()}
placeholder="Postal Code"
/>
</div>
</div>
</div>
</div>
// Professional Information Section
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Professional Information"}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6 mb-3">
<label for="occupation" class="form-label">{"Occupation"} <span class="text-danger">{"*"}</span></label>
<input
type="text"
class="form-control"
id="occupation"
name="occupation"
value={form_data.occupation.clone()}
oninput={on_input.clone()}
placeholder="Your current occupation"
/>
</div>
<div class="col-md-6 mb-3">
<label for="employer" class="form-label">{"Employer"}</label>
<input
type="text"
class="form-control"
id="employer"
name="employer"
value={form_data.employer.clone().unwrap_or_default()}
oninput={on_input.clone()}
placeholder="Current employer (optional)"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="education_level" class="form-label">{"Education Level"} <span class="text-danger">{"*"}</span></label>
<select
class="form-select"
id="education_level"
name="education_level"
value={form_data.education_level.clone()}
onchange={on_select.clone()}
>
<option value="">{"Select education level"}</option>
<option value="High School">{"High School"}</option>
<option value="Associate Degree">{"Associate Degree"}</option>
<option value="Bachelor's Degree">{"Bachelor's Degree"}</option>
<option value="Master's Degree">{"Master's Degree"}</option>
<option value="Doctorate">{"Doctorate"}</option>
<option value="Professional Certification">{"Professional Certification"}</option>
<option value="Other">{"Other"}</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="annual_income" class="form-label">{"Annual Income"}</label>
<select
class="form-select"
id="annual_income"
name="annual_income"
value={form_data.annual_income.clone().unwrap_or_default()}
onchange={on_select.clone()}
>
<option value="">{"Select income range (optional)"}</option>
<option value="Under $25,000">{"Under $25,000"}</option>
<option value="$25,000 - $50,000">{"$25,000 - $50,000"}</option>
<option value="$50,000 - $75,000">{"$50,000 - $75,000"}</option>
<option value="$75,000 - $100,000">{"$75,000 - $100,000"}</option>
<option value="$100,000 - $150,000">{"$100,000 - $150,000"}</option>
<option value="$150,000 - $250,000">{"$150,000 - $250,000"}</option>
<option value="Over $250,000">{"Over $250,000"}</option>
</select>
</div>
</div>
</div>
</div>
// Digital Services Section
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Digital Services"} <span class="text-danger">{"*"}</span></h5>
<small class="text-muted">{"Select the digital services you'd like access to"}</small>
</div>
<div class="card-body">
<div class="row">
{[
DigitalService::BankingAccess,
DigitalService::TaxFiling,
DigitalService::HealthcareAccess,
DigitalService::EducationServices,
DigitalService::BusinessLicensing,
DigitalService::PropertyServices,
DigitalService::LegalServices,
DigitalService::DigitalIdentity,
].iter().map(|service| {
let service_name = format!("{:?}", service);
let is_selected = form_data.requested_services.contains(service);
html! {
<div class="col-md-6 mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id={service_name.clone()}
value={service_name.clone()}
checked={is_selected}
onchange={on_service_change.clone()}
/>
<label class="form-check-label" for={service_name.clone()}>
<i class={format!("bi {} me-2", service.get_icon())}></i>
<strong>{service.get_display_name()}</strong>
<br />
<small class="text-muted">{service.get_description()}</small>
</label>
</div>
</div>
}
}).collect::<Html>()}
</div>
</div>
</div>
// KYC Upload Section
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"KYC Document Upload"}</h5>
<small class="text-muted">{"Upload required documents for identity verification"}</small>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{"Please prepare the following documents for upload:"}
<ul class="mb-0 mt-2">
<li>{"Government-issued photo ID (passport, driver's license)"}</li>
<li>{"Proof of address (utility bill, bank statement)"}</li>
<li>{"Passport photo page"}</li>
</ul>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">{"Photo ID"}</label>
<input type="file" class="form-control" accept="image/*,.pdf" />
<small class="text-muted">{"Upload your government-issued photo ID"}</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">{"Proof of Address"}</label>
<input type="file" class="form-control" accept="image/*,.pdf" />
<small class="text-muted">{"Upload proof of your current address"}</small>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">{"Passport Photo Page"}</label>
<input type="file" class="form-control" accept="image/*,.pdf" />
<small class="text-muted">{"Upload your passport photo page"}</small>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,173 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::DigitalResidentFormData;
#[derive(Properties, PartialEq)]
pub struct StepOneProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepOne)]
pub fn step_one(props: &StepOneProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let on_input = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let field_name = input.name();
let value = input.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"full_name" => updated_data.full_name = value,
"email" => updated_data.email = value,
"phone" => updated_data.phone = value,
"date_of_birth" => updated_data.date_of_birth = value,
"nationality" => updated_data.nationality = value,
"passport_number" => updated_data.passport_number = value,
"passport_expiry" => updated_data.passport_expiry = value,
_ => {}
}
on_change.emit(updated_data);
})
};
html! {
<div class="step-content">
<div class="step-header mb-4">
<h3 class="step-title">{"Personal Information"}</h3>
<p class="step-description text-muted">
{"Please provide your personal details for digital resident registration."}
</p>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="full_name" class="form-label">
{"Full Name"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="full_name"
name="full_name"
value={form_data.full_name.clone()}
oninput={on_input.clone()}
placeholder="Enter your full legal name"
required=true
/>
</div>
<div class="col-md-6 mb-3">
<label for="email" class="form-label">
{"Email Address"} <span class="text-danger">{"*"}</span>
</label>
<input
type="email"
class="form-control"
id="email"
name="email"
value={form_data.email.clone()}
oninput={on_input.clone()}
placeholder="your.email@example.com"
required=true
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="phone" class="form-label">
{"Phone Number"} <span class="text-danger">{"*"}</span>
</label>
<input
type="tel"
class="form-control"
id="phone"
name="phone"
value={form_data.phone.clone()}
oninput={on_input.clone()}
placeholder="+1 (555) 123-4567"
required=true
/>
</div>
<div class="col-md-6 mb-3">
<label for="date_of_birth" class="form-label">
{"Date of Birth"} <span class="text-danger">{"*"}</span>
</label>
<input
type="date"
class="form-control"
id="date_of_birth"
name="date_of_birth"
value={form_data.date_of_birth.clone()}
oninput={on_input.clone()}
required=true
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="nationality" class="form-label">
{"Nationality"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="nationality"
name="nationality"
value={form_data.nationality.clone()}
oninput={on_input.clone()}
placeholder="e.g., American, British, etc."
required=true
/>
</div>
<div class="col-md-6 mb-3">
<label for="passport_number" class="form-label">
{"Passport Number"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="passport_number"
name="passport_number"
value={form_data.passport_number.clone()}
oninput={on_input.clone()}
placeholder="Enter passport number"
required=true
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="passport_expiry" class="form-label">
{"Passport Expiry Date"} <span class="text-danger">{"*"}</span>
</label>
<input
type="date"
class="form-control"
id="passport_expiry"
name="passport_expiry"
value={form_data.passport_expiry.clone()}
oninput={on_input.clone()}
required=true
/>
</div>
</div>
<div class="alert alert-info mt-4">
<i class="bi bi-info-circle me-2"></i>
{"All personal information is encrypted and stored securely. Your data will only be used for digital resident services and verification purposes."}
</div>
</div>
}
}

View File

@ -0,0 +1,251 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::{DigitalResidentFormData, ResidentPaymentPlan};
#[derive(Properties, PartialEq)]
pub struct StepPaymentProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepPayment)]
pub fn step_payment(props: &StepPaymentProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let on_payment_plan_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
let plan_value = input.value();
let mut updated_data = form_data.clone();
updated_data.payment_plan = match plan_value.as_str() {
"Monthly" => ResidentPaymentPlan::Monthly,
"Yearly" => ResidentPaymentPlan::Yearly,
"Lifetime" => ResidentPaymentPlan::Lifetime,
_ => ResidentPaymentPlan::Monthly,
};
on_change.emit(updated_data);
})
};
let on_agreement_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let input: HtmlInputElement = e.target_unchecked_into();
let agreement_name = input.name();
let is_checked = input.checked();
let mut updated_data = form_data.clone();
match agreement_name.as_str() {
"terms" => updated_data.legal_agreements.terms = is_checked,
"privacy" => updated_data.legal_agreements.privacy = is_checked,
"compliance" => updated_data.legal_agreements.compliance = is_checked,
"articles" => updated_data.legal_agreements.articles = is_checked,
"final_agreement" => updated_data.legal_agreements.final_agreement = is_checked,
_ => {}
}
on_change.emit(updated_data);
})
};
html! {
<div class="row">
<div class="col-12">
<h4 class="mb-4">{"Payment Plan & Legal Agreements"}</h4>
// Payment Plan Selection
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Choose Your Payment Plan"}</h5>
</div>
<div class="card-body">
<div class="row">
{[
ResidentPaymentPlan::Monthly,
ResidentPaymentPlan::Yearly,
ResidentPaymentPlan::Lifetime,
].iter().map(|plan| {
let plan_name = plan.get_display_name();
let plan_price = plan.get_price();
let plan_description = plan.get_description();
let is_selected = form_data.payment_plan == *plan;
let savings = if *plan == ResidentPaymentPlan::Yearly { "Save 17%" } else { "" };
let popular = *plan == ResidentPaymentPlan::Yearly;
html! {
<div class="col-md-4 mb-3">
<div class={format!("card h-100 {}", if is_selected { "border-primary" } else { "" })}>
{if popular {
html! {
<div class="card-header bg-primary text-white text-center">
<small>{"Most Popular"}</small>
</div>
}
} else {
html! {}
}}
<div class="card-body text-center">
<div class="form-check">
<input
class="form-check-input"
type="radio"
name="payment_plan"
id={format!("plan_{}", plan_name.to_lowercase())}
value={plan_name}
checked={is_selected}
onchange={on_payment_plan_change.clone()}
/>
<label class="form-check-label w-100" for={format!("plan_{}", plan_name.to_lowercase())}>
<h5 class="card-title">{plan_name}</h5>
<div class="display-6 text-primary">{format!("{:.2}", plan_price)}</div>
{if !savings.is_empty() {
html! {
<div class="badge bg-success mb-2">{savings}</div>
}
} else {
html! {}
}}
<p class="text-muted">{plan_description}</p>
</label>
</div>
</div>
</div>
</div>
}
}).collect::<Html>()}
</div>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle me-2"></i>
{"All plans include access to selected digital services, identity verification, and customer support."}
</div>
</div>
</div>
// Legal Agreements
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Legal Agreements"}</h5>
<small class="text-muted">{"Please review and accept all required agreements"}</small>
</div>
<div class="card-body">
<div class="row">
<div class="col-12">
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="terms"
name="terms"
checked={form_data.legal_agreements.terms}
onchange={on_agreement_change.clone()}
/>
<label class="form-check-label" for="terms">
{"I agree to the "} <a href="#" target="_blank">{"Terms of Service"}</a> <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="privacy"
name="privacy"
checked={form_data.legal_agreements.privacy}
onchange={on_agreement_change.clone()}
/>
<label class="form-check-label" for="privacy">
{"I agree to the "} <a href="#" target="_blank">{"Privacy Policy"}</a> <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="compliance"
name="compliance"
checked={form_data.legal_agreements.compliance}
onchange={on_agreement_change.clone()}
/>
<label class="form-check-label" for="compliance">
{"I agree to the "} <a href="#" target="_blank">{"Compliance Agreement"}</a> <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="articles"
name="articles"
checked={form_data.legal_agreements.articles}
onchange={on_agreement_change.clone()}
/>
<label class="form-check-label" for="articles">
{"I agree to the "} <a href="#" target="_blank">{"Digital Resident Agreement"}</a> <span class="text-danger">{"*"}</span>
</label>
</div>
<div class="form-check mb-3">
<input
class="form-check-input"
type="checkbox"
id="final_agreement"
name="final_agreement"
checked={form_data.legal_agreements.final_agreement}
onchange={on_agreement_change.clone()}
/>
<label class="form-check-label" for="final_agreement">
{"I confirm that all information provided is accurate and complete, and I understand that providing false information may result in rejection of my application."} <span class="text-danger">{"*"}</span>
</label>
</div>
</div>
</div>
</div>
</div>
// Payment Summary
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"Payment Summary"}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-8">
<h6>{"Digital Resident Registration - "}{form_data.payment_plan.get_display_name()}</h6>
<p class="text-muted mb-0">{form_data.payment_plan.get_description()}</p>
<small class="text-muted">
{"Services: "}{form_data.requested_services.len()}{" selected"}
</small>
</div>
<div class="col-md-4 text-end">
<h4 class="text-primary">{format!("{:.2}", form_data.payment_plan.get_price())}</h4>
{if form_data.payment_plan == ResidentPaymentPlan::Yearly {
html! {
<small class="text-success">{"Save $60.89 vs Monthly"}</small>
}
} else {
html! {}
}}
</div>
</div>
<hr />
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>{"Important:"}</strong> {" Your registration will be reviewed after payment. Approval typically takes 3-5 business days. You will receive email updates about your application status."}
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,420 @@
use yew::prelude::*;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use web_sys::{window, console, js_sys};
use crate::models::company::{DigitalResidentFormData, DigitalResident, ResidentPaymentPlan};
use crate::services::ResidentService;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = window)]
fn confirmStripePayment(client_secret: &str) -> js_sys::Promise;
#[wasm_bindgen(js_namespace = window)]
fn initializeStripeElements(client_secret: &str);
}
#[derive(Properties, PartialEq)]
pub struct StepPaymentStripeProps {
pub form_data: DigitalResidentFormData,
pub client_secret: Option<String>,
pub processing_payment: bool,
pub on_process_payment: Callback<()>,
pub on_payment_complete: Callback<DigitalResident>,
pub on_payment_error: Callback<String>,
pub on_payment_plan_change: Callback<ResidentPaymentPlan>,
pub on_confirmation_change: Callback<bool>,
}
pub enum StepPaymentStripeMsg {
ProcessPayment,
PaymentComplete,
PaymentError(String),
PaymentPlanChanged(ResidentPaymentPlan),
ToggleConfirmation,
}
pub struct StepPaymentStripe {
form_data: DigitalResidentFormData,
payment_error: Option<String>,
selected_payment_plan: ResidentPaymentPlan,
confirmation_checked: bool,
}
impl Component for StepPaymentStripe {
type Message = StepPaymentStripeMsg;
type Properties = StepPaymentStripeProps;
fn create(ctx: &Context<Self>) -> Self {
Self {
form_data: ctx.props().form_data.clone(),
payment_error: None,
selected_payment_plan: ctx.props().form_data.payment_plan.clone(),
confirmation_checked: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
StepPaymentStripeMsg::ProcessPayment => {
if let Some(client_secret) = &ctx.props().client_secret {
console::log_1(&"🔄 User clicked 'Complete Payment' - processing with Stripe".into());
self.process_stripe_payment(ctx, client_secret.clone());
} else {
console::log_1(&"❌ No client secret available for payment".into());
self.payment_error = Some("Payment not ready. Please try again.".to_string());
}
return false;
}
StepPaymentStripeMsg::PaymentComplete => {
console::log_1(&"✅ Payment completed successfully".into());
// Create resident from form data with current payment plan
let mut updated_form_data = self.form_data.clone();
updated_form_data.payment_plan = self.selected_payment_plan.clone();
match ResidentService::create_resident_from_form(&updated_form_data) {
Ok(resident) => {
ctx.props().on_payment_complete.emit(resident);
}
Err(e) => {
console::log_1(&format!("❌ Failed to create resident: {}", e).into());
ctx.props().on_payment_error.emit(format!("Failed to create resident: {}", e));
}
}
return false;
}
StepPaymentStripeMsg::PaymentError(error) => {
console::log_1(&format!("❌ Payment failed: {}", error).into());
self.payment_error = Some(error.clone());
ctx.props().on_payment_error.emit(error);
}
StepPaymentStripeMsg::PaymentPlanChanged(plan) => {
console::log_1(&format!("💳 Payment plan changed to: {}", plan.get_display_name()).into());
self.selected_payment_plan = plan.clone();
self.payment_error = None; // Clear any previous errors
// Notify parent to create new payment intent
ctx.props().on_payment_plan_change.emit(plan);
return true;
}
StepPaymentStripeMsg::ToggleConfirmation => {
self.confirmation_checked = !self.confirmation_checked;
console::log_1(&format!("📋 Confirmation checkbox toggled: {}", self.confirmation_checked).into());
// Notify parent of confirmation state change
ctx.props().on_confirmation_change.emit(self.confirmation_checked);
}
}
true
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
self.form_data = ctx.props().form_data.clone();
// Update selected payment plan if it changed from parent
if self.selected_payment_plan != ctx.props().form_data.payment_plan {
self.selected_payment_plan = ctx.props().form_data.payment_plan.clone();
}
// Initialize Stripe Elements if client secret became available
if old_props.client_secret.is_none() && ctx.props().client_secret.is_some() {
if let Some(client_secret) = &ctx.props().client_secret {
initializeStripeElements(client_secret);
}
}
true
}
fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
if first_render {
// Initialize Stripe Elements if client secret is available
if let Some(client_secret) = &ctx.props().client_secret {
initializeStripeElements(client_secret);
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let has_client_secret = ctx.props().client_secret.is_some();
let can_process_payment = has_client_secret && !ctx.props().processing_payment && self.confirmation_checked;
html! {
<div class="step-content">
// Registration Summary
<div class="row mb-3">
<div class="col-12">
<h6 class="text-secondary mb-3">
<i class="bi bi-receipt me-2"></i>{"Registration Summary"}
</h6>
<div class="card border-0">
<div class="card-body py-3">
<div class="row g-2 small">
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-person text-primary me-2"></i>
<div>
<div class="fw-bold">{&self.form_data.full_name}</div>
<div class="text-muted">{"Digital Resident"}</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-envelope text-primary me-2"></i>
<div>
<div class="fw-bold">{&self.form_data.email}</div>
<div class="text-muted">{"Email"}</div>
</div>
</div>
</div>
{if let Some(public_key) = &self.form_data.public_key {
html! {
<div class="col-md-4">
<div class="d-flex align-items-center">
<i class="bi bi-key text-primary me-2"></i>
<div>
<div class="fw-bold" style="font-family: monospace; font-size: 0.8rem;">
{&public_key[..std::cmp::min(16, public_key.len())]}{"..."}
</div>
<div class="text-muted">{"Public Key"}</div>
</div>
</div>
</div>
}
} else {
html! {}
}}
</div>
</div>
</div>
</div>
</div>
// Confirmation Checkbox
<div class="row mb-3">
<div class="col-12">
<div class="alert alert-warning py-2 mb-0">
<div class="form-check mb-0">
<input
class="form-check-input"
type="checkbox"
id="registrationConfirmation"
checked={self.confirmation_checked}
onchange={link.callback(|_| StepPaymentStripeMsg::ToggleConfirmation)}
/>
<label class="form-check-label small" for="registrationConfirmation">
<strong>{"I confirm the accuracy of all information and authorize digital resident registration with the selected payment plan."}</strong>
</label>
</div>
</div>
</div>
</div>
// Payment Plans (Left) and Payment Form (Right)
<div class="row mb-4">
// Payment Plan Selection - Left
<div class="col-lg-6 mb-4">
<h5 class="text-secondary mb-3">
{"Choose Your Payment Plan"} <span class="text-danger">{"*"}</span>
</h5>
<div class="row">
{self.render_payment_plan_option(ctx, ResidentPaymentPlan::Monthly, "Monthly Plan", "Pay monthly with flexibility", "bi-calendar-month")}
{self.render_payment_plan_option(ctx, ResidentPaymentPlan::Yearly, "Yearly Plan", "Save 17% with annual payments", "bi-calendar-check")}
{self.render_payment_plan_option(ctx, ResidentPaymentPlan::Lifetime, "Lifetime Plan", "One-time payment for lifetime access", "bi-infinity")}
</div>
</div>
// Payment Form - Right
<div class="col-lg-6">
<h5 class="text-secondary mb-3">
{"Payment Information"} <span class="text-danger">{"*"}</span>
</h5>
<div class="card" id="payment-information-section">
<div class="card-header bg-success text-white">
<h6 class="mb-0">
<i class="bi bi-shield-check me-2"></i>{"Secure Payment Processing"}
</h6>
</div>
<div class="card-body">
// Stripe Elements will be mounted here
<div id="payment-element" style="min-height: 40px; padding: 10px; border: 1px solid #dee2e6; border-radius: 0.375rem; background-color: #ffffff;">
{if ctx.props().processing_payment {
html! {
<div class="text-center py-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Processing payment..."}</p>
</div>
}
} else if !has_client_secret {
html! {
<div class="text-center py-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Preparing payment form..."}</p>
</div>
}
} else {
html! {}
}}
</div>
// Payment button
{if has_client_secret && !ctx.props().processing_payment {
html! {
<div class="d-grid mt-3">
<button
type="button"
class="btn btn-primary btn-lg"
disabled={!can_process_payment}
onclick={link.callback(|_| StepPaymentStripeMsg::ProcessPayment)}
>
{if self.confirmation_checked {
html! {
<>
<i class="bi bi-credit-card me-2"></i>
{format!("Complete Payment - ${:.2}", self.selected_payment_plan.get_price())}
</>
}
} else {
html! {
<>
<i class="bi bi-exclamation-triangle me-2"></i>
{"Please confirm registration details"}
</>
}
}}
</button>
</div>
}
} else {
html! {}
}}
{if let Some(error) = &self.payment_error {
html! {
<div id="payment-errors" class="alert alert-danger mt-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>{"Payment Error: "}</strong>{error}
</div>
}
} else {
html! {
<div id="payment-errors" class="alert alert-danger mt-3" style="display: none;"></div>
}
}}
// Payment info text
<div class="text-center mt-3">
<small class="text-muted">
{"Payment plan: "}{self.selected_payment_plan.get_display_name()}
{" - $"}{self.selected_payment_plan.get_price()}
</small>
</div>
</div>
</div>
</div>
</div>
</div>
}
}
}
impl StepPaymentStripe {
fn render_payment_plan_option(&self, ctx: &Context<Self>, plan: ResidentPaymentPlan, title: &str, description: &str, icon: &str) -> Html {
let link = ctx.link();
let is_selected = self.selected_payment_plan == plan;
let card_class = if is_selected {
"card border-success mb-3"
} else {
"card border-secondary mb-3"
};
let on_select = link.callback(move |_| StepPaymentStripeMsg::PaymentPlanChanged(plan.clone()));
// Get pricing for this plan
let price = plan.get_price();
html! {
<div class="col-12">
<div class={card_class} style="cursor: pointer;" onclick={on_select}>
<div class="card-body">
<div class="d-flex align-items-center">
<i class={format!("bi {} fs-3 text-primary me-3", icon)}></i>
<div class="flex-grow-1">
<h6 class="card-title mb-1">{title}</h6>
<p class="card-text text-muted mb-0 small">{description}</p>
<div class="mt-1">
<span class="fw-bold text-success">{format!("${:.2}", price)}</span>
{if plan == ResidentPaymentPlan::Yearly {
html! {
<span class="badge bg-success ms-2 small">
{"17% OFF"}
</span>
}
} else if plan == ResidentPaymentPlan::Lifetime {
html! {
<span class="badge bg-warning ms-2 small">
{"BEST VALUE"}
</span>
}
} else {
html! {}
}}
</div>
</div>
<div class="text-end">
{if is_selected {
html! {
<i class="bi bi-check-circle-fill text-success fs-4"></i>
}
} else {
html! {
<i class="bi bi-circle text-muted fs-4"></i>
}
}}
</div>
</div>
</div>
</div>
</div>
}
}
fn process_stripe_payment(&mut self, ctx: &Context<Self>, client_secret: String) {
let link = ctx.link().clone();
// Trigger parent to show processing state
ctx.props().on_process_payment.emit(());
spawn_local(async move {
match Self::confirm_payment(&client_secret).await {
Ok(_) => {
link.send_message(StepPaymentStripeMsg::PaymentComplete);
}
Err(e) => {
link.send_message(StepPaymentStripeMsg::PaymentError(e));
}
}
});
}
async fn confirm_payment(client_secret: &str) -> Result<(), String> {
use wasm_bindgen_futures::JsFuture;
console::log_1(&"🔄 Confirming payment with Stripe...".into());
// Call JavaScript function to confirm payment
let promise = confirmStripePayment(client_secret);
JsFuture::from(promise).await
.map_err(|e| format!("Payment confirmation failed: {:?}", e))?;
console::log_1(&"✅ Payment confirmed successfully".into());
Ok(())
}
}

View File

@ -0,0 +1,245 @@
use yew::prelude::*;
use web_sys::{HtmlInputElement, HtmlSelectElement};
use crate::models::company::DigitalResidentFormData;
#[derive(Properties, PartialEq)]
pub struct StepThreeProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepThree)]
pub fn step_three(props: &StepThreeProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let skills_input = use_state(|| String::new());
let on_input = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let field_name = input.name();
let value = input.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"occupation" => updated_data.occupation = value,
"employer" => updated_data.employer = if value.is_empty() { None } else { Some(value) },
"annual_income" => updated_data.annual_income = if value.is_empty() { None } else { Some(value) },
_ => {}
}
on_change.emit(updated_data);
})
};
let on_education_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let select: HtmlSelectElement = e.target_unchecked_into();
let value = select.value();
let mut updated_data = form_data.clone();
updated_data.education_level = value;
on_change.emit(updated_data);
})
};
let on_income_change = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: Event| {
let select: HtmlSelectElement = e.target_unchecked_into();
let value = select.value();
let mut updated_data = form_data.clone();
updated_data.annual_income = if value.is_empty() { None } else { Some(value) };
on_change.emit(updated_data);
})
};
let on_skill_input = {
let skills_input = skills_input.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
skills_input.set(input.value());
})
};
let add_skill = {
let form_data = form_data.clone();
let on_change = on_change.clone();
let skills_input = skills_input.clone();
Callback::from(move |e: KeyboardEvent| {
if e.key() == "Enter" {
e.prevent_default();
let skill = (*skills_input).trim().to_string();
if !skill.is_empty() && !form_data.skills.contains(&skill) {
let mut updated_data = form_data.clone();
updated_data.skills.push(skill);
on_change.emit(updated_data);
skills_input.set(String::new());
}
}
})
};
let remove_skill = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |skill: String| {
let mut updated_data = form_data.clone();
updated_data.skills.retain(|s| s != &skill);
on_change.emit(updated_data);
})
};
html! {
<div class="step-content">
<div class="step-header mb-4">
<h3 class="step-title">{"Professional Information"}</h3>
<p class="step-description text-muted">
{"Tell us about your professional background and qualifications."}
</p>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="occupation" class="form-label">
{"Occupation"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="occupation"
name="occupation"
value={form_data.occupation.clone()}
oninput={on_input.clone()}
placeholder="e.g., Software Engineer, Doctor, Teacher"
required=true
/>
</div>
<div class="col-md-6 mb-3">
<label for="employer" class="form-label">
{"Current Employer"}
</label>
<input
type="text"
class="form-control"
id="employer"
name="employer"
value={form_data.employer.clone().unwrap_or_default()}
oninput={on_input.clone()}
placeholder="Company or organization name"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="education_level" class="form-label">
{"Education Level"} <span class="text-danger">{"*"}</span>
</label>
<select
class="form-select"
id="education_level"
name="education_level"
value={form_data.education_level.clone()}
onchange={on_education_change}
required=true
>
<option value="">{"Select education level"}</option>
<option value="High School">{"High School"}</option>
<option value="Associate Degree">{"Associate Degree"}</option>
<option value="Bachelor's Degree">{"Bachelor's Degree"}</option>
<option value="Master's Degree">{"Master's Degree"}</option>
<option value="Doctorate">{"Doctorate"}</option>
<option value="Professional Certification">{"Professional Certification"}</option>
<option value="Other">{"Other"}</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="annual_income" class="form-label">
{"Annual Income (Optional)"}
</label>
<select
class="form-select"
id="annual_income"
name="annual_income"
value={form_data.annual_income.clone().unwrap_or_default()}
onchange={on_income_change}
>
<option value="">{"Prefer not to say"}</option>
<option value="Under $25,000">{"Under $25,000"}</option>
<option value="$25,000 - $50,000">{"$25,000 - $50,000"}</option>
<option value="$50,000 - $75,000">{"$50,000 - $75,000"}</option>
<option value="$75,000 - $100,000">{"$75,000 - $100,000"}</option>
<option value="$100,000 - $150,000">{"$100,000 - $150,000"}</option>
<option value="$150,000 - $250,000">{"$150,000 - $250,000"}</option>
<option value="Over $250,000">{"Over $250,000"}</option>
</select>
</div>
</div>
<div class="mb-4">
<label for="skills" class="form-label">
{"Skills & Expertise"}
</label>
<input
type="text"
class="form-control"
id="skills"
value={(*skills_input).clone()}
oninput={on_skill_input}
onkeypress={add_skill}
placeholder="Type a skill and press Enter to add"
/>
<div class="form-text">
{"Add your professional skills, certifications, or areas of expertise"}
</div>
{if !form_data.skills.is_empty() {
html! {
<div class="mt-3">
<div class="d-flex flex-wrap gap-2">
{for form_data.skills.iter().map(|skill| {
let skill_clone = skill.clone();
let remove_callback = {
let remove_skill = remove_skill.clone();
let skill = skill.clone();
Callback::from(move |_: MouseEvent| {
remove_skill.emit(skill.clone());
})
};
html! {
<span class="badge bg-primary d-flex align-items-center">
{skill_clone}
<button
type="button"
class="btn-close btn-close-white ms-2"
style="font-size: 0.7em;"
onclick={remove_callback}
/>
</span>
}
})}
</div>
</div>
}
} else {
html! {}
}}
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{"Your professional information helps us recommend relevant digital services and opportunities within the ecosystem."}
</div>
</div>
}
}

View File

@ -0,0 +1,169 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::DigitalResidentFormData;
#[derive(Properties, PartialEq)]
pub struct StepTwoProps {
pub form_data: DigitalResidentFormData,
pub on_change: Callback<DigitalResidentFormData>,
}
#[function_component(StepTwo)]
pub fn step_two(props: &StepTwoProps) -> Html {
let form_data = props.form_data.clone();
let on_change = props.on_change.clone();
let on_input = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
let field_name = input.name();
let value = input.value();
let mut updated_data = form_data.clone();
match field_name.as_str() {
"current_address" => updated_data.current_address = value,
"city" => updated_data.city = value,
"country" => updated_data.country = value,
"postal_code" => updated_data.postal_code = value,
"permanent_address" => updated_data.permanent_address = if value.is_empty() { None } else { Some(value) },
_ => {}
}
on_change.emit(updated_data);
})
};
let same_as_current = {
let form_data = form_data.clone();
let on_change = on_change.clone();
Callback::from(move |_: MouseEvent| {
let mut updated_data = form_data.clone();
updated_data.permanent_address = Some(updated_data.current_address.clone());
on_change.emit(updated_data);
})
};
html! {
<div class="step-content">
<div class="step-header mb-4">
<h3 class="step-title">{"Address Information"}</h3>
<p class="step-description text-muted">
{"Please provide your current and permanent address details."}
</p>
</div>
<div class="mb-4">
<h5 class="mb-3">{"Current Address"}</h5>
<div class="mb-3">
<label for="current_address" class="form-label">
{"Street Address"} <span class="text-danger">{"*"}</span>
</label>
<textarea
class="form-control"
id="current_address"
name="current_address"
rows="3"
value={form_data.current_address.clone()}
oninput={on_input.clone()}
placeholder="Enter your full street address"
required=true
/>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<label for="city" class="form-label">
{"City"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="city"
name="city"
value={form_data.city.clone()}
oninput={on_input.clone()}
placeholder="Enter city"
required=true
/>
</div>
<div class="col-md-4 mb-3">
<label for="country" class="form-label">
{"Country"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="country"
name="country"
value={form_data.country.clone()}
oninput={on_input.clone()}
placeholder="Enter country"
required=true
/>
</div>
<div class="col-md-4 mb-3">
<label for="postal_code" class="form-label">
{"Postal Code"} <span class="text-danger">{"*"}</span>
</label>
<input
type="text"
class="form-control"
id="postal_code"
name="postal_code"
value={form_data.postal_code.clone()}
oninput={on_input.clone()}
placeholder="Enter postal code"
required=true
/>
</div>
</div>
</div>
<div class="mb-4">
<h5 class="mb-3">{"Permanent Address"}</h5>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="same_address"
onclick={same_as_current}
/>
<label class="form-check-label" for="same_address">
{"Same as current address"}
</label>
</div>
</div>
<div class="mb-3">
<label for="permanent_address" class="form-label">
{"Permanent Address"}
</label>
<textarea
class="form-control"
id="permanent_address"
name="permanent_address"
rows="3"
value={form_data.permanent_address.clone().unwrap_or_default()}
oninput={on_input.clone()}
placeholder="Enter permanent address (if different from current)"
/>
<div class="form-text">
{"Leave empty if same as current address"}
</div>
</div>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{"Your address information is used for verification purposes and to determine applicable services in your region."}
</div>
</div>
}
}

View File

@ -0,0 +1,96 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
#[derive(Properties, PartialEq)]
pub struct LoginFormProps {
pub on_submit: Callback<(String, String)>, // (email, password)
pub error_message: Option<String>,
}
#[function_component(LoginForm)]
pub fn login_form(props: &LoginFormProps) -> Html {
let email_ref = use_node_ref();
let password_ref = use_node_ref();
let on_submit = props.on_submit.clone();
let onsubmit = {
let email_ref = email_ref.clone();
let password_ref = password_ref.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
let email = email_ref
.cast::<HtmlInputElement>()
.map(|input| input.value())
.unwrap_or_default();
let password = password_ref
.cast::<HtmlInputElement>()
.map(|input| input.value())
.unwrap_or_default();
on_submit.emit((email, password));
})
};
html! {
<div class="login-container">
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card shadow login-card">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">{"Login"}</h4>
</div>
<div class="card-body">
{if let Some(error) = &props.error_message {
html! {
<div class="alert alert-danger" role="alert">
{error}
</div>
}
} else {
html! {}
}}
<form {onsubmit}>
<div class="mb-3">
<label for="email" class="form-label">{"Email address"}</label>
<input
type="email"
class="form-control"
id="email"
name="email"
ref={email_ref}
required=true
/>
</div>
<div class="mb-3">
<label for="password" class="form-label">{"Password"}</label>
<input
type="password"
class="form-control"
id="password"
name="password"
ref={password_ref}
required=true
/>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">{"Login"}</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p class="mb-0">
{"Don't have an account? "}
<a href="/register">{"Register"}</a>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,3 @@
pub mod login_form;
pub use login_form::*;

View File

@ -0,0 +1,32 @@
use yew::prelude::*;
#[function_component(Footer)]
pub fn footer() -> Html {
html! {
<footer class="footer bg-dark text-white">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col-md-4 text-center text-md-start mb-2 mb-md-0">
<small>{"Convenience, Safety and Privacy"}</small>
</div>
<div class="col-md-4 text-center mb-2 mb-md-0">
<a
class="text-white text-decoration-none mx-2"
target="_blank"
href="https://info.ourworld.tf/zdfz"
>
{"About"}
</a>
<span class="text-white">{"| "}</span>
<a class="text-white text-decoration-none mx-2" href="/contact">
{"Contact"}
</a>
</div>
<div class="col-md-4 text-center text-md-end">
<small>{"© 2024 Zanzibar Digital Freezone"}</small>
</div>
</div>
</div>
</footer>
}
}

View File

@ -0,0 +1,192 @@
use yew::prelude::*;
use web_sys::MouseEvent;
use crate::routing::{ViewContext, AppView};
#[derive(Properties, PartialEq)]
pub struct HeaderProps {
pub user_name: Option<String>,
pub entity_name: Option<String>,
pub current_context: ViewContext,
pub is_dark_mode: bool,
pub on_sidebar_toggle: Callback<MouseEvent>,
pub on_login: Callback<MouseEvent>,
pub on_logout: Callback<MouseEvent>,
pub on_context_change: Callback<ViewContext>,
pub on_navigate: Callback<AppView>,
pub on_theme_toggle: Callback<MouseEvent>,
}
#[function_component(Header)]
pub fn header(props: &HeaderProps) -> Html {
let user_name = props.user_name.clone();
let entity_name = props.entity_name.clone();
let on_sidebar_toggle = props.on_sidebar_toggle.clone();
let on_login = props.on_login.clone();
let on_logout = props.on_logout.clone();
let on_theme_toggle = props.on_theme_toggle.clone();
html! {
<header class={classes!(
"header",
if props.is_dark_mode { "bg-dark text-white" } else { "bg-light text-dark" }
)}>
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center h-100 py-2">
// Left side - Logo and mobile menu
<div class="d-flex align-items-center">
<button
class={classes!(
"navbar-toggler",
"d-md-none",
"me-3",
"btn",
"btn-sm",
if props.is_dark_mode { "btn-outline-light" } else { "btn-outline-dark" }
)}
type="button"
onclick={on_sidebar_toggle}
aria-label="Toggle navigation"
>
<i class="bi bi-list"></i>
</button>
<div class="d-flex align-items-center">
<i class="bi bi-building-gear text-primary fs-4 me-2"></i>
<div>
<h5 class="mb-0 fw-bold">{"Zanzibar Digital Freezone"}</h5>
{if let Some(entity) = entity_name {
html! { <small class="text-info">{entity}</small> }
} else {
html! {}
}}
</div>
</div>
</div>
// Right side - Theme toggle and user actions
<div class="d-flex align-items-center gap-2">
// Dark/Light mode toggle
<button
class={classes!(
"btn",
"btn-sm",
if props.is_dark_mode { "btn-outline-light" } else { "btn-outline-dark" }
)}
onclick={on_theme_toggle}
title={if props.is_dark_mode { "Switch to light mode" } else { "Switch to dark mode" }}
>
<i class={if props.is_dark_mode { "bi bi-sun" } else { "bi bi-moon" }}></i>
</button>
{if let Some(user) = user_name {
html! {
<div class="dropdown">
<button
class={classes!(
"btn",
"dropdown-toggle",
"d-flex",
"align-items-center",
if props.is_dark_mode { "btn-outline-light" } else { "btn-outline-dark" }
)}
type="button"
id="userDropdown"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="bi bi-person-circle me-2"></i>
<span class="d-none d-md-inline">{user}</span>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
// Context switcher in dropdown
<li><h6 class="dropdown-header">{"Switch Context"}</h6></li>
<li>
<button
class={classes!(
"dropdown-item",
"d-flex",
"align-items-center",
matches!(props.current_context, ViewContext::Business).then_some("active")
)}
onclick={
let on_context_change = props.on_context_change.clone();
Callback::from(move |_: MouseEvent| {
on_context_change.emit(ViewContext::Business);
})
}
>
<i class="bi bi-building me-2"></i>
{"Business Mode"}
</button>
</li>
<li>
<button
class={classes!(
"dropdown-item",
"d-flex",
"align-items-center",
matches!(props.current_context, ViewContext::Person).then_some("active")
)}
onclick={
let on_context_change = props.on_context_change.clone();
Callback::from(move |_: MouseEvent| {
on_context_change.emit(ViewContext::Person);
})
}
>
<i class="bi bi-person me-2"></i>
{"Personal Mode"}
</button>
</li>
<li><hr class="dropdown-divider" /></li>
// User menu items
<li><a class="dropdown-item" href="#"><i class="bi bi-person me-2"></i>{"Profile"}</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-gear me-2"></i>{"Settings"}</a></li>
<li><a class="dropdown-item" href="#"><i class="bi bi-question-circle me-2"></i>{"Help"}</a></li>
<li><hr class="dropdown-divider" /></li>
<li>
<button
class="dropdown-item text-danger"
onclick={on_logout}
>
<i class="bi bi-box-arrow-right me-2"></i>
{"Logout"}
</button>
</li>
</ul>
</div>
}
} else {
html! {
<div class="d-flex align-items-center gap-2">
<button
class={classes!(
"btn",
if props.is_dark_mode { "btn-outline-light" } else { "btn-outline-dark" }
)}
onclick={on_login}
>
<i class="bi bi-box-arrow-in-right me-1"></i>
{"Login"}
</button>
<button
class="btn btn-primary"
onclick={
let on_navigate = props.on_navigate.clone();
Callback::from(move |_: MouseEvent| {
on_navigate.emit(AppView::ResidentRegister);
})
}
>
<i class="bi bi-person-plus me-1"></i>
{"Register"}
</button>
</div>
}
}}
</div>
</div>
</div>
</header>
}
}

View File

@ -0,0 +1,7 @@
pub mod header;
pub mod sidebar;
pub mod footer;
pub use header::*;
pub use sidebar::*;
pub use footer::*;

View File

@ -0,0 +1,230 @@
use yew::prelude::*;
use crate::routing::{AppView, ViewContext};
#[derive(Properties, PartialEq)]
pub struct SidebarProps {
pub current_view: AppView,
pub current_context: ViewContext,
pub is_visible: bool,
pub on_view_change: Callback<AppView>,
}
#[function_component(Sidebar)]
pub fn sidebar(props: &SidebarProps) -> Html {
let current_view = props.current_view.clone();
let current_context = props.current_context.clone();
let on_view_change = props.on_view_change.clone();
let sidebar_class = if props.is_visible {
"sidebar shadow-sm border-end d-flex show"
} else {
"sidebar shadow-sm border-end d-flex"
};
// All possible navigation items
let all_nav_items = vec![
AppView::Home,
AppView::Administration,
AppView::PersonAdministration,
AppView::Residence,
AppView::Accounting,
AppView::Contracts,
AppView::Governance,
AppView::Treasury,
AppView::Entities,
];
// Filter items based on current context
let nav_items: Vec<AppView> = all_nav_items
.into_iter()
.filter(|view| view.is_available_for_context(&current_context))
.collect();
html! {
<div class={sidebar_class} id="sidebar">
<div class="py-2">
// Identity Card - Business/Residence
<div class="px-3 mb-3">
{match current_context {
ViewContext::Business => {
let business_view = AppView::Business;
let is_active = business_view == current_view;
let on_click = {
let on_view_change = on_view_change.clone();
Callback::from(move |_: MouseEvent| {
on_view_change.emit(AppView::Business);
})
};
html! {
<div
class={classes!(
"card",
"shadow-sm",
if is_active { "bg-dark text-white border-0" } else { "bg-white border-dark" },
"cursor-pointer"
)}
onclick={on_click}
style="cursor: pointer;"
>
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class={classes!(
"rounded-circle",
"d-flex",
"align-items-center",
"justify-content-center",
"me-3",
if is_active { "bg-white text-dark" } else { "bg-dark text-white" }
)} style="width: 40px; height: 40px;">
<i class="bi bi-building fs-5"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-0">{"TechCorp Solutions"}</h6>
<small class={classes!(
"font-monospace",
if is_active { "text-white-50" } else { "text-muted" }
)}>
{"BIZ-2024-001"}
</small>
</div>
</div>
</div>
</div>
}
},
ViewContext::Person => {
let residence_view = AppView::Residence;
let is_active = residence_view == current_view;
let on_click = {
let on_view_change = on_view_change.clone();
Callback::from(move |_: MouseEvent| {
on_view_change.emit(AppView::Residence);
})
};
html! {
<div
class={classes!(
"card",
"shadow-sm",
if is_active { "bg-dark text-white border-0" } else { "bg-white border-dark" },
"cursor-pointer"
)}
onclick={on_click}
style="cursor: pointer;"
>
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class={classes!(
"rounded-circle",
"d-flex",
"align-items-center",
"justify-content-center",
"me-3",
if is_active { "bg-white text-dark" } else { "bg-dark text-white" }
)} style="width: 40px; height: 40px;">
<i class="bi bi-person fs-5"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-0">{"John Doe"}</h6>
<small class={classes!(
"font-monospace",
if is_active { "text-white-50" } else { "text-muted" }
)}>
{"RES-ZNZ-2024-042"}
</small>
</div>
</div>
</div>
</div>
}
}
}}
</div>
// Horizontal divider
<div class="px-3 mb-3">
<hr class="text-muted" />
</div>
// Navigation items
<ul class="nav flex-column">
{for nav_items.iter().map(|view| {
let is_active = *view == current_view;
let view_to_emit = view.clone();
let on_click = {
let on_view_change = on_view_change.clone();
Callback::from(move |_: MouseEvent| {
on_view_change.emit(view_to_emit.clone());
})
};
html! {
<li class="nav-item">
<a
class={classes!(
"nav-link",
"d-flex",
"align-items-center",
"ps-3",
"py-2",
is_active.then_some("active"),
is_active.then_some("fw-bold"),
is_active.then_some("border-start"),
is_active.then_some("border-4"),
is_active.then_some("border-primary"),
)}
href="#"
onclick={on_click}
>
<i class={classes!("bi", view.get_icon(), "me-2")}></i>
{view.get_title(&current_context)}
</a>
</li>
}
})}
</ul>
// Divider for external applications
<div class="px-3 my-3">
<hr class="text-muted" />
</div>
// External Applications
<div class="px-3">
// Marketplace Button
<div class="card border-primary mb-3" style="cursor: pointer;">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-primary text-white d-flex align-items-center justify-content-center me-3" style="width: 35px; height: 35px;">
<i class="bi bi-shop fs-6"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-1 text-primary">{"Marketplace"}</h6>
<small class="text-muted">{"Browse contract templates"}</small>
</div>
</div>
</div>
</div>
// DeFi Button
<div class="card border-success" style="cursor: pointer;">
<div class="card-body p-3">
<div class="d-flex align-items-center">
<div class="rounded-circle bg-success text-white d-flex align-items-center justify-content-center me-3" style="width: 35px; height: 35px;">
<i class="bi bi-currency-exchange fs-6"></i>
</div>
<div class="flex-grow-1">
<h6 class="mb-1 text-success">{"DeFi"}</h6>
<small class="text-muted">{"Financial tools & escrow"}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,21 @@
pub mod layout;
pub mod forms;
pub mod cards;
pub mod view_component;
pub mod empty_state;
pub mod entities;
pub mod toast;
pub mod common;
pub mod accounting;
pub mod resident_landing_overlay;
pub use layout::*;
pub use forms::*;
pub use cards::*;
pub use view_component::*;
pub use empty_state::*;
pub use entities::*;
pub use toast::*;
pub use common::*;
pub use accounting::*;
pub use resident_landing_overlay::*;

View File

@ -0,0 +1,498 @@
use yew::prelude::*;
use web_sys::HtmlInputElement;
use crate::models::company::{DigitalResidentFormData, DigitalResident};
use crate::components::entities::resident_registration::SimpleResidentWizard;
#[derive(Properties, PartialEq)]
pub struct ResidentLandingOverlayProps {
pub on_registration_complete: Callback<()>,
pub on_sign_in: Callback<(String, String)>, // email, password
pub on_close: Option<Callback<()>>,
}
pub enum ResidentLandingMsg {
ShowSignIn,
ShowRegister,
UpdateEmail(String),
UpdatePassword(String),
UpdateConfirmPassword(String),
SignIn,
StartRegistration,
RegistrationComplete(DigitalResident),
BackToLanding,
}
pub struct ResidentLandingOverlay {
view_mode: ViewMode,
email: String,
password: String,
confirm_password: String,
show_registration_wizard: bool,
}
#[derive(PartialEq)]
enum ViewMode {
Landing,
SignIn,
Register,
}
impl Component for ResidentLandingOverlay {
type Message = ResidentLandingMsg;
type Properties = ResidentLandingOverlayProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
view_mode: ViewMode::Landing,
email: String::new(),
password: String::new(),
confirm_password: String::new(),
show_registration_wizard: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ResidentLandingMsg::ShowSignIn => {
self.view_mode = ViewMode::SignIn;
self.show_registration_wizard = false;
true
}
ResidentLandingMsg::ShowRegister => {
self.view_mode = ViewMode::Register;
self.show_registration_wizard = false;
true
}
ResidentLandingMsg::UpdateEmail(email) => {
self.email = email;
true
}
ResidentLandingMsg::UpdatePassword(password) => {
self.password = password;
true
}
ResidentLandingMsg::UpdateConfirmPassword(password) => {
self.confirm_password = password;
true
}
ResidentLandingMsg::SignIn => {
ctx.props().on_sign_in.emit((self.email.clone(), self.password.clone()));
false
}
ResidentLandingMsg::StartRegistration => {
self.view_mode = ViewMode::Register;
self.show_registration_wizard = true;
true
}
ResidentLandingMsg::RegistrationComplete(resident) => {
ctx.props().on_registration_complete.emit(());
false
}
ResidentLandingMsg::BackToLanding => {
self.view_mode = ViewMode::Landing;
self.show_registration_wizard = false;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
html! {
<>
<style>
{"@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }"}
</style>
<div class="position-fixed top-0 start-0 w-100 h-100 d-flex" style="z-index: 9999; background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%);">
{self.render_content(ctx)}
// Close button (if callback provided)
{if ctx.props().on_close.is_some() {
html! {
<button
class="btn btn-outline-light position-absolute top-0 end-0 m-3"
style="z-index: 10000;"
onclick={ctx.props().on_close.as_ref().unwrap().reform(|_| ())}
>
<i class="bi bi-x-lg"></i>
</button>
}
} else {
html! {}
}}
</div>
</>
}
}
}
impl ResidentLandingOverlay {
fn render_content(&self, ctx: &Context<Self>) -> Html {
// Determine column sizes based on view mode
let (left_col_class, right_col_class) = match self.view_mode {
ViewMode::Register if self.show_registration_wizard => ("col-lg-4", "col-lg-8"),
_ => ("col-lg-7", "col-lg-5"),
};
html! {
<div class="container-fluid h-100">
<div class="row h-100">
// Left side - Branding and description (shrinks when registration is active)
<div class={format!("{} d-flex align-items-center justify-content-center text-white p-5 transition-all", left_col_class)}
style="transition: all 0.5s ease-in-out;">
<div class="text-center text-lg-start" style="max-width: 600px;">
<div class="mb-4">
<i class="bi bi-globe2" style="font-size: 4rem; opacity: 0.9;"></i>
</div>
<h1 class="display-4 fw-bold mb-4">
{"Zanzibar Digital Freezone"}
</h1>
<h2 class="h3 mb-4 text-white-75">
{"Your Gateway to Digital Residency"}
</h2>
<p class="lead mb-4 text-white-75">
{"Join the world's most innovative digital economic zone. Become a digital resident and unlock access to global opportunities, seamless business registration, and cutting-edge financial services."}
</p>
{if !self.show_registration_wizard {
html! {
<div class="row text-center mt-5">
<div class="col-md-4 mb-3">
<div class="bg-white bg-opacity-10 rounded-3 p-3">
<i class="bi bi-shield-check display-6 mb-2"></i>
<h6 class="fw-bold">{"Secure Identity"}</h6>
<small class="text-white-75">{"Blockchain-verified digital identity"}</small>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="bg-white bg-opacity-10 rounded-3 p-3">
<i class="bi bi-building display-6 mb-2"></i>
<h6 class="fw-bold">{"Business Ready"}</h6>
<small class="text-white-75">{"Register companies in minutes"}</small>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="bg-white bg-opacity-10 rounded-3 p-3">
<i class="bi bi-globe display-6 mb-2"></i>
<h6 class="fw-bold">{"Global Access"}</h6>
<small class="text-white-75">{"Worldwide financial services"}</small>
</div>
</div>
</div>
}
} else {
html! {}
}}
</div>
</div>
// Right side - Sign in/Register form (expands when registration is active)
<div class={format!("{} d-flex align-items-center justify-content-center bg-white", right_col_class)}
style="transition: all 0.5s ease-in-out;">
<div class="w-100 h-100" style={if self.show_registration_wizard { "padding: 1rem;" } else { "max-width: 400px; padding: 2rem;" }}>
{match self.view_mode {
ViewMode::Landing => self.render_landing_form(ctx),
ViewMode::SignIn => self.render_sign_in_form(ctx),
ViewMode::Register if self.show_registration_wizard => self.render_embedded_registration_wizard(ctx),
ViewMode::Register => self.render_register_form(ctx),
}}
</div>
</div>
</div>
</div>
}
}
fn render_landing_form(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="text-center">
<div class="mb-4">
<i class="bi bi-person-circle text-primary" style="font-size: 4rem;"></i>
</div>
<h3 class="mb-4">{"Welcome to ZDF"}</h3>
<p class="text-muted mb-4">
{"Get started with your digital residency journey"}
</p>
<div class="d-grid gap-3">
<button
class="btn btn-primary btn-lg"
onclick={link.callback(|_| ResidentLandingMsg::StartRegistration)}
>
<i class="bi bi-person-plus me-2"></i>
{"Become a Digital Resident"}
</button>
<button
class="btn btn-outline-primary btn-lg"
onclick={link.callback(|_| ResidentLandingMsg::ShowSignIn)}
>
<i class="bi bi-box-arrow-in-right me-2"></i>
{"Sign In to Your Account"}
</button>
</div>
<div class="mt-4 pt-4 border-top">
<small class="text-muted">
{"Already have an account? "}
<a href="#" class="text-primary text-decoration-none" onclick={link.callback(|e: MouseEvent| {
e.prevent_default();
ResidentLandingMsg::ShowSignIn
})}>
{"Sign in here"}
</a>
</small>
</div>
</div>
}
}
fn render_sign_in_form(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let on_email_input = {
let link = link.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
link.send_message(ResidentLandingMsg::UpdateEmail(input.value()));
})
};
let on_password_input = {
let link = link.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
link.send_message(ResidentLandingMsg::UpdatePassword(input.value()));
})
};
let on_submit = {
let link = link.clone();
Callback::from(move |e: SubmitEvent| {
e.prevent_default();
link.send_message(ResidentLandingMsg::SignIn);
})
};
html! {
<div>
<div class="text-center mb-4">
<h3>{"Sign In"}</h3>
<p class="text-muted">{"Welcome back to your digital residency"}</p>
</div>
<form onsubmit={on_submit}>
<div class="mb-3">
<label for="signin-email" class="form-label">{"Email Address"}</label>
<input
type="email"
class="form-control form-control-lg"
id="signin-email"
value={self.email.clone()}
oninput={on_email_input}
placeholder="your.email@example.com"
required={true}
/>
</div>
<div class="mb-4">
<label for="signin-password" class="form-label">{"Password"}</label>
<input
type="password"
class="form-control form-control-lg"
id="signin-password"
value={self.password.clone()}
oninput={on_password_input}
placeholder="Enter your password"
required={true}
/>
</div>
<div class="d-grid mb-3">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-box-arrow-in-right me-2"></i>
{"Sign In"}
</button>
</div>
</form>
<div class="text-center">
<small class="text-muted">
{"Don't have an account? "}
<a href="#" class="text-primary text-decoration-none" onclick={link.callback(|e: MouseEvent| {
e.prevent_default();
ResidentLandingMsg::ShowRegister
})}>
{"Register here"}
</a>
</small>
</div>
<div class="text-center mt-3">
<button
class="btn btn-link text-muted"
onclick={link.callback(|_| ResidentLandingMsg::BackToLanding)}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
</div>
}
}
fn render_register_form(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let on_email_input = {
let link = link.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
link.send_message(ResidentLandingMsg::UpdateEmail(input.value()));
})
};
let on_password_input = {
let link = link.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
link.send_message(ResidentLandingMsg::UpdatePassword(input.value()));
})
};
let on_confirm_password_input = {
let link = link.clone();
Callback::from(move |e: InputEvent| {
let input: HtmlInputElement = e.target_unchecked_into();
link.send_message(ResidentLandingMsg::UpdateConfirmPassword(input.value()));
})
};
let passwords_match = self.password == self.confirm_password && !self.password.is_empty();
html! {
<div>
<div class="text-center mb-4">
<h3>{"Create Account"}</h3>
<p class="text-muted">{"Start your digital residency journey"}</p>
</div>
<div class="mb-3">
<label for="register-email" class="form-label">{"Email Address"}</label>
<input
type="email"
class="form-control form-control-lg"
id="register-email"
value={self.email.clone()}
oninput={on_email_input}
placeholder="your.email@example.com"
required={true}
/>
</div>
<div class="mb-3">
<label for="register-password" class="form-label">{"Password"}</label>
<input
type="password"
class="form-control form-control-lg"
id="register-password"
value={self.password.clone()}
oninput={on_password_input}
placeholder="Create a strong password"
required={true}
/>
</div>
<div class="mb-4">
<label for="register-confirm-password" class="form-label">{"Confirm Password"}</label>
<input
type="password"
class={format!("form-control form-control-lg {}",
if self.confirm_password.is_empty() { "" }
else if passwords_match { "is-valid" }
else { "is-invalid" }
)}
id="register-confirm-password"
value={self.confirm_password.clone()}
oninput={on_confirm_password_input}
placeholder="Confirm your password"
required={true}
/>
{if !self.confirm_password.is_empty() && !passwords_match {
html! {
<div class="invalid-feedback">
{"Passwords do not match"}
</div>
}
} else {
html! {}
}}
</div>
<div class="d-grid mb-3">
<button
type="button"
class="btn btn-primary btn-lg"
disabled={!passwords_match || self.email.is_empty()}
onclick={link.callback(|_| ResidentLandingMsg::StartRegistration)}
>
<i class="bi bi-person-plus me-2"></i>
{"Start Registration Process"}
</button>
</div>
<div class="text-center">
<small class="text-muted">
{"Already have an account? "}
<a href="#" class="text-primary text-decoration-none" onclick={link.callback(|e: MouseEvent| {
e.prevent_default();
ResidentLandingMsg::ShowSignIn
})}>
{"Sign in here"}
</a>
</small>
</div>
<div class="text-center mt-3">
<button
class="btn btn-link text-muted"
onclick={link.callback(|_| ResidentLandingMsg::BackToLanding)}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
</div>
}
}
fn render_embedded_registration_wizard(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="h-100 d-flex flex-column">
// Header with back button (always visible)
<div class="d-flex justify-content-between align-items-center p-3 border-bottom">
<h4 class="mb-0">{"Digital Resident Registration"}</h4>
<button
class="btn btn-outline-secondary btn-sm"
onclick={link.callback(|_| ResidentLandingMsg::BackToLanding)}
>
<i class="bi bi-arrow-left me-1"></i>{"Back"}
</button>
</div>
// Registration wizard content with fade-in animation
<div class="flex-grow-1 overflow-auto"
style="opacity: 0; animation: fadeIn 0.5s ease-in-out 0.25s forwards;">
<SimpleResidentWizard
on_registration_complete={link.callback(ResidentLandingMsg::RegistrationComplete)}
on_back_to_parent={link.callback(|_| ResidentLandingMsg::BackToLanding)}
success_resident_id={None}
show_failure={false}
/>
</div>
</div>
}
}
}

View File

@ -0,0 +1,170 @@
use yew::prelude::*;
use gloo::timers::callback::Timeout;
#[derive(Clone, PartialEq, Debug)]
pub enum ToastType {
Success,
Error,
Warning,
Info,
}
impl ToastType {
pub fn get_class(&self) -> &'static str {
match self {
ToastType::Success => "toast-success",
ToastType::Error => "toast-error",
ToastType::Warning => "toast-warning",
ToastType::Info => "toast-info",
}
}
pub fn get_icon(&self) -> &'static str {
match self {
ToastType::Success => "bi-check-circle-fill",
ToastType::Error => "bi-x-circle-fill",
ToastType::Warning => "bi-exclamation-triangle-fill",
ToastType::Info => "bi-info-circle-fill",
}
}
pub fn get_bg_class(&self) -> &'static str {
match self {
ToastType::Success => "bg-success",
ToastType::Error => "bg-danger",
ToastType::Warning => "bg-warning",
ToastType::Info => "bg-info",
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct ToastMessage {
pub id: u32,
pub title: String,
pub message: String,
pub toast_type: ToastType,
pub duration: Option<u32>, // Duration in milliseconds, None for persistent
}
#[derive(Properties, PartialEq)]
pub struct ToastProps {
pub toast: ToastMessage,
pub on_dismiss: Callback<u32>,
}
pub enum ToastMsg {
AutoDismiss,
}
pub struct Toast {
_timeout: Option<Timeout>,
}
impl Component for Toast {
type Message = ToastMsg;
type Properties = ToastProps;
fn create(ctx: &Context<Self>) -> Self {
let timeout = if let Some(duration) = ctx.props().toast.duration {
let link = ctx.link().clone();
Some(Timeout::new(duration, move || {
link.send_message(ToastMsg::AutoDismiss);
}))
} else {
None
};
Self {
_timeout: timeout,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ToastMsg::AutoDismiss => {
ctx.props().on_dismiss.emit(ctx.props().toast.id);
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let toast = &ctx.props().toast;
let on_dismiss = ctx.props().on_dismiss.clone();
let toast_id = toast.id;
html! {
<div class={format!("toast show {}", toast.toast_type.get_class())} role="alert">
<div class={format!("toast-header {}", toast.toast_type.get_bg_class())}>
<i class={format!("{} me-2 text-white", toast.toast_type.get_icon())}></i>
<strong class="me-auto text-white">{&toast.title}</strong>
<button
type="button"
class="btn-close btn-close-white"
onclick={move |_| on_dismiss.emit(toast_id)}
></button>
</div>
<div class="toast-body">
{&toast.message}
</div>
</div>
}
}
}
#[derive(Properties, PartialEq)]
pub struct ToastContainerProps {
pub toasts: Vec<ToastMessage>,
pub on_dismiss: Callback<u32>,
}
#[function_component(ToastContainer)]
pub fn toast_container(props: &ToastContainerProps) -> Html {
html! {
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1055;">
{for props.toasts.iter().map(|toast| {
html! {
<Toast
key={toast.id}
toast={toast.clone()}
on_dismiss={props.on_dismiss.clone()}
/>
}
})}
</div>
}
}
// Helper function to create success toast
pub fn create_success_toast(id: u32, title: &str, message: &str) -> ToastMessage {
ToastMessage {
id,
title: title.to_string(),
message: message.to_string(),
toast_type: ToastType::Success,
duration: Some(5000), // 5 seconds
}
}
// Helper function to create error toast
pub fn create_error_toast(id: u32, title: &str, message: &str) -> ToastMessage {
ToastMessage {
id,
title: title.to_string(),
message: message.to_string(),
toast_type: ToastType::Error,
duration: Some(8000), // 8 seconds for errors
}
}
// Helper function to create info toast
pub fn create_info_toast(id: u32, title: &str, message: &str) -> ToastMessage {
ToastMessage {
id,
title: title.to_string(),
message: message.to_string(),
toast_type: ToastType::Info,
duration: Some(4000), // 4 seconds
}
}

View File

@ -0,0 +1,152 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::components::EmptyState;
#[derive(Properties, PartialEq)]
pub struct ViewComponentProps {
#[prop_or_default]
pub title: Option<String>,
#[prop_or_default]
pub description: Option<String>,
#[prop_or_default]
pub breadcrumbs: Option<Vec<(String, Option<String>)>>, // (name, optional_link)
#[prop_or_default]
pub tabs: Option<HashMap<String, Html>>, // tab_name -> tab_content
#[prop_or_default]
pub default_tab: Option<String>,
#[prop_or_default]
pub actions: Option<Html>, // Action buttons in top-right
#[prop_or_default]
pub empty_state: Option<(String, String, String, Option<(String, String)>, Option<(String, String)>)>, // (icon, title, description, primary_action, secondary_action)
#[prop_or_default]
pub children: Children, // Main content when no tabs
}
#[function_component(ViewComponent)]
pub fn view_component(props: &ViewComponentProps) -> Html {
let active_tab = use_state(|| {
props.default_tab.clone().unwrap_or_else(|| {
props.tabs.as_ref()
.and_then(|tabs| tabs.keys().next().cloned())
.unwrap_or_default()
})
});
let on_tab_click = {
let active_tab = active_tab.clone();
Callback::from(move |tab_name: String| {
active_tab.set(tab_name);
})
};
html! {
<div class="container-fluid">
// Breadcrumbs (if provided)
if let Some(breadcrumbs) = &props.breadcrumbs {
<ol class="breadcrumb mb-3">
{for breadcrumbs.iter().enumerate().map(|(i, (name, link))| {
let is_last = i == breadcrumbs.len() - 1;
html! {
<li class={classes!("breadcrumb-item", is_last.then(|| "active"))}>
if let Some(href) = link {
<a href={href.clone()}>{name}</a>
} else {
{name}
}
</li>
}
})}
</ol>
}
// Page Header in Card (with integrated tabs if provided)
if props.title.is_some() || props.description.is_some() || props.actions.is_some() || props.tabs.is_some() {
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="d-flex justify-content-between align-items-end">
// Left side: Title and description
<div class="flex-grow-1">
if let Some(title) = &props.title {
<h2 class="mb-1">{title}</h2>
}
if let Some(description) = &props.description {
<p class="text-muted mb-0">{description}</p>
}
</div>
// Center: Tabs navigation (if provided)
if let Some(tabs) = &props.tabs {
<div class="flex-grow-1 d-flex justify-content-right">
<ul class="nav nav-tabs border-0" role="tablist">
{for tabs.keys().map(|tab_name| {
let is_active = *active_tab == *tab_name;
let tab_name_clone = tab_name.clone();
let on_click = {
let on_tab_click = on_tab_click.clone();
let tab_name = tab_name.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
on_tab_click.emit(tab_name.clone());
})
};
html! {
<li class="nav-item" role="presentation">
<button
class={classes!("nav-link", "px-4", "py-2", is_active.then(|| "active"))}
type="button"
role="tab"
onclick={on_click}
>
{tab_name}
</button>
</li>
}
})}
</ul>
</div>
}
// Right side: Actions
if let Some(actions) = &props.actions {
<div>
{actions.clone()}
</div>
}
</div>
</div>
</div>
</div>
</div>
}
// Tab Content (if tabs are provided)
if let Some(tabs) = &props.tabs {
<div class="tab-content">
{for tabs.iter().map(|(tab_name, content)| {
let is_active = *active_tab == *tab_name;
html! {
<div class={classes!("tab-pane", "fade", is_active.then(|| "show"), is_active.then(|| "active"))} role="tabpanel">
{content.clone()}
</div>
}
})}
</div>
} else if let Some((icon, title, description, primary_action, secondary_action)) = &props.empty_state {
// Render empty state
<EmptyState
icon={icon.clone()}
title={title.clone()}
description={description.clone()}
primary_action={primary_action.clone()}
secondary_action={secondary_action.clone()}
/>
} else {
// No tabs, render children directly
{for props.children.iter()}
}
</div>
}
}

18
platform/src/lib.rs Normal file
View File

@ -0,0 +1,18 @@
use wasm_bindgen::prelude::*;
mod app;
mod components;
mod views;
mod routing;
mod services;
mod models;
use app::App;
// This is the entry point for the web app
#[wasm_bindgen(start)]
pub fn run_app() {
wasm_logger::init(wasm_logger::Config::default());
log::info!("Starting Zanzibar Digital Freezone WASM app");
yew::Renderer::<App>::new().render();
}

View File

@ -0,0 +1,747 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct Company {
pub id: u32,
pub name: String,
pub company_type: CompanyType,
pub status: CompanyStatus,
pub registration_number: String,
pub incorporation_date: String,
pub email: Option<String>,
pub phone: Option<String>,
pub website: Option<String>,
pub address: Option<String>,
pub industry: Option<String>,
pub description: Option<String>,
pub fiscal_year_end: Option<String>,
pub shareholders: Vec<Shareholder>,
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
pub enum CompanyType {
SingleFZC,
StartupFZC,
GrowthFZC,
GlobalFZC,
CooperativeFZC,
}
impl CompanyType {
pub fn to_string(&self) -> String {
match self {
CompanyType::SingleFZC => "Single FZC".to_string(),
CompanyType::StartupFZC => "Startup FZC".to_string(),
CompanyType::GrowthFZC => "Growth FZC".to_string(),
CompanyType::GlobalFZC => "Global FZC".to_string(),
CompanyType::CooperativeFZC => "Cooperative FZC".to_string(),
}
}
pub fn from_string(s: &str) -> Option<Self> {
match s {
"Single FZC" => Some(CompanyType::SingleFZC),
"Startup FZC" => Some(CompanyType::StartupFZC),
"Growth FZC" => Some(CompanyType::GrowthFZC),
"Global FZC" => Some(CompanyType::GlobalFZC),
"Cooperative FZC" => Some(CompanyType::CooperativeFZC),
_ => None,
}
}
pub fn get_pricing(&self) -> CompanyPricing {
match self {
CompanyType::SingleFZC => CompanyPricing {
setup_fee: 20.0,
monthly_fee: 20.0,
max_shareholders: 1,
features: vec![
"1 shareholder".to_string(),
"Cannot issue digital assets".to_string(),
"Can hold external shares".to_string(),
"Connect to bank".to_string(),
"Participate in ecosystem".to_string(),
],
},
CompanyType::StartupFZC => CompanyPricing {
setup_fee: 50.0,
monthly_fee: 50.0,
max_shareholders: 5,
features: vec![
"Up to 5 shareholders".to_string(),
"Can issue digital assets".to_string(),
"Hold external shares".to_string(),
"Connect to bank".to_string(),
],
},
CompanyType::GrowthFZC => CompanyPricing {
setup_fee: 100.0,
monthly_fee: 100.0,
max_shareholders: 20,
features: vec![
"Up to 20 shareholders".to_string(),
"Can issue digital assets".to_string(),
"Hold external shares".to_string(),
"Connect to bank".to_string(),
"Hold physical assets".to_string(),
],
},
CompanyType::GlobalFZC => CompanyPricing {
setup_fee: 2000.0,
monthly_fee: 200.0,
max_shareholders: 999,
features: vec![
"Unlimited shareholders".to_string(),
"Can issue digital assets".to_string(),
"Hold external shares".to_string(),
"Connect to bank".to_string(),
"Hold physical assets".to_string(),
],
},
CompanyType::CooperativeFZC => CompanyPricing {
setup_fee: 2000.0,
monthly_fee: 200.0,
max_shareholders: 999,
features: vec![
"Unlimited members".to_string(),
"Democratic governance".to_string(),
"Collective decision-making".to_string(),
"Equitable distribution".to_string(),
],
},
}
}
pub fn get_capabilities(&self) -> HashMap<String, bool> {
let mut capabilities = HashMap::new();
// All types have these basic capabilities
capabilities.insert("digital_assets".to_string(), true);
capabilities.insert("ecosystem".to_string(), true);
capabilities.insert("ai_dispute".to_string(), true);
capabilities.insert("digital_signing".to_string(), true);
capabilities.insert("external_shares".to_string(), true);
capabilities.insert("bank_account".to_string(), true);
// Type-specific capabilities
match self {
CompanyType::SingleFZC => {
capabilities.insert("issue_assets".to_string(), false);
capabilities.insert("physical_assets".to_string(), false);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::StartupFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), false);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::GrowthFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), true);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::GlobalFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), true);
capabilities.insert("democratic".to_string(), false);
capabilities.insert("collective".to_string(), false);
},
CompanyType::CooperativeFZC => {
capabilities.insert("issue_assets".to_string(), true);
capabilities.insert("physical_assets".to_string(), true);
capabilities.insert("democratic".to_string(), true);
capabilities.insert("collective".to_string(), true);
},
}
capabilities
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum CompanyStatus {
Active,
Inactive,
Suspended,
PendingPayment,
}
impl CompanyStatus {
pub fn to_string(&self) -> String {
match self {
CompanyStatus::Active => "Active".to_string(),
CompanyStatus::Inactive => "Inactive".to_string(),
CompanyStatus::Suspended => "Suspended".to_string(),
CompanyStatus::PendingPayment => "Pending Payment".to_string(),
}
}
pub fn get_badge_class(&self) -> String {
match self {
CompanyStatus::Active => "badge bg-success".to_string(),
CompanyStatus::Inactive => "badge bg-secondary".to_string(),
CompanyStatus::Suspended => "badge bg-warning text-dark".to_string(),
CompanyStatus::PendingPayment => "badge bg-info".to_string(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct CompanyPricing {
pub setup_fee: f64,
pub monthly_fee: f64,
pub max_shareholders: u32,
pub features: Vec<String>,
}
// Registration form data
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct CompanyFormData {
// Step 1: General Information
pub company_name: String,
pub company_email: String,
pub company_phone: String,
pub company_website: Option<String>,
pub company_address: String,
pub company_industry: Option<String>,
pub company_purpose: Option<String>,
pub fiscal_year_end: Option<String>,
// Step 2: Company Type
pub company_type: CompanyType,
// Step 3: Shareholders
pub shareholder_structure: ShareholderStructure,
pub shareholders: Vec<Shareholder>,
// Step 4: Payment & Agreements
pub payment_plan: PaymentPlan,
pub legal_agreements: LegalAgreements,
}
impl Default for CompanyFormData {
fn default() -> Self {
Self {
company_name: String::new(),
company_email: String::new(),
company_phone: String::new(),
company_website: None,
company_address: String::new(),
company_industry: None,
company_purpose: None,
fiscal_year_end: None,
company_type: CompanyType::StartupFZC,
shareholder_structure: ShareholderStructure::Equal,
shareholders: vec![Shareholder {
name: String::new(),
resident_id: String::new(),
percentage: 100.0,
}],
payment_plan: PaymentPlan::Monthly,
legal_agreements: LegalAgreements::default(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct Shareholder {
pub name: String,
pub resident_id: String,
pub percentage: f64,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum ShareholderStructure {
Equal,
Custom,
}
impl ShareholderStructure {
pub fn to_string(&self) -> String {
match self {
ShareholderStructure::Equal => "equal".to_string(),
ShareholderStructure::Custom => "custom".to_string(),
}
}
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
pub enum PaymentPlan {
Monthly,
Yearly,
TwoYear,
}
impl PaymentPlan {
pub fn to_string(&self) -> String {
match self {
PaymentPlan::Monthly => "monthly".to_string(),
PaymentPlan::Yearly => "yearly".to_string(),
PaymentPlan::TwoYear => "two_year".to_string(),
}
}
pub fn from_string(s: &str) -> Option<Self> {
match s {
"monthly" => Some(PaymentPlan::Monthly),
"yearly" => Some(PaymentPlan::Yearly),
"two_year" => Some(PaymentPlan::TwoYear),
_ => None,
}
}
pub fn get_display_name(&self) -> String {
match self {
PaymentPlan::Monthly => "Monthly".to_string(),
PaymentPlan::Yearly => "Yearly".to_string(),
PaymentPlan::TwoYear => "2 Years".to_string(),
}
}
pub fn get_discount(&self) -> f64 {
match self {
PaymentPlan::Monthly => 1.0,
PaymentPlan::Yearly => 0.8, // 20% discount
PaymentPlan::TwoYear => 0.6, // 40% discount
}
}
pub fn get_badge_class(&self) -> Option<String> {
match self {
PaymentPlan::Monthly => None,
PaymentPlan::Yearly => Some("badge bg-success".to_string()),
PaymentPlan::TwoYear => Some("badge bg-warning".to_string()),
}
}
pub fn get_badge_text(&self) -> Option<String> {
match self {
PaymentPlan::Monthly => None,
PaymentPlan::Yearly => Some("20% OFF".to_string()),
PaymentPlan::TwoYear => Some("40% OFF".to_string()),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct LegalAgreements {
pub terms: bool,
pub privacy: bool,
pub compliance: bool,
pub articles: bool,
pub final_agreement: bool,
}
impl Default for LegalAgreements {
fn default() -> Self {
Self {
terms: false,
privacy: false,
compliance: false,
articles: false,
final_agreement: false,
}
}
}
impl LegalAgreements {
pub fn all_agreed(&self) -> bool {
self.terms && self.privacy && self.compliance && self.articles && self.final_agreement
}
pub fn missing_agreements(&self) -> Vec<String> {
let mut missing = Vec::new();
if !self.terms {
missing.push("Terms of Service".to_string());
}
if !self.privacy {
missing.push("Privacy Policy".to_string());
}
if !self.compliance {
missing.push("Compliance Agreement".to_string());
}
if !self.articles {
missing.push("Articles of Incorporation".to_string());
}
if !self.final_agreement {
missing.push("Final Agreement".to_string());
}
missing
}
}
// State management structures
#[derive(Clone, PartialEq)]
pub struct EntitiesState {
pub active_tab: ActiveTab,
pub companies: Vec<Company>,
pub registration_state: RegistrationState,
pub loading: bool,
pub error: Option<String>,
}
impl Default for EntitiesState {
fn default() -> Self {
Self {
active_tab: ActiveTab::Companies,
companies: Vec::new(),
registration_state: RegistrationState::default(),
loading: false,
error: None,
}
}
}
#[derive(Clone, PartialEq)]
pub struct RegistrationState {
pub current_step: u8,
pub form_data: CompanyFormData,
pub validation_errors: std::collections::HashMap<String, String>,
pub payment_intent: Option<String>, // Payment intent ID
pub auto_save_enabled: bool,
pub processing_payment: bool,
}
impl Default for RegistrationState {
fn default() -> Self {
Self {
current_step: 1,
form_data: CompanyFormData::default(),
validation_errors: std::collections::HashMap::new(),
payment_intent: None,
auto_save_enabled: true,
processing_payment: false,
}
}
}
#[derive(Clone, PartialEq)]
pub enum ActiveTab {
Companies,
RegisterCompany,
}
impl ActiveTab {
pub fn to_string(&self) -> String {
match self {
ActiveTab::Companies => "Companies".to_string(),
ActiveTab::RegisterCompany => "Register Company".to_string(),
}
}
}
// Payment-related structures
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct PaymentIntent {
pub id: String,
pub client_secret: String,
pub amount: f64,
pub currency: String,
pub status: String,
}
// Validation result
#[derive(Clone, PartialEq, Debug)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<String>,
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
}
}
pub fn invalid(errors: Vec<String>) -> Self {
Self {
is_valid: false,
errors,
}
}
}
// Digital Resident Registration Models
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DigitalResidentFormData {
// Step 1: Personal Information
pub full_name: String,
pub email: String,
pub phone: String,
pub date_of_birth: String,
pub nationality: String,
pub passport_number: String,
pub passport_expiry: String,
// Cryptographic Keys
pub public_key: Option<String>,
pub private_key: Option<String>,
pub private_key_shown: bool, // Track if private key has been shown
// Step 2: Address Information
pub current_address: String,
pub city: String,
pub country: String,
pub postal_code: String,
pub permanent_address: Option<String>,
// Step 3: Professional Information
pub occupation: String,
pub employer: Option<String>,
pub annual_income: Option<String>,
pub education_level: String,
pub skills: Vec<String>,
// Step 4: Digital Services
pub requested_services: Vec<DigitalService>,
pub preferred_language: String,
pub communication_preferences: CommunicationPreferences,
// Step 5: Payment & Agreements
pub payment_plan: ResidentPaymentPlan,
pub legal_agreements: LegalAgreements,
}
impl Default for DigitalResidentFormData {
fn default() -> Self {
Self {
full_name: String::new(),
email: String::new(),
phone: String::new(),
date_of_birth: String::new(),
nationality: String::new(),
passport_number: String::new(),
passport_expiry: String::new(),
public_key: None,
private_key: None,
private_key_shown: false,
current_address: String::new(),
city: String::new(),
country: String::new(),
postal_code: String::new(),
permanent_address: None,
occupation: String::new(),
employer: None,
annual_income: None,
education_level: String::new(),
skills: Vec::new(),
requested_services: Vec::new(),
preferred_language: "English".to_string(),
communication_preferences: CommunicationPreferences::default(),
payment_plan: ResidentPaymentPlan::Monthly,
legal_agreements: LegalAgreements::default(),
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum DigitalService {
BankingAccess,
TaxFiling,
HealthcareAccess,
EducationServices,
BusinessLicensing,
PropertyServices,
LegalServices,
DigitalIdentity,
}
impl DigitalService {
pub fn get_display_name(&self) -> &'static str {
match self {
DigitalService::BankingAccess => "Banking Access",
DigitalService::TaxFiling => "Tax Filing Services",
DigitalService::HealthcareAccess => "Healthcare Access",
DigitalService::EducationServices => "Education Services",
DigitalService::BusinessLicensing => "Business Licensing",
DigitalService::PropertyServices => "Property Services",
DigitalService::LegalServices => "Legal Services",
DigitalService::DigitalIdentity => "Digital Identity",
}
}
pub fn get_description(&self) -> &'static str {
match self {
DigitalService::BankingAccess => "Access to digital banking services and financial institutions",
DigitalService::TaxFiling => "Automated tax filing and compliance services",
DigitalService::HealthcareAccess => "Access to healthcare providers and medical services",
DigitalService::EducationServices => "Educational resources and certification programs",
DigitalService::BusinessLicensing => "Business registration and licensing services",
DigitalService::PropertyServices => "Property rental and purchase assistance",
DigitalService::LegalServices => "Legal consultation and document services",
DigitalService::DigitalIdentity => "Secure digital identity verification",
}
}
pub fn get_icon(&self) -> &'static str {
match self {
DigitalService::BankingAccess => "bi-bank",
DigitalService::TaxFiling => "bi-calculator",
DigitalService::HealthcareAccess => "bi-heart-pulse",
DigitalService::EducationServices => "bi-mortarboard",
DigitalService::BusinessLicensing => "bi-briefcase",
DigitalService::PropertyServices => "bi-house",
DigitalService::LegalServices => "bi-scales",
DigitalService::DigitalIdentity => "bi-person-badge",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct CommunicationPreferences {
pub email_notifications: bool,
pub sms_notifications: bool,
pub push_notifications: bool,
pub newsletter: bool,
}
impl Default for CommunicationPreferences {
fn default() -> Self {
Self {
email_notifications: true,
sms_notifications: false,
push_notifications: true,
newsletter: false,
}
}
}
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, Debug)]
pub enum ResidentPaymentPlan {
Monthly,
Yearly,
Lifetime,
}
impl ResidentPaymentPlan {
pub fn get_display_name(&self) -> &'static str {
match self {
ResidentPaymentPlan::Monthly => "Monthly",
ResidentPaymentPlan::Yearly => "Yearly",
ResidentPaymentPlan::Lifetime => "Lifetime",
}
}
pub fn get_price(&self) -> f64 {
match self {
ResidentPaymentPlan::Monthly => 29.99,
ResidentPaymentPlan::Yearly => 299.99, // ~17% discount
ResidentPaymentPlan::Lifetime => 999.99,
}
}
pub fn get_discount(&self) -> f64 {
match self {
ResidentPaymentPlan::Monthly => 1.0,
ResidentPaymentPlan::Yearly => 0.83, // 17% discount
ResidentPaymentPlan::Lifetime => 0.0, // Special pricing
}
}
pub fn get_description(&self) -> &'static str {
match self {
ResidentPaymentPlan::Monthly => "Pay monthly with full flexibility",
ResidentPaymentPlan::Yearly => "Save 17% with annual payment",
ResidentPaymentPlan::Lifetime => "One-time payment for lifetime access",
}
}
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DigitalResident {
pub id: u32,
pub full_name: String,
pub email: String,
pub phone: String,
pub date_of_birth: String,
pub nationality: String,
pub passport_number: String,
pub passport_expiry: String,
pub current_address: String,
pub city: String,
pub country: String,
pub postal_code: String,
pub occupation: String,
pub employer: Option<String>,
pub annual_income: Option<String>,
pub education_level: String,
pub selected_services: Vec<DigitalService>,
pub payment_plan: ResidentPaymentPlan,
pub registration_date: String,
pub status: ResidentStatus,
// KYC fields
pub kyc_documents_uploaded: bool,
pub kyc_status: KycStatus,
// Cryptographic Keys
pub public_key: Option<String>,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum ResidentStatus {
Pending,
Active,
Suspended,
Expired,
}
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub enum KycStatus {
NotStarted,
DocumentsUploaded,
UnderReview,
Approved,
Rejected,
RequiresAdditionalInfo,
}
impl KycStatus {
pub fn to_string(&self) -> String {
match self {
KycStatus::NotStarted => "Not Started".to_string(),
KycStatus::DocumentsUploaded => "Documents Uploaded".to_string(),
KycStatus::UnderReview => "Under Review".to_string(),
KycStatus::Approved => "Approved".to_string(),
KycStatus::Rejected => "Rejected".to_string(),
KycStatus::RequiresAdditionalInfo => "Requires Additional Info".to_string(),
}
}
pub fn get_badge_class(&self) -> String {
match self {
KycStatus::NotStarted => "badge bg-secondary".to_string(),
KycStatus::DocumentsUploaded => "badge bg-info".to_string(),
KycStatus::UnderReview => "badge bg-warning text-dark".to_string(),
KycStatus::Approved => "badge bg-success".to_string(),
KycStatus::Rejected => "badge bg-danger".to_string(),
KycStatus::RequiresAdditionalInfo => "badge bg-warning text-dark".to_string(),
}
}
}
impl ResidentStatus {
pub fn to_string(&self) -> String {
match self {
ResidentStatus::Pending => "Pending".to_string(),
ResidentStatus::Active => "Active".to_string(),
ResidentStatus::Suspended => "Suspended".to_string(),
ResidentStatus::Expired => "Expired".to_string(),
}
}
pub fn get_badge_class(&self) -> String {
match self {
ResidentStatus::Pending => "badge bg-warning text-dark".to_string(),
ResidentStatus::Active => "badge bg-success".to_string(),
ResidentStatus::Suspended => "badge bg-danger".to_string(),
ResidentStatus::Expired => "badge bg-secondary".to_string(),
}
}
}

View File

@ -0,0 +1,3 @@
pub mod company;
pub use company::*;

View File

@ -0,0 +1,246 @@
use wasm_bindgen::JsValue;
#[derive(Debug, Clone, PartialEq)]
pub enum ViewContext {
Business,
Person,
}
impl ViewContext {
pub fn get_title(&self) -> &'static str {
match self {
ViewContext::Business => "For Businesses",
ViewContext::Person => "For Persons",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AppView {
Login,
Home,
Administration,
PersonAdministration,
Business,
Accounting,
Contracts,
Governance,
Treasury,
Residence,
Entities,
EntitiesRegister,
EntitiesRegisterSuccess(u32), // Company ID
EntitiesRegisterFailure,
CompanyView(u32), // Company ID
ResidentRegister,
ResidentRegisterSuccess,
ResidentRegisterFailure,
ResidentLanding, // New landing page for unregistered users
}
impl AppView {
pub fn to_path(&self) -> String {
match self {
AppView::Login => "/login".to_string(),
AppView::Home => "/".to_string(),
AppView::Administration => "/administration".to_string(),
AppView::PersonAdministration => "/person-administration".to_string(),
AppView::Business => "/business".to_string(),
AppView::Accounting => "/accounting".to_string(),
AppView::Contracts => "/contracts".to_string(),
AppView::Governance => "/governance".to_string(),
AppView::Treasury => "/treasury".to_string(),
AppView::Residence => "/residence".to_string(),
AppView::Entities => "/companies".to_string(),
AppView::EntitiesRegister => "/companies/register".to_string(),
AppView::EntitiesRegisterSuccess(id) => format!("/companies/register/success/{}", id),
AppView::EntitiesRegisterFailure => "/companies/register/failure".to_string(),
AppView::CompanyView(id) => format!("/companies/{}", id),
AppView::ResidentRegister => "/resident/register".to_string(),
AppView::ResidentRegisterSuccess => "/resident/register/success".to_string(),
AppView::ResidentRegisterFailure => "/resident/register/failure".to_string(),
AppView::ResidentLanding => "/welcome".to_string(),
}
}
pub fn from_path(path: &str) -> Self {
match path {
"/login" => AppView::Login,
"/administration" => AppView::Administration,
"/person-administration" => AppView::PersonAdministration,
"/business" => AppView::Business,
"/accounting" => AppView::Accounting,
"/contracts" => AppView::Contracts,
"/governance" => AppView::Governance,
"/treasury" => AppView::Treasury,
"/residence" => AppView::Residence,
"/entities" | "/companies" => AppView::Entities,
"/entities/register" | "/companies/register" => AppView::EntitiesRegister,
"/entities/register/failure" | "/companies/register/failure" => AppView::EntitiesRegisterFailure,
"/resident/register" => AppView::ResidentRegister,
"/resident/register/success" => AppView::ResidentRegisterSuccess,
"/resident/register/failure" => AppView::ResidentRegisterFailure,
"/welcome" => AppView::ResidentLanding,
path if path.starts_with("/entities/register/success/") || path.starts_with("/companies/register/success/") => {
// Extract company ID from path like "/companies/register/success/123"
let prefix = if path.starts_with("/entities/register/success/") {
"/entities/register/success/"
} else {
"/companies/register/success/"
};
if let Some(id_str) = path.strip_prefix(prefix) {
if let Ok(id) = id_str.parse::<u32>() {
return AppView::EntitiesRegisterSuccess(id);
}
}
AppView::Entities // Fallback to entities list if parsing fails
}
path if path.starts_with("/entities/company/") || path.starts_with("/companies/") => {
// Extract company ID from path like "/companies/123"
let prefix = if path.starts_with("/entities/company/") {
"/entities/company/"
} else {
"/companies/"
};
if let Some(id_str) = path.strip_prefix(prefix) {
if let Ok(id) = id_str.parse::<u32>() {
return AppView::CompanyView(id);
}
}
AppView::Entities // Fallback to entities list if parsing fails
}
path if path.starts_with("/company/payment-success") => {
// Handle legacy payment success redirect - redirect to entities view
// The payment success will be handled by showing a toast notification
AppView::Entities
}
_ => AppView::Home, // Default to Home for root or unknown paths
}
}
pub fn get_title(&self, context: &ViewContext) -> String {
match self {
AppView::Login => "Login".to_string(),
AppView::Home => "Home".to_string(),
AppView::Administration => "Administration".to_string(),
AppView::PersonAdministration => "Administration".to_string(),
AppView::Business => "Business".to_string(),
AppView::Accounting => "Accounting".to_string(),
AppView::Contracts => "Contracts".to_string(),
AppView::Governance => "Governance".to_string(),
AppView::Treasury => "Treasury".to_string(),
AppView::Residence => "Residence".to_string(),
AppView::Entities => "Companies".to_string(),
AppView::EntitiesRegister => "Register Company".to_string(),
AppView::EntitiesRegisterSuccess(_) => "Registration Successful".to_string(),
AppView::EntitiesRegisterFailure => "Registration Failed".to_string(),
AppView::CompanyView(_) => "Company Details".to_string(),
AppView::ResidentRegister => "Register as Digital Resident".to_string(),
AppView::ResidentRegisterSuccess => "Resident Registration Successful".to_string(),
AppView::ResidentRegisterFailure => "Resident Registration Failed".to_string(),
AppView::ResidentLanding => "Welcome to Zanzibar Digital Freezone".to_string(),
}
}
pub fn get_icon(&self) -> &'static str {
match self {
AppView::Login => "bi-box-arrow-in-right",
AppView::Home => "bi-house-door",
AppView::Administration => "bi-gear",
AppView::PersonAdministration => "bi-gear",
AppView::Business => "bi-building",
AppView::Accounting => "bi-calculator",
AppView::Contracts => "bi-file-earmark-text",
AppView::Governance => "bi-people",
AppView::Treasury => "bi-safe",
AppView::Residence => "bi-house",
AppView::Entities => "bi-building",
AppView::EntitiesRegister => "bi-plus-circle",
AppView::EntitiesRegisterSuccess(_) => "bi-check-circle",
AppView::EntitiesRegisterFailure => "bi-x-circle",
AppView::CompanyView(_) => "bi-building-check",
AppView::ResidentRegister => "bi-person-plus",
AppView::ResidentRegisterSuccess => "bi-person-check",
AppView::ResidentRegisterFailure => "bi-person-x",
AppView::ResidentLanding => "bi-globe2",
}
}
pub fn is_available_for_context(&self, context: &ViewContext) -> bool {
match self {
AppView::Login | AppView::Home => true,
AppView::Administration => matches!(context, ViewContext::Business),
AppView::PersonAdministration | AppView::Residence => matches!(context, ViewContext::Person),
AppView::Business | AppView::Governance => matches!(context, ViewContext::Business),
AppView::Accounting | AppView::Contracts | AppView::Treasury => true,
AppView::Entities | AppView::EntitiesRegister | AppView::EntitiesRegisterSuccess(_)
| AppView::EntitiesRegisterFailure | AppView::CompanyView(_) => matches!(context, ViewContext::Person),
AppView::ResidentRegister | AppView::ResidentRegisterSuccess | AppView::ResidentRegisterFailure => true,
AppView::ResidentLanding => true,
}
}
pub fn get_description(&self, context: &ViewContext) -> &'static str {
match (self, context) {
(AppView::Administration, ViewContext::Business) => "Org setup, members, roles, integrations",
(AppView::PersonAdministration, ViewContext::Person) => "Account settings, billing, integrations",
(AppView::Business, ViewContext::Business) => "Business overview, registration details, certificate",
(AppView::Accounting, ViewContext::Business) => "Revenues, assets, ledgers",
(AppView::Accounting, ViewContext::Person) => "Income, holdings, logs (e.g. salary, royalties, crypto inflows)",
(AppView::Contracts, ViewContext::Business) => "Agreements, wrappers, signatures",
(AppView::Contracts, ViewContext::Person) => "Employment, freelance, operating agreements",
(AppView::Governance, ViewContext::Business) => "Voting, rules, proposals",
(AppView::Treasury, ViewContext::Business) => "Wallets, safes, asset custody",
(AppView::Treasury, ViewContext::Person) => "Your wallets, digital assets, spend permissions",
(AppView::Residence, ViewContext::Person) => "Jurisdiction, address, digital domicile",
(AppView::Entities, _) => "Your owned companies and corporate entities",
(AppView::EntitiesRegister, _) => "Register a new company or entity",
(AppView::EntitiesRegisterSuccess(_), _) => "Company registration completed successfully",
(AppView::EntitiesRegisterFailure, _) => "Company registration failed - please try again",
(AppView::CompanyView(_), _) => "Company details, status, documents, and management",
(AppView::ResidentRegister, _) => "Register as a digital resident to access exclusive services",
(AppView::ResidentRegisterSuccess, _) => "Digital resident registration completed successfully",
(AppView::ResidentRegisterFailure, _) => "Digital resident registration failed - please try again",
(AppView::ResidentLanding, _) => "Welcome to Zanzibar Digital Freezone - Your gateway to digital residency",
_ => "",
}
}
}
/// Utility functions for URL and history management
pub struct HistoryManager;
impl HistoryManager {
/// Update the browser URL using pushState (creates new history entry)
pub fn push_url(url: &str) -> Result<(), String> {
if let Some(window) = web_sys::window() {
if let Ok(history) = window.history() {
history
.push_state_with_url(&JsValue::NULL, "", Some(url))
.map_err(|e| format!("Failed to push URL: {:?}", e))?;
return Ok(());
}
}
Err("Failed to access browser history".to_string())
}
/// Update the browser URL using replaceState (replaces current history entry)
pub fn replace_url(url: &str) -> Result<(), String> {
if let Some(window) = web_sys::window() {
if let Ok(history) = window.history() {
history
.replace_state_with_url(&JsValue::NULL, "", Some(url))
.map_err(|e| format!("Failed to replace URL: {:?}", e))?;
return Ok(());
}
}
Err("Failed to access browser history".to_string())
}
/// Get the current pathname from the browser
pub fn get_current_path() -> String {
web_sys::window()
.and_then(|w| w.location().pathname().ok())
.unwrap_or_else(|| "/".to_string())
}
}

View File

@ -0,0 +1,3 @@
pub mod app_router;
pub use app_router::*;

View File

@ -0,0 +1,392 @@
use crate::models::*;
use gloo::storage::{LocalStorage, Storage};
use serde_json;
use std::collections::HashMap;
const COMPANIES_STORAGE_KEY: &str = "freezone_companies";
const REGISTRATION_FORM_KEY: &str = "freezone_registration_form";
const REGISTRATIONS_STORAGE_KEY: &str = "freezone_registrations";
const FORM_EXPIRY_HOURS: i64 = 24;
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct CompanyRegistration {
pub id: u32,
pub company_name: String,
pub company_type: CompanyType,
pub status: RegistrationStatus,
pub created_at: String,
pub form_data: CompanyFormData,
pub current_step: u8,
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum RegistrationStatus {
Draft,
PendingPayment,
PaymentFailed,
PendingApproval,
Approved,
Rejected,
}
impl RegistrationStatus {
pub fn to_string(&self) -> &'static str {
match self {
RegistrationStatus::Draft => "Draft",
RegistrationStatus::PendingPayment => "Pending Payment",
RegistrationStatus::PaymentFailed => "Payment Failed",
RegistrationStatus::PendingApproval => "Pending Approval",
RegistrationStatus::Approved => "Approved",
RegistrationStatus::Rejected => "Rejected",
}
}
pub fn get_badge_class(&self) -> &'static str {
match self {
RegistrationStatus::Draft => "bg-secondary",
RegistrationStatus::PendingPayment => "bg-warning",
RegistrationStatus::PaymentFailed => "bg-danger",
RegistrationStatus::PendingApproval => "bg-info",
RegistrationStatus::Approved => "bg-success",
RegistrationStatus::Rejected => "bg-danger",
}
}
}
pub struct CompanyService;
impl CompanyService {
/// Get all companies from local storage
pub fn get_companies() -> Vec<Company> {
match LocalStorage::get::<Vec<Company>>(COMPANIES_STORAGE_KEY) {
Ok(companies) => companies,
Err(_) => {
// Initialize with empty list if not found
let companies = Vec::new();
let _ = LocalStorage::set(COMPANIES_STORAGE_KEY, &companies);
companies
}
}
}
/// Save companies to local storage
pub fn save_companies(companies: &[Company]) -> Result<(), String> {
LocalStorage::set(COMPANIES_STORAGE_KEY, companies)
.map_err(|e| format!("Failed to save companies: {:?}", e))
}
/// Add a new company
pub fn add_company(mut company: Company) -> Result<Company, String> {
let mut companies = Self::get_companies();
// Generate new ID
let max_id = companies.iter().map(|c| c.id).max().unwrap_or(0);
company.id = max_id + 1;
// Generate registration number
company.registration_number = Self::generate_registration_number(&company.name);
companies.push(company.clone());
Self::save_companies(&companies)?;
Ok(company)
}
/// Update an existing company
pub fn update_company(updated_company: &Company) -> Result<(), String> {
let mut companies = Self::get_companies();
if let Some(company) = companies.iter_mut().find(|c| c.id == updated_company.id) {
*company = updated_company.clone();
Self::save_companies(&companies)?;
Ok(())
} else {
Err("Company not found".to_string())
}
}
/// Delete a company
pub fn delete_company(company_id: u32) -> Result<(), String> {
let mut companies = Self::get_companies();
companies.retain(|c| c.id != company_id);
Self::save_companies(&companies)
}
/// Get company by ID
pub fn get_company_by_id(company_id: u32) -> Option<Company> {
Self::get_companies().into_iter().find(|c| c.id == company_id)
}
/// Generate a registration number
fn generate_registration_number(company_name: &str) -> String {
let date = js_sys::Date::new_0();
let year = date.get_full_year();
let month = date.get_month() + 1; // JS months are 0-based
let day = date.get_date();
let prefix = company_name
.chars()
.take(3)
.collect::<String>()
.to_uppercase();
format!("FZC-{:04}{:02}{:02}-{}", year, month, day, prefix)
}
/// Save registration form data with expiration
pub fn save_registration_form(form_data: &CompanyFormData, current_step: u8) -> Result<(), String> {
let now = js_sys::Date::now() as i64;
let expires_at = now + (FORM_EXPIRY_HOURS * 60 * 60 * 1000);
let saved_form = SavedRegistrationForm {
form_data: form_data.clone(),
current_step,
saved_at: now,
expires_at,
};
LocalStorage::set(REGISTRATION_FORM_KEY, &saved_form)
.map_err(|e| format!("Failed to save form: {:?}", e))
}
/// Load registration form data if not expired
pub fn load_registration_form() -> Option<(CompanyFormData, u8)> {
match LocalStorage::get::<SavedRegistrationForm>(REGISTRATION_FORM_KEY) {
Ok(saved_form) => {
let now = js_sys::Date::now() as i64;
if now < saved_form.expires_at {
Some((saved_form.form_data, saved_form.current_step))
} else {
// Form expired, remove it
let _ = LocalStorage::delete(REGISTRATION_FORM_KEY);
None
}
}
Err(_) => None,
}
}
/// Clear saved registration form
pub fn clear_registration_form() -> Result<(), String> {
LocalStorage::delete(REGISTRATION_FORM_KEY);
Ok(())
}
/// Validate form data for a specific step
pub fn validate_step(form_data: &CompanyFormData, step: u8) -> ValidationResult {
let mut errors = Vec::new();
match step {
1 => {
if form_data.company_name.trim().is_empty() {
errors.push("Company name is required".to_string());
} else if form_data.company_name.len() < 2 {
errors.push("Company name must be at least 2 characters".to_string());
}
if form_data.company_email.trim().is_empty() {
errors.push("Company email is required".to_string());
} else if !Self::is_valid_email(&form_data.company_email) {
errors.push("Please enter a valid email address".to_string());
}
}
2 => {
// Company type is always valid since it's a dropdown
}
3 => {
if form_data.shareholders.is_empty() {
errors.push("At least one shareholder is required".to_string());
} else {
let total_percentage: f64 = form_data.shareholders.iter().map(|s| s.percentage).sum();
if (total_percentage - 100.0).abs() > 0.01 {
errors.push(format!("Shareholder percentages must add up to 100% (currently {:.1}%)", total_percentage));
}
for (i, shareholder) in form_data.shareholders.iter().enumerate() {
if shareholder.name.trim().is_empty() {
errors.push(format!("Shareholder {} name is required", i + 1));
}
if shareholder.percentage <= 0.0 || shareholder.percentage > 100.0 {
errors.push(format!("Shareholder {} percentage must be between 0 and 100", i + 1));
}
}
}
}
4 => {
if !form_data.legal_agreements.all_agreed() {
let missing = form_data.legal_agreements.missing_agreements();
errors.push(format!("Please accept all required agreements: {}", missing.join(", ")));
}
}
_ => {
errors.push("Invalid step".to_string());
}
}
if errors.is_empty() {
ValidationResult::valid()
} else {
ValidationResult::invalid(errors)
}
}
/// Simple email validation
fn is_valid_email(email: &str) -> bool {
email.contains('@') && email.contains('.') && email.len() > 5
}
/// Create a company from form data (simulated)
pub fn create_company_from_form(form_data: &CompanyFormData) -> Result<Company, String> {
let now = js_sys::Date::new_0();
let incorporation_date = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let company = Company {
id: 0, // Will be set by add_company
name: form_data.company_name.clone(),
company_type: form_data.company_type.clone(),
status: CompanyStatus::PendingPayment,
registration_number: String::new(), // Will be generated by add_company
incorporation_date,
email: Some(form_data.company_email.clone()),
phone: Some(form_data.company_phone.clone()),
website: form_data.company_website.clone(),
address: Some(form_data.company_address.clone()),
industry: form_data.company_industry.clone(),
description: form_data.company_purpose.clone(),
fiscal_year_end: form_data.fiscal_year_end.clone(),
shareholders: form_data.shareholders.clone(),
};
Self::add_company(company)
}
/// Calculate total payment amount
pub fn calculate_payment_amount(company_type: &CompanyType, payment_plan: &PaymentPlan) -> f64 {
let pricing = company_type.get_pricing();
let twin_fee = 2.0; // ZDFZ Twin fee
let monthly_total = pricing.monthly_fee + twin_fee;
let subscription_amount = match payment_plan {
PaymentPlan::Monthly => monthly_total,
PaymentPlan::Yearly => monthly_total * 12.0 * payment_plan.get_discount(),
PaymentPlan::TwoYear => monthly_total * 24.0 * payment_plan.get_discount(),
};
pricing.setup_fee + subscription_amount
}
/// Initialize with sample data for demonstration
pub fn initialize_sample_data() -> Result<(), String> {
let companies = Self::get_companies();
if companies.is_empty() {
let sample_companies = vec![
Company {
id: 1,
name: "Zanzibar Digital Solutions".to_string(),
company_type: CompanyType::StartupFZC,
status: CompanyStatus::Active,
registration_number: "FZC-20250101-ZAN".to_string(),
incorporation_date: "2025-01-01".to_string(),
email: Some("contact@zanzibar-digital.com".to_string()),
phone: Some("+255 123 456 789".to_string()),
website: Some("https://zanzibar-digital.com".to_string()),
address: Some("Stone Town, Zanzibar".to_string()),
industry: Some("Technology".to_string()),
description: Some("Digital solutions and blockchain development".to_string()),
fiscal_year_end: Some("12-31".to_string()),
shareholders: vec![
Shareholder {
name: "John Smith".to_string(),
resident_id: "ID123456789".to_string(),
percentage: 60.0,
},
Shareholder {
name: "Sarah Johnson".to_string(),
resident_id: "ID987654321".to_string(),
percentage: 40.0,
},
],
},
Company {
id: 2,
name: "Ocean Trading Co".to_string(),
company_type: CompanyType::GrowthFZC,
status: CompanyStatus::Active,
registration_number: "FZC-20250102-OCE".to_string(),
incorporation_date: "2025-01-02".to_string(),
email: Some("info@ocean-trading.com".to_string()),
phone: Some("+255 987 654 321".to_string()),
website: None,
address: Some("Pemba Island, Zanzibar".to_string()),
industry: Some("Trading".to_string()),
description: Some("International trading and logistics".to_string()),
fiscal_year_end: Some("06-30".to_string()),
shareholders: vec![
Shareholder {
name: "Ahmed Hassan".to_string(),
resident_id: "ID555666777".to_string(),
percentage: 100.0,
},
],
},
];
Self::save_companies(&sample_companies)?;
}
Ok(())
}
/// Get all registrations from local storage
pub fn get_registrations() -> Vec<CompanyRegistration> {
match LocalStorage::get::<Vec<CompanyRegistration>>(REGISTRATIONS_STORAGE_KEY) {
Ok(registrations) => registrations,
Err(_) => {
// Initialize with empty list if not found
let registrations = Vec::new();
let _ = LocalStorage::set(REGISTRATIONS_STORAGE_KEY, &registrations);
registrations
}
}
}
/// Save registrations to local storage
pub fn save_registrations(registrations: &[CompanyRegistration]) -> Result<(), String> {
LocalStorage::set(REGISTRATIONS_STORAGE_KEY, registrations)
.map_err(|e| format!("Failed to save registrations: {:?}", e))
}
/// Add or update a registration
pub fn save_registration(mut registration: CompanyRegistration) -> Result<CompanyRegistration, String> {
let mut registrations = Self::get_registrations();
if registration.id == 0 {
// Generate new ID for new registration
let max_id = registrations.iter().map(|r| r.id).max().unwrap_or(0);
registration.id = max_id + 1;
registrations.push(registration.clone());
} else {
// Update existing registration
if let Some(existing) = registrations.iter_mut().find(|r| r.id == registration.id) {
*existing = registration.clone();
} else {
return Err("Registration not found".to_string());
}
}
Self::save_registrations(&registrations)?;
Ok(registration)
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct SavedRegistrationForm {
form_data: CompanyFormData,
current_step: u8,
saved_at: i64,
expires_at: i64,
}

View File

@ -0,0 +1,223 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Plan {
pub id: String,
pub name: String,
pub price: f64,
pub features: Vec<String>,
pub popular: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentMethod {
pub id: String,
pub method_type: String,
pub last_four: String,
pub expires: Option<String>,
pub is_primary: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: String,
pub date: String,
pub description: String,
pub amount: f64,
pub status: String,
pub pdf_url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Subscription {
pub plan: Plan,
pub next_billing_date: String,
pub status: String,
}
#[derive(Debug, Clone)]
pub struct MockBillingApi {
pub current_subscription: Subscription,
pub available_plans: Vec<Plan>,
pub payment_methods: Vec<PaymentMethod>,
pub invoices: Vec<Invoice>,
}
impl Default for MockBillingApi {
fn default() -> Self {
Self::new()
}
}
impl MockBillingApi {
pub fn new() -> Self {
let available_plans = vec![
Plan {
id: "starter".to_string(),
name: "Starter".to_string(),
price: 29.0,
features: vec![
"Up to 100 transactions".to_string(),
"Basic reporting".to_string(),
"Email support".to_string(),
],
popular: false,
},
Plan {
id: "business_pro".to_string(),
name: "Business Pro".to_string(),
price: 99.0,
features: vec![
"Unlimited transactions".to_string(),
"Advanced reporting".to_string(),
"Priority support".to_string(),
"API access".to_string(),
],
popular: true,
},
Plan {
id: "enterprise".to_string(),
name: "Enterprise".to_string(),
price: 299.0,
features: vec![
"Unlimited everything".to_string(),
"Custom integrations".to_string(),
"Dedicated support".to_string(),
"SLA guarantee".to_string(),
"White-label options".to_string(),
],
popular: false,
},
];
let current_subscription = Subscription {
plan: available_plans[1].clone(), // Business Pro
next_billing_date: "January 15, 2025".to_string(),
status: "active".to_string(),
};
let payment_methods = vec![
PaymentMethod {
id: "card_4242".to_string(),
method_type: "Credit Card".to_string(),
last_four: "4242".to_string(),
expires: Some("12/26".to_string()),
is_primary: true,
},
PaymentMethod {
id: "bank_5678".to_string(),
method_type: "Bank Transfer".to_string(),
last_four: "5678".to_string(),
expires: None,
is_primary: false,
},
];
let invoices = vec![
Invoice {
id: "inv_001".to_string(),
date: "Dec 15, 2024".to_string(),
description: "Business Pro - Monthly".to_string(),
amount: 99.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKEludm9pY2UgIzAwMSkKL0NyZWF0b3IgKE1vY2sgQmlsbGluZyBBUEkpCi9Qcm9kdWNlciAoTW9jayBCaWxsaW5nIEFQSSkKL0NyZWF0aW9uRGF0ZSAoRDoyMDI0MTIxNTAwMDAwMFopCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyAzIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcyIDcyMCA3MiA3MjAgcmUKUwpRCkJUCi9GMSAxMiBUZgo3MiA3MDAgVGQKKEludm9pY2UgIzAwMSkgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL0hlbHZldGljYQo+PgplbmRvYmoKeHJlZgowIDcKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAxNzQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMjc4IDAwMDAwIG4gCjAwMDAwMDAzNzUgMDAwMDAgbiAKMDAwMDAwMDQ2OSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDcKL1Jvb3QgMiAwIFIKPj4Kc3RhcnR4cmVmCjU2NwolJUVPRgo=".to_string(),
},
Invoice {
id: "inv_002".to_string(),
date: "Nov 15, 2024".to_string(),
description: "Business Pro - Monthly".to_string(),
amount: 99.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKEludm9pY2UgIzAwMikKL0NyZWF0b3IgKE1vY2sgQmlsbGluZyBBUEkpCi9Qcm9kdWNlciAoTW9jayBCaWxsaW5nIEFQSSkKL0NyZWF0aW9uRGF0ZSAoRDoyMDI0MTExNTAwMDAwMFopCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyAzIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcyIDcyMCA3MiA3MjAgcmUKUwpRCkJUCi9GMSAxMiBUZgo3MiA3MDAgVGQKKEludm9pY2UgIzAwMikgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL0hlbHZldGljYQo+PgplbmRvYmoKeHJlZgowIDcKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAxNzQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMjc4IDAwMDAwIG4gCjAwMDAwMDAzNzUgMDAwMDAgbiAKMDAwMDAwMDQ2OSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDcKL1Jvb3QgMiAwIFIKPj4Kc3RhcnR4cmVmCjU2NwolJUVPRgo=".to_string(),
},
Invoice {
id: "inv_003".to_string(),
date: "Oct 15, 2024".to_string(),
description: "Business Pro - Monthly".to_string(),
amount: 99.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKEludm9pY2UgIzAwMykKL0NyZWF0b3IgKE1vY2sgQmlsbGluZyBBUEkpCi9Qcm9kdWNlciAoTW9jayBCaWxsaW5nIEFQSSkKL0NyZWF0aW9uRGF0ZSAoRDoyMDI0MTAxNTAwMDAwMFopCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9DYXRhbG9nCi9QYWdlcyAzIDAgUgo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0tpZHMgWzQgMCBSXQovQ291bnQgMQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcyIDcyMCA3MiA3MjAgcmUKUwpRCkJUCi9GMSAxMiBUZgo3MiA3MDAgVGQKKEludm9pY2UgIzAwMykgVGoKRVQKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL0hlbHZldGljYQo+PgplbmRvYmoKeHJlZgowIDcKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDA5IDAwMDAwIG4gCjAwMDAwMDAxNzQgMDAwMDAgbiAKMDAwMDAwMDIyMSAwMDAwMCBuIAowMDAwMDAwMjc4IDAwMDAwIG4gCjAwMDAwMDAzNzUgMDAwMDAgbiAKMDAwMDAwMDQ2OSAwMDAwMCBuIAp0cmFpbGVyCjw8Ci9TaXplIDcKL1Jvb3QgMiAwIFIKPj4Kc3RhcnR4cmVmCjU2NwolJUVPRgo=".to_string(),
},
Invoice {
id: "inv_004".to_string(),
date: "Sep 15, 2024".to_string(),
description: "Setup Fee".to_string(),
amount: 50.0,
status: "Paid".to_string(),
pdf_url: "data:application/pdf;base64,JVBERi0xLjQKJdPr6eEKMSAwIG9iago8PAovVGl0bGUgKFNldHVwIEZlZSBJbnZvaWNlKQovQ3JlYXRvciAoTW9jayBCaWxsaW5nIEFQSSkKL1Byb2R1Y2VyIChNb2NrIEJpbGxpbmcgQVBJKQovQ3JlYXRpb25EYXRlIChEOjIwMjQwOTE1MDAwMDAwWikKPj4KZW5kb2JqCjIgMCBvYmoKPDwKL1R5cGUgL0NhdGFsb2cKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagozIDAgb2JqCjw8Ci9UeXBlIC9QYWdlcwovS2lkcyBbNCAwIFJdCi9Db3VudCAxCj4+CmVuZG9iago0IDAgb2JqCjw8Ci9UeXBlIC9QYWdlCi9QYXJlbnQgMyAwIFIKL01lZGlhQm94IFswIDAgNjEyIDc5Ml0KL0NvbnRlbnRzIDUgMCBSCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9MZW5ndGggNDQKPj4Kc3RyZWFtCkJUCnEKNzIgNzIwIDcyIDcyMCByZQpTClEKQlQKL0YxIDEyIFRmCjcyIDcwMCBUZAooU2V0dXAgRmVlKSBUagpFVAplbmRzdHJlYW0KZW5kb2JqCjYgMCBvYmoKPDwKL1R5cGUgL0ZvbnQKL1N1YnR5cGUgL1R5cGUxCi9CYXNlRm9udCAvSGVsdmV0aWNhCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDE3NCAwMDAwMCBuIAowMDAwMDAwMjIxIDAwMDAwIG4gCjAwMDAwMDAyNzggMDAwMDAgbiAKMDAwMDAwMDM3NSAwMDAwMCBuIAowMDAwMDAwNDY5IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAyIDAgUgo+PgpzdGFydHhyZWYKNTY3CiUlRU9GCg==".to_string(),
},
];
Self {
current_subscription,
available_plans,
payment_methods,
invoices,
}
}
// Subscription methods
pub async fn get_current_subscription(&self) -> Result<Subscription, String> {
// Simulate API delay
Ok(self.current_subscription.clone())
}
pub async fn get_available_plans(&self) -> Result<Vec<Plan>, String> {
Ok(self.available_plans.clone())
}
pub async fn change_plan(&mut self, plan_id: &str) -> Result<Subscription, String> {
if let Some(plan) = self.available_plans.iter().find(|p| p.id == plan_id) {
self.current_subscription.plan = plan.clone();
Ok(self.current_subscription.clone())
} else {
Err("Plan not found".to_string())
}
}
pub async fn cancel_subscription(&mut self) -> Result<String, String> {
self.current_subscription.status = "cancelled".to_string();
Ok("Subscription cancelled successfully".to_string())
}
// Payment methods
pub async fn get_payment_methods(&self) -> Result<Vec<PaymentMethod>, String> {
Ok(self.payment_methods.clone())
}
pub async fn add_payment_method(&mut self, method_type: &str, last_four: &str) -> Result<PaymentMethod, String> {
let new_method = PaymentMethod {
id: format!("{}_{}", method_type.to_lowercase(), last_four),
method_type: method_type.to_string(),
last_four: last_four.to_string(),
expires: if method_type == "Credit Card" { Some("12/28".to_string()) } else { None },
is_primary: false,
};
self.payment_methods.push(new_method.clone());
Ok(new_method)
}
pub async fn remove_payment_method(&mut self, method_id: &str) -> Result<String, String> {
if let Some(pos) = self.payment_methods.iter().position(|m| m.id == method_id) {
self.payment_methods.remove(pos);
Ok("Payment method removed successfully".to_string())
} else {
Err("Payment method not found".to_string())
}
}
// Invoices
pub async fn get_invoices(&self) -> Result<Vec<Invoice>, String> {
Ok(self.invoices.clone())
}
pub async fn download_invoice(&self, invoice_id: &str) -> Result<String, String> {
if let Some(invoice) = self.invoices.iter().find(|i| i.id == invoice_id) {
Ok(invoice.pdf_url.clone())
} else {
Err("Invoice not found".to_string())
}
}
}

View File

@ -0,0 +1,7 @@
pub mod mock_billing_api;
pub mod company_service;
pub mod resident_service;
pub use mock_billing_api::*;
pub use company_service::*;
pub use resident_service::*;

View File

@ -0,0 +1,257 @@
use crate::models::company::{DigitalResident, DigitalResidentFormData, KycStatus};
use gloo::storage::{LocalStorage, Storage};
const RESIDENTS_STORAGE_KEY: &str = "freezone_residents";
const RESIDENT_REGISTRATIONS_STORAGE_KEY: &str = "freezone_resident_registrations";
const RESIDENT_FORM_KEY: &str = "freezone_resident_registration_form";
const FORM_EXPIRY_HOURS: i64 = 24;
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ResidentRegistration {
pub id: u32,
pub full_name: String,
pub email: String,
pub status: ResidentRegistrationStatus,
pub created_at: String,
pub form_data: DigitalResidentFormData,
pub current_step: u8,
}
#[derive(Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum ResidentRegistrationStatus {
Draft,
PendingPayment,
PaymentFailed,
PendingApproval,
Approved,
Rejected,
}
impl ResidentRegistrationStatus {
pub fn to_string(&self) -> &'static str {
match self {
ResidentRegistrationStatus::Draft => "Draft",
ResidentRegistrationStatus::PendingPayment => "Pending Payment",
ResidentRegistrationStatus::PaymentFailed => "Payment Failed",
ResidentRegistrationStatus::PendingApproval => "Pending Approval",
ResidentRegistrationStatus::Approved => "Approved",
ResidentRegistrationStatus::Rejected => "Rejected",
}
}
pub fn get_badge_class(&self) -> &'static str {
match self {
ResidentRegistrationStatus::Draft => "bg-secondary",
ResidentRegistrationStatus::PendingPayment => "bg-warning",
ResidentRegistrationStatus::PaymentFailed => "bg-danger",
ResidentRegistrationStatus::PendingApproval => "bg-info",
ResidentRegistrationStatus::Approved => "bg-success",
ResidentRegistrationStatus::Rejected => "bg-danger",
}
}
}
pub struct ResidentService;
impl ResidentService {
/// Get all residents from local storage
pub fn get_residents() -> Vec<DigitalResident> {
match LocalStorage::get::<Vec<DigitalResident>>(RESIDENTS_STORAGE_KEY) {
Ok(residents) => residents,
Err(_) => {
// Initialize with empty list if not found
let residents = Vec::new();
let _ = LocalStorage::set(RESIDENTS_STORAGE_KEY, &residents);
residents
}
}
}
/// Save residents to local storage
pub fn save_residents(residents: &[DigitalResident]) -> Result<(), String> {
LocalStorage::set(RESIDENTS_STORAGE_KEY, residents)
.map_err(|e| format!("Failed to save residents: {:?}", e))
}
/// Add a new resident
pub fn add_resident(mut resident: DigitalResident) -> Result<DigitalResident, String> {
let mut residents = Self::get_residents();
// Generate new ID
let max_id = residents.iter().map(|r| r.id).max().unwrap_or(0);
resident.id = max_id + 1;
residents.push(resident.clone());
Self::save_residents(&residents)?;
Ok(resident)
}
/// Get all resident registrations from local storage
pub fn get_resident_registrations() -> Vec<ResidentRegistration> {
match LocalStorage::get::<Vec<ResidentRegistration>>(RESIDENT_REGISTRATIONS_STORAGE_KEY) {
Ok(registrations) => registrations,
Err(_) => {
// Initialize with empty list if not found
let registrations = Vec::new();
let _ = LocalStorage::set(RESIDENT_REGISTRATIONS_STORAGE_KEY, &registrations);
registrations
}
}
}
/// Save resident registrations to local storage
pub fn save_resident_registrations(registrations: &[ResidentRegistration]) -> Result<(), String> {
LocalStorage::set(RESIDENT_REGISTRATIONS_STORAGE_KEY, registrations)
.map_err(|e| format!("Failed to save resident registrations: {:?}", e))
}
/// Add or update a resident registration
pub fn save_resident_registration(mut registration: ResidentRegistration) -> Result<ResidentRegistration, String> {
let mut registrations = Self::get_resident_registrations();
if registration.id == 0 {
// Generate new ID for new registration
let max_id = registrations.iter().map(|r| r.id).max().unwrap_or(0);
registration.id = max_id + 1;
registrations.push(registration.clone());
} else {
// Update existing registration
if let Some(existing) = registrations.iter_mut().find(|r| r.id == registration.id) {
*existing = registration.clone();
} else {
return Err("Registration not found".to_string());
}
}
Self::save_resident_registrations(&registrations)?;
Ok(registration)
}
/// Save registration form data with expiration
pub fn save_resident_registration_form(form_data: &DigitalResidentFormData, current_step: u8) -> Result<(), String> {
let now = js_sys::Date::now() as i64;
let expires_at = now + (FORM_EXPIRY_HOURS * 60 * 60 * 1000);
let saved_form = SavedResidentRegistrationForm {
form_data: form_data.clone(),
current_step,
saved_at: now,
expires_at,
};
LocalStorage::set(RESIDENT_FORM_KEY, &saved_form)
.map_err(|e| format!("Failed to save form: {:?}", e))
}
/// Load registration form data if not expired
pub fn load_resident_registration_form() -> Option<(DigitalResidentFormData, u8)> {
match LocalStorage::get::<SavedResidentRegistrationForm>(RESIDENT_FORM_KEY) {
Ok(saved_form) => {
let now = js_sys::Date::now() as i64;
if now < saved_form.expires_at {
Some((saved_form.form_data, saved_form.current_step))
} else {
// Form expired, remove it
let _ = LocalStorage::delete(RESIDENT_FORM_KEY);
None
}
}
Err(_) => None,
}
}
/// Clear saved registration form
pub fn clear_resident_registration_form() -> Result<(), String> {
LocalStorage::delete(RESIDENT_FORM_KEY);
Ok(())
}
/// Create a resident from form data
pub fn create_resident_from_form(form_data: &DigitalResidentFormData) -> Result<DigitalResident, String> {
let now = js_sys::Date::new_0();
let registration_date = format!(
"{:04}-{:02}-{:02}",
now.get_full_year(),
now.get_month() + 1,
now.get_date()
);
let resident = DigitalResident {
id: 0, // Will be set by add_resident
full_name: form_data.full_name.clone(),
email: form_data.email.clone(),
phone: form_data.phone.clone(),
date_of_birth: form_data.date_of_birth.clone(),
nationality: form_data.nationality.clone(),
passport_number: form_data.passport_number.clone(),
passport_expiry: form_data.passport_expiry.clone(),
current_address: form_data.current_address.clone(),
city: form_data.city.clone(),
country: form_data.country.clone(),
postal_code: form_data.postal_code.clone(),
occupation: form_data.occupation.clone(),
employer: form_data.employer.clone(),
annual_income: form_data.annual_income.clone(),
education_level: form_data.education_level.clone(),
selected_services: form_data.requested_services.clone(),
payment_plan: form_data.payment_plan.clone(),
registration_date,
status: crate::models::company::ResidentStatus::Pending,
kyc_documents_uploaded: false, // Will be updated when documents are uploaded
kyc_status: KycStatus::NotStarted,
public_key: form_data.public_key.clone(),
};
Self::add_resident(resident)
}
/// Validate form data for a specific step (simplified 2-step form)
pub fn validate_resident_step(form_data: &DigitalResidentFormData, step: u8) -> crate::models::ValidationResult {
let mut errors = Vec::new();
match step {
1 => {
// Step 1: Personal Information & KYC (simplified - only name, email, and terms required)
if form_data.full_name.trim().is_empty() {
errors.push("Full name is required".to_string());
}
if form_data.email.trim().is_empty() {
errors.push("Email is required".to_string());
} else if !Self::is_valid_email(&form_data.email) {
errors.push("Please enter a valid email address".to_string());
}
if !form_data.legal_agreements.terms {
errors.push("You must agree to the Terms of Service and Privacy Policy".to_string());
}
// Note: KYC verification is handled separately via button click
}
2 => {
// Step 2: Payment only (no additional agreements needed)
// Payment validation will be handled by Stripe
}
_ => {
errors.push("Invalid step".to_string());
}
}
if errors.is_empty() {
crate::models::ValidationResult { is_valid: true, errors: Vec::new() }
} else {
crate::models::ValidationResult { is_valid: false, errors }
}
}
/// Simple email validation
fn is_valid_email(email: &str) -> bool {
email.contains('@') && email.contains('.') && email.len() > 5
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct SavedResidentRegistrationForm {
form_data: DigitalResidentFormData,
current_step: u8,
saved_at: i64,
expires_at: i64,
}

View File

@ -0,0 +1,283 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::routing::ViewContext;
use crate::components::ViewComponent;
use crate::components::accounting::*;
#[derive(Properties, PartialEq)]
pub struct AccountingViewProps {
pub context: ViewContext,
}
#[function_component(AccountingView)]
pub fn accounting_view(props: &AccountingViewProps) -> Html {
let context = &props.context;
// Initialize state with mock data
let initial_state = {
let mut state = AccountingState::default();
// Mock revenue data
state.revenue_entries = vec![
RevenueEntry {
id: "INV-2024-001".to_string(),
date: "2024-01-15".to_string(),
invoice_number: "INV-2024-001".to_string(),
client_name: "Tech Corp Ltd".to_string(),
client_email: "billing@techcorp.com".to_string(),
client_address: "123 Tech Street, Silicon Valley, CA 94000".to_string(),
description: "Web Development Services - Q1 2024".to_string(),
quantity: 80.0,
unit_price: 150.0,
subtotal: 12000.0,
tax_rate: 0.20,
tax_amount: 2400.0,
total_amount: 14400.0,
category: RevenueCategory::ServiceRevenue,
payment_method: PaymentMethod::BankTransfer,
payment_status: PaymentStatus::Paid,
due_date: "2024-02-14".to_string(),
paid_date: Some("2024-02-10".to_string()),
notes: "Monthly retainer for web development services".to_string(),
recurring: true,
currency: "USD".to_string(),
},
RevenueEntry {
id: "INV-2024-002".to_string(),
date: "2024-01-20".to_string(),
invoice_number: "INV-2024-002".to_string(),
client_name: "StartupXYZ Inc".to_string(),
client_email: "finance@startupxyz.com".to_string(),
client_address: "456 Innovation Ave, Austin, TX 78701".to_string(),
description: "Software License - Enterprise Plan".to_string(),
quantity: 1.0,
unit_price: 8500.0,
subtotal: 8500.0,
tax_rate: 0.20,
tax_amount: 1700.0,
total_amount: 10200.0,
category: RevenueCategory::LicensingRoyalties,
payment_method: PaymentMethod::CryptoUSDC,
payment_status: PaymentStatus::Pending,
due_date: "2024-02-19".to_string(),
paid_date: None,
notes: "Annual enterprise software license".to_string(),
recurring: false,
currency: "USD".to_string(),
},
RevenueEntry {
id: "INV-2024-003".to_string(),
date: "2024-01-25".to_string(),
invoice_number: "INV-2024-003".to_string(),
client_name: "Enterprise Solutions LLC".to_string(),
client_email: "accounts@enterprise-sol.com".to_string(),
client_address: "789 Business Blvd, New York, NY 10001".to_string(),
description: "Strategic Consulting - Digital Transformation".to_string(),
quantity: 40.0,
unit_price: 250.0,
subtotal: 10000.0,
tax_rate: 0.20,
tax_amount: 2000.0,
total_amount: 12000.0,
category: RevenueCategory::ConsultingFees,
payment_method: PaymentMethod::WireTransfer,
payment_status: PaymentStatus::PartiallyPaid,
due_date: "2024-02-24".to_string(),
paid_date: Some("2024-02-15".to_string()),
notes: "Phase 1 of digital transformation project".to_string(),
recurring: false,
currency: "USD".to_string(),
},
];
// Mock expense data
state.expense_entries = vec![
ExpenseEntry {
id: "EXP-2024-001".to_string(),
date: "2024-01-10".to_string(),
receipt_number: "RENT-2024-01".to_string(),
vendor_name: "Property Management Co".to_string(),
vendor_email: "billing@propmanagement.com".to_string(),
vendor_address: "321 Real Estate Ave, Downtown, CA 90210".to_string(),
description: "Monthly Office Rent - January 2024".to_string(),
amount: 2500.0,
tax_amount: 0.0,
total_amount: 2500.0,
category: ExpenseCategory::RentLease,
payment_method: PaymentMethod::BankTransfer,
payment_status: PaymentStatus::Paid,
is_deductible: true,
receipt_url: Some("/receipts/rent-jan-2024.pdf".to_string()),
approval_status: ApprovalStatus::Approved,
approved_by: Some("John Manager".to_string()),
notes: "Monthly office rent payment".to_string(),
project_code: None,
currency: "USD".to_string(),
},
ExpenseEntry {
id: "EXP-2024-002".to_string(),
date: "2024-01-12".to_string(),
receipt_number: "SW-2024-001".to_string(),
vendor_name: "SaaS Solutions Inc".to_string(),
vendor_email: "billing@saas-solutions.com".to_string(),
vendor_address: "555 Cloud Street, Seattle, WA 98101".to_string(),
description: "Software Subscriptions Bundle".to_string(),
amount: 850.0,
tax_amount: 170.0,
total_amount: 1020.0,
category: ExpenseCategory::SoftwareLicenses,
payment_method: PaymentMethod::CreditCard,
payment_status: PaymentStatus::Paid,
is_deductible: true,
receipt_url: Some("/receipts/software-jan-2024.pdf".to_string()),
approval_status: ApprovalStatus::Approved,
approved_by: Some("Jane CFO".to_string()),
notes: "Monthly SaaS subscriptions for team productivity".to_string(),
project_code: Some("TECH-001".to_string()),
currency: "USD".to_string(),
},
ExpenseEntry {
id: "EXP-2024-003".to_string(),
date: "2024-01-18".to_string(),
receipt_number: "MKT-2024-001".to_string(),
vendor_name: "Digital Marketing Agency".to_string(),
vendor_email: "invoices@digitalmarketing.com".to_string(),
vendor_address: "777 Marketing Plaza, Los Angeles, CA 90028".to_string(),
description: "Q1 Digital Marketing Campaign".to_string(),
amount: 3200.0,
tax_amount: 640.0,
total_amount: 3840.0,
category: ExpenseCategory::MarketingAdvertising,
payment_method: PaymentMethod::CryptoBitcoin,
payment_status: PaymentStatus::Pending,
is_deductible: true,
receipt_url: None,
approval_status: ApprovalStatus::RequiresReview,
approved_by: None,
notes: "Social media and PPC advertising campaign".to_string(),
project_code: Some("MKT-Q1-2024".to_string()),
currency: "USD".to_string(),
},
];
// Mock financial reports data
state.financial_reports = vec![
FinancialReport {
id: 1,
report_type: ReportType::ProfitLoss,
period_start: "2024-01-01".to_string(),
period_end: "2024-01-31".to_string(),
generated_date: "2024-01-31".to_string(),
status: "Generated".to_string(),
},
FinancialReport {
id: 2,
report_type: ReportType::TaxSummary,
period_start: "2024-01-01".to_string(),
period_end: "2024-01-31".to_string(),
generated_date: "2024-01-31".to_string(),
status: "Generated".to_string(),
},
FinancialReport {
id: 3,
report_type: ReportType::CashFlow,
period_start: "2024-01-01".to_string(),
period_end: "2024-01-31".to_string(),
generated_date: "2024-01-25".to_string(),
status: "Generating".to_string(),
},
];
// Mock payment transactions data
state.payment_transactions = vec![
PaymentTransaction {
id: "TXN-2024-001".to_string(),
invoice_id: Some("INV-2024-001".to_string()),
expense_id: None,
date: "2024-02-10".to_string(),
amount: 14400.0,
payment_method: PaymentMethod::BankTransfer,
transaction_hash: None,
reference_number: Some("REF-2024-001".to_string()),
notes: "Full payment received".to_string(),
attached_files: vec!["/receipts/payment-inv-001.pdf".to_string()],
status: TransactionStatus::Confirmed,
},
PaymentTransaction {
id: "TXN-2024-002".to_string(),
invoice_id: Some("INV-2024-003".to_string()),
expense_id: None,
date: "2024-02-15".to_string(),
amount: 6000.0,
payment_method: PaymentMethod::CryptoBitcoin,
transaction_hash: Some("1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z".to_string()),
reference_number: None,
notes: "Partial payment - 50% of invoice".to_string(),
attached_files: vec![],
status: TransactionStatus::Confirmed,
},
];
state
};
let state = use_state(|| initial_state);
// Create tabs content using the new components
let mut tabs = HashMap::new();
match context {
ViewContext::Business => {
// Overview Tab
tabs.insert("Overview".to_string(), html! {
<OverviewTab state={state.clone()} />
});
// Revenue Tab
tabs.insert("Revenue".to_string(), html! {
<RevenueTab state={state.clone()} />
});
// Expenses Tab
tabs.insert("Expenses".to_string(), html! {
<ExpensesTab state={state.clone()} />
});
// Tax Tab
tabs.insert("Tax".to_string(), html! {
<TaxTab state={state.clone()} />
});
// Financial Reports Tab
tabs.insert("Financial Reports".to_string(), html! {
<FinancialReportsTab state={state.clone()} />
});
},
ViewContext::Person => {
// For personal context, show simplified version
tabs.insert("Income Tracking".to_string(), html! {
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
{"Personal accounting features coming soon. Switch to Business context for full accounting functionality."}
</div>
});
}
}
let (title, description) = match context {
ViewContext::Business => ("Accounting", "Professional revenue & expense tracking with invoice generation"),
ViewContext::Person => ("Accounting", "Personal income and expense tracking"),
};
html! {
<ViewComponent
title={Some(title.to_string())}
description={Some(description.to_string())}
tabs={Some(tabs)}
default_tab={match context {
ViewContext::Business => Some("Overview".to_string()),
ViewContext::Person => Some("Income Tracking".to_string()),
}}
/>
}
}

View File

@ -0,0 +1,755 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::routing::ViewContext;
use crate::components::{ViewComponent, EmptyState};
use crate::services::mock_billing_api::{MockBillingApi, Plan};
use web_sys::MouseEvent;
use wasm_bindgen::JsCast;
use gloo::timers::callback::Timeout;
#[derive(Properties, PartialEq)]
pub struct AdministrationViewProps {
pub context: ViewContext,
}
#[function_component(AdministrationView)]
pub fn administration_view(props: &AdministrationViewProps) -> Html {
// Initialize mock billing API
let billing_api = use_state(|| MockBillingApi::new());
// State for managing UI interactions
let show_plan_modal = use_state(|| false);
let show_cancel_modal = use_state(|| false);
let show_add_payment_modal = use_state(|| false);
let downloading_invoice = use_state(|| None::<String>);
let selected_plan = use_state(|| None::<String>);
let loading_action = use_state(|| None::<String>);
// Event handlers
let on_change_plan = {
let show_plan_modal = show_plan_modal.clone();
Callback::from(move |_: MouseEvent| {
show_plan_modal.set(true);
})
};
let on_cancel_subscription = {
let show_cancel_modal = show_cancel_modal.clone();
Callback::from(move |_: MouseEvent| {
show_cancel_modal.set(true);
})
};
let on_confirm_cancel_subscription = {
let billing_api = billing_api.clone();
let show_cancel_modal = show_cancel_modal.clone();
let loading_action = loading_action.clone();
Callback::from(move |_: MouseEvent| {
loading_action.set(Some("canceling".to_string()));
let billing_api_clone = billing_api.clone();
let show_cancel_modal_clone = show_cancel_modal.clone();
let loading_action_clone = loading_action.clone();
// Simulate async operation with timeout
Timeout::new(1000, move || {
let mut api = (*billing_api_clone).clone();
api.current_subscription.status = "cancelled".to_string();
billing_api_clone.set(api);
loading_action_clone.set(None);
show_cancel_modal_clone.set(false);
web_sys::console::log_1(&"Subscription canceled successfully".into());
}).forget();
})
};
let on_download_invoice = {
let billing_api = billing_api.clone();
let downloading_invoice = downloading_invoice.clone();
Callback::from(move |e: MouseEvent| {
if let Some(target) = e.target() {
if let Ok(button) = target.dyn_into::<web_sys::HtmlElement>() {
if let Some(invoice_id) = button.get_attribute("data-invoice-id") {
downloading_invoice.set(Some(invoice_id.clone()));
let billing_api_clone = billing_api.clone();
let downloading_invoice_clone = downloading_invoice.clone();
let invoice_id_clone = invoice_id.clone();
// Simulate download with timeout
Timeout::new(500, move || {
let api = (*billing_api_clone).clone();
// Find the invoice and get its PDF URL
if let Some(invoice) = api.invoices.iter().find(|i| i.id == invoice_id_clone) {
// Create a link and trigger download
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
if let Ok(anchor) = document.create_element("a") {
if let Ok(anchor) = anchor.dyn_into::<web_sys::HtmlElement>() {
anchor.set_attribute("href", &invoice.pdf_url).unwrap();
anchor.set_attribute("download", &format!("invoice_{}.pdf", invoice_id_clone)).unwrap();
anchor.click();
}
}
}
}
web_sys::console::log_1(&"Invoice downloaded successfully".into());
} else {
web_sys::console::log_1(&"Invoice not found".into());
}
downloading_invoice_clone.set(None);
}).forget();
}
}
}
})
};
let on_add_payment_method = {
let show_add_payment_modal = show_add_payment_modal.clone();
Callback::from(move |_: MouseEvent| {
show_add_payment_modal.set(true);
})
};
let on_confirm_add_payment_method = {
let billing_api = billing_api.clone();
let show_add_payment_modal = show_add_payment_modal.clone();
let loading_action = loading_action.clone();
Callback::from(move |_: MouseEvent| {
loading_action.set(Some("adding_payment".to_string()));
let billing_api_clone = billing_api.clone();
let show_add_payment_modal_clone = show_add_payment_modal.clone();
let loading_action_clone = loading_action.clone();
// Simulate async operation with timeout
Timeout::new(1000, move || {
let mut api = (*billing_api_clone).clone();
// Add a new payment method
let new_method = crate::services::mock_billing_api::PaymentMethod {
id: format!("card_{}", api.payment_methods.len() + 1),
method_type: "Credit Card".to_string(),
last_four: "•••• •••• •••• 4242".to_string(),
expires: Some("12/28".to_string()),
is_primary: false,
};
api.payment_methods.push(new_method);
billing_api_clone.set(api);
loading_action_clone.set(None);
show_add_payment_modal_clone.set(false);
web_sys::console::log_1(&"Payment method added successfully".into());
}).forget();
})
};
let on_edit_payment_method = {
let loading_action = loading_action.clone();
Callback::from(move |e: MouseEvent| {
if let Some(target) = e.target() {
if let Ok(button) = target.dyn_into::<web_sys::HtmlElement>() {
if let Some(method_id) = button.get_attribute("data-method") {
let loading_action_clone = loading_action.clone();
let method_id_clone = method_id.clone();
loading_action.set(Some(format!("editing_{}", method_id)));
// Simulate API call delay
Timeout::new(1000, move || {
loading_action_clone.set(None);
web_sys::console::log_1(&format!("Edit payment method: {}", method_id_clone).into());
}).forget();
}
}
}
})
};
let on_remove_payment_method = {
let billing_api = billing_api.clone();
let loading_action = loading_action.clone();
Callback::from(move |e: MouseEvent| {
if let Some(target) = e.target() {
if let Ok(button) = target.dyn_into::<web_sys::HtmlElement>() {
if let Some(method_id) = button.get_attribute("data-method") {
if web_sys::window()
.unwrap()
.confirm_with_message(&format!("Are you sure you want to remove this payment method?"))
.unwrap_or(false)
{
let billing_api_clone = billing_api.clone();
let loading_action_clone = loading_action.clone();
let method_id_clone = method_id.clone();
loading_action.set(Some(format!("removing_{}", method_id)));
// Simulate async operation with timeout
Timeout::new(1000, move || {
let mut api = (*billing_api_clone).clone();
// Remove the payment method
if let Some(pos) = api.payment_methods.iter().position(|m| m.id == method_id_clone) {
api.payment_methods.remove(pos);
billing_api_clone.set(api);
web_sys::console::log_1(&"Payment method removed successfully".into());
} else {
web_sys::console::log_1(&"Payment method not found".into());
}
loading_action_clone.set(None);
}).forget();
}
}
}
}
})
};
let on_select_plan = {
let selected_plan = selected_plan.clone();
Callback::from(move |e: MouseEvent| {
if let Some(target) = e.target() {
if let Ok(button) = target.dyn_into::<web_sys::HtmlElement>() {
if let Some(plan_id) = button.get_attribute("data-plan-id") {
selected_plan.set(Some(plan_id));
}
}
}
})
};
let on_confirm_plan_change = {
let billing_api = billing_api.clone();
let selected_plan = selected_plan.clone();
let show_plan_modal = show_plan_modal.clone();
let loading_action = loading_action.clone();
Callback::from(move |_: MouseEvent| {
if let Some(plan_id) = (*selected_plan).clone() {
loading_action.set(Some("changing_plan".to_string()));
let billing_api_clone = billing_api.clone();
let show_plan_modal_clone = show_plan_modal.clone();
let loading_action_clone = loading_action.clone();
let plan_id_clone = plan_id.clone();
// Simulate async operation with timeout
Timeout::new(1000, move || {
let mut api = (*billing_api_clone).clone();
// Change the plan
if let Some(plan) = api.available_plans.iter().find(|p| p.id == plan_id_clone) {
api.current_subscription.plan = plan.clone();
billing_api_clone.set(api);
web_sys::console::log_1(&"Plan changed successfully".into());
} else {
web_sys::console::log_1(&"Plan not found".into());
}
loading_action_clone.set(None);
show_plan_modal_clone.set(false);
}).forget();
}
})
};
let close_modals = {
let show_plan_modal = show_plan_modal.clone();
let show_cancel_modal = show_cancel_modal.clone();
let show_add_payment_modal = show_add_payment_modal.clone();
let selected_plan = selected_plan.clone();
Callback::from(move |_: MouseEvent| {
show_plan_modal.set(false);
show_cancel_modal.set(false);
show_add_payment_modal.set(false);
selected_plan.set(None);
})
};
// Create tabs content
let mut tabs = HashMap::new();
// Organization Setup Tab
tabs.insert("Organization Setup".to_string(), html! {
<EmptyState
icon={"building".to_string()}
title={"Organization not configured".to_string()}
description={"Set up your organization structure, hierarchy, and basic settings to get started.".to_string()}
primary_action={Some(("Setup Organization".to_string(), "#".to_string()))}
secondary_action={Some(("Import Settings".to_string(), "#".to_string()))}
/>
});
// Shareholders Tab
tabs.insert("Shareholders".to_string(), html! {
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-people me-2"></i>
{"Shareholder Information"}
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{"Name"}</th>
<th>{"Ownership %"}</th>
<th>{"Shares"}</th>
<th>{"Type"}</th>
<th>{"Status"}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="bg-primary rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="bi bi-person text-white"></i>
</div>
<div>
<div class="fw-bold">{"John Doe"}</div>
<small class="text-muted">{"Founder & CEO"}</small>
</div>
</div>
</td>
<td><span class="fw-bold">{"65%"}</span></td>
<td>{"6,500"}</td>
<td><span class="badge bg-primary">{"Ordinary"}</span></td>
<td><span class="badge bg-success">{"Active"}</span></td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="bg-info rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="bi bi-person text-white"></i>
</div>
<div>
<div class="fw-bold">{"Sarah Johnson"}</div>
<small class="text-muted">{"Co-Founder & CTO"}</small>
</div>
</div>
</td>
<td><span class="fw-bold">{"25%"}</span></td>
<td>{"2,500"}</td>
<td><span class="badge bg-primary">{"Ordinary"}</span></td>
<td><span class="badge bg-success">{"Active"}</span></td>
</tr>
<tr>
<td>
<div class="d-flex align-items-center">
<div class="bg-warning rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
<i class="bi bi-building text-dark"></i>
</div>
<div>
<div class="fw-bold">{"Innovation Ventures"}</div>
<small class="text-muted">{"Investment Fund"}</small>
</div>
</div>
</td>
<td><span class="fw-bold">{"10%"}</span></td>
<td>{"1,000"}</td>
<td><span class="badge bg-warning text-dark">{"Preferred"}</span></td>
<td><span class="badge bg-success">{"Active"}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="mt-3">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
{"Total Authorized Shares: 10,000 | Issued Shares: 10,000 | Par Value: $1.00"}
</small>
</div>
<div class="mt-4">
<div class="d-flex gap-2">
<button class="btn btn-primary">
<i class="bi bi-person-plus me-1"></i>
{"Add Shareholder"}
</button>
<button class="btn btn-outline-secondary">
<i class="bi bi-download me-1"></i>
{"Export Cap Table"}
</button>
<button class="btn btn-outline-secondary">
<i class="bi bi-file-earmark-pdf me-1"></i>
{"Generate Certificate"}
</button>
</div>
</div>
</div>
</div>
});
// Members & Roles Tab
tabs.insert("Members & Roles".to_string(), html! {
<EmptyState
icon={"person-badge".to_string()}
title={"No team members found".to_string()}
description={"Invite team members, assign roles, and control access permissions for your organization.".to_string()}
primary_action={Some(("Invite Members".to_string(), "#".to_string()))}
secondary_action={Some(("Manage Roles".to_string(), "#".to_string()))}
/>
});
// Integrations Tab
tabs.insert("Integrations".to_string(), html! {
<EmptyState
icon={"diagram-3".to_string()}
title={"No integrations configured".to_string()}
description={"Connect with external services and configure API integrations to streamline your workflow.".to_string()}
primary_action={Some(("Browse Integrations".to_string(), "#".to_string()))}
secondary_action={Some(("API Documentation".to_string(), "#".to_string()))}
/>
});
// Billing and Payments Tab
tabs.insert("Billing and Payments".to_string(), {
let current_subscription = &billing_api.current_subscription;
let current_plan = &current_subscription.plan;
html! {
<div class="row">
// Subscription Tier Pane
<div class="col-lg-4 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-star me-2"></i>
{"Current Plan"}
</h5>
</div>
<div class="card-body">
<div class="text-center mb-3">
<div class="badge bg-primary fs-6 px-3 py-2 mb-2">{&current_plan.name}</div>
<h3 class="text-primary mb-0">{format!("${:.0}", current_plan.price)}<small class="text-muted">{"/month"}</small></h3>
</div>
<ul class="list-unstyled">
{for current_plan.features.iter().map(|feature| html! {
<li class="mb-2">
<i class="bi bi-check-circle text-success me-2"></i>
{feature}
</li>
})}
</ul>
<div class="mt-3">
<small class="text-muted">{format!("Status: {}", current_subscription.status)}</small>
</div>
<div class="mt-3 d-grid gap-2">
<button
class="btn btn-outline-primary btn-sm"
onclick={on_change_plan.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == "changing_plan")}
>
{if loading_action.as_ref().map_or(false, |action| action == "changing_plan") {
"Changing..."
} else {
"Change Plan"
}}
</button>
<button
class="btn btn-outline-secondary btn-sm"
onclick={on_cancel_subscription.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == "canceling")}
>
{if loading_action.as_ref().map_or(false, |action| action == "canceling") {
"Canceling..."
} else {
"Cancel Subscription"
}}
</button>
</div>
</div>
</div>
</div>
<div class="col-lg-8">
// Payments Table Pane
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-receipt me-2"></i>
{"Payment History"}
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{"Date"}</th>
<th>{"Description"}</th>
<th>{"Amount"}</th>
<th>{"Status"}</th>
<th>{"Invoice"}</th>
</tr>
</thead>
<tbody>
{for billing_api.invoices.iter().map(|invoice| html! {
<tr>
<td>{&invoice.date}</td>
<td>{&invoice.description}</td>
<td>{format!("${:.2}", invoice.amount)}</td>
<td><span class="badge bg-success">{&invoice.status}</span></td>
<td>
<button
class="btn btn-outline-secondary btn-sm"
onclick={on_download_invoice.clone()}
data-invoice-id={invoice.id.clone()}
disabled={downloading_invoice.as_ref().map_or(false, |id| id == &invoice.id)}
>
<i class={if downloading_invoice.as_ref().map_or(false, |id| id == &invoice.id) { "bi bi-arrow-repeat" } else { "bi bi-download" }}></i>
</button>
</td>
</tr>
})}
</tbody>
</table>
</div>
</div>
</div>
// Payment Methods Pane
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-credit-card me-2"></i>
{"Payment Methods"}
</h5>
<button
class="btn btn-primary btn-sm"
onclick={on_add_payment_method.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == "adding_payment")}
>
<i class="bi bi-plus me-1"></i>
{if loading_action.as_ref().map_or(false, |action| action == "adding_payment") {
"Adding..."
} else {
"Add Method"
}}
</button>
</div>
<div class="card-body">
<div class="row">
{for billing_api.payment_methods.iter().map(|method| html! {
<div class="col-md-6 mb-3">
<div class="card border">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex align-items-center">
<div class={format!("bg-{} rounded me-3 d-flex align-items-center justify-content-center",
if method.method_type == "card" { "primary" } else { "info" })}
style="width: 40px; height: 25px;">
<i class={format!("bi bi-{} text-white",
if method.method_type == "card" { "credit-card" } else { "bank" })}></i>
</div>
<div>
<div class="fw-bold">{&method.last_four}</div>
<small class="text-muted">{&method.expires}</small>
</div>
</div>
<div>
<span class={format!("badge bg-{}",
if method.is_primary { "success" } else { "secondary" })}>
{if method.is_primary { "Primary" } else { "Backup" }}
</span>
</div>
</div>
<div class="mt-3">
<button
class="btn btn-outline-secondary btn-sm me-2"
onclick={on_edit_payment_method.clone()}
data-method={method.id.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == &format!("editing_{}", method.id))}
>
{if loading_action.as_ref().map_or(false, |action| action == &format!("editing_{}", method.id)) {
"Editing..."
} else {
"Edit"
}}
</button>
<button
class="btn btn-outline-danger btn-sm"
onclick={on_remove_payment_method.clone()}
data-method={method.id.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == &format!("removing_{}", method.id))}
>
{if loading_action.as_ref().map_or(false, |action| action == &format!("removing_{}", method.id)) {
"Removing..."
} else {
"Remove"
}}
</button>
</div>
</div>
</div>
</div>
})}
</div>
</div>
</div>
</div>
</div>
}
});
html! {
<>
<ViewComponent
title={Some("Administration".to_string())}
description={Some("Org setup, members, roles, integrations".to_string())}
tabs={Some(tabs)}
default_tab={Some("Organization Setup".to_string())}
/>
// Plan Selection Modal
if *show_plan_modal {
<div class="modal fade show" style="display: block;" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Change Plan"}</h5>
<button type="button" class="btn-close" onclick={close_modals.clone()}></button>
</div>
<div class="modal-body">
<div class="row">
{for billing_api.available_plans.iter().map(|plan| html! {
<div class="col-md-4 mb-3">
<div class={format!("card h-100 {}",
if selected_plan.as_ref().map_or(false, |id| id == &plan.id) { "border-primary" } else { "" })}>
<div class="card-body text-center">
<h5 class="card-title">{&plan.name}</h5>
<h3 class="text-primary">{format!("${:.0}", plan.price)}<small class="text-muted">{"/month"}</small></h3>
<ul class="list-unstyled mt-3">
{for plan.features.iter().map(|feature| html! {
<li class="mb-1">
<i class="bi bi-check text-success me-1"></i>
{feature}
</li>
})}
</ul>
<button
class={format!("btn btn-{} w-100",
if selected_plan.as_ref().map_or(false, |id| id == &plan.id) { "primary" } else { "outline-primary" })}
onclick={on_select_plan.clone()}
data-plan-id={plan.id.clone()}
>
{if selected_plan.as_ref().map_or(false, |id| id == &plan.id) { "Selected" } else { "Select" }}
</button>
</div>
</div>
</div>
})}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={close_modals.clone()}>{"Cancel"}</button>
<button
type="button"
class="btn btn-primary"
onclick={on_confirm_plan_change.clone()}
disabled={selected_plan.is_none() || loading_action.as_ref().map_or(false, |action| action == "changing_plan")}
>
{if loading_action.as_ref().map_or(false, |action| action == "changing_plan") {
"Changing..."
} else {
"Change Plan"
}}
</button>
</div>
</div>
</div>
</div>
}
// Cancel Subscription Modal
if *show_cancel_modal {
<div class="modal fade show" style="display: block;" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Cancel Subscription"}</h5>
<button type="button" class="btn-close" onclick={close_modals.clone()}></button>
</div>
<div class="modal-body">
<p>{"Are you sure you want to cancel your subscription? This action cannot be undone."}</p>
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
{"Your subscription will remain active until the end of the current billing period."}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={close_modals.clone()}>{"Keep Subscription"}</button>
<button
type="button"
class="btn btn-danger"
onclick={on_confirm_cancel_subscription.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == "canceling")}
>
{if loading_action.as_ref().map_or(false, |action| action == "canceling") {
"Canceling..."
} else {
"Cancel Subscription"
}}
</button>
</div>
</div>
</div>
</div>
}
// Add Payment Method Modal
if *show_add_payment_modal {
<div class="modal fade show" style="display: block;" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{"Add Payment Method"}</h5>
<button type="button" class="btn-close" onclick={close_modals.clone()}></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3">
<label class="form-label">{"Card Number"}</label>
<input type="text" class="form-control" placeholder="1234 5678 9012 3456" />
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">{"Expiry Date"}</label>
<input type="text" class="form-control" placeholder="MM/YY" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{"CVC"}</label>
<input type="text" class="form-control" placeholder="123" />
</div>
</div>
<div class="mb-3">
<label class="form-label">{"Cardholder Name"}</label>
<input type="text" class="form-control" placeholder="John Doe" />
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick={close_modals.clone()}>{"Cancel"}</button>
<button
type="button"
class="btn btn-primary"
onclick={on_confirm_add_payment_method.clone()}
disabled={loading_action.as_ref().map_or(false, |action| action == "adding_payment")}
>
{if loading_action.as_ref().map_or(false, |action| action == "adding_payment") {
"Adding..."
} else {
"Add Payment Method"
}}
</button>
</div>
</div>
</div>
</div>
}
</>
}
}

View File

@ -0,0 +1,421 @@
use yew::prelude::*;
use crate::routing::{ViewContext, AppView};
use crate::models::*;
use crate::services::CompanyService;
#[derive(Properties, PartialEq)]
pub struct BusinessViewProps {
pub context: ViewContext,
pub company_id: Option<u32>,
pub on_navigate: Option<Callback<AppView>>,
}
pub enum BusinessViewMsg {
LoadCompany,
CompanyLoaded(Company),
LoadError(String),
NavigateBack,
}
pub struct BusinessView {
company: Option<Company>,
loading: bool,
error: Option<String>,
}
impl Component for BusinessView {
type Message = BusinessViewMsg;
type Properties = BusinessViewProps;
fn create(ctx: &Context<Self>) -> Self {
// Load company data if company_id is provided
if ctx.props().company_id.is_some() {
ctx.link().send_message(BusinessViewMsg::LoadCompany);
}
Self {
company: None,
loading: ctx.props().company_id.is_some(),
error: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
BusinessViewMsg::LoadCompany => {
self.loading = true;
self.error = None;
if let Some(company_id) = ctx.props().company_id {
// Load company data
if let Some(company) = CompanyService::get_company_by_id(company_id) {
ctx.link().send_message(BusinessViewMsg::CompanyLoaded(company));
} else {
ctx.link().send_message(BusinessViewMsg::LoadError(
format!("Company with ID {} not found", company_id)
));
}
} else {
// Use sample data if no company_id provided
let sample_company = Company {
id: 1,
name: "TechCorp Solutions Ltd.".to_string(),
company_type: CompanyType::StartupFZC,
status: CompanyStatus::Active,
registration_number: "BIZ-2024-001".to_string(),
incorporation_date: "January 15, 2024".to_string(),
email: Some("contact@techcorp.zdf".to_string()),
phone: Some("+255 24 123 4567".to_string()),
website: Some("https://techcorp.zdf".to_string()),
address: Some("Stone Town Business District, Zanzibar".to_string()),
industry: Some("Technology Services".to_string()),
description: Some("Leading technology solutions provider in the digital freezone".to_string()),
fiscal_year_end: Some("12-31".to_string()),
shareholders: vec![
Shareholder {
name: "John Smith".to_string(),
resident_id: "ID123456789".to_string(),
percentage: 60.0,
},
Shareholder {
name: "Sarah Johnson".to_string(),
resident_id: "ID987654321".to_string(),
percentage: 40.0,
},
],
};
ctx.link().send_message(BusinessViewMsg::CompanyLoaded(sample_company));
}
true
}
BusinessViewMsg::CompanyLoaded(company) => {
self.company = Some(company);
self.loading = false;
true
}
BusinessViewMsg::LoadError(error) => {
self.error = Some(error);
self.loading = false;
true
}
BusinessViewMsg::NavigateBack => {
if let Some(on_navigate) = &ctx.props().on_navigate {
on_navigate.emit(AppView::Entities);
}
false
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
if self.loading {
return self.render_loading();
}
if let Some(error) = &self.error {
return self.render_error(error, ctx);
}
let company = self.company.as_ref().unwrap();
self.render_business_view(company, ctx)
}
}
impl BusinessView {
fn render_loading(&self) -> Html {
html! {
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body text-center py-5">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Loading business details..."}</p>
</div>
</div>
</div>
</div>
</div>
}
}
fn render_error(&self, error: &str, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="card border-danger">
<div class="card-body text-center py-5">
<i class="bi bi-exclamation-triangle text-danger mb-3" style="font-size: 3rem;"></i>
<h4 class="text-danger mb-3">{"Error Loading Business"}</h4>
<p class="text-muted mb-4">{error}</p>
<button
class="btn btn-primary me-2"
onclick={link.callback(|_| BusinessViewMsg::LoadCompany)}
>
<i class="bi bi-arrow-clockwise me-1"></i>{"Retry"}
</button>
{if ctx.props().on_navigate.is_some() {
html! {
<button
class="btn btn-outline-secondary"
onclick={link.callback(|_| BusinessViewMsg::NavigateBack)}
>
<i class="bi bi-arrow-left me-1"></i>{"Back to Entities"}
</button>
}
} else { html! {} }}
</div>
</div>
</div>
</div>
</div>
}
}
fn render_business_view(&self, company: &Company, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="container-fluid py-4">
{if ctx.props().company_id.is_some() && ctx.props().on_navigate.is_some() {
html! {
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<button
class="btn btn-outline-secondary me-3"
onclick={link.callback(|_| BusinessViewMsg::NavigateBack)}
>
<i class="bi bi-arrow-left me-1"></i>{"Back to Entities"}
</button>
<h2 class="mb-0">{"Business Overview"}</h2>
</div>
<span class={company.status.get_badge_class()}>
{company.status.to_string()}
</span>
</div>
<p class="text-muted mb-0">{"Complete business information and registration details"}</p>
</div>
</div>
}
} else {
html! {
<div class="row mb-4">
<div class="col">
<h2 class="mb-1">{"Business Overview"}</h2>
<p class="text-muted mb-0">{"Complete business information and registration details"}</p>
</div>
</div>
}
}}
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-building me-2"></i>
{"Business Information"}
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<h6 class="text-muted">{"Company Details"}</h6>
<table class="table table-borderless">
<tbody>
<tr>
<td class="fw-bold">{"Legal Name:"}</td>
<td>{&company.name}</td>
</tr>
<tr>
<td class="fw-bold">{"Registration ID:"}</td>
<td><code>{&company.registration_number}</code></td>
</tr>
<tr>
<td class="fw-bold">{"Founded:"}</td>
<td>{&company.incorporation_date}</td>
</tr>
<tr>
<td class="fw-bold">{"Industry:"}</td>
<td>{company.industry.as_ref().unwrap_or(&"Not specified".to_string())}</td>
</tr>
<tr>
<td class="fw-bold">{"Status:"}</td>
<td><span class={company.status.get_badge_class()}>{company.status.to_string()}</span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-4">
<h6 class="text-muted">{"Contact Information"}</h6>
<table class="table table-borderless">
<tbody>
<tr>
<td class="fw-bold">{"Address:"}</td>
<td>{company.address.as_ref().unwrap_or(&"Not specified".to_string())}</td>
</tr>
<tr>
<td class="fw-bold">{"Email:"}</td>
<td>{company.email.as_ref().unwrap_or(&"Not specified".to_string())}</td>
</tr>
<tr>
<td class="fw-bold">{"Phone:"}</td>
<td>{company.phone.as_ref().unwrap_or(&"Not specified".to_string())}</td>
</tr>
<tr>
<td class="fw-bold">{"Website:"}</td>
<td>
{if let Some(website) = &company.website {
html! {
<a href={website.clone()} target="_blank" class="text-decoration-none">
{website} <i class="bi bi-box-arrow-up-right ms-1"></i>
</a>
}
} else {
html! { "Not specified" }
}}
</td>
</tr>
<tr>
<td class="fw-bold">{"Employees:"}</td>
<td>{"12 Staff Members"}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-4">
<h6 class="text-muted">{"Shareholders"}</h6>
{if !company.shareholders.is_empty() {
html! {
<table class="table table-borderless">
<tbody>
{for company.shareholders.iter().map(|shareholder| {
html! {
<tr>
<td class="fw-bold">{&shareholder.name}</td>
<td>
<span class="badge bg-primary">
{format!("{:.1}%", shareholder.percentage)}
</span>
</td>
</tr>
}
})}
</tbody>
</table>
}
} else {
html! {
<p class="text-muted">{"No shareholders information available"}</p>
}
}}
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
{self.render_business_certificate(company)}
</div>
</div>
</div>
}
}
fn render_business_certificate(&self, company: &Company) -> Html {
html! {
// Business Registration Certificate Card (Vertical Mobile Wallet Style)
<div class="card shadow-lg border-0" style="background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%); aspect-ratio: 3/4; max-width: 300px;">
<div class="card-body text-white p-4 d-flex flex-column h-100">
<div class="text-center mb-3">
<div class="bg-white bg-opacity-20 rounded-circle d-inline-flex align-items-center justify-content-center" style="width: 60px; height: 60px;">
<i class="bi bi-award fs-3 text-white"></i>
</div>
</div>
<div class="text-center mb-3">
<h6 class="text-white-50 mb-1 text-uppercase" style="font-size: 0.7rem; letter-spacing: 1px;">{"Zanzibar Digital Freezone"}</h6>
<h5 class="fw-bold mb-0">{"Business Certificate"}</h5>
</div>
<div class="flex-grow-1">
<div class="mb-2">
<small class="text-white-50 text-uppercase" style="font-size: 0.65rem;">{"Company Name"}</small>
<div class="fw-bold" style="font-size: 0.9rem;">{&company.name}</div>
</div>
<div class="mb-2">
<small class="text-white-50 text-uppercase" style="font-size: 0.65rem;">{"Registration Number"}</small>
<div class="font-monospace fw-bold fs-6">{&company.registration_number}</div>
</div>
<div class="mb-2">
<small class="text-white-50 text-uppercase" style="font-size: 0.65rem;">{"Incorporation Date"}</small>
<div style="font-size: 0.9rem;">{&company.incorporation_date}</div>
</div>
<div class="mb-2">
<small class="text-white-50 text-uppercase" style="font-size: 0.65rem;">{"SecPK Wallet"}</small>
<div class="font-monospace" style="font-size: 0.75rem; word-break: break-all;">
{"sp1k...7x9m"}
</div>
</div>
<div class="mb-2">
<small class="text-white-50 text-uppercase" style="font-size: 0.65rem;">{"Status"}</small>
<div class="d-flex align-items-center">
<span class={format!("badge {} me-2", match company.status {
CompanyStatus::Active => "bg-success",
CompanyStatus::PendingPayment => "bg-warning",
CompanyStatus::Inactive => "bg-secondary",
CompanyStatus::Suspended => "bg-danger",
})} style="font-size: 0.7rem;">{company.status.to_string()}</span>
<i class="bi bi-shield-check"></i>
</div>
</div>
// QR Code Section
<div class="text-center mb-2">
<div class="bg-white rounded p-2 d-inline-block">
<div class="bg-dark" style="width: 80px; height: 80px; background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODAiIGhlaWdodD0iODAiIHZpZXdCb3g9IjAgMCA4MCA4MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjgwIiBoZWlnaHQ9IjgwIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMTAgMTBIMjBWMjBIMTBWMTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMzAgMTBINDBWMjBIMzBWMTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNTAgMTBINjBWMjBINTBWMTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNzAgMTBIODBWMjBINzBWMTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAgMzBIMjBWNDBIMTBWMzBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNTAgMzBINjBWNDBINTBWMzBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNzAgMzBIODBWNDBINzBWMzBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAgNTBIMjBWNjBIMTBWNTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMzAgNTBINDBWNjBIMzBWNTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNTAgNTBINjBWNjBINTBWNTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNzAgNTBIODBWNjBINzBWNTBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMTAgNzBIMjBWODBIMTBWNzBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNMzAgNzBINDBWODBIMzBWNzBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNTAgNzBINjBWODBINTBWNzBaIiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNNzAgNzBIODBWODBINzBWNzBaIiBmaWxsPSJibGFjayIvPgo8L3N2Zz4K'); background-size: cover;">
</div>
</div>
<div style="font-size: 0.65rem;" class="text-white-50">{"Scan for Verification"}</div>
</div>
</div>
<div class="mt-auto pt-2 border-top border-white border-opacity-25">
<div class="d-flex justify-content-between align-items-center">
<small class="text-white-50" style="font-size: 0.65rem;">{"Valid Until"}</small>
<small class="fw-bold" style="font-size: 0.75rem;">{"Dec 31, 2024"}</small>
</div>
<div class="text-center mt-1">
<small class="text-white-50" style="font-size: 0.65rem;">{"Digitally Verified"}</small>
</div>
</div>
</div>
</div>
}
}
}
#[function_component(BusinessViewWrapper)]
pub fn business_view(props: &BusinessViewProps) -> Html {
html! {
<BusinessView
context={props.context.clone()}
company_id={props.company_id}
on_navigate={props.on_navigate.clone()}
/>
}
}

View File

@ -0,0 +1,460 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::routing::AppView;
use crate::components::{ViewComponent, EmptyState, RegistrationWizard};
use crate::models::*;
use crate::services::{CompanyService, CompanyRegistration, RegistrationStatus};
#[derive(Properties, PartialEq)]
pub struct CompaniesViewProps {
pub on_navigate: Option<Callback<AppView>>,
#[prop_or_default]
pub show_registration: bool,
#[prop_or_default]
pub registration_success: Option<u32>,
#[prop_or_default]
pub registration_failure: bool,
}
pub enum CompaniesViewMsg {
LoadCompanies,
CompaniesLoaded(Vec<Company>),
LoadRegistrations,
RegistrationsLoaded(Vec<CompanyRegistration>),
SwitchToCompany(String),
ShowRegistration,
RegistrationComplete(Company),
BackToCompanies,
ViewCompany(u32),
ContinueRegistration(CompanyRegistration),
StartNewRegistration,
DeleteRegistration(u32),
ShowNewRegistrationForm,
HideNewRegistrationForm,
}
pub struct CompaniesView {
companies: Vec<Company>,
registrations: Vec<CompanyRegistration>,
loading: bool,
show_registration: bool,
current_registration: Option<CompanyRegistration>,
show_new_registration_form: bool,
}
impl Component for CompaniesView {
type Message = CompaniesViewMsg;
type Properties = CompaniesViewProps;
fn create(ctx: &Context<Self>) -> Self {
// Load companies and registrations on component creation
ctx.link().send_message(CompaniesViewMsg::LoadCompanies);
ctx.link().send_message(CompaniesViewMsg::LoadRegistrations);
Self {
companies: Vec::new(),
registrations: Vec::new(),
loading: true,
show_registration: ctx.props().show_registration,
current_registration: None,
show_new_registration_form: false,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
CompaniesViewMsg::LoadCompanies => {
self.loading = true;
// Load companies from service
let companies = CompanyService::get_companies();
ctx.link().send_message(CompaniesViewMsg::CompaniesLoaded(companies));
false
}
CompaniesViewMsg::CompaniesLoaded(companies) => {
self.companies = companies;
self.loading = false;
true
}
CompaniesViewMsg::LoadRegistrations => {
// Load actual registrations from service
let registrations = CompanyService::get_registrations();
ctx.link().send_message(CompaniesViewMsg::RegistrationsLoaded(registrations));
false
}
CompaniesViewMsg::RegistrationsLoaded(registrations) => {
self.registrations = registrations;
true
}
CompaniesViewMsg::SwitchToCompany(company_id) => {
// Navigate to company view
if let Some(on_navigate) = &ctx.props().on_navigate {
if let Ok(id) = company_id.parse::<u32>() {
on_navigate.emit(AppView::CompanyView(id));
}
}
false
}
CompaniesViewMsg::ShowRegistration => {
self.show_registration = true;
self.current_registration = None; // Start fresh registration
true
}
CompaniesViewMsg::StartNewRegistration => {
self.show_registration = true;
self.current_registration = None; // Start fresh registration
true
}
CompaniesViewMsg::ContinueRegistration(registration) => {
self.show_registration = true;
self.current_registration = Some(registration);
true
}
CompaniesViewMsg::RegistrationComplete(company) => {
// Add new company to list and clear current registration
let company_id = company.id;
self.companies.push(company);
self.current_registration = None;
self.show_registration = false;
// Navigate to registration success step
if let Some(on_navigate) = &ctx.props().on_navigate {
on_navigate.emit(AppView::EntitiesRegisterSuccess(company_id));
}
true
}
CompaniesViewMsg::ViewCompany(company_id) => {
// Navigate to company view
if let Some(on_navigate) = &ctx.props().on_navigate {
on_navigate.emit(AppView::CompanyView(company_id));
}
false
}
CompaniesViewMsg::BackToCompanies => {
self.show_registration = false;
self.current_registration = None;
true
}
CompaniesViewMsg::DeleteRegistration(registration_id) => {
// Remove registration from list
self.registrations.retain(|r| r.id != registration_id);
// Update storage
let _ = CompanyService::save_registrations(&self.registrations);
true
}
CompaniesViewMsg::ShowNewRegistrationForm => {
self.show_new_registration_form = true;
true
}
CompaniesViewMsg::HideNewRegistrationForm => {
self.show_new_registration_form = false;
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// Check if we should show success state
if ctx.props().registration_success.is_some() {
// Show success state
html! {
<ViewComponent
title={Some("Registration Successful".to_string())}
description={Some("Your company registration has been completed successfully".to_string())}
>
<RegistrationWizard
on_registration_complete={link.callback(CompaniesViewMsg::RegistrationComplete)}
on_back_to_companies={link.callback(|_| CompaniesViewMsg::BackToCompanies)}
success_company_id={ctx.props().registration_success}
show_failure={false}
force_fresh_start={false}
continue_registration={None}
continue_step={None}
/>
</ViewComponent>
}
} else if self.show_registration {
// Registration view
html! {
<ViewComponent
title={Some("Register New Company".to_string())}
description={Some("Complete the registration process to create your new company".to_string())}
>
<RegistrationWizard
on_registration_complete={link.callback(CompaniesViewMsg::RegistrationComplete)}
on_back_to_companies={link.callback(|_| CompaniesViewMsg::BackToCompanies)}
success_company_id={None}
show_failure={ctx.props().registration_failure}
force_fresh_start={self.current_registration.is_none()}
continue_registration={self.current_registration.as_ref().map(|r| r.form_data.clone())}
continue_step={self.current_registration.as_ref().map(|r| r.current_step)}
/>
</ViewComponent>
}
} else {
// Main companies view with unified table
html! {
<ViewComponent
title={Some("Companies".to_string())}
description={Some("Manage your companies and registrations".to_string())}
>
{self.render_companies_content(ctx)}
</ViewComponent>
}
}
}
}
impl CompaniesView {
fn render_companies_content(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
if self.loading {
return html! {
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Loading companies..."}</p>
</div>
};
}
if self.companies.is_empty() && self.registrations.is_empty() {
return html! {
<div class="text-center py-5">
<EmptyState
icon={"building".to_string()}
title={"No companies found".to_string()}
description={"Create and manage your owned companies and corporate entities for business operations.".to_string()}
primary_action={None}
secondary_action={None}
/>
<div class="mt-4">
<button
class="btn btn-success btn-lg"
onclick={link.callback(|_| CompaniesViewMsg::StartNewRegistration)}
>
<i class="bi bi-plus-circle me-2"></i>{"Register Your First Company"}
</button>
</div>
</div>
};
}
html! {
<div class="row">
<div class="col-12">
{self.render_companies_table(ctx)}
</div>
</div>
}
}
fn render_companies_table(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">
<i class="bi bi-building me-2"></i>{"Companies & Registrations"}
</h5>
<small class="text-muted">
{format!("{} companies, {} pending registrations", self.companies.len(), self.registrations.len())}
</small>
</div>
<button
class="btn btn-success"
onclick={link.callback(|_| CompaniesViewMsg::StartNewRegistration)}
>
<i class="bi bi-plus-circle me-2"></i>{"New Registration"}
</button>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{"Name"}</th>
<th>{"Type"}</th>
<th>{"Status"}</th>
<th>{"Date"}</th>
<th>{"Progress"}</th>
<th class="text-end">{"Actions"}</th>
</tr>
</thead>
<tbody>
// Render active companies first
{for self.companies.iter().map(|company| {
let company_id = company.id;
let on_view = {
let link = link.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
link.emit(CompaniesViewMsg::ViewCompany(company_id));
})
};
let on_switch = {
let link = link.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
link.emit(CompaniesViewMsg::SwitchToCompany(company_id.to_string()));
})
};
html! {
<tr key={format!("company-{}", company.id)}>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-building text-success me-2"></i>
<strong>{&company.name}</strong>
</div>
</td>
<td>{company.company_type.to_string()}</td>
<td>
<span class={company.status.get_badge_class()}>
{company.status.to_string()}
</span>
</td>
<td>{&company.incorporation_date}</td>
<td>
<span class="badge bg-success">{"Complete"}</span>
</td>
<td class="text-end">
<div class="btn-group">
<button
class="btn btn-sm btn-outline-primary"
onclick={on_view}
title="View company details"
>
<i class="bi bi-eye"></i>
</button>
<button
class="btn btn-sm btn-primary"
onclick={on_switch}
title="Switch to this entity"
>
<i class="bi bi-box-arrow-in-right"></i>
</button>
</div>
</td>
</tr>
}
})}
// Render pending registrations
{for self.registrations.iter().map(|registration| {
let registration_clone = registration.clone();
let registration_id = registration.id;
let can_continue = matches!(registration.status, RegistrationStatus::Draft | RegistrationStatus::PendingPayment | RegistrationStatus::PaymentFailed);
let on_continue = {
let link = link.clone();
let reg = registration_clone.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
link.emit(CompaniesViewMsg::ContinueRegistration(reg.clone()));
})
};
let on_delete = {
let link = link.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
if web_sys::window()
.unwrap()
.confirm_with_message("Are you sure you want to delete this registration?")
.unwrap_or(false)
{
link.emit(CompaniesViewMsg::DeleteRegistration(registration_id));
}
})
};
html! {
<tr key={format!("registration-{}", registration.id)}>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-file-earmark-text text-warning me-2"></i>
{&registration.company_name}
<small class="text-muted ms-2">{"(Registration)"}</small>
</div>
</td>
<td>{registration.company_type.to_string()}</td>
<td>
<span class={format!("badge {}", registration.status.get_badge_class())}>
{registration.status.to_string()}
</span>
</td>
<td>{&registration.created_at}</td>
<td>
<div class="d-flex align-items-center">
<div class="progress me-2" style="width: 60px; height: 8px;">
<div
class="progress-bar bg-info"
style={format!("width: {}%", (registration.current_step as f32 / 5.0 * 100.0))}
></div>
</div>
<small class="text-muted">{format!("{}/5", registration.current_step)}</small>
</div>
</td>
<td class="text-end">
<div class="btn-group">
{if can_continue {
html! {
<button
class="btn btn-sm btn-success"
onclick={on_continue}
title="Continue registration"
>
<i class="bi bi-play-circle"></i>
</button>
}
} else {
html! {
<button
class="btn btn-sm btn-outline-secondary"
disabled={true}
title="Registration complete"
>
<i class="bi bi-check-circle"></i>
</button>
}
}}
<button
class="btn btn-sm btn-outline-danger"
onclick={on_delete}
title="Delete registration"
>
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
</div>
</div>
}
}
}
#[function_component(CompaniesViewWrapper)]
pub fn companies_view(props: &CompaniesViewProps) -> Html {
html! {
<CompaniesView
on_navigate={props.on_navigate.clone()}
show_registration={props.show_registration}
registration_success={props.registration_success}
registration_failure={props.registration_failure}
/>
}
}

View File

@ -0,0 +1,583 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::routing::ViewContext;
use crate::components::ViewComponent;
#[derive(Clone, PartialEq)]
pub enum ContractStatus {
Draft,
PendingSignatures,
Signed,
Active,
Expired,
Cancelled,
}
impl ContractStatus {
fn to_string(&self) -> &'static str {
match self {
ContractStatus::Draft => "Draft",
ContractStatus::PendingSignatures => "Pending Signatures",
ContractStatus::Signed => "Signed",
ContractStatus::Active => "Active",
ContractStatus::Expired => "Expired",
ContractStatus::Cancelled => "Cancelled",
}
}
fn badge_class(&self) -> &'static str {
match self {
ContractStatus::Draft => "bg-secondary",
ContractStatus::PendingSignatures => "bg-warning text-dark",
ContractStatus::Signed => "bg-success",
ContractStatus::Active => "bg-success",
ContractStatus::Expired => "bg-danger",
ContractStatus::Cancelled => "bg-dark",
}
}
}
#[derive(Clone, PartialEq)]
pub struct ContractSigner {
pub id: String,
pub name: String,
pub email: String,
pub status: String, // "Pending", "Signed", "Rejected"
pub signed_at: Option<String>,
pub comments: Option<String>,
}
#[derive(Clone, PartialEq)]
pub struct Contract {
pub id: String,
pub title: String,
pub description: String,
pub contract_type: String,
pub status: ContractStatus,
pub created_by: String,
pub created_at: String,
pub updated_at: String,
pub signers: Vec<ContractSigner>,
pub terms_and_conditions: Option<String>,
pub effective_date: Option<String>,
pub expiration_date: Option<String>,
}
impl Contract {
fn signed_signers(&self) -> usize {
self.signers.iter().filter(|s| s.status == "Signed").count()
}
fn pending_signers(&self) -> usize {
self.signers.iter().filter(|s| s.status == "Pending").count()
}
}
#[derive(Properties, PartialEq)]
pub struct ContractsViewProps {
pub context: ViewContext,
}
pub enum ContractsMsg {
CreateContract,
EditContract(String),
DeleteContract(String),
ViewContract(String),
FilterByStatus(String),
FilterByType(String),
SearchContracts(String),
}
pub struct ContractsViewComponent {
contracts: Vec<Contract>,
filtered_contracts: Vec<Contract>,
status_filter: String,
type_filter: String,
search_filter: String,
}
impl Component for ContractsViewComponent {
type Message = ContractsMsg;
type Properties = ContractsViewProps;
fn create(_ctx: &Context<Self>) -> Self {
let contracts = Self::get_sample_contracts();
let filtered_contracts = contracts.clone();
Self {
contracts,
filtered_contracts,
status_filter: String::new(),
type_filter: String::new(),
search_filter: String::new(),
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
ContractsMsg::CreateContract => {
// Handle create contract navigation
true
}
ContractsMsg::EditContract(id) => {
// Handle edit contract navigation
true
}
ContractsMsg::DeleteContract(id) => {
// Handle delete contract
self.contracts.retain(|c| c.id != id);
self.apply_filters();
true
}
ContractsMsg::ViewContract(id) => {
// Handle view contract navigation
true
}
ContractsMsg::FilterByStatus(status) => {
self.status_filter = status;
self.apply_filters();
true
}
ContractsMsg::FilterByType(contract_type) => {
self.type_filter = contract_type;
self.apply_filters();
true
}
ContractsMsg::SearchContracts(query) => {
self.search_filter = query;
self.apply_filters();
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let context = &ctx.props().context;
let (title, description) = match context {
ViewContext::Business => ("Legal Contracts", "Manage business agreements and legal documents"),
ViewContext::Person => ("Contracts", "Personal contracts and agreements"),
};
// Create tabs content
let mut tabs = HashMap::new();
// Contracts Tab
tabs.insert("Contracts".to_string(), self.render_contracts_tab(ctx));
// Create Contract Tab
tabs.insert("Create Contract".to_string(), self.render_create_contract_tab(ctx));
html! {
<ViewComponent
title={title.to_string()}
description={description.to_string()}
tabs={tabs}
default_tab={"Contracts".to_string()}
/>
}
}
}
impl ContractsViewComponent {
fn get_sample_contracts() -> Vec<Contract> {
vec![
Contract {
id: "1".to_string(),
title: "Service Agreement - Web Development".to_string(),
description: "Development of company website and maintenance".to_string(),
contract_type: "Service Agreement".to_string(),
status: ContractStatus::PendingSignatures,
created_by: "John Smith".to_string(),
created_at: "2024-01-15".to_string(),
updated_at: "2024-01-16".to_string(),
signers: vec![
ContractSigner {
id: "s1".to_string(),
name: "Alice Johnson".to_string(),
email: "alice@example.com".to_string(),
status: "Signed".to_string(),
signed_at: Some("2024-01-16 10:30".to_string()),
comments: Some("Looks good!".to_string()),
},
ContractSigner {
id: "s2".to_string(),
name: "Bob Wilson".to_string(),
email: "bob@example.com".to_string(),
status: "Pending".to_string(),
signed_at: None,
comments: None,
},
],
terms_and_conditions: Some("# Service Agreement\n\nThis agreement outlines...".to_string()),
effective_date: Some("2024-02-01".to_string()),
expiration_date: Some("2024-12-31".to_string()),
},
Contract {
id: "2".to_string(),
title: "Non-Disclosure Agreement".to_string(),
description: "Confidentiality agreement for project collaboration".to_string(),
contract_type: "Non-Disclosure Agreement".to_string(),
status: ContractStatus::Signed,
created_by: "Sarah Davis".to_string(),
created_at: "2024-01-10".to_string(),
updated_at: "2024-01-12".to_string(),
signers: vec![
ContractSigner {
id: "s3".to_string(),
name: "Mike Brown".to_string(),
email: "mike@example.com".to_string(),
status: "Signed".to_string(),
signed_at: Some("2024-01-12 14:20".to_string()),
comments: None,
},
ContractSigner {
id: "s4".to_string(),
name: "Lisa Green".to_string(),
email: "lisa@example.com".to_string(),
status: "Signed".to_string(),
signed_at: Some("2024-01-12 16:45".to_string()),
comments: Some("Agreed to all terms".to_string()),
},
],
terms_and_conditions: Some("# Non-Disclosure Agreement\n\nThe parties agree...".to_string()),
effective_date: Some("2024-01-12".to_string()),
expiration_date: Some("2026-01-12".to_string()),
},
Contract {
id: "3".to_string(),
title: "Employment Contract - Software Engineer".to_string(),
description: "Full-time employment agreement".to_string(),
contract_type: "Employment Contract".to_string(),
status: ContractStatus::Draft,
created_by: "HR Department".to_string(),
created_at: "2024-01-20".to_string(),
updated_at: "2024-01-20".to_string(),
signers: vec![],
terms_and_conditions: Some("# Employment Contract\n\nPosition: Software Engineer...".to_string()),
effective_date: Some("2024-02-15".to_string()),
expiration_date: None,
},
]
}
fn apply_filters(&mut self) {
self.filtered_contracts = self.contracts
.iter()
.filter(|contract| {
// Status filter
if !self.status_filter.is_empty() && contract.status.to_string() != self.status_filter {
return false;
}
// Type filter
if !self.type_filter.is_empty() && contract.contract_type != self.type_filter {
return false;
}
// Search filter
if !self.search_filter.is_empty() {
let query = self.search_filter.to_lowercase();
if !contract.title.to_lowercase().contains(&query) &&
!contract.description.to_lowercase().contains(&query) {
return false;
}
}
true
})
.cloned()
.collect();
}
fn render_contracts_tab(&self, _ctx: &Context<Self>) -> Html {
html! {
<div>
// Filters Section
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"Filters"}</h5>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label for="status" class="form-label">{"Status"}</label>
<select class="form-select" id="status">
<option value="">{"All Statuses"}</option>
<option value="Draft">{"Draft"}</option>
<option value="Pending Signatures">{"Pending Signatures"}</option>
<option value="Signed">{"Signed"}</option>
<option value="Active">{"Active"}</option>
<option value="Expired">{"Expired"}</option>
<option value="Cancelled">{"Cancelled"}</option>
</select>
</div>
<div class="col-md-3">
<label for="type" class="form-label">{"Contract Type"}</label>
<select class="form-select" id="type">
<option value="">{"All Types"}</option>
<option value="Service Agreement">{"Service Agreement"}</option>
<option value="Employment Contract">{"Employment Contract"}</option>
<option value="Non-Disclosure Agreement">{"Non-Disclosure Agreement"}</option>
<option value="Service Level Agreement">{"Service Level Agreement"}</option>
<option value="Other">{"Other"}</option>
</select>
</div>
<div class="col-md-4">
<label for="search" class="form-label">{"Search"}</label>
<input type="text" class="form-control" id="search"
placeholder="Search by title or description" />
</div>
<div class="col-md-2 d-flex align-items-end">
<a href="#" class="btn btn-primary w-100">
<i class="bi bi-plus-circle me-1"></i>{"Create New"}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
// Contracts Table
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"Contracts"}</h5>
</div>
<div class="card-body">
{self.render_contracts_table(_ctx)}
</div>
</div>
</div>
</div>
</div>
}
}
fn render_contracts_table(&self, ctx: &Context<Self>) -> Html {
if self.filtered_contracts.is_empty() {
return html! {
<div class="text-center py-5">
<i class="bi bi-file-earmark-text fs-1 text-muted"></i>
<p class="mt-3 text-muted">{"No contracts found"}</p>
<a href="#" class="btn btn-primary mt-2">
<i class="bi bi-plus-circle me-1"></i>{"Create New Contract"}
</a>
</div>
};
}
html! {
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{"Contract Title"}</th>
<th>{"Type"}</th>
<th>{"Status"}</th>
<th>{"Created By"}</th>
<th>{"Signers"}</th>
<th>{"Created"}</th>
<th>{"Updated"}</th>
<th>{"Actions"}</th>
</tr>
</thead>
<tbody>
{for self.filtered_contracts.iter().map(|contract| self.render_contract_row(contract, ctx))}
</tbody>
</table>
</div>
}
}
fn render_contract_row(&self, contract: &Contract, _ctx: &Context<Self>) -> Html {
html! {
<tr>
<td>
<a href="#" class="text-decoration-none">
{&contract.title}
</a>
</td>
<td>{&contract.contract_type}</td>
<td>
<span class={format!("badge {}", contract.status.badge_class())}>
{contract.status.to_string()}
</span>
</td>
<td>{&contract.created_by}</td>
<td>{format!("{}/{}", contract.signed_signers(), contract.signers.len())}</td>
<td>{&contract.created_at}</td>
<td>{&contract.updated_at}</td>
<td>
<div class="btn-group">
<a href="#" class="btn btn-sm btn-primary" title="View">
<i class="bi bi-eye"></i>
</a>
{if matches!(contract.status, ContractStatus::Draft) {
html! {
<>
<a href="#" class="btn btn-sm btn-outline-secondary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
<a href="#" class="btn btn-sm btn-outline-danger" title="Delete">
<i class="bi bi-trash"></i>
</a>
</>
}
} else {
html! {}
}}
</div>
</td>
</tr>
}
}
fn render_create_contract_tab(&self, _ctx: &Context<Self>) -> Html {
html! {
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"Contract Details"}</h5>
</div>
<div class="card-body">
<form>
<div class="mb-3">
<label for="title" class="form-label">
{"Contract Title "}<span class="text-danger">{"*"}</span>
</label>
<input type="text" class="form-control" id="title" name="title" required=true />
</div>
<div class="mb-3">
<label for="contract_type" class="form-label">
{"Contract Type "}<span class="text-danger">{"*"}</span>
</label>
<select class="form-select" id="contract_type" name="contract_type" required=true>
<option value="" selected=true disabled=true>{"Select a contract type"}</option>
<option value="Service Agreement">{"Service Agreement"}</option>
<option value="Employment Contract">{"Employment Contract"}</option>
<option value="Non-Disclosure Agreement">{"Non-Disclosure Agreement"}</option>
<option value="Service Level Agreement">{"Service Level Agreement"}</option>
<option value="Partnership Agreement">{"Partnership Agreement"}</option>
<option value="Other">{"Other"}</option>
</select>
</div>
<div class="mb-3">
<label for="description" class="form-label">
{"Description "}<span class="text-danger">{"*"}</span>
</label>
<textarea class="form-control" id="description" name="description" rows="3" required=true></textarea>
</div>
<div class="mb-3">
<label for="content" class="form-label">{"Contract Content (Markdown)"}</label>
<textarea class="form-control" id="content" name="content" rows="10"
placeholder="# Contract Title
## 1. Introduction
This contract outlines the terms and conditions...
## 2. Scope of Work
- Task 1
- Task 2
- Task 3
## 3. Payment Terms
Payment will be made according to the following schedule:
| Milestone | Amount | Due Date |
|-----------|--------|----------|
| Start | $1,000 | Upon signing |
| Completion | $2,000 | Upon delivery |
## 4. Terms and Conditions
**Important:** All parties must agree to these terms.
> This is a blockquote for important notices.
---
*For questions, contact [support@example.com](mailto:support@example.com)*"></textarea>
<div class="form-text">
<strong>{"Markdown Support:"}</strong>{" You can use markdown formatting including headers (#), lists (-), tables (|), bold (**text**), italic (*text*), links, and more."}
</div>
</div>
<div class="mb-3">
<label for="effective_date" class="form-label">{"Effective Date"}</label>
<input type="date" class="form-control" id="effective_date" name="effective_date" />
</div>
<div class="mb-3">
<label for="expiration_date" class="form-label">{"Expiration Date"}</label>
<input type="date" class="form-control" id="expiration_date" name="expiration_date" />
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="button" class="btn btn-outline-secondary me-md-2">{"Cancel"}</button>
<button type="submit" class="btn btn-primary">{"Create Contract"}</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">{"Tips"}</h5>
</div>
<div class="card-body">
<p>{"Creating a new contract is just the first step. After creating the contract, you'll be able to:"}</p>
<ul>
<li>{"Add signers who need to approve the contract"}</li>
<li>{"Edit the contract content"}</li>
<li>{"Send the contract for signatures"}</li>
<li>{"Track the signing progress"}</li>
</ul>
<p>{"The contract will be in "}<strong>{"Draft"}</strong>{" status until you send it for signatures."}</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"Contract Templates"}</h5>
</div>
<div class="card-body">
<p>{"You can use one of our pre-defined templates to get started quickly:"}</p>
<div class="list-group">
<button type="button" class="list-group-item list-group-item-action">
{"Non-Disclosure Agreement"}
</button>
<button type="button" class="list-group-item list-group-item-action">
{"Service Agreement"}
</button>
<button type="button" class="list-group-item list-group-item-action">
{"Employment Contract"}
</button>
<button type="button" class="list-group-item list-group-item-action">
{"Service Level Agreement"}
</button>
</div>
</div>
</div>
</div>
</div>
}
}
}
#[function_component(ContractsView)]
pub fn contracts_view(props: &ContractsViewProps) -> Html {
html! {
<ContractsViewComponent context={props.context.clone()} />
}
}

View File

@ -0,0 +1,449 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::routing::AppView;
use crate::components::{ViewComponent, EmptyState, CompaniesList, RegistrationWizard};
use crate::models::*;
use crate::services::{CompanyService, CompanyRegistration, RegistrationStatus};
#[derive(Properties, PartialEq)]
pub struct EntitiesViewProps {
pub on_navigate: Option<Callback<AppView>>,
#[prop_or_default]
pub show_registration: bool,
#[prop_or_default]
pub registration_success: Option<u32>,
#[prop_or_default]
pub registration_failure: bool,
}
pub enum EntitiesViewMsg {
LoadCompanies,
CompaniesLoaded(Vec<Company>),
LoadRegistrations,
RegistrationsLoaded(Vec<CompanyRegistration>),
SwitchToCompany(String),
ShowRegistration,
RegistrationComplete(Company),
BackToCompanies,
ViewCompany(u32),
ContinueRegistration(CompanyRegistration),
StartNewRegistration,
DeleteRegistration(u32),
}
pub struct EntitiesView {
companies: Vec<Company>,
registrations: Vec<CompanyRegistration>,
loading: bool,
show_registration: bool,
current_registration: Option<CompanyRegistration>,
}
impl Component for EntitiesView {
type Message = EntitiesViewMsg;
type Properties = EntitiesViewProps;
fn create(ctx: &Context<Self>) -> Self {
// Load companies and registrations on component creation
ctx.link().send_message(EntitiesViewMsg::LoadCompanies);
ctx.link().send_message(EntitiesViewMsg::LoadRegistrations);
Self {
companies: Vec::new(),
registrations: Vec::new(),
loading: true,
show_registration: ctx.props().show_registration,
current_registration: None,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
EntitiesViewMsg::LoadCompanies => {
self.loading = true;
// Load companies from service
let companies = CompanyService::get_companies();
ctx.link().send_message(EntitiesViewMsg::CompaniesLoaded(companies));
false
}
EntitiesViewMsg::CompaniesLoaded(companies) => {
self.companies = companies;
self.loading = false;
true
}
EntitiesViewMsg::LoadRegistrations => {
// Load actual registrations from service
let registrations = CompanyService::get_registrations();
ctx.link().send_message(EntitiesViewMsg::RegistrationsLoaded(registrations));
false
}
EntitiesViewMsg::RegistrationsLoaded(registrations) => {
self.registrations = registrations;
true
}
EntitiesViewMsg::SwitchToCompany(company_id) => {
// Navigate to company view
if let Some(on_navigate) = &ctx.props().on_navigate {
if let Ok(id) = company_id.parse::<u32>() {
on_navigate.emit(AppView::CompanyView(id));
}
}
false
}
EntitiesViewMsg::ShowRegistration => {
self.show_registration = true;
self.current_registration = None; // Start fresh registration
true
}
EntitiesViewMsg::StartNewRegistration => {
self.show_registration = true;
self.current_registration = None; // Start fresh registration
true
}
EntitiesViewMsg::ContinueRegistration(registration) => {
self.show_registration = true;
self.current_registration = Some(registration);
true
}
EntitiesViewMsg::RegistrationComplete(company) => {
// Add new company to list and clear current registration
let company_id = company.id;
self.companies.push(company);
self.current_registration = None;
self.show_registration = false;
// Navigate to registration success step
if let Some(on_navigate) = &ctx.props().on_navigate {
on_navigate.emit(AppView::EntitiesRegisterSuccess(company_id));
}
true
}
EntitiesViewMsg::ViewCompany(company_id) => {
// Navigate to company view
if let Some(on_navigate) = &ctx.props().on_navigate {
on_navigate.emit(AppView::CompanyView(company_id));
}
false
}
EntitiesViewMsg::BackToCompanies => {
self.show_registration = false;
self.current_registration = None;
true
}
EntitiesViewMsg::DeleteRegistration(registration_id) => {
// Remove registration from list
self.registrations.retain(|r| r.id != registration_id);
// Update storage
let _ = CompanyService::save_registrations(&self.registrations);
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
// Check if we should show success state
if ctx.props().registration_success.is_some() {
// Show success state
html! {
<ViewComponent
title={Some("Registration Successful".to_string())}
description={Some("Your company registration has been completed successfully".to_string())}
>
<RegistrationWizard
on_registration_complete={link.callback(EntitiesViewMsg::RegistrationComplete)}
on_back_to_companies={link.callback(|_| EntitiesViewMsg::BackToCompanies)}
success_company_id={ctx.props().registration_success}
show_failure={false}
force_fresh_start={false}
continue_registration={None}
continue_step={None}
/>
</ViewComponent>
}
} else if self.show_registration {
// Registration view
html! {
<ViewComponent
title={Some("Register New Company".to_string())}
description={Some("Complete the registration process to create your new company".to_string())}
>
<RegistrationWizard
on_registration_complete={link.callback(EntitiesViewMsg::RegistrationComplete)}
on_back_to_companies={link.callback(|_| EntitiesViewMsg::BackToCompanies)}
success_company_id={None}
show_failure={ctx.props().registration_failure}
force_fresh_start={self.current_registration.is_none()}
continue_registration={self.current_registration.as_ref().map(|r| r.form_data.clone())}
continue_step={self.current_registration.as_ref().map(|r| r.current_step)}
/>
</ViewComponent>
}
} else {
// Tabbed view with Companies and Register tabs
let mut tabs = HashMap::new();
// Companies tab - shows established companies with detailed info
tabs.insert("Companies".to_string(), self.render_companies_tab(ctx));
// Register tab - shows pending registrations in table format + new registration button
tabs.insert("Register".to_string(), self.render_register_tab(ctx));
html! {
<ViewComponent
title={Some("Companies".to_string())}
description={Some("Manage your companies and registrations".to_string())}
tabs={Some(tabs)}
default_tab={Some("Companies".to_string())}
/>
}
}
}
}
impl EntitiesView {
fn render_companies_tab(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
if self.companies.is_empty() && !self.loading {
html! {
<div class="text-center py-5">
<EmptyState
icon={"building".to_string()}
title={"No companies found".to_string()}
description={"Create and manage your owned companies and corporate entities for business operations.".to_string()}
primary_action={None}
secondary_action={None}
/>
<div class="mt-4">
<button
class="btn btn-success btn-lg"
onclick={link.callback(|_| EntitiesViewMsg::StartNewRegistration)}
>
<i class="bi bi-plus-circle me-2"></i>{"Register Your First Company"}
</button>
</div>
</div>
}
} else if self.loading {
html! {
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{"Loading..."}</span>
</div>
<p class="text-muted">{"Loading companies..."}</p>
</div>
}
} else {
html! {
<CompaniesList
companies={self.companies.clone()}
on_view_company={link.callback(|id: u32| EntitiesViewMsg::SwitchToCompany(id.to_string()))}
on_switch_to_entity={link.callback(|id: u32| EntitiesViewMsg::SwitchToCompany(id.to_string()))}
/>
}
}
}
fn render_register_tab(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
html! {
<div class="row">
<div class="col-12">
// Header with new registration button
<div class="card mb-4">
<div class="card-body text-center py-4">
<div class="mb-3">
<i class="bi bi-plus-circle-fill text-success" style="font-size: 3rem;"></i>
</div>
<h4 class="mb-3">{"Start New Registration"}</h4>
<p class="text-muted mb-4">
{"Begin the process to register a new company or legal entity"}
</p>
<button
class="btn btn-success btn-lg"
onclick={link.callback(|_| EntitiesViewMsg::StartNewRegistration)}
>
<i class="bi bi-file-earmark-plus me-2"></i>{"Start Registration"}
</button>
<div class="mt-3">
<small class="text-muted">
{"The registration process takes 5 steps and can be saved at any time"}
</small>
</div>
</div>
</div>
// Pending registrations table
{self.render_registrations_table(ctx)}
</div>
</div>
}
}
fn render_registrations_table(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
if self.registrations.is_empty() {
return html! {
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-file-earmark-text me-2"></i>{"Pending Registrations"}
</h5>
</div>
<div class="card-body text-center py-5">
<i class="bi bi-file-earmark-text display-4 text-muted mb-3"></i>
<h6 class="text-muted">{"No pending registrations"}</h6>
<p class="text-muted small">{"Your company registration applications will appear here"}</p>
</div>
</div>
};
}
html! {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0">
<i class="bi bi-file-earmark-text me-2"></i>{"Pending Registrations"}
</h5>
<small class="text-muted">
{format!("{} pending registrations", self.registrations.len())}
</small>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{"Company Name"}</th>
<th>{"Type"}</th>
<th>{"Status"}</th>
<th>{"Created"}</th>
<th>{"Progress"}</th>
<th class="text-end">{"Actions"}</th>
</tr>
</thead>
<tbody>
{for self.registrations.iter().map(|registration| {
let registration_clone = registration.clone();
let registration_id = registration.id;
let can_continue = matches!(registration.status, RegistrationStatus::Draft | RegistrationStatus::PendingPayment | RegistrationStatus::PaymentFailed);
let on_continue = {
let link = link.clone();
let reg = registration_clone.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
link.send_message(EntitiesViewMsg::ContinueRegistration(reg.clone()));
})
};
let on_delete = {
let link = link.clone();
Callback::from(move |e: MouseEvent| {
e.prevent_default();
if web_sys::window()
.unwrap()
.confirm_with_message("Are you sure you want to delete this registration?")
.unwrap_or(false)
{
link.send_message(EntitiesViewMsg::DeleteRegistration(registration_id));
}
})
};
html! {
<tr key={format!("registration-{}", registration.id)}>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-file-earmark-text text-warning me-2"></i>
<div>
<strong>{&registration.company_name}</strong>
{if registration.company_name.is_empty() || registration.company_name == "Draft Registration" {
html! { <small class="text-muted d-block">{"Draft Registration"}</small> }
} else {
html! {}
}}
</div>
</div>
</td>
<td>{registration.company_type.to_string()}</td>
<td>
<span class={format!("badge {}", registration.status.get_badge_class())}>
{registration.status.to_string()}
</span>
</td>
<td>{&registration.created_at}</td>
<td>
<div class="d-flex align-items-center">
<div class="progress me-2" style="width: 80px; height: 8px;">
<div
class="progress-bar bg-info"
style={format!("width: {}%", (registration.current_step as f32 / 5.0 * 100.0))}
></div>
</div>
<small class="text-muted">{format!("{}/5", registration.current_step)}</small>
</div>
</td>
<td class="text-end">
<div class="btn-group">
{if can_continue {
html! {
<button
class="btn btn-sm btn-success"
onclick={on_continue}
title="Continue registration"
>
<i class="bi bi-play-circle me-1"></i>{"Continue"}
</button>
}
} else {
html! {
<button
class="btn btn-sm btn-outline-secondary"
disabled={true}
title="Registration complete"
>
<i class="bi bi-check-circle me-1"></i>{"Complete"}
</button>
}
}}
<button
class="btn btn-sm btn-outline-danger"
onclick={on_delete}
title="Delete registration"
>
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
</div>
</div>
}
}
}
#[function_component(EntitiesViewWrapper)]
pub fn entities_view(props: &EntitiesViewProps) -> Html {
html! {
<EntitiesView
on_navigate={props.on_navigate.clone()}
show_registration={props.show_registration}
registration_success={props.registration_success}
registration_failure={props.registration_failure}
/>
}
}

View File

@ -0,0 +1,652 @@
use yew::prelude::*;
use std::collections::HashMap;
use crate::routing::ViewContext;
use crate::components::{ViewComponent, EmptyState};
#[derive(Properties, PartialEq)]
pub struct GovernanceViewProps {
pub context: ViewContext,
}
pub enum GovernanceMsg {
SwitchTab(String),
CreateProposal,
VoteOnProposal(u32, String),
LoadProposals,
}
pub struct GovernanceView {
active_tab: String,
proposals: Vec<Proposal>,
loading: bool,
}
#[derive(Clone, PartialEq)]
pub struct Proposal {
pub id: u32,
pub title: String,
pub description: String,
pub creator_name: String,
pub status: ProposalStatus,
pub vote_start_date: Option<String>,
pub vote_end_date: Option<String>,
pub created_at: String,
pub yes_votes: u32,
pub no_votes: u32,
pub abstain_votes: u32,
pub total_votes: u32,
}
#[derive(Clone, PartialEq)]
pub enum ProposalStatus {
Draft,
Active,
Approved,
Rejected,
Cancelled,
}
impl ProposalStatus {
pub fn to_string(&self) -> &'static str {
match self {
ProposalStatus::Draft => "Draft",
ProposalStatus::Active => "Active",
ProposalStatus::Approved => "Approved",
ProposalStatus::Rejected => "Rejected",
ProposalStatus::Cancelled => "Cancelled",
}
}
pub fn get_badge_class(&self) -> &'static str {
match self {
ProposalStatus::Draft => "badge bg-secondary",
ProposalStatus::Active => "badge bg-success",
ProposalStatus::Approved => "badge bg-primary",
ProposalStatus::Rejected => "badge bg-danger",
ProposalStatus::Cancelled => "badge bg-warning",
}
}
}
impl Component for GovernanceView {
type Message = GovernanceMsg;
type Properties = GovernanceViewProps;
fn create(_ctx: &Context<Self>) -> Self {
Self {
active_tab: "Overview".to_string(),
proposals: Self::get_sample_proposals(),
loading: false,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
GovernanceMsg::SwitchTab(tab) => {
self.active_tab = tab;
true
}
GovernanceMsg::CreateProposal => {
// Handle create proposal logic
true
}
GovernanceMsg::VoteOnProposal(_id, _vote_type) => {
// Handle voting logic
true
}
GovernanceMsg::LoadProposals => {
self.loading = true;
// Load proposals from service
true
}
}
}
fn view(&self, ctx: &Context<Self>) -> Html {
// Create tabs content
let mut tabs = HashMap::new();
// Overview Tab
tabs.insert("Overview".to_string(), self.render_overview(ctx));
// Proposals Tab
tabs.insert("Proposals".to_string(), self.render_proposals(ctx));
// Create Proposal Tab
tabs.insert("Create Proposal".to_string(), self.render_create_proposal(ctx));
html! {
<ViewComponent
title={"Governance".to_string()}
description={"Voting, rules, proposals".to_string()}
tabs={tabs}
default_tab={"Overview".to_string()}
/>
}
}
}
impl GovernanceView {
fn get_sample_proposals() -> Vec<Proposal> {
vec![
Proposal {
id: 1,
title: "Increase Block Rewards for Validators".to_string(),
description: "Proposal to increase validator rewards by 15% to improve network security and encourage more participation.".to_string(),
creator_name: "Alice Johnson".to_string(),
status: ProposalStatus::Active,
vote_start_date: Some("2024-01-15".to_string()),
vote_end_date: Some("2024-01-22".to_string()),
created_at: "2024-01-10".to_string(),
yes_votes: 45,
no_votes: 12,
abstain_votes: 8,
total_votes: 65,
},
Proposal {
id: 2,
title: "Community Development Fund Allocation".to_string(),
description: "Allocate 100,000 tokens from treasury for community development initiatives and grants.".to_string(),
creator_name: "Bob Smith".to_string(),
status: ProposalStatus::Draft,
vote_start_date: None,
vote_end_date: None,
created_at: "2024-01-12".to_string(),
yes_votes: 0,
no_votes: 0,
abstain_votes: 0,
total_votes: 0,
},
]
}
fn render_overview(&self, ctx: &Context<Self>) -> Html {
let link = ctx.link();
let default_date = "9999-12-31".to_string();
let nearest_proposal = self.proposals.iter()
.filter(|p| p.status == ProposalStatus::Active)
.min_by_key(|p| p.vote_end_date.as_ref().unwrap_or(&default_date));
html! {
<div>
// Dashboard Main Content
<div class="row mb-3">
// Latest Proposal (left)
<div class="col-lg-8 mb-4 mb-lg-0">
{if let Some(proposal) = nearest_proposal {
self.render_latest_proposal(proposal, link)
} else {
self.render_no_active_proposals()
}}
</div>
// Your Vote (right narrow)
<div class="col-lg-4">
{self.render_your_vote()}
</div>
</div>
// Active Proposals Section (below)
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">{"Active Proposals"}</h5>
</div>
<div class="card-body">
<div class="row">
{for self.proposals.iter().take(3).map(|proposal| {
self.render_proposal_card(proposal)
})}
</div>
</div>
</div>
</div>
</div>
</div>
}
}
fn render_latest_proposal(&self, proposal: &Proposal, _link: &html::Scope<Self>) -> Html {
html! {
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{"Latest Proposal"}</h5>
<div>
<span class="badge bg-warning text-dark me-2">
{format!("Ends: {}", proposal.vote_end_date.as_ref().unwrap_or(&"TBD".to_string()))}
</span>
<a href="#" class="btn btn-sm btn-outline-primary">{"View Full Proposal"}</a>
</div>
</div>
<div class="card-body">
<h4 class="card-title">{&proposal.title}</h4>
<h6 class="card-subtitle mb-3 text-muted">{format!("Proposed by {}", &proposal.creator_name)}</h6>
<div class="mb-4">
<p>{&proposal.description}</p>
</div>
<div class="row">
<div class="col-md-6">
<h6 class="text-muted">{"Proposal Details"}</h6>
<table class="table table-borderless">
<tbody>
<tr>
<td class="fw-bold">{"Status:"}</td>
<td><span class={proposal.status.get_badge_class()}>{proposal.status.to_string()}</span></td>
</tr>
<tr>
<td class="fw-bold">{"Created:"}</td>
<td>{&proposal.created_at}</td>
</tr>
<tr>
<td class="fw-bold">{"Voting Start:"}</td>
<td>{proposal.vote_start_date.as_ref().unwrap_or(&"Not set".to_string())}</td>
</tr>
<tr>
<td class="fw-bold">{"Voting End:"}</td>
<td>{proposal.vote_end_date.as_ref().unwrap_or(&"Not set".to_string())}</td>
</tr>
</tbody>
</table>
</div>
<div class="col-md-6">
<h6 class="text-muted">{"Voting Statistics"}</h6>
<table class="table table-borderless">
<tbody>
<tr>
<td class="fw-bold">{"Total Votes:"}</td>
<td>{proposal.total_votes}</td>
</tr>
<tr>
<td class="fw-bold">{"Yes Votes:"}</td>
<td class="text-success">{proposal.yes_votes}</td>
</tr>
<tr>
<td class="fw-bold">{"No Votes:"}</td>
<td class="text-danger">{proposal.no_votes}</td>
</tr>
<tr>
<td class="fw-bold">{"Abstain:"}</td>
<td class="text-secondary">{proposal.abstain_votes}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
}
}
fn render_no_active_proposals(&self) -> Html {
html! {
<div class="card h-100">
<div class="card-body text-center py-5">
<i class="bi bi-clipboard-check fs-1 text-muted mb-3"></i>
<h5>{"No active proposals requiring votes"}</h5>
<p class="text-muted">{"When new proposals are created, they will appear here for voting."}</p>
<a href="#" class="btn btn-primary mt-3">{"Create Proposal"}</a>
</div>
</div>
}
}
fn render_your_vote(&self) -> Html {
// Get the latest active proposal for voting
let active_proposal = self.proposals.iter()
.find(|p| p.status == ProposalStatus::Active);
if let Some(proposal) = active_proposal {
let proposal_id = proposal.id;
let yes_percent = if proposal.total_votes > 0 {
(proposal.yes_votes * 100 / proposal.total_votes)
} else { 0 };
let no_percent = if proposal.total_votes > 0 {
(proposal.no_votes * 100 / proposal.total_votes)
} else { 0 };
let abstain_percent = if proposal.total_votes > 0 {
(proposal.abstain_votes * 100 / proposal.total_votes)
} else { 0 };
html! {
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">{"Cast Your Vote"}</h5>
</div>
<div class="card-body">
<div class="mb-3">
<h6 class="mb-2">{"Current Results"}</h6>
<div class="progress mb-2" style="height: 20px;">
<div class="progress-bar bg-success" role="progressbar" style={format!("width: {}%", yes_percent)}>
{format!("{}%", yes_percent)}
</div>
<div class="progress-bar bg-danger" role="progressbar" style={format!("width: {}%", no_percent)}>
{format!("{}%", no_percent)}
</div>
<div class="progress-bar bg-secondary" role="progressbar" style={format!("width: {}%", abstain_percent)}>
{format!("{}%", abstain_percent)}
</div>
</div>
<div class="d-flex justify-content-between text-muted small">
<span>{format!("{} votes", proposal.total_votes)}</span>
<span>{if proposal.total_votes >= 20 { "Quorum reached" } else { "Quorum needed" }}</span>
</div>
</div>
<div class="mb-3">
<h6 class="mb-2">{"Your Voting Power"}</h6>
<div class="text-center">
<h4 class="text-primary mb-1">{"1,250"}</h4>
<small class="text-muted">{"tokens"}</small>
</div>
</div>
<form>
<div class="mb-3">
<input type="text" class="form-control form-control-sm" placeholder="Optional comment" />
</div>
<div class="d-grid gap-2">
<button
type="button"
class="btn btn-success btn-sm"
>
{"Vote Yes"}
</button>
<button
type="button"
class="btn btn-danger btn-sm"
>
{"Vote No"}
</button>
<button
type="button"
class="btn btn-secondary btn-sm"
>
{"Abstain"}
</button>
</div>
</form>
</div>
</div>
}
} else {
html! {
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">{"Your Vote"}</h5>
</div>
<div class="card-body text-center">
<div class="mb-3">
<i class="bi bi-ballot fs-1 text-muted"></i>
</div>
<h6 class="mb-2">{"No Active Proposals"}</h6>
<p class="text-muted small mb-3">{"No proposals are currently open for voting"}</p>
<div class="mb-3">
<h6 class="mb-2">{"Your Voting Power"}</h6>
<h4 class="text-primary mb-1">{"1,250"}</h4>
<small class="text-muted">{"tokens"}</small>
</div>
<div class="d-grid">
<a href="#" class="btn btn-outline-primary btn-sm">{"View Vote History"}</a>
</div>
</div>
</div>
}
}
}
fn render_proposal_card(&self, proposal: &Proposal) -> Html {
html! {
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">{&proposal.title}</h5>
<h6 class="card-subtitle mb-2 text-muted">{format!("By {}", &proposal.creator_name)}</h6>
<p class="card-text">{format!("{}...", &proposal.description.chars().take(100).collect::<String>())}</p>
<div class="d-flex justify-content-between align-items-center">
<span class={proposal.status.get_badge_class()}>
{proposal.status.to_string()}
</span>
<a href="#" class="btn btn-sm btn-outline-primary">{"View Details"}</a>
</div>
</div>
<div class="card-footer text-muted text-center">
<span>{format!("Voting ends: {}", proposal.vote_end_date.as_ref().unwrap_or(&"TBD".to_string()))}</span>
</div>
</div>
</div>
}
}
fn render_proposals(&self, _ctx: &Context<Self>) -> Html {
html! {
<div>
// Info Alert
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i>{" About Proposals"}</h5>
<p>{"Proposals are formal requests for changes to the platform that require community approval. Each proposal includes a detailed description, implementation plan, and voting period. Browse the list below to see all active and past proposals."}</p>
<div class="mt-2">
<a href="#" class="btn btn-sm btn-outline-primary">
<i class="bi bi-file-text"></i>{" Proposal Guidelines"}
</a>
</div>
</div>
</div>
// Filter Controls
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<form class="row g-3">
<div class="col-md-4">
<label for="status" class="form-label">{"Status"}</label>
<select class="form-select" id="status">
<option selected=true>{"All Statuses"}</option>
<option value="Draft">{"Draft"}</option>
<option value="Active">{"Active"}</option>
<option value="Approved">{"Approved"}</option>
<option value="Rejected">{"Rejected"}</option>
<option value="Cancelled">{"Cancelled"}</option>
</select>
</div>
<div class="col-md-6">
<label for="search" class="form-label">{"Search"}</label>
<input type="text" class="form-control" id="search" placeholder="Search by title or description" />
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">{"Filter"}</button>
</div>
</form>
</div>
</div>
</div>
</div>
// Proposals List
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{"All Proposals"}</h5>
<a href="#" class="btn btn-sm btn-primary">{"Create New Proposal"}</a>
</div>
<div class="card-body">
{if !self.proposals.is_empty() {
html! {
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{"Title"}</th>
<th>{"Creator"}</th>
<th>{"Status"}</th>
<th>{"Created"}</th>
<th>{"Voting Period"}</th>
<th>{"Actions"}</th>
</tr>
</thead>
<tbody>
{for self.proposals.iter().map(|proposal| {
html! {
<tr>
<td>{&proposal.title}</td>
<td>{&proposal.creator_name}</td>
<td>
<span class={proposal.status.get_badge_class()}>
{proposal.status.to_string()}
</span>
</td>
<td>{&proposal.created_at}</td>
<td>
{if let (Some(start), Some(end)) = (&proposal.vote_start_date, &proposal.vote_end_date) {
format!("{} to {}", start, end)
} else {
"Not set".to_string()
}}
</td>
<td>
<a href="#" class="btn btn-sm btn-primary">{"View"}</a>
</td>
</tr>
}
})}
</tbody>
</table>
</div>
}
} else {
html! {
<div class="alert alert-info text-center py-5">
<i class="bi bi-info-circle fs-1 mb-3"></i>
<h5>{"No proposals found"}</h5>
<p>{"There are no proposals in the system yet."}</p>
<a href="#" class="btn btn-primary mt-3">{"Create New Proposal"}</a>
</div>
}
}}
</div>
</div>
</div>
</div>
</div>
}
}
fn render_create_proposal(&self, _ctx: &Context<Self>) -> Html {
html! {
<div>
// Info Alert
<div class="row">
<div class="col-12">
<div class="alert alert-info alert-dismissible fade show">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<h5><i class="bi bi-info-circle"></i>{" About Creating Proposals"}</h5>
<p>{"Creating a proposal is an important step in our community governance process. Well-crafted proposals clearly state the problem, solution, and implementation details. The community will review and vote on your proposal, so be thorough and thoughtful in your submission."}</p>
<div class="mt-2">
<a href="#" class="btn btn-sm btn-outline-primary">
<i class="bi bi-file-earmark-text"></i>{" Proposal Templates"}
</a>
</div>
</div>
</div>
</div>
// Proposal Form and Guidelines
<div class="row mb-4">
// Proposal Form Column
<div class="col-lg-8">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">{"New Proposal"}</h5>
</div>
<div class="card-body">
<form>
<div class="mb-3">
<label for="title" class="form-label">{"Title"}</label>
<input type="text" class="form-control" id="title" placeholder="Enter a clear, concise title for your proposal" />
<div class="form-text">{"Make it descriptive and specific"}</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">{"Description"}</label>
<textarea class="form-control" id="description" rows="8" placeholder="Provide a detailed description of your proposal..."></textarea>
<div class="form-text">{"Explain the purpose, benefits, and implementation details"}</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label for="voting_start_date" class="form-label">{"Voting Start Date"}</label>
<input type="date" class="form-control" id="voting_start_date" />
<div class="form-text">{"When should voting begin?"}</div>
</div>
<div class="col-md-6">
<label for="voting_end_date" class="form-label">{"Voting End Date"}</label>
<input type="date" class="form-control" id="voting_end_date" />
<div class="form-text">{"When should voting end?"}</div>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="draft" />
<label class="form-check-label" for="draft">
{"Save as draft (not ready for voting yet)"}
</label>
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">{"Submit Proposal"}</button>
<a href="#" class="btn btn-outline-secondary">{"Cancel"}</a>
</div>
</form>
</div>
</div>
</div>
// Guidelines Column
<div class="col-lg-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">{"Proposal Guidelines"}</h5>
</div>
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item bg-transparent">
<strong>{"Be specific:"}</strong>{" Clearly state what you're proposing and why."}
</li>
<li class="list-group-item bg-transparent">
<strong>{"Provide context:"}</strong>{" Explain the current situation and why change is needed."}
</li>
<li class="list-group-item bg-transparent">
<strong>{"Consider implementation:"}</strong>{" Outline how your proposal could be implemented."}
</li>
<li class="list-group-item bg-transparent">
<strong>{"Address concerns:"}</strong>{" Anticipate potential objections and address them."}
</li>
<li class="list-group-item bg-transparent">
<strong>{"Be respectful:"}</strong>{" Focus on ideas, not individuals or groups."}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
}
}
}
#[function_component(GovernanceViewWrapper)]
pub fn governance_view(props: &GovernanceViewProps) -> Html {
html! {
<GovernanceView context={props.context.clone()} />
}
}

View File

@ -0,0 +1,82 @@
use yew::prelude::*;
use crate::components::FeatureCard;
use crate::routing::ViewContext;
#[derive(Properties, PartialEq)]
pub struct HomeViewProps {
pub context: ViewContext,
}
#[function_component(HomeView)]
pub fn home_view(props: &HomeViewProps) -> Html {
html! {
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h1 class="card-title text-center mb-4">{"Zanzibar Digital Freezone"}</h1>
<p class="card-text text-center lead mb-5">{"Convenience, Safety and Privacy"}</p>
<div class="row g-3 mb-4">
// Left Column (3 items)
<div class="col-md-6">
// Card 1: Frictionless Collaboration
<FeatureCard
title="Frictionless Collaboration"
description="Direct communication and transactions between individuals and organizations, making processes efficient and cost-effective."
icon="bi-people-fill"
color_variant="primary"
/>
// Card 2: Frictionless Banking
<FeatureCard
title="Frictionless Banking"
description="Simplified financial transactions without the complications and fees of traditional banking systems."
icon="bi-currency-exchange"
color_variant="success"
/>
// Card 3: Tax Efficiency
<FeatureCard
title="Tax Efficiency"
description="Lower taxes making business operations more profitable and competitive in the global market."
icon="bi-graph-up-arrow"
color_variant="info"
/>
</div>
// Right Column (2 items)
<div class="col-md-6">
// Card 4: Global Ecommerce
<FeatureCard
title="Global Ecommerce"
description="Easily expand your business globally with streamlined operations and tools to reach customers worldwide."
icon="bi-globe"
color_variant="warning"
/>
// Card 5: Clear Regulations
<FeatureCard
title="Clear Regulations"
description="Clear regulations and efficient dispute resolution mechanisms providing a stable business environment."
icon="bi-shield-check"
color_variant="danger"
/>
</div>
</div>
<div class="text-center">
<a
href="https://info.ourworld.tf/zdfz"
target="_blank"
class="btn btn-primary btn-lg"
>
{"Learn More"}
</a>
</div>
</div>
</div>
</div>
</div>
}
}

Some files were not shown because too many files have changed in this diff Show More