From ccfc176e4069839e933b0498fbd442dbaaaf3be4 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Mon, 13 Apr 2026 12:03:47 +0700 Subject: [PATCH] fix: valuation page Vietnamese diacritics, correct API routes, update tests - Add proper Vietnamese diacritics to all valuation components (form, results, history) and their test assertions - Fix valuation API client to use /analytics/valuation endpoint - Return empty history gracefully (no server endpoint yet) Co-Authored-By: Claude Opus 4 (1M context) --- .../(dashboard)/dashboard/valuation/page.tsx | 6 ++-- .../__tests__/valuation-form.spec.tsx | 32 +++++++++---------- .../__tests__/valuation-history.spec.tsx | 32 +++++++++---------- .../__tests__/valuation-results.spec.tsx | 20 ++++++------ .../components/valuation/valuation-form.tsx | 32 +++++++++---------- .../valuation/valuation-history.tsx | 28 ++++++++-------- .../valuation/valuation-results.tsx | 32 +++++++++---------- apps/web/lib/valuation-api.ts | 25 ++++++++++----- 8 files changed, 108 insertions(+), 99 deletions(-) diff --git a/apps/web/app/[locale]/(dashboard)/dashboard/valuation/page.tsx b/apps/web/app/[locale]/(dashboard)/dashboard/valuation/page.tsx index fe6ec6d..24b562d 100644 --- a/apps/web/app/[locale]/(dashboard)/dashboard/valuation/page.tsx +++ b/apps/web/app/[locale]/(dashboard)/dashboard/valuation/page.tsx @@ -34,9 +34,9 @@ export default function ValuationPage() { return (
-

Dinh gia AI

+

Định giá AI

- Su dung AI de uoc tinh gia tri bat dong san dua tren du lieu thi truong + Sử dụng AI để ước tính giá trị bất động sản dựa trên dữ liệu thị trường

@@ -50,7 +50,7 @@ export default function ValuationPage() { {predictMutation.isError && (
- Khong the dinh gia. Vui long thu lai sau. + Không thể định giá. Vui lòng thử lại sau.
)} diff --git a/apps/web/components/valuation/__tests__/valuation-form.spec.tsx b/apps/web/components/valuation/__tests__/valuation-form.spec.tsx index fa5911d..3a485f8 100644 --- a/apps/web/components/valuation/__tests__/valuation-form.spec.tsx +++ b/apps/web/components/valuation/__tests__/valuation-form.spec.tsx @@ -26,65 +26,65 @@ vi.mock('@/lib/validations/valuation', () => ({ describe('ValuationForm', () => { it('renders form title', () => { render(); - expect(screen.getByText('Dinh gia bat dong san')).toBeInTheDocument(); + expect(screen.getByText('Định giá bất động sản')).toBeInTheDocument(); }); it('renders property type select', () => { render(); - expect(screen.getByLabelText('Loai bat dong san *')).toBeInTheDocument(); + expect(screen.getByLabelText('Loại bất động sản *')).toBeInTheDocument(); }); it('renders city select', () => { render(); - expect(screen.getByLabelText('Tinh/Thanh pho *')).toBeInTheDocument(); + expect(screen.getByLabelText('Tỉnh/Thành phố *')).toBeInTheDocument(); }); it('renders district input', () => { render(); - expect(screen.getByLabelText('Quan/Huyen *')).toBeInTheDocument(); + expect(screen.getByLabelText('Quận/Huyện *')).toBeInTheDocument(); }); it('renders area input', () => { render(); - expect(screen.getByLabelText('Dien tich (m2) *')).toBeInTheDocument(); + expect(screen.getByLabelText('Diện tích (m²) *')).toBeInTheDocument(); }); it('renders bedroom, bathroom, floors inputs', () => { render(); - expect(screen.getByLabelText('Phong ngu')).toBeInTheDocument(); - expect(screen.getByLabelText('Phong tam')).toBeInTheDocument(); - expect(screen.getByLabelText('So tang')).toBeInTheDocument(); + expect(screen.getByLabelText('Phòng ngủ')).toBeInTheDocument(); + expect(screen.getByLabelText('Phòng tắm')).toBeInTheDocument(); + expect(screen.getByLabelText('Số tầng')).toBeInTheDocument(); }); it('renders frontage and road width inputs', () => { render(); - expect(screen.getByLabelText('Mat tien (m)')).toBeInTheDocument(); - expect(screen.getByLabelText('Do rong duong (m)')).toBeInTheDocument(); + expect(screen.getByLabelText('Mặt tiền (m)')).toBeInTheDocument(); + expect(screen.getByLabelText('Độ rộng đường (m)')).toBeInTheDocument(); }); it('renders year built input', () => { render(); - expect(screen.getByLabelText('Nam xay dung')).toBeInTheDocument(); + expect(screen.getByLabelText('Năm xây dựng')).toBeInTheDocument(); }); it('renders legal paper checkbox', () => { render(); - expect(screen.getByLabelText('Co so do/giay to hop phap')).toBeInTheDocument(); + expect(screen.getByLabelText('Có sổ đỏ/giấy tờ hợp pháp')).toBeInTheDocument(); }); it('renders submit button', () => { render(); - expect(screen.getByText('Dinh gia ngay')).toBeInTheDocument(); + expect(screen.getByText('Định giá ngay')).toBeInTheDocument(); }); it('shows loading text when isLoading', () => { render(); - expect(screen.getByText('Dang dinh gia...')).toBeInTheDocument(); + expect(screen.getByText('Đang định giá...')).toBeInTheDocument(); }); it('disables submit button when loading', () => { render(); - expect(screen.getByText('Dang dinh gia...')).toBeDisabled(); + expect(screen.getByText('Đang định giá...')).toBeDisabled(); }); it('renders property type options', () => { @@ -101,6 +101,6 @@ describe('ValuationForm', () => { it('renders description text', () => { render(); - expect(screen.getByText(/Nhap thong tin bat dong san de nhan uoc tinh gia tu AI/)).toBeInTheDocument(); + expect(screen.getByText(/Nhập thông tin bất động sản để nhận ước tính giá từ AI/)).toBeInTheDocument(); }); }); diff --git a/apps/web/components/valuation/__tests__/valuation-history.spec.tsx b/apps/web/components/valuation/__tests__/valuation-history.spec.tsx index f6faad1..13906f1 100644 --- a/apps/web/components/valuation/__tests__/valuation-history.spec.tsx +++ b/apps/web/components/valuation/__tests__/valuation-history.spec.tsx @@ -38,7 +38,7 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByText('Lich su dinh gia')).toBeInTheDocument(); + expect(screen.getByText('Lịch sử định giá')).toBeInTheDocument(); }); it('renders total count description', () => { @@ -51,7 +51,7 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByText('2 lan dinh gia truoc do')).toBeInTheDocument(); + expect(screen.getByText('2 lần định giá trước đó')).toBeInTheDocument(); }); it('renders property type labels', () => { @@ -64,8 +64,8 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByText('Can ho')).toBeInTheDocument(); - expect(screen.getByText('Nha rieng')).toBeInTheDocument(); + expect(screen.getByText('Căn hộ')).toBeInTheDocument(); + expect(screen.getByText('Nhà riêng')).toBeInTheDocument(); }); it('renders district and area for each item', () => { @@ -78,8 +78,8 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByText(/Quận 1.*80 m2/)).toBeInTheDocument(); - expect(screen.getByText(/Quận 7.*120 m2/)).toBeInTheDocument(); + expect(screen.getByText(/Quận 1.*80 m²/)).toBeInTheDocument(); + expect(screen.getByText(/Quận 7.*120 m²/)).toBeInTheDocument(); }); it('renders formatted prices', () => { @@ -92,8 +92,8 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByText('5.00 ty')).toBeInTheDocument(); - expect(screen.getByText('8.50 ty')).toBeInTheDocument(); + expect(screen.getByText('5.00 tỷ')).toBeInTheDocument(); + expect(screen.getByText('8.50 tỷ')).toBeInTheDocument(); }); it('calls onSelect when an item is clicked', async () => { @@ -107,7 +107,7 @@ describe('ValuationHistory', () => { onSelect={onSelect} />, ); - await userEvent.click(screen.getByText('Can ho')); + await userEvent.click(screen.getByText('Căn hộ')); expect(onSelect).toHaveBeenCalledWith('val-1'); }); @@ -121,7 +121,7 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByText('Chua co lich su dinh gia')).toBeInTheDocument(); + expect(screen.getByText('Chưa có lịch sử định giá')).toBeInTheDocument(); }); it('shows loading state', () => { @@ -135,7 +135,7 @@ describe('ValuationHistory', () => { isLoading />, ); - expect(screen.getByText('Dang tai...')).toBeInTheDocument(); + expect(screen.getByText('Đang tải...')).toBeInTheDocument(); }); it('shows pagination when multiple pages', () => { @@ -149,8 +149,8 @@ describe('ValuationHistory', () => { />, ); expect(screen.getByText('Trang 1/3')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Truoc' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Tiep' })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: 'Trước' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Tiếp' })).not.toBeDisabled(); }); it('calls onPageChange with next page', async () => { @@ -164,7 +164,7 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - await userEvent.click(screen.getByRole('button', { name: 'Tiep' })); + await userEvent.click(screen.getByRole('button', { name: 'Tiếp' })); expect(onPageChange).toHaveBeenCalledWith(2); }); @@ -178,8 +178,8 @@ describe('ValuationHistory', () => { onSelect={vi.fn()} />, ); - expect(screen.getByRole('button', { name: 'Tiep' })).toBeDisabled(); - expect(screen.getByRole('button', { name: 'Truoc' })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: 'Tiếp' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Trước' })).not.toBeDisabled(); }); it('hides pagination when single page', () => { diff --git a/apps/web/components/valuation/__tests__/valuation-results.spec.tsx b/apps/web/components/valuation/__tests__/valuation-results.spec.tsx index a8d98c4..b688184 100644 --- a/apps/web/components/valuation/__tests__/valuation-results.spec.tsx +++ b/apps/web/components/valuation/__tests__/valuation-results.spec.tsx @@ -43,7 +43,7 @@ const mockResult: ValuationResult = { describe('ValuationResults', () => { it('renders estimated price', () => { render(); - expect(screen.getByText('5.00 ty VND')).toBeInTheDocument(); + expect(screen.getByText('5.00 tỷ VNĐ')).toBeInTheDocument(); }); it('renders confidence percentage', () => { @@ -53,17 +53,17 @@ describe('ValuationResults', () => { it('renders price per m2', () => { render(); - expect(screen.getByText('62.5 tr/m2')).toBeInTheDocument(); + expect(screen.getByText('62.5 tr/m²')).toBeInTheDocument(); }); it('renders price range', () => { render(); - expect(screen.getByText(/4\.50 ty.*5\.50 ty/)).toBeInTheDocument(); + expect(screen.getByText(/4\.50 tỷ.*5\.50 tỷ/)).toBeInTheDocument(); }); it('renders price drivers section', () => { render(); - expect(screen.getByText('Yeu to anh huong gia')).toBeInTheDocument(); + expect(screen.getByText('Yếu tố ảnh hưởng giá')).toBeInTheDocument(); expect(screen.getByText('Vị trí trung tâm')).toBeInTheDocument(); expect(screen.getByText('Tầng thấp')).toBeInTheDocument(); }); @@ -80,31 +80,31 @@ describe('ValuationResults', () => { it('renders comparables section', () => { render(); - expect(screen.getByText('Bat dong san tuong tu')).toBeInTheDocument(); + expect(screen.getByText('Bất động sản tương tự')).toBeInTheDocument(); expect(screen.getByText('Căn hộ tương tự A')).toBeInTheDocument(); expect(screen.getByText('Căn hộ tương tự B')).toBeInTheDocument(); }); it('shows comparable count', () => { render(); - expect(screen.getByText(/2 bat dong san/)).toBeInTheDocument(); + expect(screen.getByText(/2 bất động sản/)).toBeInTheDocument(); }); it('shows similarity percentage for comparables', () => { render(); - expect(screen.getByText('92% tuong tu')).toBeInTheDocument(); - expect(screen.getByText('85% tuong tu')).toBeInTheDocument(); + expect(screen.getByText('92% tương tự')).toBeInTheDocument(); + expect(screen.getByText('85% tương tự')).toBeInTheDocument(); }); it('hides drivers section when empty', () => { const noDrivers = { ...mockResult, priceDrivers: [] }; render(); - expect(screen.queryByText('Yeu to anh huong gia')).not.toBeInTheDocument(); + expect(screen.queryByText('Yếu tố ảnh hưởng giá')).not.toBeInTheDocument(); }); it('hides comparables section when empty', () => { const noComps = { ...mockResult, comparables: [] }; render(); - expect(screen.queryByText('Bat dong san tuong tu')).not.toBeInTheDocument(); + expect(screen.queryByText('Bất động sản tương tự')).not.toBeInTheDocument(); }); }); diff --git a/apps/web/components/valuation/valuation-form.tsx b/apps/web/components/valuation/valuation-form.tsx index 6082735..c3a8674 100644 --- a/apps/web/components/valuation/valuation-form.tsx +++ b/apps/web/components/valuation/valuation-form.tsx @@ -58,9 +58,9 @@ export function ValuationForm({ onSubmit, isLoading }: ValuationFormProps) { return ( - Dinh gia bat dong san + Định giá bất động sản - Nhap thong tin bat dong san de nhan uoc tinh gia tu AI + Nhập thông tin bất động sản để nhận ước tính giá từ AI @@ -68,9 +68,9 @@ export function ValuationForm({ onSubmit, isLoading }: ValuationFormProps) { {/* Row 1: Property type + City */}
- + {CITIES.map((c) => (