801 lines
65 KiB
Rust
801 lines
65 KiB
Rust
use yew::prelude::*;
|
|
use wasm_bindgen::JsCast;
|
|
use crate::components::accounting::models::*;
|
|
use js_sys;
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct ExpensesTabProps {
|
|
pub state: UseStateHandle<AccountingState>,
|
|
}
|
|
|
|
#[function_component(ExpensesTab)]
|
|
pub fn expenses_tab(props: &ExpensesTabProps) -> Html {
|
|
let state = &props.state;
|
|
|
|
html! {
|
|
<div class="animate-fade-in-up">
|
|
// Expense Form Modal
|
|
{if state.show_expense_form {
|
|
html! {
|
|
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{"Add New Expense"}</h5>
|
|
<button type="button" class="btn-close" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_form = false;
|
|
state.set(new_state);
|
|
})
|
|
}></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Receipt Number"}</label>
|
|
<input type="text" class="form-control" value={state.expense_form.receipt_number.clone()} readonly=true />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Date"}</label>
|
|
<input type="date" class="form-control" value={state.expense_form.date.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.date = input.value();
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Vendor Name"}</label>
|
|
<input type="text" class="form-control" value={state.expense_form.vendor_name.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.vendor_name = input.value();
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Vendor Email"}</label>
|
|
<input type="email" class="form-control" value={state.expense_form.vendor_email.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.vendor_email = input.value();
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">{"Description"}</label>
|
|
<textarea class="form-control" rows="3" value={state.expense_form.description.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.description = input.value();
|
|
state.set(new_state);
|
|
})
|
|
}></textarea>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Amount"}</label>
|
|
<input type="number" step="0.01" class="form-control" value={state.expense_form.amount.to_string()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.amount = input.value().parse().unwrap_or(0.0);
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Tax Amount"}</label>
|
|
<input type="number" step="0.01" class="form-control" value={state.expense_form.tax_amount.to_string()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.tax_amount = input.value().parse().unwrap_or(0.0);
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Tax Deductible"}</label>
|
|
<select class="form-select" onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let select: web_sys::HtmlSelectElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_form.is_deductible = select.value() == "true";
|
|
state.set(new_state);
|
|
})
|
|
}>
|
|
<option value="true" selected={state.expense_form.is_deductible}>{"Yes"}</option>
|
|
<option value="false" selected={!state.expense_form.is_deductible}>{"No"}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Project Code (Optional)"}</label>
|
|
<input type="text" class="form-control" value={state.expense_form.project_code.clone().unwrap_or_default()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
let value = input.value();
|
|
new_state.expense_form.project_code = if value.is_empty() { None } else { Some(value) };
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_form = false;
|
|
state.set(new_state);
|
|
})
|
|
}>{"Cancel"}</button>
|
|
<button type="button" class="btn btn-danger" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
// Calculate total
|
|
new_state.expense_form.total_amount = new_state.expense_form.amount + new_state.expense_form.tax_amount;
|
|
|
|
// Add to entries
|
|
new_state.expense_entries.push(new_state.expense_form.clone());
|
|
|
|
// Reset form
|
|
new_state.show_expense_form = false;
|
|
new_state.expense_form = AccountingState::default().expense_form;
|
|
state.set(new_state);
|
|
})
|
|
}>{"Add Expense"}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}}
|
|
|
|
// Expense Detail Modal
|
|
{if state.show_expense_detail {
|
|
if let Some(expense_id) = &state.selected_expense_id {
|
|
if let Some(expense) = state.expense_entries.iter().find(|e| &e.id == expense_id) {
|
|
let expense_transactions: Vec<&PaymentTransaction> = state.payment_transactions.iter()
|
|
.filter(|t| t.expense_id.as_ref() == Some(expense_id))
|
|
.collect();
|
|
let total_paid: f64 = expense_transactions.iter().map(|t| t.amount).sum();
|
|
let remaining_balance = expense.total_amount - total_paid;
|
|
|
|
html! {
|
|
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{format!("Expense Details - {}", expense.receipt_number)}</h5>
|
|
<button type="button" class="btn-close" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_detail = false;
|
|
new_state.selected_expense_id = None;
|
|
state.set(new_state);
|
|
})
|
|
}></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row g-4">
|
|
// Expense Information
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">{"Expense Information"}</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-2">
|
|
<div class="col-6"><strong>{"Receipt #:"}</strong></div>
|
|
<div class="col-6">{&expense.receipt_number}</div>
|
|
<div class="col-6"><strong>{"Date:"}</strong></div>
|
|
<div class="col-6">{&expense.date}</div>
|
|
<div class="col-6"><strong>{"Category:"}</strong></div>
|
|
<div class="col-6">{expense.category.to_string()}</div>
|
|
<div class="col-6"><strong>{"Status:"}</strong></div>
|
|
<div class="col-6">
|
|
<span class={format!("badge bg-{}", expense.payment_status.get_color())}>
|
|
{expense.payment_status.to_string()}
|
|
</span>
|
|
</div>
|
|
<div class="col-6"><strong>{"Total Amount:"}</strong></div>
|
|
<div class="col-6 fw-bold text-danger">{format!("${:.2}", expense.total_amount)}</div>
|
|
<div class="col-6"><strong>{"Amount Paid:"}</strong></div>
|
|
<div class="col-6 fw-bold text-primary">{format!("${:.2}", total_paid)}</div>
|
|
<div class="col-6"><strong>{"Remaining:"}</strong></div>
|
|
<div class="col-6 fw-bold text-warning">{format!("${:.2}", remaining_balance)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
// Vendor Information
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h6 class="mb-0">{"Vendor Information"}</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-2">
|
|
<div class="col-4"><strong>{"Name:"}</strong></div>
|
|
<div class="col-8">{&expense.vendor_name}</div>
|
|
<div class="col-4"><strong>{"Email:"}</strong></div>
|
|
<div class="col-8">{&expense.vendor_email}</div>
|
|
<div class="col-4"><strong>{"Address:"}</strong></div>
|
|
<div class="col-8">{&expense.vendor_address}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
// Payment Transactions
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">{"Payment Transactions"}</h6>
|
|
<button class="btn btn-sm btn-primary" onclick={
|
|
let state = state.clone();
|
|
let expense_id = expense.id.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_transaction_form = true;
|
|
new_state.transaction_form.expense_id = Some(expense_id.clone());
|
|
state.set(new_state);
|
|
})
|
|
}>
|
|
<i class="bi bi-plus-circle me-1"></i>{"Record Payment"}
|
|
</button>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{if expense_transactions.is_empty() {
|
|
html! {
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="bi bi-credit-card fs-1 mb-2 d-block"></i>
|
|
<p class="mb-0">{"No payments recorded yet"}</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">{"Date"}</th>
|
|
<th class="border-0 py-3">{"Amount"}</th>
|
|
<th class="border-0 py-3">{"Method"}</th>
|
|
<th class="border-0 py-3">{"Reference"}</th>
|
|
<th class="border-0 py-3">{"Status"}</th>
|
|
<th class="border-0 py-3">{"Notes"}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{for expense_transactions.iter().map(|transaction| {
|
|
html! {
|
|
<tr>
|
|
<td class="py-3">{&transaction.date}</td>
|
|
<td class="py-3 fw-bold text-danger">{format!("${:.2}", transaction.amount)}</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>
|
|
{transaction.payment_method.to_string()}
|
|
</div>
|
|
</td>
|
|
<td class="py-3">
|
|
{if let Some(hash) = &transaction.transaction_hash {
|
|
html! { <code class="small">{&hash[..12]}{"..."}</code> }
|
|
} else if let Some(ref_num) = &transaction.reference_number {
|
|
html! { <span>{ref_num}</span> }
|
|
} else {
|
|
html! { <span class="text-muted">{"-"}</span> }
|
|
}}
|
|
</td>
|
|
<td class="py-3">
|
|
<span class={format!("badge bg-{}", transaction.status.get_color())}>
|
|
{transaction.status.to_string()}
|
|
</span>
|
|
</td>
|
|
<td class="py-3">{&transaction.notes}</td>
|
|
</tr>
|
|
}
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_detail = false;
|
|
new_state.selected_expense_id = None;
|
|
state.set(new_state);
|
|
})
|
|
}>{"Close"}</button>
|
|
<button type="button" class="btn btn-primary" onclick={
|
|
let state = state.clone();
|
|
let expense_id = expense.id.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_transaction_form = true;
|
|
new_state.transaction_form.expense_id = Some(expense_id.clone());
|
|
state.set(new_state);
|
|
})
|
|
}>
|
|
<i class="bi bi-credit-card me-2"></i>{"Record Payment"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}
|
|
} else {
|
|
html! {}
|
|
}
|
|
} else {
|
|
html! {}
|
|
}}
|
|
|
|
// Transaction Form Modal (for expense payments)
|
|
{if state.show_transaction_form && state.transaction_form.expense_id.is_some() {
|
|
html! {
|
|
<div class="modal fade show d-block" style="background-color: rgba(0,0,0,0.5);">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">{"Record Expense Payment"}</h5>
|
|
<button type="button" class="btn-close" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_transaction_form = false;
|
|
state.set(new_state);
|
|
})
|
|
}></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form>
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Expense Receipt Number"}</label>
|
|
<input type="text" class="form-control" value={state.transaction_form.expense_id.clone().unwrap_or_default()} readonly=true />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">{"Payment Amount"}</label>
|
|
<input type="number" step="0.01" class="form-control" value={state.transaction_form.amount.to_string()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.transaction_form.amount = input.value().parse().unwrap_or(0.0);
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">{"Payment Method"}</label>
|
|
<select class="form-select" onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let select: web_sys::HtmlSelectElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.transaction_form.payment_method = match select.value().as_str() {
|
|
"BankTransfer" => PaymentMethod::BankTransfer,
|
|
"CreditCard" => PaymentMethod::CreditCard,
|
|
"CryptoBitcoin" => PaymentMethod::CryptoBitcoin,
|
|
"CryptoEthereum" => PaymentMethod::CryptoEthereum,
|
|
"CryptoUSDC" => PaymentMethod::CryptoUSDC,
|
|
"Cash" => PaymentMethod::Cash,
|
|
"Check" => PaymentMethod::Check,
|
|
_ => PaymentMethod::BankTransfer,
|
|
};
|
|
state.set(new_state);
|
|
})
|
|
}>
|
|
<option value="BankTransfer" selected={matches!(state.transaction_form.payment_method, PaymentMethod::BankTransfer)}>{"Bank Transfer"}</option>
|
|
<option value="CreditCard" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CreditCard)}>{"Credit Card"}</option>
|
|
<option value="CryptoBitcoin" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoBitcoin)}>{"Bitcoin"}</option>
|
|
<option value="CryptoEthereum" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoEthereum)}>{"Ethereum"}</option>
|
|
<option value="CryptoUSDC" selected={matches!(state.transaction_form.payment_method, PaymentMethod::CryptoUSDC)}>{"USDC"}</option>
|
|
<option value="Cash" selected={matches!(state.transaction_form.payment_method, PaymentMethod::Cash)}>{"Cash"}</option>
|
|
<option value="Check" selected={matches!(state.transaction_form.payment_method, PaymentMethod::Check)}>{"Check"}</option>
|
|
</select>
|
|
</div>
|
|
{if matches!(state.transaction_form.payment_method, PaymentMethod::CryptoBitcoin | PaymentMethod::CryptoEthereum | PaymentMethod::CryptoUSDC | PaymentMethod::CryptoOther) {
|
|
html! {
|
|
<div class="col-12">
|
|
<label class="form-label">{"Transaction Hash"}</label>
|
|
<input type="text" class="form-control" placeholder="0x..." value={state.transaction_form.transaction_hash.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.transaction_form.transaction_hash = input.value();
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {
|
|
<div class="col-12">
|
|
<label class="form-label">{"Reference Number"}</label>
|
|
<input type="text" class="form-control" placeholder="REF-2024-001" value={state.transaction_form.reference_number.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.transaction_form.reference_number = input.value();
|
|
state.set(new_state);
|
|
})
|
|
} />
|
|
</div>
|
|
}
|
|
}}
|
|
<div class="col-12">
|
|
<label class="form-label">{"Notes"}</label>
|
|
<textarea class="form-control" rows="3" value={state.transaction_form.notes.clone()} onchange={
|
|
let state = state.clone();
|
|
Callback::from(move |e: Event| {
|
|
let input: web_sys::HtmlInputElement = e.target_unchecked_into();
|
|
let mut new_state = (*state).clone();
|
|
new_state.transaction_form.notes = input.value();
|
|
state.set(new_state);
|
|
})
|
|
}></textarea>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label">{"Attach Files"}</label>
|
|
<input type="file" class="form-control" multiple=true accept=".pdf,.jpg,.jpeg,.png" />
|
|
<small class="text-muted">{"Upload receipts, confirmations, or other supporting documents"}</small>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_transaction_form = false;
|
|
state.set(new_state);
|
|
})
|
|
}>{"Cancel"}</button>
|
|
<button type="button" class="btn btn-success" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
|
|
// Create new transaction
|
|
let transaction_count = new_state.payment_transactions.len() + 1;
|
|
let new_transaction = PaymentTransaction {
|
|
id: format!("TXN-2024-{:03}", transaction_count),
|
|
invoice_id: None,
|
|
expense_id: new_state.transaction_form.expense_id.clone(),
|
|
date: js_sys::Date::new_0().to_iso_string().as_string().unwrap()[..10].to_string(),
|
|
amount: new_state.transaction_form.amount,
|
|
payment_method: new_state.transaction_form.payment_method.clone(),
|
|
transaction_hash: if new_state.transaction_form.transaction_hash.is_empty() { None } else { Some(new_state.transaction_form.transaction_hash.clone()) },
|
|
reference_number: if new_state.transaction_form.reference_number.is_empty() { None } else { Some(new_state.transaction_form.reference_number.clone()) },
|
|
notes: new_state.transaction_form.notes.clone(),
|
|
attached_files: new_state.transaction_form.attached_files.clone(),
|
|
status: TransactionStatus::Confirmed,
|
|
};
|
|
|
|
new_state.payment_transactions.push(new_transaction);
|
|
new_state.show_transaction_form = false;
|
|
new_state.transaction_form = TransactionForm::default();
|
|
state.set(new_state);
|
|
})
|
|
}>{"Record Payment"}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}}
|
|
|
|
// Expense Actions and Table
|
|
<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">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h5 class="mb-0 fw-bold">{"Expense Entries"}</h5>
|
|
<small class="text-muted">{"Click on any row to view details"}</small>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-outline-primary btn-sm" onclick={
|
|
Callback::from(move |_| {
|
|
web_sys::window()
|
|
.unwrap()
|
|
.alert_with_message("Expense filter feature coming soon!")
|
|
.unwrap();
|
|
})
|
|
}>
|
|
<i class="bi bi-funnel me-2"></i>{"Filter"}
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick={
|
|
let expense_entries = state.expense_entries.clone();
|
|
Callback::from(move |_| {
|
|
// Create CSV content
|
|
let mut csv_content = "Receipt Number,Date,Vendor Name,Vendor Email,Description,Amount,Tax Amount,Total Amount,Category,Payment Method,Payment Status,Tax Deductible,Approval Status,Approved By,Notes,Project Code,Currency\n".to_string();
|
|
|
|
for entry in &expense_entries {
|
|
csv_content.push_str(&format!(
|
|
"{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n",
|
|
entry.receipt_number,
|
|
entry.date,
|
|
entry.vendor_name,
|
|
entry.vendor_email,
|
|
entry.description.replace(",", ";"),
|
|
entry.amount,
|
|
entry.tax_amount,
|
|
entry.total_amount,
|
|
entry.category.to_string(),
|
|
entry.payment_method.to_string(),
|
|
entry.payment_status.to_string(),
|
|
entry.is_deductible,
|
|
entry.approval_status.to_string(),
|
|
entry.approved_by.as_ref().unwrap_or(&"".to_string()),
|
|
entry.notes.replace(",", ";"),
|
|
entry.project_code.as_ref().unwrap_or(&"".to_string()),
|
|
entry.currency
|
|
));
|
|
}
|
|
|
|
// Create and download file
|
|
let window = web_sys::window().unwrap();
|
|
let document = window.document().unwrap();
|
|
let element = document.create_element("a").unwrap();
|
|
element.set_attribute("href", &format!("data:text/csv;charset=utf-8,{}", js_sys::encode_uri_component(&csv_content))).unwrap();
|
|
element.set_attribute("download", "expenses_export.csv").unwrap();
|
|
element.set_attribute("style", "display: none").unwrap();
|
|
document.body().unwrap().append_child(&element).unwrap();
|
|
let html_element: web_sys::HtmlElement = element.clone().dyn_into().unwrap();
|
|
html_element.click();
|
|
document.body().unwrap().remove_child(&element).unwrap();
|
|
})
|
|
}>
|
|
<i class="bi bi-download me-2"></i>{"Export"}
|
|
</button>
|
|
<button class="btn btn-danger btn-sm" onclick={
|
|
let state = state.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_form = true;
|
|
let expense_count = new_state.expense_entries.len() + 1;
|
|
new_state.expense_form.receipt_number = format!("EXP-2024-{:03}", expense_count);
|
|
new_state.expense_form.id = new_state.expense_form.receipt_number.clone();
|
|
state.set(new_state);
|
|
})
|
|
}>
|
|
<i class="bi bi-plus-circle me-2"></i>{"Add Expense"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="border-0 py-3 px-4">{"Receipt #"}</th>
|
|
<th class="border-0 py-3">{"Vendor"}</th>
|
|
<th class="border-0 py-3">{"Description"}</th>
|
|
<th class="border-0 py-3">{"Amount"}</th>
|
|
<th class="border-0 py-3">{"Payment Method"}</th>
|
|
<th class="border-0 py-3">{"Status"}</th>
|
|
<th class="border-0 py-3">{"Approval"}</th>
|
|
<th class="border-0 py-3">{"Actions"}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{for state.expense_entries.iter().map(|entry| {
|
|
html! {
|
|
<tr class="border-bottom">
|
|
<td class="py-3 px-4 cursor-pointer" style="cursor: pointer;" onclick={
|
|
let state = state.clone();
|
|
let expense_id = entry.id.clone();
|
|
Callback::from(move |_| {
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_detail = true;
|
|
new_state.selected_expense_id = Some(expense_id.clone());
|
|
state.set(new_state);
|
|
})
|
|
}>
|
|
<div class="fw-bold text-primary">{&entry.receipt_number}</div>
|
|
<small class="text-muted">{&entry.date}</small>
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="fw-semibold">{&entry.vendor_name}</div>
|
|
<small class="text-muted">{&entry.vendor_email}</small>
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="fw-semibold">{&entry.description}</div>
|
|
<small class="text-muted">
|
|
<span class={format!("badge bg-{} bg-opacity-10 text-{} me-1", entry.category.get_color(), entry.category.get_color())}>
|
|
{entry.category.to_string()}
|
|
</span>
|
|
{if entry.is_deductible { "• Tax Deductible" } else { "" }}
|
|
{if let Some(project) = &entry.project_code {
|
|
html! { <span class="ms-1">{format!("• {}", project)}</span> }
|
|
} else {
|
|
html! {}
|
|
}}
|
|
</small>
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="fw-bold text-danger">{format!("${:.2}", entry.total_amount)}</div>
|
|
<small class="text-muted">{format!("${:.2} + ${:.2} tax", entry.amount, entry.tax_amount)}</small>
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="d-flex align-items-center">
|
|
<i class={format!("bi bi-{} text-{} me-2", entry.payment_method.get_icon(), entry.payment_method.get_color())}></i>
|
|
<span class="small">{entry.payment_method.to_string()}</span>
|
|
</div>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class={format!("badge bg-{} bg-opacity-10 text-{}", entry.payment_status.get_color(), entry.payment_status.get_color())}>
|
|
{entry.payment_status.to_string()}
|
|
</span>
|
|
</td>
|
|
<td class="py-3">
|
|
<span class={format!("badge bg-{} bg-opacity-10 text-{}", entry.approval_status.get_color(), entry.approval_status.get_color())}>
|
|
{entry.approval_status.to_string()}
|
|
</span>
|
|
{
|
|
if let Some(approver) = &entry.approved_by {
|
|
html! { <small class="d-block text-muted">{format!("by {}", approver)}</small> }
|
|
} else {
|
|
html! {}
|
|
}
|
|
}
|
|
</td>
|
|
<td class="py-3">
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots-vertical"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick={
|
|
let state = state.clone();
|
|
let expense_id = entry.id.clone();
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_expense_detail = true;
|
|
new_state.selected_expense_id = Some(expense_id.clone());
|
|
state.set(new_state);
|
|
})
|
|
}><i class="bi bi-eye me-2"></i>{"View Details"}</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick={
|
|
let state = state.clone();
|
|
let expense_id = entry.id.clone();
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
let mut new_state = (*state).clone();
|
|
new_state.show_transaction_form = true;
|
|
new_state.transaction_form.expense_id = Some(expense_id.clone());
|
|
state.set(new_state);
|
|
})
|
|
}><i class="bi bi-credit-card me-2"></i>{"Record Payment"}</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick={
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
web_sys::window()
|
|
.unwrap()
|
|
.alert_with_message("Edit expense feature coming soon!")
|
|
.unwrap();
|
|
})
|
|
}><i class="bi bi-pencil me-2"></i>{"Edit"}</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick={
|
|
let receipt_url = entry.receipt_url.clone();
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
if let Some(url) = &receipt_url {
|
|
web_sys::window().unwrap().open_with_url(url).unwrap();
|
|
} else {
|
|
web_sys::window()
|
|
.unwrap()
|
|
.alert_with_message("No receipt available for this expense")
|
|
.unwrap();
|
|
}
|
|
})
|
|
}><i class="bi bi-file-earmark me-2"></i>{"View Receipt"}</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick={
|
|
let state = state.clone();
|
|
let expense_id = entry.id.clone();
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
let mut new_state = (*state).clone();
|
|
// Find and update the expense approval status
|
|
if let Some(expense) = new_state.expense_entries.iter_mut().find(|e| e.id == expense_id) {
|
|
expense.approval_status = ApprovalStatus::Approved;
|
|
expense.approved_by = Some("Current User".to_string());
|
|
}
|
|
state.set(new_state);
|
|
})
|
|
}><i class="bi bi-check-circle me-2"></i>{"Approve"}</a></li>
|
|
<li><hr class="dropdown-divider" /></li>
|
|
<li><a class="dropdown-item" href="#" onclick={
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
web_sys::window()
|
|
.unwrap()
|
|
.alert_with_message("Duplicate expense feature coming soon!")
|
|
.unwrap();
|
|
})
|
|
}><i class="bi bi-files me-2"></i>{"Duplicate"}</a></li>
|
|
<li><a class="dropdown-item text-danger" href="#" onclick={
|
|
let state = state.clone();
|
|
let expense_id = entry.id.clone();
|
|
Callback::from(move |e: web_sys::MouseEvent| {
|
|
e.prevent_default();
|
|
e.stop_propagation();
|
|
if web_sys::window().unwrap().confirm_with_message("Are you sure you want to delete this expense?").unwrap() {
|
|
let mut new_state = (*state).clone();
|
|
new_state.expense_entries.retain(|e| e.id != expense_id);
|
|
state.set(new_state);
|
|
}
|
|
})
|
|
}><i class="bi bi-trash me-2"></i>{"Delete"}</a></li>
|
|
</ul>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
} |