feat(web): centralise Vietnamese price formatting across all pages
Create a single `currency.ts` utility with `formatPrice`, `formatVND`, `formatPricePerM2`, and `parseVND` to replace 9+ duplicated inline formatters. This fixes inconsistent decimal handling (1.5M was truncated to "1 triệu") and standardises price/m² display. Integrated across property cards, listing detail, dashboard, analytics, payments, pricing, and admin moderation pages with 19 new unit tests. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -6,6 +6,7 @@ import Link from 'next/link';
|
||||
import { ListingStatusBadge } from '@/components/listings/listing-status-badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
|
||||
import { useMarketReport, useHeatmap } from '@/lib/hooks/use-analytics';
|
||||
import { useListingsSearch } from '@/lib/hooks/use-listings';
|
||||
|
||||
@@ -17,18 +18,6 @@ const DistrictBarChart = dynamic(
|
||||
const CITY = 'Ho Chi Minh';
|
||||
const PERIOD = '2026-Q1';
|
||||
|
||||
function formatPrice(priceStr: string): string {
|
||||
const num = Number(priceStr);
|
||||
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 formatPriceM2(price: number): string {
|
||||
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`;
|
||||
return `${price.toLocaleString('vi-VN')} đ/m²`;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
@@ -105,9 +94,9 @@ export default function DashboardPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Bảng điều khiển</h1>
|
||||
<h1 className="text-2xl font-bold sm:text-3xl">Bảng điều khiển</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Tổng quan thị trường và tin đăng của bạn
|
||||
</p>
|
||||
@@ -136,7 +125,7 @@ export default function DashboardPage() {
|
||||
/>
|
||||
<StatCard
|
||||
title="Giá TB thị trường"
|
||||
value={loading ? '...' : formatPriceM2(avgPriceM2)}
|
||||
value={loading ? '...' : formatPricePerM2(avgPriceM2)}
|
||||
trend={avgYoy}
|
||||
description="YoY"
|
||||
/>
|
||||
@@ -186,7 +175,7 @@ export default function DashboardPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">Giá TB/m²</span>
|
||||
<span className="font-semibold">
|
||||
{loading ? '...' : formatPriceM2(avgPriceM2)}
|
||||
{loading ? '...' : formatPricePerM2(avgPriceM2)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -214,7 +203,7 @@ export default function DashboardPage() {
|
||||
|
||||
{/* Recent listings */}
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardHeader className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Tin đăng gần đây</CardTitle>
|
||||
<CardDescription>Danh sách tin đăng mới nhất của bạn</CardDescription>
|
||||
|
||||
Reference in New Issue
Block a user