mirror of
https://github.com/acedanger/finance.git
synced 2025-12-06 07:00:13 -08:00
Fix: Update button remaining disabled in transaction edit mode
This commit resolves an issue where the Update button in the transaction form would remain disabled when attempting to edit a transaction. The problem was in how the transactionStore was managing state updates during transaction editing. Key changes: - Enhanced startEditingTransaction function in transactionStore.ts to ensure proper reactivity - Added clean copy creation of transaction objects to avoid reference issues - Implemented a state update cycle with null value first to force reactivity - Added a small timeout to ensure state changes are properly detected by components The Transaction form now correctly enables the Update button when in edit mode, regardless of account selection state.
This commit is contained in:
@@ -1,238 +1,269 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
// Import store atoms and actions
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
cancelEditingTransaction,
|
||||
currentAccountId as currentAccountIdStore,
|
||||
currentAccountId,
|
||||
loadTransactionsForAccount,
|
||||
transactionSaved,
|
||||
transactionToEdit as transactionToEditStore,
|
||||
transactionToEdit,
|
||||
triggerRefresh,
|
||||
} from '../stores/transactionStore';
|
||||
import type { Transaction } from '../types';
|
||||
|
||||
export default function AddTransactionForm() {
|
||||
// --- Read state from store ---
|
||||
const currentAccountId = useStore(currentAccountIdStore);
|
||||
const transactionToEdit = useStore(transactionToEditStore);
|
||||
const accountId = useStore(currentAccountId);
|
||||
const editingTransaction = useStore(transactionToEdit);
|
||||
|
||||
// --- State Variables ---
|
||||
// Form state - initialize with empty values to avoid hydration mismatch
|
||||
const [date, setDate] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [category, setCategory] = useState('');
|
||||
const [type, setType] = useState('WITHDRAWAL');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const isEditMode = !!editingId;
|
||||
|
||||
// --- Effects ---
|
||||
// Effect to set default date on mount
|
||||
// Set initial date only on client-side after component mounts
|
||||
useEffect(() => {
|
||||
// Only set default date if not editing
|
||||
if (!transactionToEdit) {
|
||||
setDate(new Date().toISOString().split('T')[0]);
|
||||
// Only run this effect on the client side to prevent hydration mismatch
|
||||
if (!date) {
|
||||
const today = new Date();
|
||||
setDate(today.toISOString().split('T')[0]);
|
||||
}
|
||||
}, [transactionToEdit]); // Rerun if edit mode changes
|
||||
}, []);
|
||||
|
||||
// Effect to populate form when editing
|
||||
// Reset form when accountId changes or when switching from edit to add mode
|
||||
useEffect(() => {
|
||||
if (transactionToEdit) {
|
||||
setEditingId(transactionToEdit.id);
|
||||
// Format date correctly for input type="date"
|
||||
if (!editingTransaction) {
|
||||
resetForm();
|
||||
}
|
||||
}, [accountId, editingTransaction === null]);
|
||||
|
||||
// Populate form when editing a transaction
|
||||
useEffect(() => {
|
||||
if (editingTransaction) {
|
||||
let dateStr: string;
|
||||
try {
|
||||
const dateObj = new Date(transactionToEdit.date);
|
||||
// Check if date is valid before formatting
|
||||
if (!Number.isNaN(dateObj.getTime())) {
|
||||
// Directly format the date object (usually interpreted as UTC midnight)
|
||||
// into the YYYY-MM-DD format required by the input.
|
||||
// No timezone adjustment needed here.
|
||||
setDate(dateObj.toISOString().split('T')[0]);
|
||||
if (editingTransaction.date instanceof Date) {
|
||||
dateStr = editingTransaction.date.toISOString().split('T')[0];
|
||||
} else {
|
||||
console.warn('Invalid date received for editing:', transactionToEdit.date);
|
||||
setDate(''); // Set to empty if invalid
|
||||
// Handle string dates safely
|
||||
const parsedDate = new Date(String(editingTransaction.date));
|
||||
dateStr = Number.isNaN(parsedDate.getTime())
|
||||
? new Date().toISOString().split('T')[0] // Fallback to today if invalid
|
||||
: parsedDate.toISOString().split('T')[0];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing date for editing:', e);
|
||||
setDate(''); // Set to empty on error
|
||||
} catch (error) {
|
||||
console.error('Error parsing date for edit:', error);
|
||||
dateStr = new Date().toISOString().split('T')[0]; // Fallback to today
|
||||
}
|
||||
setDescription(transactionToEdit.description);
|
||||
setAmount(transactionToEdit.amount.toString());
|
||||
setError(null); // Clear errors when starting edit
|
||||
} else {
|
||||
// Reset form if transactionToEdit becomes null (e.g., after saving or cancelling)
|
||||
// but only if not already resetting via handleCancel or handleSubmit
|
||||
if (!isLoading) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
}, [transactionToEdit, isLoading]); // Add isLoading dependency
|
||||
|
||||
// --- Helper Functions ---
|
||||
setDate(dateStr);
|
||||
setDescription(editingTransaction.description);
|
||||
setAmount(String(Math.abs(editingTransaction.amount)));
|
||||
setCategory(editingTransaction.category || '');
|
||||
setType(editingTransaction.amount < 0 ? 'WITHDRAWAL' : 'DEPOSIT');
|
||||
}
|
||||
}, [editingTransaction]);
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingId(null);
|
||||
setDate(new Date().toISOString().split('T')[0]);
|
||||
// Get today's date in YYYY-MM-DD format for the date input
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
setDate(today);
|
||||
setDescription('');
|
||||
setAmount('');
|
||||
setCategory('');
|
||||
setType('WITHDRAWAL');
|
||||
setError(null);
|
||||
// Don't reset isLoading here, it's handled in submit/cancel
|
||||
setSuccessMessage(null);
|
||||
};
|
||||
|
||||
const validateForm = (): string[] => {
|
||||
const errors: string[] = [];
|
||||
if (!description || description.trim().length < 2) {
|
||||
errors.push('Description must be at least 2 characters long');
|
||||
}
|
||||
if (!amount) {
|
||||
errors.push('Amount is required');
|
||||
} else {
|
||||
const amountNum = Number.parseFloat(amount);
|
||||
if (Number.isNaN(amountNum)) {
|
||||
errors.push('Amount must be a valid number');
|
||||
} else if (amountNum === 0) {
|
||||
errors.push('Amount cannot be zero');
|
||||
}
|
||||
}
|
||||
if (!date) {
|
||||
errors.push('Date is required');
|
||||
} else {
|
||||
try {
|
||||
const dateObj = new Date(`${date}T00:00:00`); // Treat input as local date
|
||||
if (Number.isNaN(dateObj.getTime())) {
|
||||
errors.push('Invalid date format');
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push('Invalid date format');
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
};
|
||||
|
||||
// --- Event Handlers ---
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!accountId) {
|
||||
setError('No account selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!date || !description || !amount) {
|
||||
setError('Date, description and amount are required');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
if (isLoading || !currentAccountId) {
|
||||
if (!currentAccountId) setError('No account selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
const validationErrors = validateForm();
|
||||
if (validationErrors.length > 0) {
|
||||
setError(validationErrors.join('. '));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
// Calculate final amount based on type
|
||||
const finalAmount = type === 'DEPOSIT' ? Math.abs(Number(amount)) : -Math.abs(Number(amount));
|
||||
|
||||
try {
|
||||
// Ensure date is sent in a consistent format (e.g., YYYY-MM-DD)
|
||||
// The API should handle parsing this.
|
||||
const transactionData = {
|
||||
accountId: currentAccountId,
|
||||
date: date, // Send as YYYY-MM-DD string
|
||||
description: description.trim(),
|
||||
amount: Number.parseFloat(amount),
|
||||
};
|
||||
let response;
|
||||
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
const url = editingId ? `/api/transactions/${editingId}` : '/api/transactions';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(transactionData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMsg = `Failed to ${isEditMode ? 'update' : 'create'} transaction`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
errorMsg = errorData.error || errorMsg;
|
||||
} catch (jsonError) {
|
||||
// Ignore if response is not JSON
|
||||
errorMsg = `${response.status}: ${response.statusText}`;
|
||||
}
|
||||
throw new Error(errorMsg);
|
||||
if (editingTransaction) {
|
||||
// Update existing transaction
|
||||
response = await fetch(`/api/transactions/${editingTransaction.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accountId,
|
||||
date,
|
||||
description,
|
||||
amount: finalAmount,
|
||||
category: category || undefined,
|
||||
type,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
// Create new transaction
|
||||
response = await fetch('/api/transactions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accountId,
|
||||
date,
|
||||
description,
|
||||
amount: finalAmount,
|
||||
category: category || undefined,
|
||||
type,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const savedTransaction: Transaction = await response.json();
|
||||
transactionSaved(savedTransaction); // Call store action instead of prop callback
|
||||
resetForm(); // Reset form on success
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Transaction operation failed');
|
||||
}
|
||||
|
||||
const savedTransaction = await response.json();
|
||||
|
||||
// Handle success
|
||||
setSuccessMessage(
|
||||
editingTransaction
|
||||
? 'Transaction updated successfully!'
|
||||
: 'Transaction added successfully!',
|
||||
);
|
||||
|
||||
// Reset form
|
||||
resetForm();
|
||||
|
||||
// Clear editing state
|
||||
if (editingTransaction) {
|
||||
cancelEditingTransaction();
|
||||
}
|
||||
|
||||
// Notify about saved transaction
|
||||
transactionSaved(savedTransaction);
|
||||
|
||||
// Reload transactions to ensure the list is up to date
|
||||
await loadTransactionsForAccount(accountId);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
console.error('Transaction error:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (editingTransaction) {
|
||||
cancelEditingTransaction();
|
||||
}
|
||||
resetForm();
|
||||
cancelEditingTransaction(); // Call store action instead of prop callback
|
||||
};
|
||||
|
||||
// --- JSX ---
|
||||
return (
|
||||
<form id="add-transaction-form-react" onSubmit={handleSubmit} noValidate>
|
||||
<h4>{isEditMode ? 'Edit Transaction' : 'New Transaction'}</h4>
|
||||
<div className="transaction-form-container">
|
||||
<h3>{editingTransaction ? 'Edit Transaction' : 'Add Transaction'}</h3>
|
||||
|
||||
{successMessage && <div className="success-message">{successMessage}</div>}
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="txn-date-react">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="txn-date-react"
|
||||
name="date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="txn-description-react">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
id="txn-description-react"
|
||||
name="description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
required
|
||||
minLength={2}
|
||||
maxLength={100}
|
||||
placeholder="e.g. Groceries"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="txn-amount-react">Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
id="txn-amount-react"
|
||||
name="amount"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
step="0.01"
|
||||
required
|
||||
placeholder="e.g. -25.50 or 1200.00"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<small className="help-text">Use negative numbers for expenses (e.g., -50.00)</small>
|
||||
</div>
|
||||
<div className="button-group">
|
||||
<button
|
||||
type="submit"
|
||||
className={`form-submit-btn ${isLoading ? 'loading' : ''}`}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Saving...' : isEditMode ? 'Update Transaction' : 'Save Transaction'}
|
||||
</button>
|
||||
{isEditMode && (
|
||||
<button type="button" className="cancel-btn" onClick={handleCancel} disabled={isLoading}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="txn-date">Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="txn-date"
|
||||
value={date}
|
||||
onChange={(e) => setDate(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="txn-description">Description:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="txn-description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
placeholder="e.g., Grocery store"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group amount-group">
|
||||
<label htmlFor="txn-amount">Amount:</label>
|
||||
<div className="amount-input-group">
|
||||
<select value={type} onChange={(e) => setType(e.target.value)} disabled={isSubmitting}>
|
||||
<option value="WITHDRAWAL">-</option>
|
||||
<option value="DEPOSIT">+</option>
|
||||
</select>
|
||||
<span className="currency-symbol">$</span>
|
||||
<input
|
||||
type="number"
|
||||
id="txn-amount"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
step="0.01"
|
||||
min="0"
|
||||
placeholder="0.00"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="txn-category">Category (optional):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="txn-category"
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
placeholder="e.g., Food, Bills, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={isSubmitting}
|
||||
className="cancel-btn"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
<button
|
||||
type="submit"
|
||||
// Allow submitting if we're editing a transaction, even if no account is currently selected
|
||||
disabled={isSubmitting || (!editingTransaction && !accountId)}
|
||||
className="submit-btn"
|
||||
>
|
||||
{isSubmitting ? 'Processing...' : editingTransaction ? 'Update' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const { account } = Astro.props;
|
||||
---
|
||||
<main class="main-content">
|
||||
<header class="main-header">
|
||||
<h1>Transactions for <span id="current-account-name">{account.name} (***{account.last4})</span></h1>
|
||||
<h1>Transactions for <span id="current-account-name">{account.name} (***{account.accountNumber.slice(-3)})</span></h1>
|
||||
</header>
|
||||
<TransactionTable client:load />
|
||||
</main>
|
||||
@@ -1,31 +1,41 @@
|
||||
---
|
||||
import type { Account } from '../types';
|
||||
import AccountSummary from './AccountSummary.tsx'; // Import the React component instead of the Astro one
|
||||
import AddTransactionForm from './AddTransactionForm.tsx';
|
||||
import type { Account } from "../types";
|
||||
import AccountSummary from "./AccountSummary.tsx"; // Import the React component instead of the Astro one
|
||||
import AddTransactionForm from "./AddTransactionForm.tsx";
|
||||
|
||||
interface Props {
|
||||
accounts: Account[];
|
||||
initialAccount: Account;
|
||||
accounts: Account[];
|
||||
initialAccount: Account;
|
||||
}
|
||||
|
||||
const { accounts, initialAccount } = Astro.props;
|
||||
---
|
||||
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2>My finances</h2>
|
||||
{/* Add button to toggle form visibility */}
|
||||
<button id="toggle-add-txn-btn" aria-expanded="false" aria-controls="add-transaction-section">
|
||||
<button
|
||||
id="toggle-add-txn-btn"
|
||||
aria-expanded="false"
|
||||
aria-controls="add-transaction-section"
|
||||
>
|
||||
+ New Txn
|
||||
</button>
|
||||
</div>
|
||||
<nav class="account-nav">
|
||||
<h3>Accounts</h3>
|
||||
<select id="account-select" name="account">
|
||||
{accounts.map(account => (
|
||||
<option value={account.id} selected={account.id === initialAccount.id}>
|
||||
{account.name} (***{account.last4})
|
||||
</option>
|
||||
))}
|
||||
{
|
||||
accounts.map((account) => (
|
||||
<option
|
||||
value={account.id}
|
||||
selected={account.id === initialAccount.id}
|
||||
>
|
||||
{account.name} (***{account.accountNumber.slice(-3)})
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</nav>
|
||||
|
||||
@@ -34,31 +44,38 @@ const { accounts, initialAccount } = Astro.props;
|
||||
|
||||
{/* Section to contain the React form, initially hidden */}
|
||||
<section id="add-transaction-section" class="collapsible collapsed">
|
||||
{/*
|
||||
{
|
||||
/*
|
||||
Use the React component here.
|
||||
It now gets its state (currentAccountId, transactionToEdit)
|
||||
directly from the Nano Store.
|
||||
*/}
|
||||
<AddTransactionForm client:load />
|
||||
*/
|
||||
}
|
||||
<AddTransactionForm client:load />
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
{/* Keep the script for toggling visibility for now */}
|
||||
<script>
|
||||
const toggleButton = document.getElementById('toggle-add-txn-btn');
|
||||
const formSection = document.getElementById('add-transaction-section');
|
||||
const toggleButton = document.getElementById("toggle-add-txn-btn");
|
||||
const formSection = document.getElementById("add-transaction-section");
|
||||
|
||||
if (toggleButton && formSection) {
|
||||
toggleButton.addEventListener('click', () => {
|
||||
const isExpanded = toggleButton.getAttribute('aria-expanded') === 'true';
|
||||
toggleButton.setAttribute('aria-expanded', String(!isExpanded));
|
||||
formSection.classList.toggle('collapsed');
|
||||
formSection.classList.toggle('expanded');
|
||||
toggleButton.addEventListener("click", () => {
|
||||
const isExpanded =
|
||||
toggleButton.getAttribute("aria-expanded") === "true";
|
||||
toggleButton.setAttribute("aria-expanded", String(!isExpanded));
|
||||
formSection.classList.toggle("collapsed");
|
||||
formSection.classList.toggle("expanded");
|
||||
// Optional: Focus first field when expanding
|
||||
if (!isExpanded) {
|
||||
// Cast the result to HTMLElement before calling focus
|
||||
(formSection.querySelector('input, select, textarea') as HTMLElement)?.focus();
|
||||
(
|
||||
formSection.querySelector(
|
||||
"input, select, textarea",
|
||||
) as HTMLElement
|
||||
)?.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,55 +1,82 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
currentAccountId as currentAccountIdStore,
|
||||
currentTransactions as currentTransactionsStore,
|
||||
refreshKey,
|
||||
startEditingTransaction,
|
||||
triggerRefresh,
|
||||
loadTransactionsForAccount,
|
||||
} from '../stores/transactionStore';
|
||||
import type { Transaction } from '../types';
|
||||
import { formatCurrency, formatDate } from '../utils';
|
||||
|
||||
type TransactionTableProps = {};
|
||||
|
||||
export default function TransactionTable({}: TransactionTableProps) {
|
||||
export default function TransactionTable() {
|
||||
const currentAccountId = useStore(currentAccountIdStore);
|
||||
const refreshCounter = useStore(refreshKey);
|
||||
const transactions = useStore(currentTransactionsStore);
|
||||
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch transactions when account ID changes or refresh is triggered
|
||||
const fetchTransactions = useCallback(async () => {
|
||||
if (!currentAccountId) {
|
||||
setTransactions([]);
|
||||
console.log('TransactionTable: No account selected, skipping transaction load');
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchTransactions = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch(`/api/accounts/${currentAccountId}/transactions`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch transactions');
|
||||
}
|
||||
const data: Transaction[] = await response.json();
|
||||
setTransactions(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An unknown error occurred');
|
||||
setTransactions([]);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchTransactions();
|
||||
try {
|
||||
console.log(`TransactionTable: Loading transactions for account ${currentAccountId}`);
|
||||
await loadTransactionsForAccount(currentAccountId);
|
||||
console.log('TransactionTable: Transactions loaded successfully');
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'An unknown error occurred';
|
||||
console.error('TransactionTable: Error loading transactions:', errorMessage);
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [currentAccountId]);
|
||||
|
||||
const sortedTransactions = [...transactions].sort(
|
||||
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
|
||||
);
|
||||
// Effect for loading transactions when account changes or refresh is triggered
|
||||
useEffect(() => {
|
||||
fetchTransactions();
|
||||
}, [fetchTransactions, refreshCounter]);
|
||||
|
||||
// Safe sort function that handles invalid dates gracefully
|
||||
const safeSort = (transactions: Transaction[]) => {
|
||||
if (!Array.isArray(transactions)) {
|
||||
console.warn('Expected transactions array but received:', transactions);
|
||||
return [];
|
||||
}
|
||||
|
||||
return [...transactions].sort((a, b) => {
|
||||
try {
|
||||
// Safely parse dates with validation
|
||||
const dateA = a.date ? new Date(a.date).getTime() : 0;
|
||||
const dateB = b.date ? new Date(b.date).getTime() : 0;
|
||||
|
||||
// If either date is invalid, use a fallback approach
|
||||
if (Number.isNaN(dateA) || Number.isNaN(dateB)) {
|
||||
console.warn('Found invalid date during sort:', { a: a.date, b: b.date });
|
||||
// Sort by ID as fallback or keep original order
|
||||
return (b.id || '').localeCompare(a.id || '');
|
||||
}
|
||||
|
||||
return dateB - dateA; // Newest first
|
||||
} catch (error) {
|
||||
console.error('Error during transaction sort:', error);
|
||||
return 0; // Keep original order on error
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Format transactions to display in table - with better error handling
|
||||
const sortedTransactions = Array.isArray(transactions) ? safeSort(transactions) : [];
|
||||
|
||||
const handleDelete = async (txnId: string) => {
|
||||
if (!confirm('Are you sure you want to delete this transaction?')) {
|
||||
@@ -75,16 +102,10 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
}
|
||||
|
||||
console.log(`Transaction ${txnId} deleted successfully.`);
|
||||
|
||||
setTransactions((currentTransactions) =>
|
||||
currentTransactions.filter((txn) => txn.id !== txnId),
|
||||
);
|
||||
|
||||
triggerRefresh();
|
||||
triggerRefresh(); // This will reload transactions
|
||||
} catch (error) {
|
||||
alert(error instanceof Error ? error.message : 'Failed to delete transaction');
|
||||
console.error('Delete error:', error);
|
||||
} finally {
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,7 +128,7 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
// Helper function to render loading state
|
||||
const renderLoading = () => (
|
||||
<tr>
|
||||
<td colSpan={4} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<td colSpan={5} style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
Loading transactions...
|
||||
</td>
|
||||
</tr>
|
||||
@@ -117,11 +138,12 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
const renderEmpty = () => (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={4}
|
||||
colSpan={5}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic',
|
||||
color: '#777',
|
||||
padding: '2rem 1rem',
|
||||
}}
|
||||
>
|
||||
No transactions found for this account.
|
||||
@@ -129,15 +151,16 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
</tr>
|
||||
);
|
||||
|
||||
// Helper function to render transaction rows
|
||||
// Helper function to render transaction rows with better error handling
|
||||
const renderRows = () =>
|
||||
sortedTransactions.map((txn) => (
|
||||
<tr key={txn.id} data-txn-id={txn.id}>
|
||||
<td>{formatDate(txn.date)}</td>
|
||||
<td>{txn.description}</td>
|
||||
<td>{txn.description || 'No description'}</td>
|
||||
<td className={`amount-col ${txn.amount >= 0 ? 'amount-positive' : 'amount-negative'}`}>
|
||||
{formatCurrency(txn.amount)}
|
||||
</td>
|
||||
<td>{txn.category || 'Uncategorized'}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
@@ -162,7 +185,7 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
return (
|
||||
<div id="transaction-section" className={isLoading ? 'loading' : ''}>
|
||||
{error && (
|
||||
<div className="error-message" style={{ padding: '1rem' }}>
|
||||
<div className="error-message" style={{ padding: '1rem', marginBottom: '1rem' }}>
|
||||
Error loading transactions: {error}
|
||||
</div>
|
||||
)}
|
||||
@@ -172,17 +195,16 @@ export default function TransactionTable({}: TransactionTableProps) {
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
<th className="amount-col">Amount</th>
|
||||
<th>Category</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="transaction-table-body">
|
||||
{isLoading
|
||||
? renderLoading()
|
||||
: error
|
||||
? null // Error message is shown above the table
|
||||
: sortedTransactions.length === 0
|
||||
? renderEmpty()
|
||||
: renderRows()}
|
||||
: sortedTransactions.length === 0
|
||||
? renderEmpty()
|
||||
: renderRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user