24 KiB
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
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
// 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
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ạiagentId)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
GET /agents/:agentId/profile
Cấu Trúc Phản Hồi (DTO Hồ Sơ Công Khai):
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:
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
// 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ẫudialog.tsx— Hộp thoại modaltabs.tsx— Điều hướng tabtable.tsx— Bảng dữ liệu
Các Mẫu Component Chính
Component Badge
<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
<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ủ đề:
--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-24cho các phần hero
Mẫu Layout Ví Dụ
<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
// 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
// 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
// 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
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
// 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
{
"@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
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
// 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á
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.totalReviewsvàavgReviewRating - Cơ hội: Tạo component
ReviewCardvàRatingStarscó 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
// 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
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):
// [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:
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
- Thiết kế API DTO cho hồ sơ môi giới công khai
- Tạo backend query handler để lấy hồ sơ môi giới công khai
- Tạo frontend API client (agents-api.ts)
- Xây dựng cấu trúc trang theo mẫu trang chi tiết bất động sản
- Tạo agent detail client component với các phần
- Thêm phần hiển thị đánh giá với xếp hạng sao
- Thêm phần hiển thị bất động sản tái sử dụng PropertyCard
- Tạo JSON-LD dữ liệu có cấu trúc cho SEO
- Kiểm tra ISR & metadata generation
- Thêm các tuyến quốc tế (ví dụ: /en/agents/[id] qua locale)