checkpoint
This commit is contained in:
parent
fdbb4b84c3
commit
1c96fa4087
267
platform/src/components/inbox.rs
Normal file
267
platform/src/components/inbox.rs
Normal 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;">
|
||||
{¬ification.title}
|
||||
</h6>
|
||||
<small class="text-muted ms-2 flex-shrink-0" style="font-size: 0.75rem;">
|
||||
{¬ification.timestamp}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<p class="text-muted mb-2 small" style="font-size: 0.8rem; line-height: 1.4;">
|
||||
{¬ification.message}
|
||||
</p>
|
||||
|
||||
if let Some(action_text) = ¬ification.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>
|
||||
</>
|
||||
}
|
||||
}
|
@ -26,7 +26,6 @@ pub fn sidebar(props: &SidebarProps) -> Html {
|
||||
AppView::Home,
|
||||
AppView::Administration,
|
||||
AppView::PersonAdministration,
|
||||
AppView::Residence,
|
||||
AppView::Accounting,
|
||||
AppView::Contracts,
|
||||
AppView::Governance,
|
||||
|
@ -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::*;
|
145
platform/src/components/residence_card.rs
Normal file
145
platform/src/components/residence_card.rs
Normal 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>
|
||||
</>
|
||||
}
|
||||
}
|
@ -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,72 +62,144 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
|
||||
</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 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">
|
||||
<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>
|
||||
|
||||
// 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">
|
||||
<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! {
|
||||
@ -147,6 +222,7 @@ pub fn view_component(props: &ViewComponentProps) -> Html {
|
||||
// No tabs, render children directly
|
||||
{for props.children.iter()}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
}
|
||||
}
|
@ -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,24 +261,27 @@ impl CompaniesView {
|
||||
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 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">{"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>
|
||||
<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">
|
||||
|
@ -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">
|
||||
<h5 class="mb-0">{"Filters"}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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="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">
|
||||
<h5 class="mb-0">{"Contracts"}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
{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">
|
||||
<h5 class="mb-0">{"Contract Details"}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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">
|
||||
<h5 class="mb-0">{"Tips"}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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">
|
||||
<h5 class="mb-0">{"Contract Templates"}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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">
|
||||
|
@ -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 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>
|
||||
|
||||
// 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"
|
||||
/>
|
||||
// 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>
|
||||
|
||||
<div class="text-center">
|
||||
<a
|
||||
href="https://info.ourworld.tf/zdfz"
|
||||
target="_blank"
|
||||
class="btn btn-primary btn-lg"
|
||||
>
|
||||
{"Learn More"}
|
||||
</a>
|
||||
// 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>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
@ -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>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<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>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<h6>{"Two-Factor Authentication"}</h6>
|
||||
<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>
|
||||
<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>
|
||||
<div class="card-body">
|
||||
<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="text-center mb-3">
|
||||
<div class="badge bg-primary fs-6 px-3 py-2 mb-2">{¤t_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>
|
||||
<div class="card-body">
|
||||
<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="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
@ -483,26 +491,28 @@ 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>
|
||||
<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="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()}
|
||||
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="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! {
|
||||
<>
|
||||
<ViewComponent
|
||||
title={Some("Administration".to_string())}
|
||||
description={Some("Account settings, billing, integrations".to_string())}
|
||||
tabs={Some(tabs)}
|
||||
default_tab={Some("Account Settings".to_string())}
|
||||
/>
|
||||
<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={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 {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user