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::Home,
AppView::Administration, AppView::Administration,
AppView::PersonAdministration, AppView::PersonAdministration,
AppView::Residence,
AppView::Accounting, AppView::Accounting,
AppView::Contracts, AppView::Contracts,
AppView::Governance, AppView::Governance,

View File

@ -8,6 +8,8 @@ pub mod toast;
pub mod common; pub mod common;
pub mod accounting; pub mod accounting;
pub mod resident_landing_overlay; pub mod resident_landing_overlay;
pub mod inbox;
pub mod residence_card;
pub use layout::*; pub use layout::*;
pub use forms::*; pub use forms::*;
@ -18,4 +20,6 @@ pub use entities::*;
pub use toast::*; pub use toast::*;
pub use common::*; pub use common::*;
pub use accounting::*; pub use accounting::*;
pub use resident_landing_overlay::*; 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) pub empty_state: Option<(String, String, String, Option<(String, String)>, Option<(String, String)>)>, // (icon, title, description, primary_action, secondary_action)
#[prop_or_default] #[prop_or_default]
pub children: Children, // Main content when no tabs 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)] #[function_component(ViewComponent)]
@ -40,7 +42,8 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
}; };
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) // Breadcrumbs (if provided)
if let Some(breadcrumbs) = &props.breadcrumbs { if let Some(breadcrumbs) = &props.breadcrumbs {
<ol class="breadcrumb mb-3"> <ol class="breadcrumb mb-3">
@ -59,72 +62,144 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
</ol> </ol>
} }
// Page Header in Card (with integrated tabs if provided) if props.use_modern_header {
if props.title.is_some() || props.description.is_some() || props.actions.is_some() || props.tabs.is_some() { // Modern header style without card wrapper
<div class="row mb-4"> if props.title.is_some() || props.description.is_some() || props.actions.is_some() {
<div class="col-12"> <div class="d-flex justify-content-between align-items-end mb-4">
<div class="card"> // Left side: Title and description
<div class="card-body"> <div>
<div class="d-flex justify-content-between align-items-end"> if let Some(title) = &props.title {
// Left side: Title and description <h2 class="mb-1 fw-bold">{title}</h2>
<div class="flex-grow-1"> }
if let Some(title) = &props.title { if let Some(description) = &props.description {
<h2 class="mb-1">{title}</h2> <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">
<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>
} }
if let Some(description) = &props.description {
<p class="text-muted mb-0">{description}</p> // Right side: Actions
if let Some(actions) = &props.actions {
<div>
{actions.clone()}
</div>
} }
</div> </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> </div>
</div> </div>
</div> }
} }
// Tab Content (if tabs are provided) // Tab Content (if tabs are provided)
if let Some(tabs) = &props.tabs { 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)| { {for tabs.iter().map(|(tab_name, content)| {
let is_active = *active_tab == *tab_name; let is_active = *active_tab == *tab_name;
html! { html! {
@ -147,6 +222,7 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
// No tabs, render children directly // No tabs, render children directly
{for props.children.iter()} {for props.children.iter()}
} }
</div>
</div> </div>
} }
} }

View File

@ -254,12 +254,30 @@ pub fn accounting_view(props: &AccountingViewProps) -> Html {
}); });
}, },
ViewContext::Person => { ViewContext::Person => {
// For personal context, show simplified version // Show same functionality as business context
tabs.insert("Income Tracking".to_string(), html! { // Overview Tab
<div class="alert alert-info"> tabs.insert("Overview".to_string(), html! {
<i class="bi bi-info-circle me-2"></i> <OverviewTab state={state.clone()} />
{"Personal accounting features coming soon. Switch to Business context for full accounting functionality."} });
</div>
// 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())} title={Some(title.to_string())}
description={Some(description.to_string())} description={Some(description.to_string())}
tabs={Some(tabs)} tabs={Some(tabs)}
default_tab={match context { default_tab={Some("Overview".to_string())}
ViewContext::Business => Some("Overview".to_string()), use_modern_header={true}
ViewContext::Person => Some("Income Tracking".to_string()),
}}
/> />
} }
} }

View File

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

View File

@ -170,10 +170,11 @@ impl Component for ContractsViewComponent {
html! { html! {
<ViewComponent <ViewComponent
title={title.to_string()} title={Some(title.to_string())}
description={description.to_string()} description={Some(description.to_string())}
tabs={tabs} tabs={Some(tabs)}
default_tab={"Contracts".to_string()} default_tab={Some("Contracts".to_string())}
use_modern_header={true}
/> />
} }
} }
@ -296,11 +297,14 @@ impl ContractsViewComponent {
// Filters Section // Filters Section
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0">{"Filters"}</h5> <div class="d-flex align-items-center mb-3">
</div> <div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
<div class="card-body"> <i class="bi bi-funnel text-primary fs-5"></i>
</div>
<h5 class="mb-0">{"Filters"}</h5>
</div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-3"> <div class="col-md-3">
<label for="status" class="form-label">{"Status"}</label> <label for="status" class="form-label">{"Status"}</label>
@ -344,11 +348,14 @@ impl ContractsViewComponent {
// Contracts Table // Contracts Table
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0">{"Contracts"}</h5> <div class="d-flex align-items-center mb-3">
</div> <div class="bg-success bg-opacity-10 rounded-3 p-2 me-3">
<div class="card-body"> <i class="bi bi-file-earmark-text text-success fs-5"></i>
</div>
<h5 class="mb-0">{"Contracts"}</h5>
</div>
{self.render_contracts_table(_ctx)} {self.render_contracts_table(_ctx)}
</div> </div>
</div> </div>
@ -442,11 +449,14 @@ impl ContractsViewComponent {
html! { html! {
<div class="row"> <div class="row">
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0">{"Contract Details"}</h5> <div class="d-flex align-items-center mb-4">
</div> <div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
<div class="card-body"> <i class="bi bi-file-earmark-plus text-primary fs-5"></i>
</div>
<h5 class="mb-0">{"Contract Details"}</h5>
</div>
<form> <form>
<div class="mb-3"> <div class="mb-3">
<label for="title" class="form-label"> <label for="title" class="form-label">
@ -531,11 +541,14 @@ Payment will be made according to the following schedule:
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0">{"Tips"}</h5> <div class="d-flex align-items-center mb-3">
</div> <div class="bg-info bg-opacity-10 rounded-3 p-2 me-3">
<div class="card-body"> <i class="bi bi-lightbulb text-info fs-5"></i>
</div>
<h5 class="mb-0">{"Tips"}</h5>
</div>
<p>{"Creating a new contract is just the first step. After creating the contract, you'll be able to:"}</p> <p>{"Creating a new contract is just the first step. After creating the contract, you'll be able to:"}</p>
<ul> <ul>
<li>{"Add signers who need to approve the contract"}</li> <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> </div>
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0">{"Contract Templates"}</h5> <div class="d-flex align-items-center mb-3">
</div> <div class="bg-warning bg-opacity-10 rounded-3 p-2 me-3">
<div class="card-body"> <i class="bi bi-file-earmark-code text-warning fs-5"></i>
</div>
<h5 class="mb-0">{"Contract Templates"}</h5>
</div>
<p>{"You can use one of our pre-defined templates to get started quickly:"}</p> <p>{"You can use one of our pre-defined templates to get started quickly:"}</p>
<div class="list-group"> <div class="list-group">
<button type="button" class="list-group-item list-group-item-action"> <button type="button" class="list-group-item list-group-item-action">

View File

@ -1,5 +1,5 @@
use yew::prelude::*; use yew::prelude::*;
use crate::components::FeatureCard; use crate::components::{Inbox, ResidenceCard, ResidenceStatus};
use crate::routing::ViewContext; use crate::routing::ViewContext;
#[derive(Properties, PartialEq)] #[derive(Properties, PartialEq)]
@ -9,74 +9,125 @@ pub struct HomeViewProps {
#[function_component(HomeView)] #[function_component(HomeView)]
pub fn home_view(props: &HomeViewProps) -> Html { 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! { html! {
<div class="row"> <>
<div class="col-md-12"> <style>
<div class="card"> {r#"
<div class="card-body"> .welcome-section {
<h1 class="card-title text-center mb-4">{"Zanzibar Digital Freezone"}</h1> background: linear-gradient(135deg, rgba(0,153,255,0.05) 0%, rgba(0,204,102,0.05) 100%);
<p class="card-text text-center lead mb-5">{"Convenience, Safety and Privacy"}</p> border-radius: 16px;
border: 1px solid rgba(0,153,255,0.1);
<div class="row g-3 mb-4"> }
// Left Column (3 items) .greeting-card {
<div class="col-md-6"> border: 1px solid #e9ecef;
// Card 1: Frictionless Collaboration border-radius: 12px;
<FeatureCard transition: all 0.2s ease;
title="Frictionless Collaboration" }
description="Direct communication and transactions between individuals and organizations, making processes efficient and cost-effective." .greeting-card:hover {
icon="bi-people-fill" border-color: #dee2e6;
color_variant="primary" box-shadow: 0 4px 12px rgba(0,0,0,0.08);
/> }
.time-badge {
// Card 2: Frictionless Banking background: linear-gradient(135deg, #0099FF 0%, #00CC66 100%);
<FeatureCard color: white;
title="Frictionless Banking" padding: 4px 12px;
description="Simplified financial transactions without the complications and fees of traditional banking systems." border-radius: 20px;
icon="bi-currency-exchange" font-size: 0.8rem;
color_variant="success" font-weight: 500;
/> }
.stats-item {
// Card 3: Tax Efficiency text-align: center;
<FeatureCard padding: 1rem;
title="Tax Efficiency" border-radius: 8px;
description="Lower taxes making business operations more profitable and competitive in the global market." background: #f8f9fa;
icon="bi-graph-up-arrow" transition: all 0.2s ease;
color_variant="info" }
/> .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="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>
// Right Column (2 items) // Quick Actions
<div class="col-md-6"> <div class="row g-3 mb-3">
// Card 4: Global Ecommerce <div class="col-4">
<FeatureCard <a href="/companies/register" class="text-decoration-none">
title="Global Ecommerce" <div class="stats-item">
description="Easily expand your business globally with streamlined operations and tools to reach customers worldwide." <i class="bi bi-building-add text-primary mb-2" style="font-size: 1.5rem;"></i>
icon="bi-globe" <div class="stats-label">{"Register Company"}</div>
color_variant="warning" </div>
/> </a>
</div>
// Card 5: Clear Regulations <div class="col-4">
<FeatureCard <a href="/governance" class="text-decoration-none">
title="Clear Regulations" <div class="stats-item">
description="Clear regulations and efficient dispute resolution mechanisms providing a stable business environment." <i class="bi bi-hand-thumbs-up text-success mb-2" style="font-size: 1.5rem;"></i>
icon="bi-shield-check" <div class="stats-label">{"Vote on Proposals"}</div>
color_variant="danger" </div>
/> </a>
</div> </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> </div>
<div class="text-center"> // Inbox Component
<a <Inbox />
href="https://info.ourworld.tf/zdfz" </div>
target="_blank"
class="btn btn-primary btn-lg" // Right Column: Residence Card
> <div class="col-lg-6">
{"Learn More"} <div class="d-flex align-items-center justify-content-center h-100">
</a> <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>
</div> </div>
</div> </div>
</div> </>
} }
} }

View File

@ -273,29 +273,33 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
// Account Settings Tab (Person-specific) // Account Settings Tab (Person-specific)
tabs.insert("Account Settings".to_string(), html! { tabs.insert("Account Settings".to_string(), html! {
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0"> <div class="d-flex align-items-center mb-4">
<i class="bi bi-person-gear me-2"></i> <div class="bg-primary bg-opacity-10 rounded-3 p-3 me-3">
{"Personal Account Settings"} <i class="bi bi-person-gear text-primary fs-4"></i>
</h5> </div>
</div> <div>
<div class="card-body"> <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="row">
<div class="col-md-6 mb-3"> <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" /> <input type="text" class="form-control" value="Timur Gordon" />
</div> </div>
<div class="col-md-6 mb-3"> <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" /> <input type="email" class="form-control" value="john.doe@example.com" />
</div> </div>
<div class="col-md-6 mb-3"> <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" /> <input type="tel" class="form-control" value="+1 (555) 123-4567" />
</div> </div>
<div class="col-md-6 mb-3"> <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"> <select class="form-select">
<option selected=true>{"English"}</option> <option selected=true>{"English"}</option>
<option>{"French"}</option> <option>{"French"}</option>
@ -304,7 +308,7 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
</select> </select>
</div> </div>
<div class="col-12 mb-3"> <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"> <select class="form-select">
<option selected=true>{"UTC+00:00 (GMT)"}</option> <option selected=true>{"UTC+00:00 (GMT)"}</option>
<option>{"UTC-05:00 (EST)"}</option> <option>{"UTC-05:00 (EST)"}</option>
@ -313,7 +317,7 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
</select> </select>
</div> </div>
</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-primary me-2">{"Save Changes"}</button>
<button class="btn btn-outline-secondary">{"Reset"}</button> <button class="btn btn-outline-secondary">{"Reset"}</button>
</div> </div>
@ -323,52 +327,56 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
// Privacy & Security Tab // Privacy & Security Tab
tabs.insert("Privacy & Security".to_string(), html! { tabs.insert("Privacy & Security".to_string(), html! {
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0"> <div class="d-flex align-items-center mb-4">
<i class="bi bi-shield-lock me-2"></i> <div class="bg-success bg-opacity-10 rounded-3 p-3 me-3">
{"Privacy & Security Settings"} <i class="bi bi-shield-lock text-success fs-4"></i>
</h5> </div>
</div> <div>
<div class="card-body"> <h5 class="mb-1">{"Privacy & Security Settings"}</h5>
<div class="mb-4"> <p class="text-muted mb-0">{"Manage your security preferences and privacy controls"}</p>
<h6>{"Two-Factor Authentication"}</h6> </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"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="twoFactorAuth" checked=true /> <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"} {"Enable two-factor authentication"}
</label> </label>
</div> </div>
<small class="text-muted">{"Adds an extra layer of security to your account"}</small> <small class="text-muted">{"Adds an extra layer of security to your account"}</small>
</div> </div>
<div class="mb-4"> <div class="mb-4 p-3 bg-light rounded-3">
<h6>{"Login Notifications"}</h6> <h6 class="fw-medium mb-3">{"Login Notifications"}</h6>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="loginNotifications" checked=true /> <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"} {"Email me when someone logs into my account"}
</label> </label>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4 p-3 bg-light rounded-3">
<h6>{"Data Privacy"}</h6> <h6 class="fw-medium mb-3">{"Data Privacy"}</h6>
<div class="form-check form-switch mb-2"> <div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="dataSharing" /> <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"} {"Allow anonymous usage analytics"}
</label> </label>
</div> </div>
<div class="form-check form-switch"> <div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="marketingEmails" /> <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"} {"Receive marketing communications"}
</label> </label>
</div> </div>
</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-primary me-2">{"Update Security Settings"}</button>
<button class="btn btn-outline-danger">{"Download My Data"}</button> <button class="btn btn-outline-danger">{"Download My Data"}</button>
</div> </div>
@ -385,14 +393,14 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
<div class="row"> <div class="row">
// Subscription Tier Pane // Subscription Tier Pane
<div class="col-lg-4 mb-4"> <div class="col-lg-4 mb-4">
<div class="card h-100"> <div class="card border-0 shadow-sm h-100">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0"> <div class="d-flex align-items-center mb-3">
<i class="bi bi-star me-2"></i> <div class="bg-warning bg-opacity-10 rounded-3 p-2 me-3">
{"Current Plan"} <i class="bi bi-star text-warning fs-5"></i>
</h5> </div>
</div> <h5 class="mb-0">{"Current Plan"}</h5>
<div class="card-body"> </div>
<div class="text-center mb-3"> <div class="text-center mb-3">
<div class="badge bg-primary fs-6 px-3 py-2 mb-2">{&current_plan.name}</div> <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> <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"> <div class="col-lg-8">
// Payments Table Pane // Payments Table Pane
<div class="card mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-header"> <div class="card-body p-4">
<h5 class="mb-0"> <div class="d-flex align-items-center mb-3">
<i class="bi bi-receipt me-2"></i> <div class="bg-info bg-opacity-10 rounded-3 p-2 me-3">
{"Payment History"} <i class="bi bi-receipt text-info fs-5"></i>
</h5> </div>
</div> <h5 class="mb-0">{"Payment History"}</h5>
<div class="card-body"> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
@ -483,26 +491,28 @@ pub fn person_administration_view(props: &PersonAdministrationViewProps) -> Html
</div> </div>
// Payment Methods Pane // Payment Methods Pane
<div class="card"> <div class="card border-0 shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-body p-4">
<h5 class="mb-0"> <div class="d-flex justify-content-between align-items-center mb-3">
<i class="bi bi-credit-card me-2"></i> <div class="d-flex align-items-center">
{"Payment Methods"} <div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
</h5> <i class="bi bi-credit-card text-primary fs-5"></i>
<button </div>
class="btn btn-primary btn-sm" <h5 class="mb-0">{"Payment Methods"}</h5>
onclick={on_add_payment_method.clone()} </div>
disabled={loading_action.as_ref().map_or(false, |action| action == "adding_payment")} <button
> class="btn btn-primary btn-sm"
<i class="bi bi-plus me-1"></i> onclick={on_add_payment_method.clone()}
{if loading_action.as_ref().map_or(false, |action| action == "adding_payment") { disabled={loading_action.as_ref().map_or(false, |action| action == "adding_payment")}
"Adding..." >
} else { <i class="bi bi-plus me-1"></i>
"Add Method" {if loading_action.as_ref().map_or(false, |action| action == "adding_payment") {
}} "Adding..."
</button> } else {
</div> "Add Method"
<div class="card-body"> }}
</button>
</div>
<div class="row"> <div class="row">
{for billing_api.payment_methods.iter().map(|method| html! { {for billing_api.payment_methods.iter().map(|method| html! {
<div class="col-md-6 mb-3"> <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! { html! {
<> <>
<ViewComponent <div class="container-fluid px-3 px-md-4 px-lg-5 px-xl-6">
title={Some("Administration".to_string())} <div class="row">
description={Some("Account settings, billing, integrations".to_string())} <div class="col-12">
tabs={Some(tabs)} <div class="d-flex justify-content-between align-items-center mb-4">
default_tab={Some("Account Settings".to_string())} <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={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 // Plan Selection Modal
if *show_plan_modal { if *show_plan_modal {

View File

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

View File

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