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)
|
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() {
|
function getBaseUrl() {
|
||||||
const base = BACKEND_URL?.replace(/\/$/, '') || '';
|
const base = BACKEND_URL?.replace(/\/$/, '') || '';
|
||||||
return base || '';
|
return base || '';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
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 AccountPage from './AccountPage';
|
||||||
import AppearancePage from './AppearancePage';
|
import AppearancePage from './AppearancePage';
|
||||||
import BalanceChart from './BalanceChart';
|
import BalanceChart from './BalanceChart';
|
||||||
@@ -161,9 +161,14 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
|||||||
|
|
||||||
// Manual forms moved to ManualManagement page
|
// 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 [editingTxId, setEditingTxId] = useState<number | null>(null);
|
||||||
const [editingCategoryIds, setEditingCategoryIds] = useState<number[]>([]);
|
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() {
|
async function loadAll() {
|
||||||
setLoading(true);
|
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 categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
|
||||||
|
|
||||||
|
|
||||||
function beginEditCategories(t: Transaction) {
|
function beginEditTransaction(t: Transaction) {
|
||||||
setEditingTxId(t.id);
|
setEditingTxId(t.id);
|
||||||
setEditingCategoryIds([...(t.category_ids || [])]);
|
setEditingCategoryIds([...(t.category_ids || [])]);
|
||||||
|
setEditingAmount(String(t.amount));
|
||||||
|
setEditingDescription(t.description || '');
|
||||||
|
setEditingDate(t.date || '');
|
||||||
}
|
}
|
||||||
function cancelEditCategories() {
|
function cancelEditTransaction() {
|
||||||
setEditingTxId(null);
|
setEditingTxId(null);
|
||||||
setEditingCategoryIds([]);
|
setEditingCategoryIds([]);
|
||||||
|
setEditingAmount('');
|
||||||
|
setEditingDescription('');
|
||||||
|
setEditingDate('');
|
||||||
}
|
}
|
||||||
async function saveEditCategories() {
|
async function saveEditTransaction() {
|
||||||
if (editingTxId == null) return;
|
if (editingTxId == null) return;
|
||||||
|
const amountNum = Number(editingAmount);
|
||||||
|
if (Number.isNaN(amountNum)) {
|
||||||
|
alert('Amount must be a number.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
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)));
|
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) {
|
} 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 style={{ textAlign: 'right' }}>Amount</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Categories</th>
|
<th>Categories</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{visible.map(t => (
|
{visible.map(t => (
|
||||||
<tr key={t.id}>
|
<tr key={t.id}>
|
||||||
<td>{t.date || ''}</td>
|
{/* Date cell */}
|
||||||
<td className="amount">{formatAmount(t.amount)}</td>
|
|
||||||
<td>{t.description || ''}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{editingTxId === t.id ? (
|
{editingTxId === t.id ? (
|
||||||
<div className="space-y" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<input
|
||||||
<select multiple className="input" value={editingCategoryIds.map(String)} onChange={(e) => {
|
className="input"
|
||||||
const opts = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
|
type="date"
|
||||||
setEditingCategoryIds(opts);
|
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 => (
|
{categories.map(c => (
|
||||||
<option key={c.id} value={c.id}>{c.name}</option>
|
<option key={c.id} value={c.id}>{c.name}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<button className="btn small" onClick={saveEditCategories}>Save</button>
|
|
||||||
<button className="btn small" onClick={cancelEditCategories}>Cancel</button>
|
|
||||||
</div>
|
</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>
|
||||||
<span>{t.category_ids.map(id => categoryNameById(id)).join(', ') || '—'}</span>
|
)}
|
||||||
<button className="btn small" onClick={() => beginEditCategories(t)}>Change</button>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user