Files
finance/src/components/AddTransactionForm.tsx

239 lines
7.6 KiB
TypeScript

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,
} 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<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isEditMode = !!editingId;
// --- Effects ---
// Effect to set default date on mount
useEffect(() => {
// Only set default date if not editing
if (!transactionToEdit) {
setDate(new Date().toISOString().split('T')[0]);
}
}, [transactionToEdit]); // Rerun if edit mode changes
// Effect to populate form when editing
useEffect(() => {
if (transactionToEdit) {
setEditingId(transactionToEdit.id);
// Format date correctly for input type="date"
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]);
} else {
console.warn('Invalid date received for editing:', transactionToEdit.date);
setDate(''); // Set to empty if invalid
}
} catch (e) {
console.error('Error parsing date for editing:', e);
setDate(''); // Set to empty on error
}
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 ---
const resetForm = () => {
setEditingId(null);
setDate(new Date().toISOString().split('T')[0]);
setDescription('');
setAmount('');
setError(null);
// Don't reset isLoading here, it's handled in submit/cancel
};
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();
setError(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 {
// 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),
};
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);
}
const savedTransaction: Transaction = await response.json();
transactionSaved(savedTransaction); // Call store action instead of prop callback
resetForm(); // Reset form on success
} catch (err) {
setError(err instanceof Error ? err.message : 'An unexpected error occurred');
} finally {
setIsLoading(false);
}
};
const handleCancel = () => {
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>
{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}>
Cancel
</button>
)}
</div>
</form>
);
}