checkpoint

This commit is contained in:
Timur Gordon 2025-06-30 15:49:32 +02:00
parent fdbb4b84c3
commit 1c96fa4087
12 changed files with 856 additions and 262 deletions

View File

@ -0,0 +1,267 @@
use yew::prelude::*;
#[derive(Clone, PartialEq)]
pub struct NotificationItem {
pub id: String,
pub title: String,
pub message: String,
pub notification_type: NotificationType,
pub timestamp: String,
pub is_read: bool,
pub action_required: bool,
pub action_text: Option<String>,
pub action_url: Option<String>,
}
#[derive(Clone, PartialEq)]
pub enum NotificationType {
Success,
Info,
Warning,
Action,
Vote,
}
impl NotificationType {
pub fn get_icon(&self) -> &'static str {
match self {
NotificationType::Success => "bi-check-circle-fill",
NotificationType::Info => "bi-info-circle-fill",
NotificationType::Warning => "bi-exclamation-triangle-fill",
NotificationType::Action => "bi-bell-fill",
NotificationType::Vote => "bi-hand-thumbs-up-fill",
}
}
pub fn get_color(&self) -> &'static str {
match self {
NotificationType::Success => "text-success",
NotificationType::Info => "text-info",
NotificationType::Warning => "text-warning",
NotificationType::Action => "text-primary",
NotificationType::Vote => "text-purple",
}
}
pub fn get_bg_color(&self) -> &'static str {
match self {
NotificationType::Success => "bg-success-subtle",
NotificationType::Info => "bg-info-subtle",
NotificationType::Warning => "bg-warning-subtle",
NotificationType::Action => "bg-primary-subtle",
NotificationType::Vote => "bg-purple-subtle",
}
}
}
#[derive(Properties, PartialEq)]
pub struct InboxProps {
#[prop_or_default]
pub notifications: Vec<NotificationItem>,
}
#[function_component(Inbox)]
pub fn inbox(props: &InboxProps) -> Html {
// Mock notifications for demo
let notifications = if props.notifications.is_empty() {
vec![
NotificationItem {
id: "1".to_string(),
title: "Company Registration Successful".to_string(),
message: "Your company 'TechCorp FZC' has been successfully registered.".to_string(),
notification_type: NotificationType::Success,
timestamp: "2 hours ago".to_string(),
is_read: true,
action_required: false,
action_text: Some("View Company".to_string()),
action_url: Some("/companies/1".to_string()),
},
NotificationItem {
id: "2".to_string(),
title: "Vote Required".to_string(),
message: "New governance proposal requires your vote: 'Budget Allocation Q1 2025'".to_string(),
notification_type: NotificationType::Vote,
timestamp: "1 day ago".to_string(),
is_read: true,
action_required: true,
action_text: Some("Vote Now".to_string()),
action_url: Some("/governance".to_string()),
},
NotificationItem {
id: "3".to_string(),
title: "Payment Successful".to_string(),
message: "Monthly subscription payment of $50.00 processed successfully.".to_string(),
notification_type: NotificationType::Success,
timestamp: "3 days ago".to_string(),
is_read: true,
action_required: false,
action_text: None,
action_url: None,
},
NotificationItem {
id: "4".to_string(),
title: "Document Review Required".to_string(),
message: "Please review and sign the updated Terms of Service.".to_string(),
notification_type: NotificationType::Action,
timestamp: "1 week ago".to_string(),
is_read: true,
action_required: true,
action_text: Some("Review".to_string()),
action_url: Some("/contracts".to_string()),
},
]
} else {
props.notifications.clone()
};
let unread_count = notifications.iter().filter(|n| !n.is_read).count();
html! {
<>
<style>
{r#"
.inbox-card {
border: 1px solid #e9ecef;
border-radius: 12px;
transition: all 0.2s ease;
}
.inbox-card:hover {
border-color: #dee2e6;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.notification-item {
border-radius: 8px;
transition: all 0.2s ease;
cursor: pointer;
}
.notification-item:hover {
background-color: #f8f9fa !important;
}
.notification-item.unread {
border-left: 3px solid #0d6efd;
}
.notification-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.action-btn {
font-size: 0.8rem;
padding: 4px 12px;
border-radius: 6px;
border: 1px solid #dee2e6;
background: white;
color: #495057;
transition: all 0.2s ease;
}
.action-btn:hover {
background: #f8f9fa;
border-color: #adb5bd;
color: #212529;
}
.action-btn.primary {
background: #0d6efd;
border-color: #0d6efd;
color: white;
}
.action-btn.primary:hover {
background: #0b5ed7;
border-color: #0a58ca;
color: white;
}
.bg-purple-subtle {
background-color: rgba(102, 16, 242, 0.1);
}
.text-purple {
color: #6610f2;
}
"#}
</style>
<div class="inbox-card bg-white">
<div class="card-body p-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex align-items-center">
<i class="bi bi-inbox-fill text-primary me-2" style="font-size: 1.2rem;"></i>
<h5 class="mb-0 fw-semibold">{"Inbox"}</h5>
</div>
if unread_count > 0 {
<span class="badge bg-primary rounded-pill">{unread_count}</span>
}
</div>
<div class="d-flex flex-column gap-3">
{for notifications.iter().take(4).map(|notification| {
html! {
<div class={classes!(
"notification-item",
"p-3",
(!notification.is_read).then(|| "unread")
)}>
<div class="d-flex align-items-start">
<div class={classes!(
"notification-icon",
"me-3",
"flex-shrink-0",
notification.notification_type.get_bg_color()
)}>
<i class={classes!(
"bi",
notification.notification_type.get_icon(),
notification.notification_type.get_color()
)}></i>
</div>
<div class="flex-grow-1 min-w-0">
<div class="d-flex align-items-start justify-content-between mb-1">
<h6 class={classes!(
"mb-0",
"text-truncate",
(!notification.is_read).then(|| "fw-semibold")
)} style="font-size: 0.9rem;">
{&notification.title}
</h6>
<small class="text-muted ms-2 flex-shrink-0" style="font-size: 0.75rem;">
{&notification.timestamp}
</small>
</div>
<p class="text-muted mb-2 small" style="font-size: 0.8rem; line-height: 1.4;">
{&notification.message}
</p>
if let Some(action_text) = &notification.action_text {
<button class={classes!(
"action-btn",
notification.action_required.then(|| "primary")
)}>
{action_text}
if notification.action_required {
<i class="bi bi-arrow-right ms-1" style="font-size: 0.7rem;"></i>
}
</button>
}
</div>
</div>
</div>
}
})}
</div>
if notifications.len() > 4 {
<div class="text-center mt-3 pt-3 border-top">
<button class="btn btn-outline-primary btn-sm">
{"View All Notifications"}
<i class="bi bi-arrow-right ms-1"></i>
</button>
</div>
}
</div>
</div>
</>
}
}

View File

@ -26,7 +26,6 @@ pub fn sidebar(props: &SidebarProps) -> Html {
AppView::Home,
AppView::Administration,
AppView::PersonAdministration,
AppView::Residence,
AppView::Accounting,
AppView::Contracts,
AppView::Governance,

View File

@ -8,6 +8,8 @@ pub mod toast;
pub mod common;
pub mod accounting;
pub mod resident_landing_overlay;
pub mod inbox;
pub mod residence_card;
pub use layout::*;
pub use forms::*;
@ -19,3 +21,5 @@ pub use toast::*;
pub use common::*;
pub use accounting::*;
pub use resident_landing_overlay::*;
pub use inbox::*;
pub use residence_card::*;

View File

@ -0,0 +1,145 @@
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct ResidenceCardProps {
pub user_name: String,
#[prop_or_default]
pub email: Option<String>,
#[prop_or_default]
pub public_key: Option<String>,
#[prop_or_default]
pub resident_id: Option<String>,
#[prop_or_default]
pub status: ResidenceStatus,
}
#[derive(Clone, PartialEq)]
pub enum ResidenceStatus {
Active,
Pending,
Suspended,
}
impl ResidenceStatus {
pub fn get_badge_class(&self) -> &'static str {
match self {
ResidenceStatus::Active => "bg-success",
ResidenceStatus::Pending => "bg-warning",
ResidenceStatus::Suspended => "bg-danger",
}
}
pub fn get_text(&self) -> &'static str {
match self {
ResidenceStatus::Active => "ACTIVE",
ResidenceStatus::Pending => "PENDING",
ResidenceStatus::Suspended => "SUSPENDED",
}
}
}
impl Default for ResidenceStatus {
fn default() -> Self {
ResidenceStatus::Active
}
}
#[function_component(ResidenceCard)]
pub fn residence_card(props: &ResidenceCardProps) -> Html {
html! {
<>
<style>
{r#"
.residence-card-container {
perspective: 1000px;
}
.residence-card {
transform-style: preserve-3d;
transition: transform 0.3s ease;
}
.residence-card:hover {
transform: rotateY(5deg) rotateX(5deg);
}
"#}
</style>
<div class="residence-card-container d-flex align-items-center justify-content-center">
<div class="residence-card">
<div class="card border-0 shadow-lg" style="width: 350px; background: white; border-radius: 15px;">
// Header with Zanzibar flag gradient
<div style="background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%); height: 80px; border-radius: 15px 15px 0 0; position: relative;">
<div class="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-between px-4">
<div>
<h6 class="mb-0 text-white" style="font-size: 0.9rem; font-weight: 600;">{"DIGITAL RESIDENT"}</h6>
<small class="text-white" style="opacity: 0.9; font-size: 0.75rem;">{"Zanzibar Digital Freezone"}</small>
</div>
<i class="bi bi-shield-check-fill text-white" style="font-size: 1.5rem; opacity: 0.9;"></i>
</div>
</div>
// Card body with white background
<div class="card-body p-4" style="background: white; border-radius: 0 0 15px 15px;">
<div class="mb-3">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"FULL NAME"}</div>
<div class="h5 mb-0 text-dark" style="font-weight: 600;">
{&props.user_name}
</div>
</div>
<div class="mb-3">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"EMAIL"}</div>
<div class="text-dark" style="font-size: 0.9rem;">
{props.email.as_ref().unwrap_or(&"resident@zanzibar-freezone.com".to_string())}
</div>
</div>
<div class="mb-3">
<div class="text-muted small d-flex align-items-center" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">
<i class="bi bi-key me-1" style="font-size: 0.8rem;"></i>
{"PUBLIC KEY"}
</div>
<div class="text-dark" style="font-size: 0.7rem; font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace; word-break: break-all; line-height: 1.3;">
{if let Some(public_key) = &props.public_key {
format!("{}...", &public_key[..std::cmp::min(24, public_key.len())])
} else {
"zdf1qxy2mlyjkjkpskpsw9fxtpugs450add72nyktmzqau...".to_string()
}}
</div>
</div>
<div class="mb-3">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"RESIDENT SINCE"}</div>
<div class="text-dark" style="font-size: 0.8rem;">
{"2025"}
</div>
</div>
<div class="d-flex justify-content-between align-items-end mb-3">
<div>
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"RESIDENT ID"}</div>
<div class="text-dark" style="font-weight: 600;">
{props.resident_id.as_ref().unwrap_or(&"ZDF-2025-****".to_string())}
</div>
</div>
<div class="text-end">
<div class="text-muted small" style="font-size: 0.7rem; font-weight: 500; letter-spacing: 0.5px;">{"STATUS"}</div>
<div class={classes!("badge", props.status.get_badge_class())} style="color: white; font-weight: 500;">
{props.status.get_text()}
</div>
</div>
</div>
// QR Code at bottom
<div class="text-center border-top pt-3" style="border-color: #e9ecef !important;">
<div class="d-inline-block p-2 rounded" style="background: #f8f9fa;">
<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-muted small mt-2" style="font-size: 0.7rem;">{"Scan to verify"}</div>
</div>
</div>
</div>
</div>
</div>
</>
}
}

View File

@ -20,6 +20,8 @@ pub struct ViewComponentProps {
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
#[prop_or_default]
pub use_modern_header: bool, // Use modern header style without card wrapper
}
#[function_component(ViewComponent)]
@ -40,7 +42,8 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
};
html! {
<div class="container-fluid">
<div class="container-fluid" style="max-width: 1100px;">
<div class="px-3 px-md-4 px-lg-5 px-xl-6">
// Breadcrumbs (if provided)
if let Some(breadcrumbs) = &props.breadcrumbs {
<ol class="breadcrumb mb-3">
@ -59,7 +62,78 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
</ol>
}
// Page Header in Card (with integrated tabs if provided)
if props.use_modern_header {
// Modern header style without card wrapper
if props.title.is_some() || props.description.is_some() || props.actions.is_some() {
<div class="d-flex justify-content-between align-items-end mb-4">
// Left side: Title and description
<div>
if let Some(title) = &props.title {
<h2 class="mb-1 fw-bold">{title}</h2>
}
if let Some(description) = &props.description {
<p class="text-muted mb-0">{description}</p>
}
</div>
// Right side: Actions
if let Some(actions) = &props.actions {
<div>
{actions.clone()}
</div>
}
</div>
}
// Modern tabs navigation (if provided)
if let Some(tabs) = &props.tabs {
<div class="mb-0">
<ul class="nav nav-tabs border-bottom-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-3",
"py-2",
"small",
"border",
"border-bottom-0",
"bg-light",
"text-muted",
if is_active {
"active bg-white text-dark border-primary border-bottom-0"
} else {
"border-light"
}
)}
type="button"
role="tab"
onclick={on_click}
style={if is_active { "margin-bottom: -1px; z-index: 1; position: relative;" } else { "" }}
>
{tab_name}
</button>
</li>
}
})}
</ul>
</div>
}
} else {
// Original header style with card wrapper
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">
@ -121,10 +195,11 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
</div>
</div>
}
}
// Tab Content (if tabs are provided)
if let Some(tabs) = &props.tabs {
<div class="tab-content">
<div class="tab-content border border-top-0 rounded-bottom bg-white p-4">
{for tabs.iter().map(|(tab_name, content)| {
let is_active = *active_tab == *tab_name;
html! {
@ -148,5 +223,6 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
{for props.children.iter()}
}
</div>
</div>
}
}

View File

@ -254,12 +254,30 @@ pub fn accounting_view(props: &AccountingViewProps) -> Html {
});
},
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>
// Show same functionality as business context
// 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()} />
});
}
}
@ -274,10 +292,8 @@ pub fn accounting_view(props: &AccountingViewProps) -> Html {
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()),
}}
default_tab={Some("Overview".to_string())}
use_modern_header={true}
/>
}
}

View File

@ -164,6 +164,7 @@ impl Component for CompaniesView {
<ViewComponent
title={Some("Registration Successful".to_string())}
description={Some("Your company registration has been completed successfully".to_string())}
use_modern_header={true}
>
<RegistrationWizard
on_registration_complete={link.callback(CompaniesViewMsg::RegistrationComplete)}
@ -182,6 +183,7 @@ impl Component for CompaniesView {
<ViewComponent
title={Some("Register New Company".to_string())}
description={Some("Complete the registration process to create your new company".to_string())}
use_modern_header={true}
>
<RegistrationWizard
on_registration_complete={link.callback(CompaniesViewMsg::RegistrationComplete)}
@ -200,6 +202,7 @@ impl Component for CompaniesView {
<ViewComponent
title={Some("Companies".to_string())}
description={Some("Manage your companies and registrations".to_string())}
use_modern_header={true}
>
{self.render_companies_content(ctx)}
</ViewComponent>
@ -258,16 +261,20 @@ impl CompaniesView {
let link = ctx.link();
html! {
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-building text-primary fs-5"></i>
</div>
<div>
<h5 class="mb-0">
<i class="bi bi-building me-2"></i>{"Companies & Registrations"}
</h5>
<h5 class="mb-0">{"Companies & Registrations"}</h5>
<small class="text-muted">
{format!("{} companies, {} pending registrations", self.companies.len(), self.registrations.len())}
</small>
</div>
</div>
<button
class="btn btn-success"
onclick={link.callback(|_| CompaniesViewMsg::StartNewRegistration)}
@ -275,7 +282,6 @@ impl CompaniesView {
<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">

View File

@ -170,10 +170,11 @@ impl Component for ContractsViewComponent {
html! {
<ViewComponent
title={title.to_string()}
description={description.to_string()}
tabs={tabs}
default_tab={"Contracts".to_string()}
title={Some(title.to_string())}
description={Some(description.to_string())}
tabs={Some(tabs)}
default_tab={Some("Contracts".to_string())}
use_modern_header={true}
/>
}
}
@ -296,11 +297,14 @@ impl ContractsViewComponent {
// Filters Section
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-funnel text-primary fs-5"></i>
</div>
<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>
@ -344,11 +348,14 @@ impl ContractsViewComponent {
// Contracts Table
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-success bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-file-earmark-text text-success fs-5"></i>
</div>
<h5 class="mb-0">{"Contracts"}</h5>
</div>
<div class="card-body">
{self.render_contracts_table(_ctx)}
</div>
</div>
@ -442,11 +449,14 @@ impl ContractsViewComponent {
html! {
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-file-earmark-plus text-primary fs-5"></i>
</div>
<h5 class="mb-0">{"Contract Details"}</h5>
</div>
<div class="card-body">
<form>
<div class="mb-3">
<label for="title" class="form-label">
@ -531,11 +541,14 @@ Payment will be made according to the following schedule:
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-info bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-lightbulb text-info fs-5"></i>
</div>
<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>
@ -547,11 +560,14 @@ Payment will be made according to the following schedule:
</div>
</div>
<div class="card">
<div class="card-header">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-warning bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-file-earmark-code text-warning fs-5"></i>
</div>
<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">

View File

@ -1,5 +1,5 @@
use yew::prelude::*;
use crate::components::FeatureCard;
use crate::components::{Inbox, ResidenceCard, ResidenceStatus};
use crate::routing::ViewContext;
#[derive(Properties, PartialEq)]
@ -9,74 +9,125 @@ pub struct HomeViewProps {
#[function_component(HomeView)]
pub fn home_view(props: &HomeViewProps) -> Html {
// Mock user data - in a real app this would come from authentication/user context
let user_name = "Timur Gordon".to_string();
let user_email = Some("timur@example.com".to_string());
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>
<>
<style>
{r#"
.welcome-section {
background: linear-gradient(135deg, rgba(0,153,255,0.05) 0%, rgba(0,204,102,0.05) 100%);
border-radius: 16px;
border: 1px solid rgba(0,153,255,0.1);
}
.greeting-card {
border: 1px solid #e9ecef;
border-radius: 12px;
transition: all 0.2s ease;
}
.greeting-card:hover {
border-color: #dee2e6;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.time-badge {
background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.stats-item {
text-align: center;
padding: 1rem;
border-radius: 8px;
background: #f8f9fa;
transition: all 0.2s ease;
}
.stats-item:hover {
background: #e9ecef;
transform: translateY(-2px);
}
.stats-number {
font-size: 1.5rem;
font-weight: 700;
color: #0d6efd;
}
.stats-label {
font-size: 0.8rem;
color: #6c757d;
font-weight: 500;
}
"#}
</style>
<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 class="container-fluid py-4 px-3 px-md-4 px-lg-5 px-xl-6">
<div class="row g-4">
// Left Column: Greeting and Inbox
<div class="col-lg-6">
// Welcome Section
<div class="welcome-section p-4 mb-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<h1 class="h3 mb-1 fw-bold text-dark">
{"Hello, "}{&user_name}{"! 👋"}
</h1>
<p class="text-muted mb-0">
{"Welcome back to your Digital Freezone dashboard"}
</p>
</div>
</div>
<div class="text-center">
<a
href="https://info.ourworld.tf/zdfz"
target="_blank"
class="btn btn-primary btn-lg"
>
{"Learn More"}
// Quick Actions
<div class="row g-3 mb-3">
<div class="col-4">
<a href="/companies/register" class="text-decoration-none">
<div class="stats-item">
<i class="bi bi-building-add text-primary mb-2" style="font-size: 1.5rem;"></i>
<div class="stats-label">{"Register Company"}</div>
</div>
</a>
</div>
<div class="col-4">
<a href="/governance" class="text-decoration-none">
<div class="stats-item">
<i class="bi bi-hand-thumbs-up text-success mb-2" style="font-size: 1.5rem;"></i>
<div class="stats-label">{"Vote on Proposals"}</div>
</div>
</a>
</div>
<div class="col-4">
<a href="/treasury" class="text-decoration-none">
<div class="stats-item">
<i class="bi bi-wallet2 text-info mb-2" style="font-size: 1.5rem;"></i>
<div class="stats-label">{"Manage Wallet"}</div>
</div>
</a>
</div>
</div>
</div>
// Inbox Component
<Inbox />
</div>
// Right Column: Residence Card
<div class="col-lg-6">
<div class="d-flex align-items-center justify-content-center h-100">
<ResidenceCard
user_name={user_name}
email={user_email}
public_key={Some("zdf1qxy2mlyjkjkpskpsw9fxtpugs450add72nyktmzqau...".to_string())}
resident_id={Some("ZDF-2025-0001".to_string())}
status={ResidenceStatus::Active}
/>
</div>
</div>
</div>
</div>
</>
}
}

View File

@ -273,29 +273,33 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
// Account Settings Tab (Person-specific)
tabs.insert("Account Settings".to_string(), html! {
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-person-gear me-2"></i>
{"Personal Account Settings"}
</h5>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="bg-primary bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-person-gear text-primary fs-4"></i>
</div>
<div class="card-body">
<div>
<h5 class="mb-1">{"Personal Account Settings"}</h5>
<p class="text-muted mb-0">{"Manage your personal information and preferences"}</p>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">{"Full Name"}</label>
<label class="form-label fw-medium">{"Full Name"}</label>
<input type="text" class="form-control" value="Timur Gordon" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{"Email Address"}</label>
<label class="form-label fw-medium">{"Email Address"}</label>
<input type="email" class="form-control" value="john.doe@example.com" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{"Phone Number"}</label>
<label class="form-label fw-medium">{"Phone Number"}</label>
<input type="tel" class="form-control" value="+1 (555) 123-4567" />
</div>
<div class="col-md-6 mb-3">
<label class="form-label">{"Preferred Language"}</label>
<label class="form-label fw-medium">{"Preferred Language"}</label>
<select class="form-select">
<option selected=true>{"English"}</option>
<option>{"French"}</option>
@ -304,7 +308,7 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
</select>
</div>
<div class="col-12 mb-3">
<label class="form-label">{"Time Zone"}</label>
<label class="form-label fw-medium">{"Time Zone"}</label>
<select class="form-select">
<option selected=true>{"UTC+00:00 (GMT)"}</option>
<option>{"UTC-05:00 (EST)"}</option>
@ -313,7 +317,7 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
</select>
</div>
</div>
<div class="mt-4">
<div class="mt-4 pt-3 border-top">
<button class="btn btn-primary me-2">{"Save Changes"}</button>
<button class="btn btn-outline-secondary">{"Reset"}</button>
</div>
@ -323,52 +327,56 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
// Privacy & Security Tab
tabs.insert("Privacy & Security".to_string(), html! {
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-shield-lock me-2"></i>
{"Privacy & Security Settings"}
</h5>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="bg-success bg-opacity-10 rounded-3 p-3 me-3">
<i class="bi bi-shield-lock text-success fs-4"></i>
</div>
<div class="card-body">
<div class="mb-4">
<h6>{"Two-Factor Authentication"}</h6>
<div>
<h5 class="mb-1">{"Privacy & Security Settings"}</h5>
<p class="text-muted mb-0">{"Manage your security preferences and privacy controls"}</p>
</div>
</div>
<div class="mb-4 p-3 bg-light rounded-3">
<h6 class="fw-medium mb-3">{"Two-Factor Authentication"}</h6>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="twoFactorAuth" checked=true />
<label class="form-check-label" for="twoFactorAuth">
<label class="form-check-label fw-medium" for="twoFactorAuth">
{"Enable two-factor authentication"}
</label>
</div>
<small class="text-muted">{"Adds an extra layer of security to your account"}</small>
</div>
<div class="mb-4">
<h6>{"Login Notifications"}</h6>
<div class="mb-4 p-3 bg-light rounded-3">
<h6 class="fw-medium mb-3">{"Login Notifications"}</h6>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="loginNotifications" checked=true />
<label class="form-check-label" for="loginNotifications">
<label class="form-check-label fw-medium" for="loginNotifications">
{"Email me when someone logs into my account"}
</label>
</div>
</div>
<div class="mb-4">
<h6>{"Data Privacy"}</h6>
<div class="mb-4 p-3 bg-light rounded-3">
<h6 class="fw-medium mb-3">{"Data Privacy"}</h6>
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="dataSharing" />
<label class="form-check-label" for="dataSharing">
<label class="form-check-label fw-medium" for="dataSharing">
{"Allow anonymous usage analytics"}
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="marketingEmails" />
<label class="form-check-label" for="marketingEmails">
<label class="form-check-label fw-medium" for="marketingEmails">
{"Receive marketing communications"}
</label>
</div>
</div>
<div class="mt-4">
<div class="mt-4 pt-3 border-top">
<button class="btn btn-primary me-2">{"Update Security Settings"}</button>
<button class="btn btn-outline-danger">{"Download My Data"}</button>
</div>
@ -385,14 +393,14 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> 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 class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-warning bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-star text-warning fs-5"></i>
</div>
<h5 class="mb-0">{"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>
@ -438,14 +446,14 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
<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 class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-3">
<div class="bg-info bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-receipt text-info fs-5"></i>
</div>
<h5 class="mb-0">{"Payment History"}</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
@ -483,12 +491,15 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
</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>
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
<i class="bi bi-credit-card text-primary fs-5"></i>
</div>
<h5 class="mb-0">{"Payment Methods"}</h5>
</div>
<button
class="btn btn-primary btn-sm"
onclick={on_add_payment_method.clone()}
@ -502,7 +513,6 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
}}
</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">
@ -566,25 +576,28 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
}
});
// 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 for your personal account.".to_string()}
primary_action={Some(("Browse Integrations".to_string(), "#".to_string()))}
secondary_action={Some(("API Documentation".to_string(), "#".to_string()))}
/>
});
html! {
<>
<div class="container-fluid px-3 px-md-4 px-lg-5 px-xl-6">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1">{"Settings"}</h1>
<p class="text-muted mb-0">{"Manage your account settings and preferences"}</p>
</div>
</div>
<ViewComponent
title={Some("Administration".to_string())}
description={Some("Account settings, billing, integrations".to_string())}
title={None::<String>}
description={None::<String>}
tabs={Some(tabs)}
default_tab={Some("Account Settings".to_string())}
use_modern_header={true}
/>
</div>
</div>
</div>
// Plan Selection Modal
if *show_plan_modal {

View File

@ -74,7 +74,7 @@ pub fn residence_view(props: &ResidenceViewProps) -> Html {
</tr>
<tr>
<td class="fw-bold">{"Email:"}</td>
<td>{"john.doe@resident.zdf"}</td>
<td>{"timur@resident.zdf"}</td>
</tr>
</tbody>
</table>

View File

@ -1005,6 +1005,7 @@ pub fn treasury_view(_props: &TreasuryViewProps) -> Html {
description={Some("Manage wallets, digital assets, and transactions".to_string())}
tabs={Some(tabs)}
default_tab={Some("Overview".to_string())}
use_modern_header={true}
/>
// Import Wallet Modal