207 lines
13 KiB
Rust
207 lines
13 KiB
Rust
use yew::prelude::*;
|
|
use crate::components::accounting::models::*;
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct OverviewTabProps {
|
|
pub state: UseStateHandle<AccountingState>,
|
|
}
|
|
|
|
#[function_component(OverviewTab)]
|
|
pub fn overview_tab(props: &OverviewTabProps) -> Html {
|
|
let state = &props.state;
|
|
|
|
// Calculate totals
|
|
let total_revenue: f64 = state.revenue_entries.iter().map(|r| r.total_amount).sum();
|
|
let total_expenses: f64 = state.expense_entries.iter().map(|e| e.total_amount).sum();
|
|
let net_profit = total_revenue - total_expenses;
|
|
let pending_revenue: f64 = state.revenue_entries.iter()
|
|
.filter(|r| r.payment_status == PaymentStatus::Pending)
|
|
.map(|r| r.total_amount)
|
|
.sum();
|
|
let pending_expenses: f64 = state.expense_entries.iter()
|
|
.filter(|e| e.payment_status == PaymentStatus::Pending)
|
|
.map(|e| e.total_amount)
|
|
.sum();
|
|
|
|
html! {
|
|
<div class="animate-fade-in-up">
|
|
// Key Statistics Cards
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card border-warning shadow-soft card-hover">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h6 class="text-warning mb-1">{"Pending Items"}</h6>
|
|
<h3 class="mb-0 fw-bold text-dark">{format!("${:.2}", pending_revenue + pending_expenses)}</h3>
|
|
</div>
|
|
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
|
|
<i class="bi bi-clock text-warning fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<small class="text-muted">{format!("${:.2} revenue, ${:.2} expenses", pending_revenue, pending_expenses)}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-info shadow-soft card-hover">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h6 class="text-info mb-1">{"Avg Invoice Value"}</h6>
|
|
<h3 class="mb-0 fw-bold text-dark">{format!("${:.2}", total_revenue / state.revenue_entries.len() as f64)}</h3>
|
|
</div>
|
|
<div class="bg-info bg-opacity-10 rounded-circle p-3">
|
|
<i class="bi bi-receipt text-info fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<small class="text-muted">{format!("{} invoices total", state.revenue_entries.len())}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-success shadow-soft card-hover">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h6 class="text-success mb-1">{"Tax Deductible"}</h6>
|
|
<h3 class="mb-0 fw-bold text-dark">{format!("${:.2}", state.expense_entries.iter().filter(|e| e.is_deductible).map(|e| e.total_amount).sum::<f64>())}</h3>
|
|
</div>
|
|
<div class="bg-success bg-opacity-10 rounded-circle p-3">
|
|
<i class="bi bi-receipt-cutoff text-success fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<small class="text-muted">{"100% of expenses deductible"}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<div class="card border-primary shadow-soft card-hover">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h6 class="text-primary mb-1">{"Collection Rate"}</h6>
|
|
<h3 class="mb-0 fw-bold text-dark">{"85.2%"}</h3>
|
|
</div>
|
|
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
|
|
<i class="bi bi-percent text-primary fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<small class="text-muted">{"Above industry avg"}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
// Recent Transactions
|
|
<div class="row g-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-soft border-0">
|
|
<div class="card-header bg-white border-bottom-0 py-3">
|
|
<h5 class="mb-0 fw-bold">{"Recent Transactions"}</h5>
|
|
<small class="text-muted">{"Latest payments made and received"}</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{if state.payment_transactions.is_empty() {
|
|
html! {
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-credit-card fs-1 text-muted mb-3 d-block"></i>
|
|
<h6 class="text-muted">{"No transactions recorded yet"}</h6>
|
|
<p class="text-muted mb-0">{"Transactions will appear here once you record payments"}</p>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="border-0 py-3 px-4">{"Date"}</th>
|
|
<th class="border-0 py-3">{"Type"}</th>
|
|
<th class="border-0 py-3">{"Reference"}</th>
|
|
<th class="border-0 py-3">{"Amount"}</th>
|
|
<th class="border-0 py-3">{"Method"}</th>
|
|
<th class="border-0 py-3">{"Status"}</th>
|
|
<th class="border-0 py-3">{"Notes"}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{for state.payment_transactions.iter().take(10).map(|transaction| {
|
|
let (transaction_type, reference, amount_color) = if let Some(invoice_id) = &transaction.invoice_id {
|
|
("Revenue", invoice_id.clone(), "text-success")
|
|
} else if let Some(expense_id) = &transaction.expense_id {
|
|
("Expense", expense_id.clone(), "text-danger")
|
|
} else {
|
|
("Unknown", "N/A".to_string(), "text-muted")
|
|
};
|
|
|
|
html! {
|
|
<tr>
|
|
<td class="py-3 px-4">
|
|
<div class="fw-semibold">{&transaction.date}</div>
|
|
<small class="text-muted">{&transaction.id}</small>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class={format!("badge bg-{} bg-opacity-10 text-{}",
|
|
if transaction_type == "Revenue" { "success" } else { "danger" },
|
|
if transaction_type == "Revenue" { "success" } else { "danger" }
|
|
)}>
|
|
{transaction_type}
|
|
</span>
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="fw-semibold">{&reference}</div>
|
|
{if let Some(hash) = &transaction.transaction_hash {
|
|
html! { <small class="text-muted"><code>{&hash[..12]}{"..."}</code></small> }
|
|
} else if let Some(ref_num) = &transaction.reference_number {
|
|
html! { <small class="text-muted">{ref_num}</small> }
|
|
} else {
|
|
html! {}
|
|
}}
|
|
</td>
|
|
<td class="py-3">
|
|
<div class={format!("fw-bold {}", amount_color)}>
|
|
{if transaction_type == "Revenue" { "+" } else { "-" }}
|
|
{format!("${:.2}", transaction.amount)}
|
|
</div>
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="d-flex align-items-center">
|
|
<i class={format!("bi bi-{} text-{} me-2", transaction.payment_method.get_icon(), transaction.payment_method.get_color())}></i>
|
|
<span class="small">{transaction.payment_method.to_string()}</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class={format!("badge bg-{}", transaction.status.get_color())}>
|
|
{transaction.status.to_string()}
|
|
</span>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class="text-muted">{&transaction.notes}</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
} |