freezone/platform/src/views/administration_view.rs
2025-06-30 14:22:02 +02:00

755 lines
40 KiB
Rust

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