import { useStore } from '@nanostores/react'; import type React from 'react'; import { useEffect, useState } from 'react'; // Import store atoms and actions import { cancelEditingTransaction, currentAccountId as currentAccountIdStore, transactionSaved, transactionToEdit as transactionToEditStore, 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); // --- State Variables --- const [date, setDate] = useState(''); const [description, setDescription] = useState(''); const [amount, setAmount] = useState(''); const [editingId, setEditingId] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); const isEditMode = !!editingId; // --- Effects --- // Effect to set default date on mount useEffect(() => { if (!transactionToEdit) { setDate(new Date().toISOString().split('T')[0]); } }, [transactionToEdit]); // Effect to populate form when editing useEffect(() => { if (transactionToEdit) { setEditingId(transactionToEdit.id); try { const dateObj = new Date(transactionToEdit.date); if (!Number.isNaN(dateObj.getTime())) { setDate(dateObj.toISOString().split('T')[0]); } else { console.warn('Invalid date received for editing:', transactionToEdit.date); setDate(''); } } catch (e) { console.error('Error parsing date for editing:', e); setDate(''); } setDescription(transactionToEdit.description); setAmount(transactionToEdit.amount.toString()); setError(null); setSuccessMessage(null); } else if (!isLoading) { resetForm(); } }, [transactionToEdit, isLoading]); // Clear success message after 5 seconds useEffect(() => { if (successMessage) { const timer = setTimeout(() => { setSuccessMessage(null); }, 5000); return () => clearTimeout(timer); } }, [successMessage]); // --- Helper Functions --- const resetForm = () => { setEditingId(null); setDate(new Date().toISOString().split('T')[0]); setDescription(''); setAmount(''); setError(null); 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`); 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(); 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); try { const transactionData = { accountId: currentAccountId, date: date, description: description.trim(), amount: Number.parseFloat(amount), }; 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) { errorMsg = `${response.status}: ${response.statusText}`; } throw new Error(errorMsg); } const savedTransaction: Transaction = await response.json(); // First notify about the saved transaction transactionSaved(savedTransaction); // Then explicitly trigger a refresh to ensure balance updates triggerRefresh(); // Set success message before clearing form setSuccessMessage( isEditMode ? 'Transaction updated successfully' : 'Transaction created successfully', ); // Only reset the form after the success message is shown setTimeout(() => { resetForm(); // Optionally collapse the form after success const addTransactionSection = document.getElementById('add-transaction-section'); const toggleAddTxnBtn = document.getElementById('toggle-add-txn-btn'); if (addTransactionSection?.classList.contains('expanded')) { addTransactionSection.classList.replace('expanded', 'collapsed'); toggleAddTxnBtn?.setAttribute('aria-expanded', 'false'); } }, 2000); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred'; setError(errorMessage); setSuccessMessage(null); } finally { setIsLoading(false); } }; const handleCancel = () => { resetForm(); cancelEditingTransaction(); }; // --- JSX --- return (

{isEditMode ? 'Edit Transaction' : 'New Transaction'}

{error && (
{error}
)} {successMessage && (
{successMessage}
)}
setDate(e.target.value)} required disabled={isLoading} />
setDescription(e.target.value)} required minLength={2} maxLength={100} placeholder="e.g. Groceries" disabled={isLoading} />
setAmount(e.target.value)} step="0.01" required placeholder="e.g. -25.50 or 1200.00" disabled={isLoading} /> Use negative numbers for expenses (e.g., -50.00)
{isEditMode && ( )}
); }