diff --git a/apps/web/app/(dashboard)/analytics/page.tsx b/apps/web/app/(dashboard)/analytics/page.tsx index 8e2436c..9012094 100644 --- a/apps/web/app/(dashboard)/analytics/page.tsx +++ b/apps/web/app/(dashboard)/analytics/page.tsx @@ -15,12 +15,12 @@ import { const DistrictBarChart = dynamic( () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), - { ssr: false, loading: () =>
Dang tai bieu do...
}, + { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const PriceTrendChart = dynamic( () => import('@/components/charts/price-trend-chart').then((mod) => mod.PriceTrendChart), - { ssr: false, loading: () =>
Dang tai bieu do...
}, + { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const CITIES = ['Ho Chi Minh', 'Ha Noi', 'Da Nang']; @@ -29,14 +29,14 @@ const TREND_PERIODS = ['2025-Q1', '2025-Q2', '2025-Q3', '2025-Q4', '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)} ty`; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; + 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/m2`; - return `${price.toLocaleString('vi-VN')} d/m2`; + if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`; + return `${price.toLocaleString('vi-VN')} đ/m²`; } function YoYBadge({ value }: { value: number | null }) { @@ -91,7 +91,7 @@ export default function AnalyticsPage() { setTrendDistrict(firstDistrict); } }) - .catch(() => setError('Khong the tai du lieu phan tich')) + .catch(() => setError('Không thể tải dữ liệu phân tích')) .finally(() => setLoading(false)); }, [city, period]); @@ -131,16 +131,16 @@ export default function AnalyticsPage() { const trendChartData = priceTrend.map((p) => ({ period: p.period, 'Gia/m2': Math.round(p.avgPriceM2 / 1_000_000), - 'Tin dang': p.totalListings, + 'Tin đăng': p.totalListings, })); return (
-

Phan tich thi truong

+

Phân tích thị trường

- Bao cao thi truong bat dong san - {period} + Báo cáo thị trường bất động sản - {period}

@@ -163,7 +163,7 @@ export default function AnalyticsPage() {
- Tong tin dang + Tổng tin đăng {loading ? '...' : totalListings.toLocaleString('vi-VN')} @@ -171,7 +171,7 @@ export default function AnalyticsPage() { - Gia TB/m2 + Giá TB/m² {loading ? '...' : formatPriceM2(avgPriceM2)} @@ -179,15 +179,15 @@ export default function AnalyticsPage() { - Ngay trung binh de ban + Ngày trung bình để bán - {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`} + {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`} - So quan/huyen + Số quận/huyện {loading ? '...' : new Set(marketReport.map((d) => d.district)).size} @@ -198,9 +198,9 @@ export default function AnalyticsPage() { {/* Tabs */} - Tong quan - Xu huong gia - Chi tiet quan + Tổng quan + Xu hướng giá + Chi tiết quận {/* Overview Tab */} @@ -209,17 +209,17 @@ export default function AnalyticsPage() { {/* Bar Chart - Price by District */} - Gia trung binh theo quan - Trieu VND/m2 tai {city} + Giá trung bình theo quận + Triệu VND/m² tại {city} {loading ? (
- Dang tai... + Đang tải...
) : barChartData.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : ( @@ -230,17 +230,17 @@ export default function AnalyticsPage() { {/* Heatmap - Card Grid */} - Ban do gia theo quan - So sanh gia trung binh/m2 tai {city} + Bản đồ giá theo quận + So sánh giá trung bình/m² tại {city} {loading ? (
- Dang tai... + Đang tải...
) : heatmap.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : (
@@ -267,7 +267,7 @@ export default function AnalyticsPage() { {formatPriceM2(point.avgPriceM2)}
- {point.totalListings} tin dang + {point.totalListings} tin đăng
); @@ -299,20 +299,20 @@ export default function AnalyticsPage() { - Xu huong gia - {trendDistrict || 'Chon quan'} + Xu hướng giá - {trendDistrict || 'Chọn quận'} - Bien dong gia trung binh/m2 qua cac quy (Can ho) + Biến động giá trung bình/m² qua các quý (Căn hộ) {trendLoading ? (
- Dang tai... + Đang tải...
) : trendChartData.length === 0 ? (
- Chua co du lieu xu huong + Chưa có dữ liệu xu hướng
) : ( @@ -328,31 +328,31 @@ export default function AnalyticsPage() { {/* Stats Table */} - Thong ke chi tiet theo quan + Thống kê chi tiết theo quận - Du lieu thi truong bat dong san tai {city} - {period} + Dữ liệu thị trường bất động sản tại {city} - {period} {loading ? (
- Dang tai... + Đang tải...
) : districtStats.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : (
- - - - - - + + + + + + @@ -391,17 +391,17 @@ export default function AnalyticsPage() { {/* Market Report Cards */} - Bao cao thi truong - Tong hop chi so thi truong theo tung quan + Báo cáo thị trường + Tổng hợp chỉ số thị trường theo từng quận {loading ? (
- Dang tai... + Đang tải...
) : marketReport.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : (
@@ -411,25 +411,25 @@ export default function AnalyticsPage() {

{district.district}

- Gia trung vi + Giá trung vị {formatPrice(district.medianPrice)}
- Gia/m2 + Giá/m² {formatPriceM2(district.avgPriceM2)}
- Tin dang + Tin đăng {district.totalListings}
- Ton kho + Tồn kho {district.inventoryLevel}
- Thay doi YoY + Thay đổi YoY
diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx index ef80e3d..07e40f8 100644 --- a/apps/web/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -16,7 +16,7 @@ import { listingsApi, type ListingDetail, type PaginatedResult } from '@/lib/lis const DistrictBarChart = dynamic( () => import('@/components/charts/district-bar-chart').then((mod) => mod.DistrictBarChart), - { ssr: false, loading: () =>
Dang tai bieu do...
}, + { ssr: false, loading: () =>
Đang tải biểu đồ...
}, ); const CITY = 'Ho Chi Minh'; @@ -24,14 +24,14 @@ 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)} ty`; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; + 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/m2`; - return `${price.toLocaleString('vi-VN')} d/m2`; + if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`; + return `${price.toLocaleString('vi-VN')} đ/m²`; } interface StatCardProps { @@ -124,35 +124,35 @@ export default function DashboardPage() {
-

Bang dieu khien

+

Bảng điều khiển

- Tong quan thi truong va tin dang cua ban + Tổng quan thị trường và tin đăng của bạn

- +
{/* Stats overview */}
- Gia trung binh theo quan - {CITY} - {PERIOD} (trieu VND/m2) + Giá trung bình theo quận + {CITY} - {PERIOD} (triệu VND/m²) {loading ? (
- Dang tai... + Đang tải...
) : chartData.length === 0 ? (
- Chua co du lieu + Chưa có dữ liệu
) : ( [`${value} tr/m2`, 'Gia']} + tooltipFormatter={(value) => [`${value} tr/m²`, 'Giá']} /> )}
@@ -190,30 +190,30 @@ export default function DashboardPage() { {/* Market summary */} - Thi truong {CITY} - Chi so chinh - {PERIOD} + Thị trường {CITY} + Chỉ số chính - {PERIOD}
- Tong tin dang + Tổng tin đăng {loading ? '...' : totalListings.toLocaleString('vi-VN')}
- Gia TB/m2 + Giá TB/m² {loading ? '...' : formatPriceM2(avgPriceM2)}
- Ngay TB de ban + Ngày TB để bán - {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngay`} + {loading ? '...' : `${avgDaysOnMarket.toFixed(0)} ngày`}
- So quan + Số quận {loading ? '...' : new Set(marketReport.map((d) => d.district)).size} @@ -221,7 +221,7 @@ export default function DashboardPage() {
@@ -233,26 +233,26 @@ export default function DashboardPage() {
- Tin dang gan day - Danh sach tin dang moi nhat cua ban + Tin đăng gần đây + Danh sách tin đăng mới nhất của bạn
{loading ? (
- Dang tai... + Đang tải...
) : !listings || listings.data.length === 0 ? (
-

Chua co tin dang nao

+

Chưa có tin đăng nào

@@ -292,8 +292,8 @@ export default function DashboardPage() {
- {listing.viewCount} luot xem - {listing.inquiryCount} lien he + {listing.viewCount} lượt xem + {listing.inquiryCount} liên hệ
))} diff --git a/apps/web/app/(dashboard)/listings/page.tsx b/apps/web/app/(dashboard)/listings/page.tsx index 4a46433..9f66c1b 100644 --- a/apps/web/app/(dashboard)/listings/page.tsx +++ b/apps/web/app/(dashboard)/listings/page.tsx @@ -16,8 +16,8 @@ import { import { PROPERTY_TYPES, TRANSACTION_TYPES, LISTING_STATUSES } from '@/lib/validations/listings'; function formatPrice(priceVND: string): string { const num = Number(priceVND); - if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)} ty`; - if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`; + 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'); } @@ -77,13 +77,13 @@ export default function ListingsPage() { {/* Header */}
-

Quan ly tin dang

+

Quản lý tin đăng

- Quan ly, theo doi va cap nhat cac tin dang cua ban + Quản lý, theo dõi và cập nhật các tin đăng của bạn

- +
@@ -91,13 +91,13 @@ export default function ListingsPage() {
- Tong tin dang + Tổng tin đăng {loading ? '...' : stats.total} - Dang hoat dong + Đang hoạt động {loading ? '...' : stats.active} @@ -105,7 +105,7 @@ export default function ListingsPage() { - Cho duyet + Chờ duyệt {loading ? '...' : stats.pending} @@ -113,7 +113,7 @@ export default function ListingsPage() { - Tong luot xem + Tổng lượt xem {loading ? '...' : stats.views} @@ -128,7 +128,7 @@ export default function ListingsPage() { } className="w-40" > - + {TRANSACTION_TYPES.map((t) => ( + {PROPERTY_TYPES.map((t) => ( + {Object.entries(LISTING_STATUSES).map(([key, { label }]) => (
@@ -187,10 +187,10 @@ export default function ListingsPage() {
) : !result || result.data.length === 0 ? (
-

Chua co tin dang nao

+

Chưa có tin đăng nào

@@ -228,7 +228,7 @@ export default function ListingsPage() {

- {listing.property.areaM2} m2 + {listing.property.areaM2} m² {listing.property.bedrooms != null && ( @@ -242,9 +242,9 @@ export default function ListingsPage() { )}
- {listing.viewCount} luot xem - {listing.inquiryCount} lien he - {listing.saveCount} da luu + {listing.viewCount} lượt xem + {listing.inquiryCount} liên hệ + {listing.saveCount} đã lưu
@@ -259,14 +259,14 @@ export default function ListingsPage() {
QuanLoai BDSGia trung viGia/m2Tin dangNgay banQuậnLoại BĐSGiá trung vịGiá/m²Tin đăngNgày bán YoY
- - - - - - - - + + + + + + + + @@ -311,7 +311,7 @@ export default function ListingsPage() { - + @@ -338,7 +338,7 @@ export default function ListingsPage() { disabled={filters.page <= 1} onClick={() => setFilters((f) => ({ ...f, page: f.page - 1 }))} > - Truoc + Trước Trang {result.page} / {result.totalPages} @@ -349,7 +349,7 @@ export default function ListingsPage() { disabled={filters.page >= result.totalPages} onClick={() => setFilters((f) => ({ ...f, page: f.page + 1 }))} > - Tiep + Tiếp )} diff --git a/apps/web/app/(public)/listings/[id]/page.tsx b/apps/web/app/(public)/listings/[id]/page.tsx index d153d7a..aa7e3c3 100644 --- a/apps/web/app/(public)/listings/[id]/page.tsx +++ b/apps/web/app/(public)/listings/[id]/page.tsx @@ -17,7 +17,7 @@ const ListingMap = dynamic( ssr: false, loading: () => (
-

Dang tai ban do...

+

Đang tải bản đồ...

), }, @@ -45,7 +45,7 @@ export default function PublicListingDetailPage() { listingsApi .getById(id) .then(setListing) - .catch((err) => setError(err instanceof Error ? err.message : 'Khong tai duoc tin dang')) + .catch((err) => setError(err instanceof Error ? err.message : 'Không tải được tin đăng')) .finally(() => setLoading(false)); }, [id]); @@ -74,9 +74,9 @@ export default function PublicListingDetailPage() { -

{error || 'Khong tim thay tin dang'}

+

{error || 'Không tìm thấy tin đăng'}

- + ); @@ -90,9 +90,9 @@ export default function PublicListingDetailPage() {
{/* Breadcrumb */} @@ -121,12 +121,12 @@ export default function PublicListingDetailPage() {

{formatPrice(listing.priceVND)} VND

{listing.pricePerM2 != null && (

- ~{listing.pricePerM2.toLocaleString('vi-VN')} VND/m2 + ~{listing.pricePerM2.toLocaleString('vi-VN')} VND/m²

)} {listing.rentPriceMonthly && (

- Thue: {formatPrice(listing.rentPriceMonthly)}/thang + Thuê: {formatPrice(listing.rentPriceMonthly)}/tháng

)}
@@ -137,18 +137,18 @@ export default function PublicListingDetailPage() { {/* Quick specs bar */}
- + {property.bedrooms != null && ( - + )} {property.bathrooms != null && ( - + )} {property.floors != null && ( - + )} {property.direction && ( - + )}
@@ -158,7 +158,7 @@ export default function PublicListingDetailPage() { {/* Description */} - Mo ta + Mô tả

{property.description}

@@ -168,19 +168,19 @@ export default function PublicListingDetailPage() { {/* Details */} - Thong tin chi tiet + Thông tin chi tiết
- - - - - - - - - + + + + + + + + +
@@ -189,7 +189,7 @@ export default function PublicListingDetailPage() { {property.amenities && property.amenities.length > 0 && ( - Tien ich + Tiện ích
@@ -206,7 +206,7 @@ export default function PublicListingDetailPage() { {/* Map */} - Vi tri tren ban do + Vị trí trên bản đồ - Lien he + Liên hệ
@@ -242,22 +242,22 @@ export default function PublicListingDetailPage() { - Goi ngay + Gọi ngay {agent && (
-

Moi gioi

+

Môi giới

{agent.agency &&

{agent.agency}

} {listing.commissionPct != null && ( -

Hoa hong: {listing.commissionPct}%

+

Hoa hồng: {listing.commissionPct}%

)}
)} @@ -270,20 +270,20 @@ export default function PublicListingDetailPage() {

{listing.viewCount}

-

Luot xem

+

Lượt xem

{listing.saveCount}

-

Luot luu

+

Lượt lưu

{listing.inquiryCount}

-

Lien he

+

Liên hệ

{listing.publishedAt && (

- Dang ngay {new Date(listing.publishedAt).toLocaleDateString('vi-VN')} + Đăng ngày {new Date(listing.publishedAt).toLocaleDateString('vi-VN')}

)} diff --git a/apps/web/app/(public)/search/page.tsx b/apps/web/app/(public)/search/page.tsx index 2310b3c..8b82a32 100644 --- a/apps/web/app/(public)/search/page.tsx +++ b/apps/web/app/(public)/search/page.tsx @@ -14,7 +14,7 @@ const ListingMap = dynamic( ssr: false, loading: () => (
-

Dang tai ban do...

+

Đang tải bản đồ...

), }, diff --git a/apps/web/components/charts/district-bar-chart.tsx b/apps/web/components/charts/district-bar-chart.tsx index 833fc11..5001e49 100644 --- a/apps/web/components/charts/district-bar-chart.tsx +++ b/apps/web/components/charts/district-bar-chart.tsx @@ -27,8 +27,8 @@ export function DistrictBarChart({ tooltipFormatter, }: DistrictBarChartProps) { const defaultFormatter: TooltipFormatter = (value, name) => [ - name === dataKey ? `${value} tr/m2` : String(value), - name === dataKey ? 'Gia' : 'Tin dang', + name === dataKey ? `${value} tr/m²` : String(value), + name === dataKey ? 'Giá' : 'Tin đăng', ]; return ( diff --git a/apps/web/components/charts/price-trend-chart.tsx b/apps/web/components/charts/price-trend-chart.tsx index afe9ee7..1c7dbe6 100644 --- a/apps/web/components/charts/price-trend-chart.tsx +++ b/apps/web/components/charts/price-trend-chart.tsx @@ -12,7 +12,7 @@ import { } from 'recharts'; interface PriceTrendChartProps { - data: { period: string; 'Gia/m2': number; 'Tin dang': number }[]; + data: { period: string; 'Gia/m2': number; 'Tin đăng': number }[]; height?: number; } @@ -37,7 +37,7 @@ export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) { fontSize: '0.875rem', }} formatter={(value, name) => [ - name === 'Gia/m2' ? `${value} tr/m2` : value, + name === 'Gia/m2' ? `${value} tr/m²` : value, name, ]} /> @@ -54,7 +54,7 @@ export function PriceTrendChart({ data, height = 350 }: PriceTrendChartProps) {
Tin dangLoaiGiaDien tichTrang thaiLuot xemLien heNgay dangTin đăngLoạiGiáDiện tíchTrạng tháiLượt xemLiên hệNgày đăng
{formatPrice(listing.priceVND)} {listing.property.areaM2} m2{listing.property.areaM2} m²