mirror of
https://github.com/dat515-2025/Group-8.git
synced 2026-03-22 06:57:47 +01:00
feat(frontend): implemented multiple transaction selections in UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { type Category, type Transaction, type BalancePoint, deleteTransaction, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api';
|
||||
import AccountPage from './AccountPage';
|
||||
import AppearancePage from './AppearancePage';
|
||||
@@ -168,7 +168,14 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
// Sidebar toggle for mobile
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
|
||||
// Multi-select state for transactions and bulk category assignment
|
||||
const [selectedTxIds, setSelectedTxIds] = useState<number[]>([]);
|
||||
const [bulkCategoryIds, setBulkCategoryIds] = useState<number[]>([]);
|
||||
const toggleSelectTx = useCallback((id: number) => {
|
||||
setSelectedTxIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
|
||||
}, []);
|
||||
const clearSelection = useCallback(() => setSelectedTxIds([]), []);
|
||||
const selectAllVisible = useCallback((ids: number[]) => setSelectedTxIds(ids), []);
|
||||
|
||||
async function loadAll() {
|
||||
setLoading(true);
|
||||
@@ -241,7 +248,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { loadAll(); }, [startDate, endDate]);
|
||||
useEffect(() => { loadAll(); clearSelection(); }, [startDate, endDate]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let arr = [...transactions];
|
||||
@@ -267,6 +274,9 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
const pageEnd = pageStart + pageSize;
|
||||
const visible = sortedDesc.slice(pageStart, pageEnd);
|
||||
|
||||
// Reset selection when page or filters impacting visible set change
|
||||
useEffect(() => { clearSelection(); }, [page, minAmount, maxAmount, filterCategoryId, searchText]);
|
||||
|
||||
function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; }
|
||||
|
||||
|
||||
@@ -416,7 +426,55 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
<div className="muted">
|
||||
Showing {visible.length} of {filtered.length} (page {Math.min(page + 1, Math.max(1, totalPages))}/{Math.max(1, totalPages)})
|
||||
</div>
|
||||
<div className="actions">
|
||||
<div className="actions" style={{ gap: 8, alignItems: 'center' }}>
|
||||
{selectedTxIds.length > 0 && (
|
||||
<>
|
||||
<span className="muted">Selected: {selectedTxIds.length}</span>
|
||||
<select
|
||||
className="input"
|
||||
multiple
|
||||
value={bulkCategoryIds.map(String)}
|
||||
onChange={(e) => {
|
||||
const ids = Array.from(e.currentTarget.selectedOptions).map(o => Number(o.value));
|
||||
setBulkCategoryIds(ids);
|
||||
}}
|
||||
>
|
||||
{categories.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn primary"
|
||||
onClick={async () => {
|
||||
if (bulkCategoryIds.length === 0) {
|
||||
alert('Pick at least one category to assign.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Apply selected categories to each selected transaction, replacing their categories
|
||||
const updates = await Promise.allSettled(
|
||||
selectedTxIds.map(id => updateTransaction(id, { category_ids: bulkCategoryIds }))
|
||||
);
|
||||
const fulfilled = updates.filter(u => u.status === 'fulfilled') as PromiseFulfilledResult<Transaction>[];
|
||||
const updatedById = new Map<number, Transaction>(fulfilled.map(f => [f.value.id, f.value]));
|
||||
setTransactions(prev => prev.map(t => updatedById.get(t.id) || t));
|
||||
try { setBalanceSeries(await getBalanceSeries(startDate || undefined, endDate || undefined)); } catch {}
|
||||
if (fulfilled.length !== selectedTxIds.length) {
|
||||
alert(`Assigned categories to ${fulfilled.length} of ${selectedTxIds.length} selected transactions. Some updates failed.`);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert(e?.message || 'Failed to assign categories');
|
||||
} finally {
|
||||
clearSelection();
|
||||
setBulkCategoryIds([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Apply categories to selected
|
||||
</button>
|
||||
<button className="btn" onClick={clearSelection}>Clear selection</button>
|
||||
</>
|
||||
)}
|
||||
<button className="btn primary" disabled={page <= 0} onClick={() => setPage(p => Math.max(0, p - 1))}>Previous</button>
|
||||
<button className="btn primary" disabled={page >= totalPages - 1} onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}>Next</button>
|
||||
</div>
|
||||
@@ -424,6 +482,22 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
<table className="table responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: 36 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="Select all on page"
|
||||
checked={visible.length > 0 && visible.every(v => selectedTxIds.includes(v.id))}
|
||||
indeterminate={(visible.some(v => selectedTxIds.includes(v.id)) && !visible.every(v => selectedTxIds.includes(v.id))) as any}
|
||||
onChange={(e) => {
|
||||
if (e.currentTarget.checked) {
|
||||
selectAllVisible(visible.map(v => v.id));
|
||||
} else {
|
||||
// remove only currently visible from selection
|
||||
setSelectedTxIds(prev => prev.filter(id => !visible.some(v => v.id === id)));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th>Date</th>
|
||||
<th style={{ textAlign: 'right' }}>Amount</th>
|
||||
<th>Description</th>
|
||||
@@ -433,7 +507,15 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map(t => (
|
||||
<tr key={t.id}>
|
||||
<tr key={t.id} style={{ backgroundColor: selectedTxIds.includes(t.id) ? 'rgba(88, 136, 255, 0.1)' : undefined }}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Select transaction ${t.id}`}
|
||||
checked={selectedTxIds.includes(t.id)}
|
||||
onChange={() => toggleSelectTx(t.id)}
|
||||
/>
|
||||
</td>
|
||||
{/* Date cell */}
|
||||
<td data-label="Date">
|
||||
{editingTxId === t.id ? (
|
||||
|
||||
Reference in New Issue
Block a user