refactor(web): dedup tỷ/triệu compact formatters (GOO-206)

- Add `formatCompact` as an exported alias for `formatPrice` in lib/currency.ts
- Replace 5 inline copies of the tỷ/triệu compact formatter:
  - components/map/listing-map.tsx (local `formatPrice` fn)
  - components/agents/agent-profile-client.tsx (local `fmtVND` fn)
  - app/(dashboard)/dashboard/saved-searches/page.tsx (local `formatPrice` fn)
  - app/(public)/page.tsx (local `formatVnd` fn + `vndFmt` Intl instance)
  - components/listings/price-history-chart.tsx (local `formatMillions` + `priceToMillions`)

All call sites now import from the canonical lib/currency module.
PriceHistoryChart now stores raw VND in chart data (was: millions) so
formatCompact emits correct tỷ/triệu labels using canonical thresholds.

Pre-existing test failures in inquiry/lead/AVM specs are unrelated to this change.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 12:37:43 +07:00
parent 05a629cf21
commit 865a28009f
20 changed files with 878 additions and 53 deletions

View File

@@ -39,6 +39,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Link } from '@/i18n/navigation';
import type { AgentPublicProfile, AgentReviewItem } from '@/lib/agents-api';
import { formatCompact } from '@/lib/currency';
import { shimmerBlurDataURL } from '@/lib/image-blur';
import type { ListingDetail } from '@/lib/listings-api';
@@ -48,12 +49,6 @@ import type { ListingDetail } from '@/lib/listings-api';
const VND = new Intl.NumberFormat('vi-VN');
function fmtVND(value: string | number | bigint): string {
const n = typeof value === 'bigint' ? Number(value) : Number(value);
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)} tỷ`;
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(0)} tr`;
return VND.format(n);
}
function qualityLabel(score: number): string {
if (score >= 80) return 'Xuất sắc';
@@ -145,7 +140,7 @@ const listingColumns: DataTableColumn<ListingDetail>[] = [
sortValue: (row) => Number(row.priceVND),
cell: (row) => (
<span className="text-xs font-semibold tabular-nums text-primary">
{fmtVND(row.priceVND)}
{formatCompact(row.priceVND)}
</span>
),
width: '12%',
@@ -160,7 +155,7 @@ const listingColumns: DataTableColumn<ListingDetail>[] = [
cell: (row) =>
row.pricePerM2 != null ? (
<span className="text-xs tabular-nums text-foreground-muted">
{fmtVND(row.pricePerM2)}
{formatCompact(row.pricePerM2)}
</span>
) : (
<span className="text-xs text-foreground-dim"></span>
@@ -384,7 +379,7 @@ export function AgentProfileClient({
/>
<KpiCard
label="Giá TB"
value={avgPriceVND != null ? fmtVND(avgPriceVND) : '—'}
value={avgPriceVND != null ? formatCompact(avgPriceVND) : '—'}
icon={<BarChart2 className="h-4 w-4" />}
footnote="Danh mục hiện tại"
/>