# 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; 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 Đã xác minh Info badge Outline badge Danger ``` #### Mẫu Card ```typescript {/* Nội dung */} ``` #### 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
{/* Nội dung */}
``` --- ## 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: (endpoint: string, headers?: HeadersInit) => request(endpoint, { method: 'GET', headers }), post: (endpoint: string, body?: unknown, headers?: HeadersInit) => request(endpoint, { method: 'POST', body, headers }), patch: (endpoint: string, body?: unknown, headers?: HeadersInit) => request(endpoint, { method: 'PATCH', body, headers }), delete: (endpoint: string, headers?: HeadersInit) => request(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 { 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: ``` ### 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 { // 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 <> } ``` **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(`/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)