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:
@@ -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[] = [];
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user