755 lines
40 KiB
Rust
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">{"John Doe"}</div>
|
|
<small class="text-muted">{"Founder & CEO"}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="fw-bold">{"65%"}</span></td>
|
|
<td>{"6,500"}</td>
|
|
<td><span class="badge bg-primary">{"Ordinary"}</span></td>
|
|
<td><span class="badge bg-success">{"Active"}</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-info rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
|
<i class="bi bi-person text-white"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{"Sarah Johnson"}</div>
|
|
<small class="text-muted">{"Co-Founder & CTO"}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="fw-bold">{"25%"}</span></td>
|
|
<td>{"2,500"}</td>
|
|
<td><span class="badge bg-primary">{"Ordinary"}</span></td>
|
|
<td><span class="badge bg-success">{"Active"}</span></td>
|
|
</tr>
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-warning rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
|
<i class="bi bi-building text-dark"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{"Innovation Ventures"}</div>
|
|
<small class="text-muted">{"Investment Fund"}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td><span class="fw-bold">{"10%"}</span></td>
|
|
<td>{"1,000"}</td>
|
|
<td><span class="badge bg-warning text-dark">{"Preferred"}</span></td>
|
|
<td><span class="badge bg-success">{"Active"}</span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="mt-3">
|
|
<small class="text-muted">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
{"Total Authorized Shares: 10,000 | Issued Shares: 10,000 | Par Value: $1.00"}
|
|
</small>
|
|
</div>
|
|
<div class="mt-4">
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-primary">
|
|
<i class="bi bi-person-plus me-1"></i>
|
|
{"Add Shareholder"}
|
|
</button>
|
|
<button class="btn btn-outline-secondary">
|
|
<i class="bi bi-download me-1"></i>
|
|
{"Export Cap Table"}
|
|
</button>
|
|
<button class="btn btn-outline-secondary">
|
|
<i class="bi bi-file-earmark-pdf me-1"></i>
|
|
{"Generate Certificate"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
});
|
|
|
|
// Members & Roles Tab
|
|
tabs.insert("Members & Roles".to_string(), html! {
|
|
<EmptyState
|
|
icon={"person-badge".to_string()}
|
|
title={"No team members found".to_string()}
|
|
description={"Invite team members, assign roles, and control access permissions for your organization.".to_string()}
|
|
primary_action={Some(("Invite Members".to_string(), "#".to_string()))}
|
|
secondary_action={Some(("Manage Roles".to_string(), "#".to_string()))}
|
|
/>
|
|
});
|
|
|
|
// Integrations Tab
|
|
tabs.insert("Integrations".to_string(), html! {
|
|
<EmptyState
|
|
icon={"diagram-3".to_string()}
|
|
title={"No integrations configured".to_string()}
|
|
description={"Connect with external services and configure API integrations to streamline your workflow.".to_string()}
|
|
primary_action={Some(("Browse Integrations".to_string(), "#".to_string()))}
|
|
secondary_action={Some(("API Documentation".to_string(), "#".to_string()))}
|
|
/>
|
|
});
|
|
|
|
// Billing and Payments Tab
|
|
tabs.insert("Billing and Payments".to_string(), {
|
|
let current_subscription = &billing_api.current_subscription;
|
|
let current_plan = ¤t_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">{¤t_plan.name}</div>
|
|
<h3 class="text-primary mb-0">{format!("${:.0}", current_plan.price)}<small class="text-muted">{"/month"}</small></h3>
|
|
</div>
|
|
<ul class="list-unstyled">
|
|
{for current_plan.features.iter().map(|feature| html! {
|
|
<li class="mb-2">
|
|
<i class="bi bi-check-circle text-success me-2"></i>
|
|
{feature}
|
|
</li>
|
|
})}
|
|
</ul>
|
|
<div class="mt-3">
|
|
<small class="text-muted">{format!("Status: {}", current_subscription.status)}</small>
|
|
</div>
|
|
<div class="mt-3 d-grid gap-2">
|
|
<button
|
|
class="btn btn-outline-primary btn-sm"
|
|
onclick={on_change_plan.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == "changing_plan")}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == "changing_plan") {
|
|
"Changing..."
|
|
} else {
|
|
"Change Plan"
|
|
}}
|
|
</button>
|
|
<button
|
|
class="btn btn-outline-secondary btn-sm"
|
|
onclick={on_cancel_subscription.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == "canceling")}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == "canceling") {
|
|
"Canceling..."
|
|
} else {
|
|
"Cancel Subscription"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-8">
|
|
// Payments Table Pane
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-receipt me-2"></i>
|
|
{"Payment History"}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>{"Date"}</th>
|
|
<th>{"Description"}</th>
|
|
<th>{"Amount"}</th>
|
|
<th>{"Status"}</th>
|
|
<th>{"Invoice"}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{for billing_api.invoices.iter().map(|invoice| html! {
|
|
<tr>
|
|
<td>{&invoice.date}</td>
|
|
<td>{&invoice.description}</td>
|
|
<td>{format!("${:.2}", invoice.amount)}</td>
|
|
<td><span class="badge bg-success">{&invoice.status}</span></td>
|
|
<td>
|
|
<button
|
|
class="btn btn-outline-secondary btn-sm"
|
|
onclick={on_download_invoice.clone()}
|
|
data-invoice-id={invoice.id.clone()}
|
|
disabled={downloading_invoice.as_ref().map_or(false, |id| id == &invoice.id)}
|
|
>
|
|
<i class={if downloading_invoice.as_ref().map_or(false, |id| id == &invoice.id) { "bi bi-arrow-repeat" } else { "bi bi-download" }}></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
// Payment Methods Pane
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-credit-card me-2"></i>
|
|
{"Payment Methods"}
|
|
</h5>
|
|
<button
|
|
class="btn btn-primary btn-sm"
|
|
onclick={on_add_payment_method.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == "adding_payment")}
|
|
>
|
|
<i class="bi bi-plus me-1"></i>
|
|
{if loading_action.as_ref().map_or(false, |action| action == "adding_payment") {
|
|
"Adding..."
|
|
} else {
|
|
"Add Method"
|
|
}}
|
|
</button>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
{for billing_api.payment_methods.iter().map(|method| html! {
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card border">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div class="d-flex align-items-center">
|
|
<div class={format!("bg-{} rounded me-3 d-flex align-items-center justify-content-center",
|
|
if method.method_type == "card" { "primary" } else { "info" })}
|
|
style="width: 40px; height: 25px;">
|
|
<i class={format!("bi bi-{} text-white",
|
|
if method.method_type == "card" { "credit-card" } else { "bank" })}></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{&method.last_four}</div>
|
|
<small class="text-muted">{&method.expires}</small>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<span class={format!("badge bg-{}",
|
|
if method.is_primary { "success" } else { "secondary" })}>
|
|
{if method.is_primary { "Primary" } else { "Backup" }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<button
|
|
class="btn btn-outline-secondary btn-sm me-2"
|
|
onclick={on_edit_payment_method.clone()}
|
|
data-method={method.id.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == &format!("editing_{}", method.id))}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == &format!("editing_{}", method.id)) {
|
|
"Editing..."
|
|
} else {
|
|
"Edit"
|
|
}}
|
|
</button>
|
|
<button
|
|
class="btn btn-outline-danger btn-sm"
|
|
onclick={on_remove_payment_method.clone()}
|
|
data-method={method.id.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == &format!("removing_{}", method.id))}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == &format!("removing_{}", method.id)) {
|
|
"Removing..."
|
|
} else {
|
|
"Remove"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
});
|
|
|
|
html! {
|
|
<>
|
|
<ViewComponent
|
|
title={Some("Administration".to_string())}
|
|
description={Some("Org setup, members, roles, integrations".to_string())}
|
|
tabs={Some(tabs)}
|
|
default_tab={Some("Organization Setup".to_string())}
|
|
/>
|
|
|
|
// Plan Selection Modal
|
|
if *show_plan_modal {
|
|
<div class="modal fade show" style="display: block;" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{"Change Plan"}</h5>
|
|
<button type="button" class="btn-close" onclick={close_modals.clone()}></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
{for billing_api.available_plans.iter().map(|plan| html! {
|
|
<div class="col-md-4 mb-3">
|
|
<div class={format!("card h-100 {}",
|
|
if selected_plan.as_ref().map_or(false, |id| id == &plan.id) { "border-primary" } else { "" })}>
|
|
<div class="card-body text-center">
|
|
<h5 class="card-title">{&plan.name}</h5>
|
|
<h3 class="text-primary">{format!("${:.0}", plan.price)}<small class="text-muted">{"/month"}</small></h3>
|
|
<ul class="list-unstyled mt-3">
|
|
{for plan.features.iter().map(|feature| html! {
|
|
<li class="mb-1">
|
|
<i class="bi bi-check text-success me-1"></i>
|
|
{feature}
|
|
</li>
|
|
})}
|
|
</ul>
|
|
<button
|
|
class={format!("btn btn-{} w-100",
|
|
if selected_plan.as_ref().map_or(false, |id| id == &plan.id) { "primary" } else { "outline-primary" })}
|
|
onclick={on_select_plan.clone()}
|
|
data-plan-id={plan.id.clone()}
|
|
>
|
|
{if selected_plan.as_ref().map_or(false, |id| id == &plan.id) { "Selected" } else { "Select" }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick={close_modals.clone()}>{"Cancel"}</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick={on_confirm_plan_change.clone()}
|
|
disabled={selected_plan.is_none() || loading_action.as_ref().map_or(false, |action| action == "changing_plan")}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == "changing_plan") {
|
|
"Changing..."
|
|
} else {
|
|
"Change Plan"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// Cancel Subscription Modal
|
|
if *show_cancel_modal {
|
|
<div class="modal fade show" style="display: block;" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{"Cancel Subscription"}</h5>
|
|
<button type="button" class="btn-close" onclick={close_modals.clone()}></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>{"Are you sure you want to cancel your subscription? This action cannot be undone."}</p>
|
|
<div class="alert alert-warning">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
{"Your subscription will remain active until the end of the current billing period."}
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick={close_modals.clone()}>{"Keep Subscription"}</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-danger"
|
|
onclick={on_confirm_cancel_subscription.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == "canceling")}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == "canceling") {
|
|
"Canceling..."
|
|
} else {
|
|
"Cancel Subscription"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// Add Payment Method Modal
|
|
if *show_add_payment_modal {
|
|
<div class="modal fade show" style="display: block;" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{"Add Payment Method"}</h5>
|
|
<button type="button" class="btn-close" onclick={close_modals.clone()}></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form>
|
|
<div class="mb-3">
|
|
<label class="form-label">{"Card Number"}</label>
|
|
<input type="text" class="form-control" placeholder="1234 5678 9012 3456" />
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">{"Expiry Date"}</label>
|
|
<input type="text" class="form-control" placeholder="MM/YY" />
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">{"CVC"}</label>
|
|
<input type="text" class="form-control" placeholder="123" />
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">{"Cardholder Name"}</label>
|
|
<input type="text" class="form-control" placeholder="John Doe" />
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick={close_modals.clone()}>{"Cancel"}</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary"
|
|
onclick={on_confirm_add_payment_method.clone()}
|
|
disabled={loading_action.as_ref().map_or(false, |action| action == "adding_payment")}
|
|
>
|
|
{if loading_action.as_ref().map_or(false, |action| action == "adding_payment") {
|
|
"Adding..."
|
|
} else {
|
|
"Add Payment Method"
|
|
}}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</>
|
|
}
|
|
} |