Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 29s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 2m42s
Deploy / Build Web Image (push) Failing after 27s
Deploy / Build AI Services Image (push) Failing after 29s
E2E Tests / Playwright E2E (push) Failing after 43s
Deploy / Build API Image (push) Failing after 1m31s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 6s
Security Scanning / Trivy Scan — API Image (push) Failing after 5m35s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 3m45s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Security Scanning / Trivy Scan — Web Image (push) Failing after 13m51s
Security Scanning / Trivy Filesystem Scan (push) Failing after 14m46s
Security Scanning / Security Gate (push) Has been cancelled
743 lines
24 KiB
Markdown
743 lines
24 KiB
Markdown
# Trang Hồ Sơ Công Khai Môi Giới GoodGo — Báo Cáo Khám Phá Toàn Diện
|
|
|
|
**Ngày:** 11 tháng 4 năm 2026
|
|
**Phạm vi:** Khám phá toàn stack để triển khai trang hồ sơ công khai `/agents/[id]`
|
|
**Codebase:** GoodGo Platform (Next.js 15 + NestJS 10 + PostgreSQL + Prisma)
|
|
|
|
---
|
|
|
|
## 1. CẤU TRÚC ỨNG DỤNG WEB & ĐỊNH TUYẾN
|
|
|
|
### Cấu Trúc Tệp
|
|
```
|
|
apps/web/
|
|
├── app/ # Next.js 15 App Router (Server Components)
|
|
│ ├── [locale]/ # Quốc tế hóa (i18n) ở cấp gốc
|
|
│ │ ├── (admin)/ # Các tuyến admin (được bảo vệ)
|
|
│ │ ├── (auth)/ # Các tuyến xác thực (đăng nhập, v.v.)
|
|
│ │ ├── (dashboard)/ # Bảng điều khiển người dùng đã xác thực
|
|
│ │ └── (public)/ # Các tuyến công khai
|
|
│ │ ├── listings/[id]/ # Mẫu trang chi tiết bất động sản hiện có
|
|
│ │ ├── search/
|
|
│ │ ├── compare/
|
|
│ │ ├── pricing/
|
|
│ │ └── page.tsx # Trang đích
|
|
│ ├── layout.tsx # Layout gốc
|
|
│ ├── robots.ts # SEO: robots.txt
|
|
│ └── sitemap.ts # SEO: sitemap.xml
|
|
├── components/
|
|
│ ├── ui/ # Thư viện UI (button, card, badge, input, v.v.)
|
|
│ ├── listings/ # Các component dành riêng cho bất động sản
|
|
│ ├── search/ # Các component tìm kiếm & thẻ bất động sản
|
|
│ ├── seo/ # JSON-LD, dữ liệu có cấu trúc
|
|
│ └── providers/ # Các context provider
|
|
├── lib/
|
|
│ ├── api-client.ts # Fetch wrapper với bảo vệ CSRF
|
|
│ ├── listings-api.ts # API client cho bất động sản
|
|
│ ├── profile-api.ts # API client hồ sơ xác thực/môi giới
|
|
│ ├── listings-server.ts # Lấy dữ liệu phía server
|
|
│ ├── currency.ts # Tiện ích định dạng tiền tệ
|
|
│ └── validations/ # Zod schemas (bất động sản, xác thực, v.v.)
|
|
└── public/ # Tài nguyên tĩnh
|
|
```
|
|
|
|
### Các Mẫu Định Tuyến
|
|
|
|
**Các Tuyến Công Khai (dưới `(public)`):**
|
|
- `/` → Trang chủ/trang đích
|
|
- `/listings/[id]` → Chi tiết bất động sản (MẪU HIỆN CÓ)
|
|
- `/search` → Trang kết quả tìm kiếm
|
|
- `/compare` → Trang so sánh bất động sản
|
|
- `/pricing` → Trang bảng giá
|
|
|
|
**Nhận Xét Chính:** Nhóm tuyến `(public)` dành cho người dùng chưa xác thực. **Hồ sơ môi giới nên theo cùng mẫu: `/agents/[id]`** trong nhóm `(public)`.
|
|
|
|
---
|
|
|
|
## 2. MÃ LIÊN QUAN ĐẾN MÔI GIỚI HIỆN CÓ
|
|
|
|
### Kiểu Hồ Sơ Môi Giới (Frontend)
|
|
**Tệp:** `apps/web/lib/profile-api.ts`
|
|
|
|
```typescript
|
|
export interface AgentProfile {
|
|
id: string;
|
|
email: string | null;
|
|
phone: string;
|
|
fullName: string;
|
|
avatarUrl: string | null;
|
|
role: string;
|
|
kycStatus: string;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
licenseNumber: string | null;
|
|
agency: string | null;
|
|
qualityScore: number | null;
|
|
serviceAreas: string[];
|
|
isVerified: boolean;
|
|
}
|
|
```
|
|
|
|
### Các Endpoint API Môi Giới Hiện Có (Backend)
|
|
**Tệp:** `apps/api/src/modules/agents/presentation/controllers/agents.controller.ts`
|
|
|
|
```typescript
|
|
// Endpoints:
|
|
GET /agents/me/dashboard # Bảng điều khiển môi giới (đã xác thực)
|
|
POST /agents/:agentId/recalculate-score # Tính lại điểm chất lượng (admin)
|
|
|
|
// Trả về:
|
|
interface AgentDashboardData {
|
|
agentId: string;
|
|
qualityScore: number;
|
|
totalDeals: number;
|
|
responseTimeAvg: number | null;
|
|
isVerified: boolean;
|
|
totalLeads: number;
|
|
leadsByStatus: Record<string, number>;
|
|
conversionRate: number;
|
|
totalInquiries: number;
|
|
unreadInquiries: number;
|
|
totalListings: number;
|
|
activeListings: number;
|
|
avgReviewRating: number;
|
|
totalReviews: number;
|
|
}
|
|
```
|
|
|
|
### Schema Prisma — Model Agent
|
|
**Tệp:** `prisma/schema.prisma`
|
|
|
|
```prisma
|
|
model Agent {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id])
|
|
licenseNumber String?
|
|
agency String?
|
|
qualityScore Float @default(0)
|
|
totalDeals Int @default(0)
|
|
responseTimeAvg Int?
|
|
bio String?
|
|
serviceAreas Json // JSON array: ["quan-1", "quan-7", "thu-duc"]
|
|
isVerified Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
listings Listing[]
|
|
leads Lead[]
|
|
|
|
@@index([qualityScore])
|
|
@@index([isVerified])
|
|
}
|
|
```
|
|
|
|
**Các model liên quan:**
|
|
- `User` — Tài khoản người dùng của môi giới (fullName, avatarUrl, phone, email, role, kycStatus)
|
|
- `Listing` — Các bất động sản môi giới đại diện (có khóa ngoại `agentId`)
|
|
- `Lead` — Các đầu mối được môi giới theo dõi
|
|
|
|
---
|
|
|
|
## 3. CÁC ENDPOINT API MÔI GIỚI CẦN THIẾT CHO HỒ SƠ CÔNG KHAI
|
|
|
|
Dựa trên kiến trúc hiện có, chúng ta cần tạo một **endpoint công khai** để lấy dữ liệu hồ sơ môi giới:
|
|
|
|
### Endpoint Đề Xuất
|
|
|
|
```http
|
|
GET /agents/:agentId/profile
|
|
```
|
|
|
|
**Cấu Trúc Phản Hồi (DTO Hồ Sơ Công Khai):**
|
|
```typescript
|
|
interface AgentPublicProfile {
|
|
id: string;
|
|
fullName: string;
|
|
avatarUrl: string | null;
|
|
|
|
// Các trường dành riêng cho môi giới
|
|
licenseNumber: string | null;
|
|
agency: string | null;
|
|
qualityScore: number;
|
|
bio: string | null;
|
|
serviceAreas: string[];
|
|
isVerified: boolean;
|
|
|
|
// Thống kê
|
|
totalListings: number;
|
|
activeListings: number;
|
|
avgReviewRating: number;
|
|
totalReviews: number;
|
|
|
|
// Liên hệ (tùy chọn, có thể yêu cầu tùy chọn người dùng)
|
|
phone?: string;
|
|
|
|
// Dấu thời gian
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
```
|
|
|
|
**Các Endpoint Liên Quan Cần Thiết:**
|
|
```http
|
|
GET /listings?agentId=:agentId&status=ACTIVE # Bất động sản đang hoạt động của môi giới
|
|
GET /reviews/stats?targetType=AGENT&targetId=:agentId # Thống kê đánh giá môi giới
|
|
GET /reviews?targetType=AGENT&targetId=:agentId&limit=10 # Đánh giá gần đây của môi giới
|
|
```
|
|
|
|
Các endpoint này đã tồn tại và công khai (không yêu cầu xác thực).
|
|
|
|
---
|
|
|
|
## 4. CÁC COMPONENT UI DÙNG CHUNG & MẪU THIẾT KẾ
|
|
|
|
### Tailwind/Hệ Thống Thiết Kế
|
|
**Tệp:** `apps/web/tailwind.config.ts`
|
|
|
|
```typescript
|
|
// Các biến CSS được sử dụng (hỗ trợ chế độ tối)
|
|
colors: {
|
|
primary, primary-foreground
|
|
secondary, secondary-foreground
|
|
destructive, destructive-foreground
|
|
muted, muted-foreground
|
|
accent, accent-foreground
|
|
card, card-foreground
|
|
}
|
|
|
|
// Biến Radius: var(--radius)
|
|
borderRadius: {
|
|
lg: 'var(--radius)',
|
|
md: 'calc(var(--radius) - 2px)',
|
|
sm: 'calc(var(--radius) - 4px)',
|
|
}
|
|
```
|
|
|
|
### Các Component UI Có Sẵn
|
|
**Tệp:** `apps/web/components/ui/`
|
|
|
|
- `button.tsx` — Nút có kiểu dáng với các biến thể
|
|
- `card.tsx` — Thẻ (CardContent, v.v.)
|
|
- `badge.tsx` — Huy hiệu với các biến thể
|
|
- `input.tsx`, `label.tsx` — Các điều khiển biểu mẫu
|
|
- `dialog.tsx` — Hộp thoại modal
|
|
- `tabs.tsx` — Điều hướng tab
|
|
- `table.tsx` — Bảng dữ liệu
|
|
|
|
### Các Mẫu Component Chính
|
|
|
|
#### Component Badge
|
|
```typescript
|
|
<Badge variant="default">Đã xác minh</Badge>
|
|
<Badge variant="secondary">Info badge</Badge>
|
|
<Badge variant="outline">Outline badge</Badge>
|
|
<Badge variant="destructive">Danger</Badge>
|
|
```
|
|
|
|
#### Mẫu Card
|
|
```typescript
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
{/* Nội dung */}
|
|
</CardContent>
|
|
</Card>
|
|
```
|
|
|
|
#### Thẻ Bất Động Sản (Hiển Thị Bất Động Sản) - TÁI SỬ DỤNG CÁI NÀY
|
|
**Tệp:** `apps/web/components/search/property-card.tsx`
|
|
|
|
Hiển thị bất động sản với:
|
|
- Thư viện ảnh
|
|
- Giá (đã định dạng)
|
|
- Tiêu đề & địa chỉ
|
|
- Huy hiệu loại, diện tích, số phòng ngủ
|
|
- Huy hiệu loại giao dịch
|
|
|
|
**Có thể tái sử dụng cho:** Hiển thị bất động sản của môi giới
|
|
|
|
---
|
|
|
|
## 5. KIỂU DÁNG & MẪU THIẾT KẾ
|
|
|
|
### CSS Toàn Cục
|
|
**Tệp:** `apps/web/app/globals.css`
|
|
|
|
Sử dụng biến CSS để tạo chủ đề:
|
|
```css
|
|
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l))
|
|
--background: hsl(...)
|
|
--card: hsl(...)
|
|
/* Hỗ trợ chế độ tối qua [data-theme="dark"] */
|
|
```
|
|
|
|
### Kiểu Chữ
|
|
- Font: Inter (được cấu hình trong tailwind.config.ts qua biến CSS `--font-inter`)
|
|
- Các cấp tiêu đề: h1, h2, h3, h4
|
|
- Sử dụng các lớp: `text-lg font-bold`, `text-sm text-muted-foreground`
|
|
|
|
### Khoảng Cách
|
|
- Tailwind tiêu chuẩn: `p-4`, `mt-8`, `gap-3`, v.v.
|
|
- Padding card: `p-4`
|
|
- Padding phần: `py-16`, `py-24` cho các phần hero
|
|
|
|
### Mẫu Layout Ví Dụ
|
|
```typescript
|
|
<section className="py-16 md:py-24">
|
|
<div className="mx-auto max-w-7xl px-4">
|
|
{/* Nội dung */}
|
|
</div>
|
|
</section>
|
|
```
|
|
|
|
---
|
|
|
|
## 6. QUẢN LÝ TRẠNG THÁI & LẤY DỮ LIỆU
|
|
|
|
### Mẫu API Client
|
|
**Tệp:** `apps/web/lib/api-client.ts`
|
|
|
|
```typescript
|
|
// Cách sử dụng:
|
|
const apiClient = {
|
|
get: <T>(endpoint: string, headers?: HeadersInit) => request<T>(endpoint, { method: 'GET', headers }),
|
|
post: <T>(endpoint: string, body?: unknown, headers?: HeadersInit) => request<T>(endpoint, { method: 'POST', body, headers }),
|
|
patch: <T>(endpoint: string, body?: unknown, headers?: HeadersInit) => request<T>(endpoint, { method: 'PATCH', body, headers }),
|
|
delete: <T>(endpoint: string, headers?: HeadersInit) => request<T>(endpoint, { method: 'DELETE', headers }),
|
|
};
|
|
|
|
// Bảo vệ CSRF được bao gồm tự động
|
|
```
|
|
|
|
### Mẫu Lấy Dữ Liệu Phía Server
|
|
**Tệp:** `apps/web/lib/listings-server.ts`
|
|
|
|
```typescript
|
|
// Ví dụ: lấy dữ liệu trên server tại thời điểm build hoặc thời điểm request
|
|
export async function fetchListingById(id: string) {
|
|
try {
|
|
const res = await fetch(`${API_BASE_URL}/listings/${id}`, {
|
|
next: { revalidate: 3600 } // ISR: xác thực lại mỗi 1 giờ
|
|
});
|
|
if (!res.ok) return null;
|
|
return res.json();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Mẫu Lấy Dữ Liệu Phía Client
|
|
```typescript
|
|
// Trong React component (sử dụng 'use client')
|
|
const [data, setData] = useState(null);
|
|
|
|
React.useEffect(() => {
|
|
apiClient
|
|
.get('/endpoint')
|
|
.then((res) => setData(res))
|
|
.catch(() => setError(true))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
```
|
|
|
|
### Không Có Trạng Thái Toàn Cục (Zustand)
|
|
- **Hiện tại không có Zustand store** cho dữ liệu môi giới trong codebase
|
|
- **Mẫu:** Lấy dữ liệu trong page component → truyền xuống các component con
|
|
- Dashboard sử dụng useState cục bộ để lấy hồ sơ
|
|
|
|
---
|
|
|
|
## 7. MẪU SEO & DỮ LIỆU CÓ CẤU TRÚC
|
|
|
|
### Mẫu Tạo Metadata
|
|
**Tệp:** `apps/web/app/[locale]/(public)/listings/[id]/page.tsx`
|
|
|
|
```typescript
|
|
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
const listing = await fetchListingById(params.id);
|
|
if (!listing) {
|
|
return { title: 'Không tìm thấy' };
|
|
}
|
|
|
|
return {
|
|
title: `${property.title} - ${formatPrice(listing.priceVND)} VND`,
|
|
description: '...',
|
|
alternates: {
|
|
canonical: `${siteUrl}/${params.locale}/listings/${params.id}`,
|
|
languages: { vi: '...', en: '...' }
|
|
},
|
|
openGraph: {
|
|
type: 'article',
|
|
locale: params.locale === 'vi' ? 'vi_VN' : 'en_US',
|
|
url: canonicalUrl,
|
|
title,
|
|
images: [{ url: firstImage.url, width: 1200, height: 630 }],
|
|
},
|
|
twitter: {
|
|
card: 'summary_large_image',
|
|
title,
|
|
images: [firstImage.url],
|
|
},
|
|
};
|
|
}
|
|
```
|
|
|
|
### Dữ Liệu Có Cấu Trúc JSON-LD
|
|
**Tệp:** `apps/web/components/seo/json-ld.tsx`
|
|
|
|
```typescript
|
|
// Ví dụ: Schema RealEstateListing
|
|
export function generateListingJsonLd(listing, siteUrl) {
|
|
return {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'RealEstateListing',
|
|
name: property.title,
|
|
url: `${siteUrl}/listings/${listing.id}`,
|
|
offers: { '@type': 'Offer', price: priceNum, priceCurrency: 'VND' },
|
|
// ... thêm thuộc tính
|
|
};
|
|
}
|
|
|
|
// Cách dùng trong trang:
|
|
<JsonLd data={listingJsonLd} />
|
|
<JsonLd data={breadcrumbJsonLd} />
|
|
```
|
|
|
|
### Cho Hồ Sơ Môi Giới (Schema.org)
|
|
**Schema phù hợp:** `LocalBusiness` hoặc `ProfessionalService`
|
|
|
|
```json
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "LocalBusiness",
|
|
"name": "Agent Full Name",
|
|
"description": "Bio",
|
|
"url": "https://goodgo.vn/en/agents/[id]",
|
|
"image": "avatarUrl",
|
|
"address": {
|
|
"@type": "PostalAddress",
|
|
"addressLocality": "Ho Chi Minh"
|
|
},
|
|
"aggregateRating": {
|
|
"@type": "AggregateRating",
|
|
"ratingValue": avgReviewRating,
|
|
"reviewCount": totalReviews
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. CÁC COMPONENT THẺ BẤT ĐỘNG SẢN HIỆN CÓ
|
|
|
|
### Component Thẻ Bất Động Sản
|
|
**Tệp:** `apps/web/components/search/property-card.tsx`
|
|
|
|
```typescript
|
|
interface PropertyCardProps {
|
|
listing: ListingDetail;
|
|
compact?: boolean;
|
|
}
|
|
|
|
// Hiển thị:
|
|
// - Thư viện ảnh (với huy hiệu đếm)
|
|
// - Giá (đã định dạng)
|
|
// - Tiêu đề & địa chỉ
|
|
// - Huy hiệu: Loại giao dịch, loại bất động sản, diện tích, phòng ngủ, phòng tắm, hướng
|
|
// - Nút so sánh
|
|
```
|
|
|
|
### Được Sử Dụng Trong:
|
|
- Bất động sản nổi bật trên trang chủ
|
|
- Kết quả tìm kiếm
|
|
- Trang so sánh
|
|
|
|
**Cho Hồ Sơ Môi Giới:** Có thể tái sử dụng component này để hiển thị bất động sản của môi giới!
|
|
|
|
---
|
|
|
|
## 9. CÁC COMPONENT ĐÁNH GIÁ & XẾP HẠNG
|
|
|
|
### Các Endpoint API Đánh Giá
|
|
**Tệp:** `apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts`
|
|
|
|
```typescript
|
|
// Endpoints:
|
|
GET /reviews # Danh sách đánh giá theo đối tượng (phân trang)
|
|
GET /reviews/stats # Lấy thống kê xếp hạng tổng hợp
|
|
GET /reviews/me # Lấy đánh giá của người dùng đã xác thực
|
|
POST /reviews # Tạo đánh giá (đã xác thực)
|
|
DELETE /reviews/:id # Xóa đánh giá của bản thân
|
|
|
|
// Tham số truy vấn:
|
|
GET /reviews?targetType=AGENT&targetId=:id&page=1&limit=20
|
|
GET /reviews/stats?targetType=AGENT&targetId=:id
|
|
```
|
|
|
|
### DTO Đánh Giá
|
|
```typescript
|
|
interface ReviewItemData {
|
|
id: string;
|
|
userId: string;
|
|
targetType: string;
|
|
targetId: string;
|
|
rating: number; // 1-5
|
|
comment: string | null;
|
|
createdAt: string;
|
|
// Thông tin người dùng:
|
|
user: {
|
|
id: string;
|
|
fullName: string;
|
|
avatarUrl: string | null;
|
|
};
|
|
}
|
|
|
|
interface ReviewStatsData {
|
|
targetType: string;
|
|
targetId: string;
|
|
totalReviews: number;
|
|
averageRating: number;
|
|
ratingDistribution: {
|
|
"1": number;
|
|
"2": number;
|
|
"3": number;
|
|
"4": number;
|
|
"5": number;
|
|
};
|
|
}
|
|
```
|
|
|
|
### Chưa Có Component Hiển Thị Đánh Giá
|
|
- **Hồ sơ bảng điều khiển** hiển thị `agentProfile.totalReviews` và `avgReviewRating`
|
|
- **Cơ hội:** Tạo component `ReviewCard` và `RatingStars` có thể tái sử dụng cho hồ sơ môi giới
|
|
|
|
---
|
|
|
|
## 10. ĐỊNH NGHĨA KIỂU & GIAO DIỆN
|
|
|
|
### Các Kiểu Cốt Lõi Sử Dụng Ở Frontend
|
|
|
|
```typescript
|
|
// Từ listings-api.ts
|
|
export type TransactionType = 'SALE' | 'RENT';
|
|
export type PropertyType = 'APARTMENT' | 'HOUSE' | 'VILLA' | 'LAND' | 'OFFICE' | 'SHOPHOUSE';
|
|
export type ListingStatus = 'DRAFT' | 'PENDING_REVIEW' | 'ACTIVE' | 'RESERVED' | 'SOLD' | 'RENTED' | 'EXPIRED' | 'REJECTED';
|
|
export type Direction = 'NORTH' | 'SOUTH' | 'EAST' | 'WEST' | 'NORTHEAST' | 'NORTHWEST' | 'SOUTHEAST' | 'SOUTHWEST';
|
|
|
|
// Từ profile-api.ts
|
|
export interface AgentProfile {
|
|
id: string;
|
|
email: string | null;
|
|
phone: string;
|
|
fullName: string;
|
|
avatarUrl: string | null;
|
|
role: string;
|
|
kycStatus: string;
|
|
isActive: boolean;
|
|
createdAt: string;
|
|
licenseNumber: string | null;
|
|
agency: string | null;
|
|
qualityScore: number | null;
|
|
serviceAreas: string[];
|
|
isVerified: boolean;
|
|
}
|
|
|
|
// Từ listings-api.ts
|
|
export interface ListingDetail {
|
|
id: string;
|
|
status: ListingStatus;
|
|
transactionType: TransactionType;
|
|
priceVND: string;
|
|
publishedAt: string | null;
|
|
property: {
|
|
id: string;
|
|
propertyType: PropertyType;
|
|
title: string;
|
|
areaM2: number;
|
|
address: string;
|
|
ward: string;
|
|
district: string;
|
|
city: string;
|
|
media: PropertyMedia[];
|
|
// ... hơn 15 thuộc tính khác
|
|
};
|
|
}
|
|
```
|
|
|
|
### Các Schema Xác Thực (Zod)
|
|
**Tệp:** `apps/web/lib/validations/listings.ts`
|
|
|
|
```typescript
|
|
export const TRANSACTION_TYPES = [
|
|
{ value: 'SALE', label: 'Bán' },
|
|
{ value: 'RENT', label: 'Cho thuê' },
|
|
] as const;
|
|
|
|
export const PROPERTY_TYPES = [
|
|
{ value: 'APARTMENT', label: 'Căn hộ' },
|
|
{ value: 'HOUSE', label: 'Nhà riêng' },
|
|
// ... thêm
|
|
] as const;
|
|
|
|
export const LISTING_STATUSES = {
|
|
DRAFT: { label: 'Nháp', variant: 'secondary' },
|
|
ACTIVE: { label: 'Đang bán', variant: 'success' },
|
|
// ...
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 11. DANH SÁCH KIỂM TRA TRIỂN KHAI TRANG HỒ SƠ MÔI GIỚI
|
|
|
|
### Backend (NestJS API)
|
|
|
|
**DTO & Giao Diện Mới:**
|
|
```
|
|
✓ CreateGetAgentPublicProfileQuery
|
|
✓ GetAgentPublicProfileHandler
|
|
✓ AgentPublicProfileDto (phản hồi)
|
|
✓ Cập nhật agent.repository.ts với phương thức: getPublicProfile(agentId: string)
|
|
✓ Cập nhật prisma-agent.repository.ts để lấy dữ liệu qua Prisma
|
|
```
|
|
|
|
**Endpoint Mới:**
|
|
```
|
|
✓ GET /agents/:agentId/profile (công khai, không xác thực)
|
|
✓ Trả về: AgentPublicProfileDto
|
|
✓ Xác thực agentId tồn tại
|
|
✓ Trả về 404 nếu không tìm thấy
|
|
```
|
|
|
|
**Tận Dụng Hiện Có:**
|
|
- Các truy vấn đánh giá: Đã công khai
|
|
- Các truy vấn bất động sản: Đã công khai
|
|
- Hồ sơ người dùng: Liên kết đến agent.userId
|
|
|
|
### Frontend (Next.js)
|
|
|
|
**Các Tệp Mới:**
|
|
```
|
|
✓ apps/web/app/[locale]/(public)/agents/ (thư mục)
|
|
✓ apps/web/app/[locale]/(public)/agents/[id]/ (thư mục)
|
|
✓ apps/web/app/[locale]/(public)/agents/[id]/page.tsx (server component)
|
|
✓ apps/web/app/[locale]/(public)/agents/[id]/layout.tsx (tùy chọn)
|
|
✓ apps/web/lib/agents-api.ts (API client)
|
|
✓ apps/web/lib/agents-server.ts (lấy dữ liệu phía server cho ISR)
|
|
✓ apps/web/components/agents/ (thư mục mới)
|
|
✓ apps/web/components/agents/agent-detail-client.tsx (client component)
|
|
✓ apps/web/components/agents/agent-listings-section.tsx
|
|
✓ apps/web/components/agents/agent-reviews-section.tsx
|
|
```
|
|
|
|
**Cấu Trúc Trang (theo mẫu bất động sản):**
|
|
```typescript
|
|
// [id]/page.tsx (Server Component)
|
|
export async function generateMetadata({ params }): Promise<Metadata> {
|
|
// Lấy thông tin môi giới
|
|
// Trả về title, description, OG image, canonical URL
|
|
}
|
|
|
|
export default async function AgentProfilePage({ params }) {
|
|
// Lấy hồ sơ môi giới (phía server, ISR)
|
|
// Render JsonLd breadcrumb
|
|
// Render JsonLd LocalBusiness hoặc ProfessionalService
|
|
// Truyền xuống client component
|
|
return <>
|
|
<JsonLd data={agentJsonLd} />
|
|
<AgentDetailClient agent={agent} />
|
|
</>
|
|
}
|
|
```
|
|
|
|
**Client Component:**
|
|
- Hiển thị thông tin môi giới (tên, avatar, bio, giấy phép, công ty)
|
|
- Hiển thị điểm chất lượng & huy hiệu
|
|
- Render phần đánh giá
|
|
- Render phần bất động sản
|
|
- Nút liên hệ/yêu cầu tư vấn (tùy chọn)
|
|
|
|
**API Client:**
|
|
```typescript
|
|
export const agentsApi = {
|
|
getById: (id: string) => apiClient.get<AgentPublicProfile>(`/agents/${id}/profile`),
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 12. CÁC TỆP CHÍNH CẦN THAM KHẢO/ĐIỀU CHỈNH
|
|
|
|
### Backend
|
|
|
|
| Tệp | Mục Đích |
|
|
|------|---------|
|
|
| `apps/api/src/modules/agents/presentation/controllers/agents.controller.ts` | Thêm endpoint công khai mới |
|
|
| `apps/api/src/modules/agents/domain/repositories/agent.repository.ts` | Thêm giao diện cho phương thức hồ sơ công khai |
|
|
| `apps/api/src/modules/agents/infrastructure/repositories/prisma-agent.repository.ts` | Triển khai lấy hồ sơ công khai |
|
|
| `apps/api/src/modules/agents/application/queries/` | Tạo query handler mới cho hồ sơ công khai |
|
|
| `prisma/schema.prisma` | Tham chiếu cho model Agent |
|
|
|
|
### Frontend — Ví Dụ Tham Khảo
|
|
|
|
| Tệp | Mục Đích | Mẫu Tái Sử Dụng |
|
|
|------|---------|---|
|
|
| `apps/web/app/[locale]/(public)/listings/[id]/page.tsx` | Trang chi tiết bất động sản | Dùng làm template cho trang môi giới |
|
|
| `apps/web/components/search/property-card.tsx` | Thẻ bất động sản | Tái sử dụng cho bất động sản của môi giới |
|
|
| `apps/web/lib/listings-api.ts` | API client bất động sản | Tạo agents-api.ts tương tự |
|
|
| `apps/web/lib/listings-server.ts` | Lấy dữ liệu phía server | Tạo agents-server.ts tương tự |
|
|
| `apps/web/components/seo/json-ld.tsx` | Dữ liệu có cấu trúc | Điều chỉnh cho schema LocalBusiness |
|
|
| `apps/web/lib/currency.ts` | Định dạng giá | Tái sử dụng cho giá bất động sản |
|
|
| `apps/web/tailwind.config.ts` | Hệ thống thiết kế | Tham khảo cho kiểu dáng |
|
|
|
|
---
|
|
|
|
## 13. TÓM TẮT CÁC PHÁT HIỆN CHÍNH
|
|
|
|
### Những Gì Đã Có (Có Thể Tái Sử Dụng)
|
|
✅ Model Agent trong Prisma với tất cả các trường cần thiết
|
|
✅ Các endpoint API cho bất động sản, đánh giá (công khai)
|
|
✅ Các component UI: Card, Badge, Button, v.v.
|
|
✅ Hệ thống thiết kế Tailwind với chế độ tối
|
|
✅ Mẫu SEO với tạo metadata & JSON-LD
|
|
✅ Component thư viện ảnh cho bất động sản
|
|
✅ Component PropertyCard để hiển thị bất động sản
|
|
✅ API client với bảo vệ CSRF
|
|
✅ Lấy dữ liệu phía server với mẫu ISR
|
|
|
|
### Những Gì Cần Xây Dựng (Hồ Sơ Môi Giới)
|
|
🔨 Trang `/agents/[id]` (server component với metadata)
|
|
🔨 Component `AgentDetailClient` (render phía client)
|
|
🔨 Endpoint công khai: `GET /agents/:agentId/profile`
|
|
🔨 Phần bất động sản của môi giới (tái sử dụng PropertyCard)
|
|
🔨 Phần đánh giá của môi giới (lấy & hiển thị đánh giá)
|
|
🔨 Component hiển thị sao xếp hạng/tổng hợp
|
|
🔨 agents-api.ts (lấy hồ sơ môi giới)
|
|
🔨 agents-server.ts (lấy dữ liệu phía server cho ISR)
|
|
🔨 Schema JSON-LD LocalBusiness cho môi giới
|
|
|
|
### Các Quyết Định Kiến Trúc
|
|
- **Định tuyến:** Đặt tại `apps/web/app/[locale]/(public)/agents/[id]/page.tsx`
|
|
- **Mẫu:** Theo mẫu trang chi tiết bất động sản chính xác
|
|
- **Metadata:** Sử dụng hàm server generateMetadata()
|
|
- **Component:** Chia thành Server Component (trang) + Client Component (tương tác)
|
|
- **SEO:** Bao gồm breadcrumb + JSON-LD LocalBusiness
|
|
- **Kiểu dáng:** Sử dụng các token Tailwind hiện có + component
|
|
- **Lấy dữ liệu:** Lấy dữ liệu phía server với ISR revalidation (3600 giây)
|
|
|
|
---
|
|
|
|
## 14. CÁC BƯỚC TIẾP THEO
|
|
|
|
1. **Thiết kế API DTO** cho hồ sơ môi giới công khai
|
|
2. **Tạo backend query handler** để lấy hồ sơ môi giới công khai
|
|
3. **Tạo frontend API client** (agents-api.ts)
|
|
4. **Xây dựng cấu trúc trang** theo mẫu trang chi tiết bất động sản
|
|
5. **Tạo agent detail client component** với các phần
|
|
6. **Thêm phần hiển thị đánh giá** với xếp hạng sao
|
|
7. **Thêm phần hiển thị bất động sản** tái sử dụng PropertyCard
|
|
8. **Tạo JSON-LD** dữ liệu có cấu trúc cho SEO
|
|
9. **Kiểm tra ISR & metadata** generation
|
|
10. **Thêm các tuyến quốc tế** (ví dụ: /en/agents/[id] qua locale)
|