mirror of
https://github.com/acedanger/finance.git
synced 2025-12-05 22:50:12 -08:00
- 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
261 lines
7.9 KiB
TypeScript
261 lines
7.9 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { useStore } from "@nanostores/react";
|
|
import type { Transaction } from "../types";
|
|
// Import store atoms and actions
|
|
import {
|
|
currentAccountId as currentAccountIdStore,
|
|
transactionToEdit as transactionToEditStore,
|
|
cancelEditingTransaction,
|
|
transactionSaved,
|
|
} from "../stores/transactionStore";
|
|
|
|
// Remove props that now come from the store
|
|
interface AddTransactionFormProps {}
|
|
|
|
export default function AddTransactionForm({}: AddTransactionFormProps) {
|
|
// --- 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 (!isNaN(dateObj.getTime())) {
|
|
// Adjust for timezone offset to prevent date shifting
|
|
const timezoneOffset = dateObj.getTimezoneOffset() * 60000; //offset in milliseconds
|
|
const adjustedDate = new Date(dateObj.getTime() - timezoneOffset);
|
|
setDate(adjustedDate.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 = parseFloat(amount);
|
|
if (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 (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: 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>
|
|
);
|
|
}
|