mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
feat: Refactor transaction management with nanostores and convert components to React
- Added @nanostores/react for state management. - Created AccountSummary component to display account balance. - Replaced AddTransactionForm.astro with AddTransactionForm.tsx for better state handling. - Introduced TransactionTable.tsx for displaying transactions with edit/delete functionality. - Updated Sidebar.astro and MainContent.astro to use React components. - Implemented transactionStore.ts for managing current account ID and transaction editing state. - Removed obsolete AddTransactionForm.astro and related scripts. - Enhanced error handling and loading states in transaction forms. This fixes issues #7, #8, #9, #10, #11
This commit is contained in:
@@ -4,6 +4,11 @@ import Sidebar from '../components/Sidebar.astro';
|
||||
import MainContent from '../components/MainContent.astro';
|
||||
import type { Account, Transaction } from '../types';
|
||||
|
||||
export interface Props {
|
||||
account: Account;
|
||||
transactions: Transaction[];
|
||||
}
|
||||
|
||||
// Fetch accounts from API
|
||||
const accountsResponse = await fetch('http://localhost:4321/api/accounts');
|
||||
const accounts: Account[] = await accountsResponse.json();
|
||||
@@ -48,7 +53,7 @@ TODO: Performance & Monitoring
|
||||
<BaseLayout title="Bank Transactions Dashboard">
|
||||
<div class="dashboard-layout">
|
||||
<Sidebar accounts={accounts} initialAccount={initialAccount} />
|
||||
<MainContent account={initialAccount} transactions={initialTransactions} />
|
||||
<MainContent account={initialAccount} />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
@@ -56,36 +61,17 @@ TODO: Performance & Monitoring
|
||||
// Import types for client-side script
|
||||
type Transaction = import('../types').Transaction;
|
||||
type Account = import('../types').Account;
|
||||
type TransactionEventDetail = import('../types/events').TransactionEventDetail;
|
||||
|
||||
// Import store atoms and actions
|
||||
import { currentAccountId, startEditingTransaction } from '../stores/transactionStore';
|
||||
|
||||
// --- DOM Elements ---
|
||||
const accountSelect = document.getElementById('account-select') as HTMLSelectElement;
|
||||
const currentAccountNameSpan = document.getElementById('current-account-name');
|
||||
const accountBalanceSpan = document.getElementById('account-balance');
|
||||
const transactionTableBody = document.getElementById('transaction-table-body');
|
||||
const transactionSection = document.getElementById('transaction-section');
|
||||
const addTransactionSection = document.getElementById('add-transaction-section');
|
||||
const toggleAddTxnBtn = document.getElementById('toggle-add-txn-btn');
|
||||
|
||||
// --- Helper Functions ---
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount);
|
||||
}
|
||||
|
||||
function formatDate(dateString: string): string {
|
||||
const date = new Date(dateString + 'T00:00:00');
|
||||
return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric' }).format(date);
|
||||
}
|
||||
|
||||
async function fetchAccountTransactions(accountId: string): Promise<Transaction[]> {
|
||||
try {
|
||||
const response = await fetch(`/api/accounts/${accountId}/transactions`);
|
||||
if (!response.ok) throw new Error('Failed to fetch transactions');
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching transactions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAccountDetails(accountId: string): Promise<Account | null> {
|
||||
try {
|
||||
const response = await fetch(`/api/accounts/${accountId}`);
|
||||
@@ -97,157 +83,59 @@ TODO: Performance & Monitoring
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update UI Function ---
|
||||
// --- Update UI Function (Further Simplified) ---
|
||||
async function updateUIForAccount(accountId: string): Promise<void> {
|
||||
console.log("Updating UI for account:", accountId);
|
||||
console.log("Updating Account Header for account:", accountId);
|
||||
|
||||
if (transactionSection) {
|
||||
transactionSection.classList.add('loading');
|
||||
}
|
||||
// Update the store with the current account ID
|
||||
currentAccountId.set(accountId);
|
||||
|
||||
// Only update the non-React part (header span)
|
||||
currentAccountNameSpan?.classList.add('loading-inline');
|
||||
|
||||
try {
|
||||
// Fetch latest account details and transactions
|
||||
const [account, transactions] = await Promise.all([
|
||||
fetchAccountDetails(accountId),
|
||||
fetchAccountTransactions(accountId)
|
||||
]);
|
||||
const account = await fetchAccountDetails(accountId);
|
||||
|
||||
if (!account || !transactionTableBody || !currentAccountNameSpan || !accountBalanceSpan) {
|
||||
console.error("Required data or UI elements not found!");
|
||||
if (!account || !currentAccountNameSpan) {
|
||||
console.error("Account data or header element not found!");
|
||||
if (currentAccountNameSpan) currentAccountNameSpan.textContent = 'Error';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update header
|
||||
currentAccountNameSpan.textContent = `${account.name} (***${account.last4})`;
|
||||
|
||||
// Update summary
|
||||
accountBalanceSpan.textContent = formatCurrency(account.balance);
|
||||
|
||||
// Update table
|
||||
updateTransactionTable(transactions);
|
||||
} catch (error) {
|
||||
console.error('Error updating UI:', error);
|
||||
if (transactionTableBody) {
|
||||
transactionTableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; color: #dc3545;">
|
||||
Failed to load transactions. Please try again.
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
console.error('Error updating account header:', error);
|
||||
if (currentAccountNameSpan) currentAccountNameSpan.textContent = 'Error';
|
||||
} finally {
|
||||
if (transactionSection) {
|
||||
transactionSection.classList.remove('loading');
|
||||
}
|
||||
currentAccountNameSpan?.classList.remove('loading-inline');
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactionTable(transactions: Transaction[]): void {
|
||||
if (!transactionTableBody) return;
|
||||
|
||||
const sortedTransactions = [...transactions].sort((a, b) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
);
|
||||
|
||||
transactionTableBody.innerHTML = '';
|
||||
|
||||
if (sortedTransactions.length === 0) {
|
||||
transactionTableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="4" style="text-align: center; font-style: italic; color: #777;">
|
||||
No transactions found for this account.
|
||||
</td>
|
||||
</tr>`;
|
||||
return;
|
||||
}
|
||||
|
||||
sortedTransactions.forEach(txn => {
|
||||
const row = document.createElement('tr');
|
||||
row.setAttribute('data-txn-id', txn.id);
|
||||
row.innerHTML = `
|
||||
<td>${formatDate(txn.date)}</td>
|
||||
<td>${txn.description}</td>
|
||||
<td class="amount-col ${txn.amount >= 0 ? 'amount-positive' : 'amount-negative'}">
|
||||
${formatCurrency(txn.amount)}
|
||||
</td>
|
||||
<td>
|
||||
<button class="action-btn edit-btn" title="Edit transaction">Edit</button>
|
||||
<button class="action-btn delete-btn" title="Delete transaction">Delete</button>
|
||||
</td>
|
||||
`;
|
||||
transactionTableBody.appendChild(row);
|
||||
});
|
||||
|
||||
setupTransactionButtons();
|
||||
}
|
||||
|
||||
function setupTransactionButtons(): void {
|
||||
const transactionRows = transactionTableBody?.querySelectorAll('tr[data-txn-id]');
|
||||
transactionRows?.forEach(row => {
|
||||
const txnId = row.getAttribute('data-txn-id');
|
||||
if (!txnId) return;
|
||||
|
||||
// Delete button handler
|
||||
const deleteBtn = row.querySelector('.delete-btn');
|
||||
deleteBtn?.addEventListener('click', () => handleDeleteTransaction(txnId));
|
||||
|
||||
// Edit button handler
|
||||
const editBtn = row.querySelector('.edit-btn');
|
||||
editBtn?.addEventListener('click', () => handleEditTransaction(txnId));
|
||||
});
|
||||
}
|
||||
|
||||
// --- Transaction Actions ---
|
||||
async function handleDeleteTransaction(txnId: string): Promise<void> {
|
||||
if (!confirm('Are you sure you want to delete this transaction?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (transactionSection) {
|
||||
transactionSection.classList.add('loading');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/transactions/${txnId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Failed to delete transaction');
|
||||
}
|
||||
|
||||
// Refresh the current account view
|
||||
const currentAccountId = accountSelect?.value;
|
||||
if (currentAccountId) {
|
||||
await updateUIForAccount(currentAccountId);
|
||||
}
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : 'Failed to delete transaction');
|
||||
} finally {
|
||||
if (transactionSection) {
|
||||
transactionSection.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEditTransaction(txnId: string): Promise<void> {
|
||||
try {
|
||||
// Find transaction in current transactions list
|
||||
const currentAccountId = accountSelect?.value;
|
||||
if (!currentAccountId) return;
|
||||
|
||||
const transactions = await fetchAccountTransactions(currentAccountId);
|
||||
const accountId = currentAccountId.get();
|
||||
if (!accountId) return;
|
||||
|
||||
const response = await fetch(`/api/accounts/${accountId}/transactions`);
|
||||
if (!response.ok) throw new Error('Failed to fetch transactions for edit');
|
||||
const transactions: Transaction[] = await response.json();
|
||||
const transaction = transactions.find(t => t.id === txnId);
|
||||
|
||||
|
||||
if (!transaction) {
|
||||
throw new Error('Transaction not found');
|
||||
throw new Error('Transaction not found for editing');
|
||||
}
|
||||
|
||||
// Trigger edit mode in form
|
||||
document.dispatchEvent(new CustomEvent<TransactionEventDetail>('editTransaction', {
|
||||
detail: { transaction }
|
||||
}));
|
||||
startEditingTransaction(transaction);
|
||||
|
||||
// Manually expand the form section if it's collapsed
|
||||
if (addTransactionSection?.classList.contains('collapsed')) {
|
||||
addTransactionSection.classList.replace('collapsed', 'expanded');
|
||||
toggleAddTxnBtn?.setAttribute('aria-expanded', 'true');
|
||||
addTransactionSection.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : 'Failed to load transaction for editing');
|
||||
@@ -262,24 +150,11 @@ TODO: Performance & Monitoring
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for transaction events
|
||||
document.addEventListener('transactionCreated', ((event: CustomEvent<TransactionEventDetail>) => {
|
||||
const currentAccountId = accountSelect?.value;
|
||||
if (currentAccountId) {
|
||||
updateUIForAccount(currentAccountId);
|
||||
}
|
||||
}) as EventListener);
|
||||
|
||||
document.addEventListener('transactionUpdated', ((event: CustomEvent<TransactionEventDetail>) => {
|
||||
const currentAccountId = accountSelect?.value;
|
||||
if (currentAccountId) {
|
||||
updateUIForAccount(currentAccountId);
|
||||
}
|
||||
}) as EventListener);
|
||||
|
||||
// Initial load with transactions if available
|
||||
const initialAccountId = accountSelect?.value;
|
||||
if (initialAccountId) {
|
||||
updateUIForAccount(initialAccountId);
|
||||
// --- Initial Load ---
|
||||
const initialAccountIdValue = accountSelect?.value;
|
||||
if (initialAccountIdValue) {
|
||||
updateUIForAccount(initialAccountIdValue);
|
||||
} else {
|
||||
currentAccountId.set(null);
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user