fix(web): consolidate inline currency formatters into shared lib (GOO-205)

Remove 8 inline formatPrice/formatVND/formatPriceM2 functions scattered
across components and pages, replacing them with imports from
@/lib/currency. Add formatVNDFull (full locale, no compact notation) for
chuyen-nhuong pages. Fix price-history-chart off-by-1000 bug caused by
double-dividing through priceToMillions then formatMillions. Add k/m²
branch to formatPricePerM2 for sub-million values.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 14:17:32 +07:00
parent dfb398131d
commit e850ac48d7
10 changed files with 90 additions and 87 deletions

View File

@@ -10,6 +10,7 @@ import {
useUpdateSavedSearch,
} from '@/lib/hooks/use-saved-searches';
import { type SavedSearch, type SavedSearchFilters } from '@/lib/saved-search-api';
import { formatPrice } from '@/lib/currency';
const PROPERTY_TYPE_LABELS: Record<string, string> = {
APARTMENT: 'Chung cư',
@@ -25,12 +26,6 @@ const TRANSACTION_TYPE_LABELS: Record<string, string> = {
RENT: 'Cho thuê',
};
function formatPrice(value: string): string {
const num = Number(value);
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} tỷ`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`;
return num.toLocaleString('vi-VN');
}
function formatFilters(filters: SavedSearchFilters): string[] {
const parts: string[] = [];

View File

@@ -18,27 +18,12 @@ import {
useTrendingAreas,
} from '@/lib/hooks/use-analytics';
import { listingsApi, type ListingDetail } from '@/lib/listings-api';
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
const vndFmt = new Intl.NumberFormat('vi-VN', {
style: 'currency',
currency: 'VND',
maximumFractionDigits: 0,
});
function formatVnd(value: number): string {
if (value >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)} tỷ`;
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(0)} tr`;
return vndFmt.format(value);
}
function formatPriceM2(value: number): string {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)} tr/m²`;
return `${Math.round(value / 1000)}k/m²`;
}
function currentPeriod(): string {
const now = new Date();
@@ -138,7 +123,7 @@ function KpiStrip({ city }: { city: string }) {
<section className="mb-6 grid grid-cols-2 gap-3 md:grid-cols-4">
<KpiCard
label="GGI HCM"
value={data ? formatPriceM2(data.avgPricePerM2) : '—'}
value={data ? formatPricePerM2(data.avgPricePerM2) : '—'}
delta={data?.priceChangePct?.d7}
footnote="Chỉ số giá TB/m²"
icon={<BarChart3 className="h-3.5 w-3.5" />}
@@ -146,7 +131,7 @@ function KpiStrip({ city }: { city: string }) {
/>
<KpiCard
label="Giá TB"
value={data ? formatVnd(data.avgPrice) : '—'}
value={data ? formatPrice(data.avgPrice) : '—'}
delta={data?.priceChangePct?.d30}
footnote="Toàn thành phố"
icon={<Building2 className="h-3.5 w-3.5" />}
@@ -154,7 +139,7 @@ function KpiStrip({ city }: { city: string }) {
/>
<KpiCard
label="Giá trung vị"
value={data ? formatVnd(data.medianPrice) : '—'}
value={data ? formatPrice(data.medianPrice) : '—'}
footnote="Median price"
icon={<Layers className="h-3.5 w-3.5" />}
loading={isLoading}
@@ -352,7 +337,7 @@ function RecentListings() {
const price = Number(r.priceVND);
return (
<span className="font-mono text-sm font-semibold tabular-nums text-foreground">
{formatVnd(price)}
{formatPrice(price)}
</span>
);
},
@@ -367,7 +352,7 @@ function RecentListings() {
cell: (r) =>
r.pricePerM2 ? (
<span className="text-xs tabular-nums text-foreground-muted">
{formatPriceM2(r.pricePerM2)}
{formatPricePerM2(r.pricePerM2)}
</span>
) : (
<span className="text-foreground-dim"></span>
@@ -457,7 +442,7 @@ export default function MarketDashboardPage() {
{
id: 'price',
header: 'Giá TB/m²',
cell: (r) => formatPriceM2(r.avgPriceM2),
cell: (r) => formatPricePerM2(r.avgPriceM2),
align: 'right' as const,
numeric: true,
sortable: true,