mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 06:57:47 +01:00
feat(frontend): Added options for modifying and deleting transactions in the UI
This commit is contained in:
@@ -19,6 +19,17 @@ export type Transaction = {
|
||||
date?: string | null; // ISO date (YYYY-MM-DD)
|
||||
};
|
||||
|
||||
export async function deleteTransaction(id: number): Promise<void> {
|
||||
const res = await fetch(`${getBaseUrl()}/transactions/${id}/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: getHeaders('none'),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || 'Failed to delete transaction');
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseUrl() {
|
||||
const base = BACKEND_URL?.replace(/\/$/, '') || '';
|
||||
return base || '';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { type Category, type Transaction, type BalancePoint, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
|
||||
import { type Category, type Transaction, type BalancePoint, deleteTransaction, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
|
||||
import AccountPage from './AccountPage';
|
||||
import AppearancePage from './AppearancePage';
|
||||
import BalanceChart from './BalanceChart';
|
||||
@@ -161,9 +161,14 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
|
||||
// Manual forms moved to ManualManagement page
|
||||
|
||||
// Inline edit state for transaction categories
|
||||
// Inline edit state for transaction editing
|
||||
const [editingTxId, setEditingTxId] = useState<number | null>(null);
|
||||
const [editingCategoryIds, setEditingCategoryIds] = useState<number[]>([]);
|
||||
const [editingAmount, setEditingAmount] = useState<string>('');
|
||||
const [editingDescription, setEditingDescription] = useState<string>('');
|
||||
const [editingDate, setEditingDate] = useState<string>(''); // YYYY-MM-DD
|
||||
|
||||
|
||||
|
||||
async function loadAll() {
|
||||
setLoading(true);
|
||||
@@ -259,22 +264,50 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
|
||||
|
||||
|
||||
function beginEditCategories(t: Transaction) {
|
||||
function beginEditTransaction(t: Transaction) {
|
||||
setEditingTxId(t.id);
|
||||
setEditingCategoryIds([...(t.category_ids || [])]);
|
||||
setEditingAmount(String(t.amount));
|
||||
setEditingDescription(t.description || '');
|
||||
setEditingDate(t.date || '');
|
||||
}
|
||||
function cancelEditCategories() {
|
||||
function cancelEditTransaction() {
|
||||
setEditingTxId(null);
|
||||
setEditingCategoryIds([]);
|
||||
setEditingAmount('');
|
||||
setEditingDescription('');
|
||||
setEditingDate('');
|
||||
}
|
||||
async function saveEditCategories() {
|
||||
async function saveEditTransaction() {
|
||||
if (editingTxId == null) return;
|
||||
const amountNum = Number(editingAmount);
|
||||
if (Number.isNaN(amountNum)) {
|
||||
alert('Amount must be a number.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const updated = await updateTransaction(editingTxId, { category_ids: editingCategoryIds });
|
||||
const updated = await updateTransaction(editingTxId, {
|
||||
amount: amountNum,
|
||||
description: editingDescription,
|
||||
date: editingDate || undefined,
|
||||
category_ids: editingCategoryIds,
|
||||
});
|
||||
setTransactions(prev => prev.map(p => (p.id === updated.id ? updated : p)));
|
||||
cancelEditCategories();
|
||||
// Optionally refresh balance series to reflect changes immediately
|
||||
try { setBalanceSeries(await getBalanceSeries(startDate || undefined, endDate || undefined)); } catch {}
|
||||
cancelEditTransaction();
|
||||
} catch (err: any) {
|
||||
alert(err?.message || 'Failed to update transaction categories');
|
||||
alert(err?.message || 'Failed to update transaction');
|
||||
}
|
||||
}
|
||||
async function handleDeleteTransaction(id: number) {
|
||||
if (!confirm('Delete this transaction? This cannot be undone.')) return;
|
||||
try {
|
||||
await deleteTransaction(id);
|
||||
setTransactions(prev => prev.filter(t => t.id !== id));
|
||||
try { setBalanceSeries(await getBalanceSeries(startDate || undefined, endDate || undefined)); } catch {}
|
||||
} catch (err: any) {
|
||||
alert(err?.message || 'Failed to delete transaction');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,32 +416,91 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
<th style={{ textAlign: 'right' }}>Amount</th>
|
||||
<th>Description</th>
|
||||
<th>Categories</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map(t => (
|
||||
<tr key={t.id}>
|
||||
<td>{t.date || ''}</td>
|
||||
<td className="amount">{formatAmount(t.amount)}</td>
|
||||
<td>{t.description || ''}</td>
|
||||
{/* Date cell */}
|
||||
<td>
|
||||
{editingTxId === t.id ? (
|
||||
<div className="space-y" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<select multiple className="input" value={editingCategoryIds.map(String)} onChange={(e) => {
|
||||
const opts = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
|
||||
setEditingCategoryIds(opts);
|
||||
}}>
|
||||
<input
|
||||
className="input"
|
||||
type="date"
|
||||
value={editingDate}
|
||||
onChange={(e) => setEditingDate(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
t.date || ''
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Amount cell */}
|
||||
<td className="amount" style={{ textAlign: 'right' }}>
|
||||
{editingTxId === t.id ? (
|
||||
<input
|
||||
className="input"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value={editingAmount}
|
||||
onChange={(e) => setEditingAmount(e.target.value)}
|
||||
style={{ textAlign: 'right' }}
|
||||
/>
|
||||
) : (
|
||||
formatAmount(t.amount)
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Description cell */}
|
||||
<td>
|
||||
{editingTxId === t.id ? (
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={editingDescription}
|
||||
onChange={(e) => setEditingDescription(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
t.description || ''
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Categories cell */}
|
||||
<td>
|
||||
{editingTxId === t.id ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<select
|
||||
multiple
|
||||
className="input"
|
||||
value={editingCategoryIds.map(String)}
|
||||
onChange={(e) => {
|
||||
const opts = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
|
||||
setEditingCategoryIds(opts);
|
||||
}}
|
||||
>
|
||||
{categories.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="btn small" onClick={saveEditCategories}>Save</button>
|
||||
<button className="btn small" onClick={cancelEditCategories}>Cancel</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-x" style={{ display: 'flex', alignItems: 'center', gap: 8, justifyContent: 'space-between' }}>
|
||||
<span>{t.category_ids.map(id => categoryNameById(id)).join(', ') || '—'}</span>
|
||||
<button className="btn small" onClick={() => beginEditCategories(t)}>Change</button>
|
||||
<span>{t.category_ids.map(id => categoryNameById(id)).join(', ') || '—'}</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Actions cell */}
|
||||
<td>
|
||||
{editingTxId === t.id ? (
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button className="btn small" onClick={saveEditTransaction}>Save</button>
|
||||
<button className="btn small" onClick={cancelEditTransaction}>Cancel</button>
|
||||
<button className="btn small" onClick={() => handleDeleteTransaction(t.id)}>Delete</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
||||
<button className="btn small" onClick={() => beginEditTransaction(t)}>Edit</button>
|
||||
<button className="btn small" onClick={() => handleDeleteTransaction(t.id)}>Delete</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user