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:
@@ -24,16 +24,7 @@ import {
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { adminApi, type ModerationQueueItem, type PaginatedResult } from '@/lib/admin-api';
|
||||
|
||||
function formatPrice(price: number): string {
|
||||
if (price >= 1_000_000_000) {
|
||||
return `${(price / 1_000_000_000).toFixed(1)} tỷ`;
|
||||
}
|
||||
if (price >= 1_000_000) {
|
||||
return `${(price / 1_000_000).toFixed(0)} triệu`;
|
||||
}
|
||||
return price.toLocaleString('vi-VN');
|
||||
}
|
||||
import { formatPrice } from '@/lib/currency';
|
||||
|
||||
function moderationScoreBadge(score: number | null) {
|
||||
if (score === null) return <Badge variant="secondary">N/A</Badge>;
|
||||
@@ -159,14 +150,14 @@ export default function AdminModerationPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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-2xl font-bold tracking-tight">Kiểm duyệt tin đăng</h1>
|
||||
<h1 className="text-xl font-bold tracking-tight sm:text-2xl">Kiểm duyệt tin đăng</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Duyệt hoặc từ chối các tin đăng chờ phê duyệt
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selected.size > 0 && (
|
||||
<>
|
||||
<Button
|
||||
@@ -224,6 +215,7 @@ export default function AdminModerationPage() {
|
||||
checked={selected.size === result.data.length && result.data.length > 0}
|
||||
onChange={toggleSelectAll}
|
||||
className="rounded border-input"
|
||||
aria-label="Chọn tất cả tin đăng"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>Tiêu đề</TableHead>
|
||||
@@ -244,6 +236,7 @@ export default function AdminModerationPage() {
|
||||
checked={selected.has(item.listingId)}
|
||||
onChange={() => toggleSelect(item.listingId)}
|
||||
className="rounded border-input"
|
||||
aria-label={`Chọn tin: ${item.propertyTitle}`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { adminApi, type DashboardStats, type RevenueStatsItem } from '@/lib/admin-api';
|
||||
import { formatVND } from '@/lib/currency';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
@@ -72,7 +73,7 @@ function RevenueChart({ data }: { data: RevenueStatsItem[] }) {
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">{item.period}</span>
|
||||
<span className="font-medium">
|
||||
{item.totalRevenue.toLocaleString('vi-VN')} VND
|
||||
{formatVND(item.totalRevenue)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-full bg-muted">
|
||||
@@ -82,8 +83,8 @@ function RevenueChart({ data }: { data: RevenueStatsItem[] }) {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-3 text-xs text-muted-foreground">
|
||||
<span>Subscription: {item.subscriptionRevenue.toLocaleString('vi-VN')}</span>
|
||||
<span>Listing fee: {item.listingFeeRevenue.toLocaleString('vi-VN')}</span>
|
||||
<span>Subscription: {formatVND(item.subscriptionRevenue)}</span>
|
||||
<span>Listing fee: {formatVND(item.listingFeeRevenue)}</span>
|
||||
<span>{item.transactionCount} GD</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user