feat: click a coin row to view its trade history; edit transactions

Matches CoinMarketCap's flow: clicking anywhere on a coin's row in the
holdings table opens its transaction history (previously only a dedicated
icon button did). The table's own History icon is gone since the row now
does that job; the remaining "+"/delete icons get stopPropagation so they
don't also trigger the row click.

Adds the missing piece: transactions could be viewed and deleted but not
edited. New PATCH /crypto/transactions/{id} (partial update, never calls
CoinGecko, same oversell guard as creating a sell) backs a pencil button on
each history row that opens CryptoTransactionModal pre-filled — the same
dialog used for "+", now doing double duty as create/edit.

86/86 backend tests passing (4 new). Verified live against the real API:
create, edit quantity/note, 404 on unknown id, cleanup — real user data
untouched throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
This commit is contained in:
ZProger
2026-08-30 05:06:20 +03:00
parent dc9421e08a
commit c3309b9d60
12 changed files with 238 additions and 45 deletions

View File

@@ -11,7 +11,7 @@ import { CryptoTransactionModal } from "@/components/crypto/CryptoTransactionMod
import { useCryptoHistory, useCryptoHoldings, useDeleteCryptoHolding, useRefreshCryptoPrices } from "@/hooks/useCrypto";
import { getIntlLocale } from "@/lib/format";
import { useTranslation } from "@/lib/i18n";
import type { CryptoHolding, CryptoRange } from "@/types";
import type { CryptoHolding, CryptoRange, CryptoTransaction } from "@/types";
function formatSyncedAt(iso: string): string {
return new Intl.DateTimeFormat(getIntlLocale(), { dateStyle: "medium", timeStyle: "short" }).format(new Date(iso));
@@ -33,6 +33,11 @@ export function CryptoPage() {
const [addOpen, setAddOpen] = useState(false);
const [tradingHolding, setTradingHolding] = useState<CryptoHolding | null>(null);
const [historyHolding, setHistoryHolding] = useState<CryptoHolding | null>(null);
// Set together with tradingHolding when editing an entry from the history
// list — CryptoTransactionModal is the same dialog for both "+" (new
// trade, transaction stays null) and this (pre-filled, PATCHes instead of
// POSTing). Cleared whenever that dialog closes, same as tradingHolding.
const [editingTransaction, setEditingTransaction] = useState<CryptoTransaction | null>(null);
const holdings = data?.holdings ?? [];
const totalValue = holdings.reduce((sum, holding) => sum + (holding.value !== null ? Number(holding.value) : 0), 0);
@@ -43,6 +48,22 @@ export function CryptoPage() {
}
}
function handleEditTransaction(transaction: CryptoTransaction) {
setEditingTransaction(transaction);
setTradingHolding(historyHolding);
setHistoryHolding(null);
}
function closeTradeModal() {
setTradingHolding(null);
setEditingTransaction(null);
}
function openTradeModal(holding: CryptoHolding) {
setEditingTransaction(null);
setTradingHolding(holding);
}
return (
<div className="space-y-5">
<CryptoOverviewCard
@@ -89,7 +110,7 @@ export function CryptoPage() {
<CryptoHoldingsTable
items={holdings}
hidden={hidden}
onTrade={setTradingHolding}
onTrade={openTradeModal}
onViewHistory={setHistoryHolding}
onDelete={handleDelete}
/>
@@ -99,12 +120,18 @@ export function CryptoPage() {
</Card>
<CryptoAddModal open={addOpen} onClose={() => setAddOpen(false)} />
<CryptoTransactionModal open={tradingHolding !== null} onClose={() => setTradingHolding(null)} holding={tradingHolding} />
<CryptoTransactionModal
open={tradingHolding !== null}
onClose={closeTradeModal}
holding={tradingHolding}
transaction={editingTransaction}
/>
<CryptoTransactionHistoryModal
open={historyHolding !== null}
onClose={() => setHistoryHolding(null)}
holding={historyHolding}
hidden={hidden}
onEdit={handleEditTransaction}
/>
</div>
);