diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ee4401b..61f2604 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,7 +9,7 @@ This project is a web user interface (UI) for a CRUD (Create, Read, Update, Dele * **Framework:** Astro (latest version) * **Language:** TypeScript, JavaScript (client-side scripts), HTML, CSS * **Styling:** Plain CSS (`src/styles/global.css`) -* **Data:** Using Astro's built-in API routes in `src/pages/api/` with a temporary in-memory store (`src/data/store.ts`). **The goal is to eventually replace the in-memory store with a persistent database.** +* **Data:** Using Astro's built-in API routes in `src/pages/api/` with persistent database storage via Prisma ORM (`src/data/db.service.ts`). * **Development Environment:** VS Code Dev Container using private Docker image (`ghcr.io/acedanger/finance-devcontainer:latest`) ## Development Environment @@ -41,20 +41,22 @@ This project is a web user interface (UI) for a CRUD (Create, Read, Update, Dele * **Components:** Separate Astro components exist for major UI sections (Sidebar, MainContent, TransactionTable, AddTransactionForm, AccountSummary). * **API Integration:** * API routes structure implemented in `src/pages/api/` - * Temporary data store in `src/data/store.ts` - * All API endpoints implemented and ready to use: + * Database integration using Prisma ORM in `src/data/db.service.ts` + * All API endpoints implemented and fully functional: * GET /api/accounts - List all accounts * GET /api/accounts/:id - Get single account details * GET /api/accounts/:id/transactions - Get transactions for an account * POST /api/transactions - Create new transaction * PUT /api/transactions/:id - Update existing transaction * DELETE /api/transactions/:id - Delete transaction - * Error handling and validation included - * Prepared for future database integration with modular store design + * Comprehensive error handling and validation + * Database persistence with proper transaction support * **Account Switching:** Selecting an account from the dropdown in the sidebar correctly updates the Main Content area (header, transaction table) and the Account Summary section using client-side JavaScript (` \ No newline at end of file + diff --git a/src/components/TransactionTable.tsx b/src/components/TransactionTable.tsx index 0686382..915c83e 100644 --- a/src/components/TransactionTable.tsx +++ b/src/components/TransactionTable.tsx @@ -1,54 +1,82 @@ 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([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(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); + 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]); + + // Effect for loading transactions when account changes or refresh is triggered + useEffect(() => { fetchTransactions(); - }, [currentAccountId, refreshCounter]); + }, [fetchTransactions, refreshCounter]); - const sortedTransactions = [...transactions].sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(), - ); + // 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?')) { @@ -74,14 +102,7 @@ export default function TransactionTable({}: TransactionTableProps) { } console.log(`Transaction ${txnId} deleted successfully.`); - - // Remove from local state - setTransactions((currentTransactions) => - currentTransactions.filter((txn) => txn.id !== txnId), - ); - - // Trigger refresh to update balances and table - triggerRefresh(); + triggerRefresh(); // This will reload transactions } catch (error) { alert(error instanceof Error ? error.message : 'Failed to delete transaction'); console.error('Delete error:', error); @@ -107,7 +128,7 @@ export default function TransactionTable({}: TransactionTableProps) { // Helper function to render loading state const renderLoading = () => ( - + Loading transactions... @@ -117,11 +138,12 @@ export default function TransactionTable({}: TransactionTableProps) { const renderEmpty = () => ( No transactions found for this account. @@ -129,15 +151,16 @@ export default function TransactionTable({}: TransactionTableProps) { ); - // Helper function to render transaction rows + // Helper function to render transaction rows with better error handling const renderRows = () => sortedTransactions.map((txn) => ( {formatDate(txn.date)} - {txn.description} + {txn.description || 'No description'} = 0 ? 'amount-positive' : 'amount-negative'}`}> {formatCurrency(txn.amount)} + {txn.category || 'Uncategorized'}