fix: копеечные цены крипты не округлялись в $0

This commit is contained in:
ZProger
2026-09-04 02:37:09 +03:00
parent bd97e316cc
commit c0f446b3b3
6 changed files with 35 additions and 12 deletions

View File

@@ -7,7 +7,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
# title, /api/health (which the frontend reads to show it in Settings), and # title, /api/health (which the frontend reads to show it in Settings), and
# embedded in exported backups so an old file can be told apart from a # embedded in exported backups so an old file can be told apart from a
# current one. # current one.
APP_VERSION = "1.1.4" APP_VERSION = "1.1.5"
class Settings(BaseSettings): class Settings(BaseSettings):

View File

@@ -1,7 +1,7 @@
{ {
"name": "aurum-frontend", "name": "aurum-frontend",
"private": true, "private": true,
"version": "1.1.4", "version": "1.1.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -1,5 +1,5 @@
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts";
import { formatCurrency, maskAmount } from "@/lib/format"; import { formatCryptoAmount, maskAmount } from "@/lib/format";
import { useTranslation } from "@/lib/i18n"; import { useTranslation } from "@/lib/i18n";
import type { CryptoHolding } from "@/types"; import type { CryptoHolding } from "@/types";
@@ -65,7 +65,7 @@ function DonutTooltip({
<div className="rounded-lg border border-border bg-surface-1 px-3 py-2 text-sm shadow-md"> <div className="rounded-lg border border-border bg-surface-1 px-3 py-2 text-sm shadow-md">
<p className="font-medium text-text-primary">{slice.key === "other" ? otherLabel : slice.name}</p> <p className="font-medium text-text-primary">{slice.key === "other" ? otherLabel : slice.name}</p>
<p className="text-text-muted"> <p className="text-text-muted">
{maskAmount(formatCurrency(slice.amount), hidden)} · {slice.percent.toFixed(1)}% {maskAmount(formatCryptoAmount(slice.amount), hidden)} · {slice.percent.toFixed(1)}%
</p> </p>
</div> </div>
); );
@@ -132,7 +132,7 @@ export function CryptoAllocationBody({ holdings, isLoading, hidden }: CryptoAllo
<td className="py-1.5 pr-3 text-right font-medium tabular-nums text-text-primary"> <td className="py-1.5 pr-3 text-right font-medium tabular-nums text-text-primary">
{slice.percent.toFixed(1)}% {slice.percent.toFixed(1)}%
</td> </td>
<td className="py-1.5 text-right tabular-nums text-text-muted">{maskAmount(formatCurrency(slice.amount), hidden)}</td> <td className="py-1.5 text-right tabular-nums text-text-muted">{maskAmount(formatCryptoAmount(slice.amount), hidden)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -1,5 +1,5 @@
import { Bitcoin, Plus, Trash2 } from "lucide-react"; import { Bitcoin, Plus, Trash2 } from "lucide-react";
import { formatCurrency, maskAmount } from "@/lib/format"; import { formatCryptoAmount, maskAmount } from "@/lib/format";
import { useTranslation } from "@/lib/i18n"; import { useTranslation } from "@/lib/i18n";
import type { CryptoHolding, CryptoPortfolio } from "@/types"; import type { CryptoHolding, CryptoPortfolio } from "@/types";
@@ -106,7 +106,7 @@ export function CryptoHoldingsTable({
</td> </td>
<td className="py-3 pr-3 text-right tabular-nums text-text-primary"> <td className="py-3 pr-3 text-right tabular-nums text-text-primary">
{holding.current_price !== null {holding.current_price !== null
? maskAmount(formatCurrency(holding.current_price), hidden) ? maskAmount(formatCryptoAmount(holding.current_price), hidden)
: t("crypto.pendingPrice")} : t("crypto.pendingPrice")}
</td> </td>
<td className="py-3 pr-3 text-right"> <td className="py-3 pr-3 text-right">
@@ -120,20 +120,20 @@ export function CryptoHoldingsTable({
</td> </td>
<td className="py-3 pr-3 text-right"> <td className="py-3 pr-3 text-right">
<span className="block tabular-nums text-text-primary"> <span className="block tabular-nums text-text-primary">
{holding.value !== null ? maskAmount(formatCurrency(holding.value), hidden) : t("crypto.pendingPrice")} {holding.value !== null ? maskAmount(formatCryptoAmount(holding.value), hidden) : t("crypto.pendingPrice")}
</span> </span>
<span className="block text-xs tabular-nums text-text-muted"> <span className="block text-xs tabular-nums text-text-muted">
{maskAmount(`${Number(holding.quantity)} ${holding.symbol}`, hidden)} {maskAmount(`${Number(holding.quantity)} ${holding.symbol}`, hidden)}
</span> </span>
</td> </td>
<td className="py-3 pr-3 text-right tabular-nums text-text-primary"> <td className="py-3 pr-3 text-right tabular-nums text-text-primary">
{holding.avg_buy_price !== null ? maskAmount(formatCurrency(holding.avg_buy_price), hidden) : "—"} {holding.avg_buy_price !== null ? maskAmount(formatCryptoAmount(holding.avg_buy_price), hidden) : "—"}
</td> </td>
<td className="py-3 pr-3 text-right"> <td className="py-3 pr-3 text-right">
{holding.profit_loss !== null && holding.profit_loss_percent !== null ? ( {holding.profit_loss !== null && holding.profit_loss_percent !== null ? (
<> <>
<span className="block tabular-nums" style={{ color: profitColor }}> <span className="block tabular-nums" style={{ color: profitColor }}>
{maskAmount(`${sign}${formatCurrency(holding.profit_loss)}`, hidden)} {maskAmount(`${sign}${formatCryptoAmount(holding.profit_loss)}`, hidden)}
</span> </span>
<span className="block text-xs tabular-nums" style={{ color: profitColor }}> <span className="block text-xs tabular-nums" style={{ color: profitColor }}>
{sign} {sign}

View File

@@ -1,7 +1,7 @@
import { Pencil, Trash2 } from "lucide-react"; import { Pencil, Trash2 } from "lucide-react";
import { Dialog } from "@/components/ui/Dialog"; import { Dialog } from "@/components/ui/Dialog";
import { useCryptoTransactions, useDeleteCryptoTransaction } from "@/hooks/useCrypto"; import { useCryptoTransactions, useDeleteCryptoTransaction } from "@/hooks/useCrypto";
import { formatCurrency, formatTransactionDate, maskAmount } from "@/lib/format"; import { formatCryptoAmount, formatTransactionDate, maskAmount } from "@/lib/format";
import { useTranslation } from "@/lib/i18n"; import { useTranslation } from "@/lib/i18n";
import type { CryptoHolding, CryptoTransaction } from "@/types"; import type { CryptoHolding, CryptoTransaction } from "@/types";
@@ -47,7 +47,7 @@ export function CryptoTransactionHistoryModal({ open, onClose, holding, hidden,
</span> </span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block text-sm text-text-primary"> <span className="block text-sm text-text-primary">
{maskAmount(`${Number(tx.quantity)} ${holding.symbol} · ${formatCurrency(tx.price_per_unit)}`, hidden)} {maskAmount(`${Number(tx.quantity)} ${holding.symbol} · ${formatCryptoAmount(tx.price_per_unit)}`, hidden)}
</span> </span>
<span className="block text-xs text-text-muted"> <span className="block text-xs text-text-muted">
{formatTransactionDate(tx.date, true)} {formatTransactionDate(tx.date, true)}

View File

@@ -17,6 +17,29 @@ export function formatCurrency(amount: number | string, currency: string = getCu
}).format(value); }).format(value);
} }
/** Same currency formatting as formatCurrency, but scales decimal precision
* down to the value's own magnitude instead of always rounding to whole
* units — a low-cap memecoin can genuinely price at $0.000000006894, and
* formatCurrency's fixed 0 fraction digits would render that as "$0",
* indistinguishable from actually being worthless. Values >= 1 still show
* just 2 decimals (a coin price doesn't need more than cents once it's
* above a dollar). For the Crypto tab's per-coin price/holdings/avg-buy-price
* cells and per-transaction price — anywhere a single coin's own value
* needs to be told apart from zero, not just a portfolio-wide total. */
export function formatCryptoAmount(amount: number | string, currency: string = getCurrency()): string {
const value = typeof amount === "string" ? Number(amount) : amount;
const abs = Math.abs(value);
const maximumFractionDigits =
abs === 0 || abs >= 1
? 2
: // Leading zeros right after the decimal point before the first
// significant digit, plus 4 more digits of real precision beyond
// that — e.g. 0.000000006894 has 8 leading zeros, so this shows
// 12 decimal places, landing exactly on "6894" and nothing more.
Math.min(20, Math.max(0, -Math.floor(Math.log10(abs)) - 1) + 4);
return new Intl.NumberFormat(getIntlLocale(), { style: "currency", currency, maximumFractionDigits }).format(value);
}
export function formatSignedCurrency(amount: number | string, currency: string = getCurrency()): string { export function formatSignedCurrency(amount: number | string, currency: string = getCurrency()): string {
const value = typeof amount === "string" ? Number(amount) : amount; const value = typeof amount === "string" ? Number(amount) : amount;
const sign = value > 0 ? "+" : ""; const sign = value > 0 ? "+" : "";