Files
goodgo-platform/docs/audits/AGENT_PROFILE_EXPLORATION.md
Ho Ngoc Hai e78d706b42 chore: update infrastructure configs, audit docs, and env template
- Update Docker Compose configs for Redis, Typesense, and MinIO services
- Update GitHub Actions deploy workflow with improved caching and steps
- Extend .env.example with Stringee, Zalo OA, and FCM config keys
- Update audit documentation with latest findings and recommendations
- Update CHANGELOG and README with recent feature additions

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 05:17:38 +07:00

744 lines
21 KiB
Markdown

# GoodGo Agent Public Profile Page — Comprehensive Exploration Report
**Date:** April 11, 2026
**Scope:** Full stack exploration for implementing `/agents/[id]` public profile page
**Codebase:** GoodGo Platform (Next.js 15 + NestJS 10 + PostgreSQL + Prisma)
---
## 1. WEB APP STRUCTURE & ROUTING
### File Structure
```
apps/web/
├── app/ # Next.js 15 App Router (Server Components)
│ ├── [locale]/ # Internationalization (i18n) at root level
│ │ ├── (admin)/ # Admin routes (protected)
│ │ ├── (auth)/ # Auth routes (sign-in, etc.)
│ │ ├── (dashboard)/ # Authenticated user dashboard
│ │ └── (public)/ # Public-facing routes
│ │ ├── listings/[id]/ # Existing listing detail page pattern
│ │ ├── search/
│ │ ├── compare/
│ │ ├── pricing/
│ │ └── page.tsx # Landing page
│ ├── layout.tsx # Root layout
│ ├── robots.ts # SEO: robots.txt
│ └── sitemap.ts # SEO: sitemap.xml
├── components/
│ ├── ui/ # UI library (button, card, badge, input, etc.)
│ ├── listings/ # Listing-specific components
│ ├── search/ # Search & property card components
│ ├── seo/ # JSON-LD, structured data
│ └── providers/ # Context providers
├── lib/
│ ├── api-client.ts # Fetch wrapper with CSRF protection
│ ├── listings-api.ts # API client for listings
│ ├── profile-api.ts # Auth/agent profile API client
│ ├── listings-server.ts # Server-side data fetching
│ ├── currency.ts # Currency formatting utilities
│ └── validations/ # Zod schemas (listings, auth, etc.)
└── public/ # Static assets
```
### Routing Patterns
**Public Routes (under `(public)`):**
- `/` → Home/landing page
- `/listings/[id]` → Listing detail (EXISTING PATTERN)
- `/search` → Search results page
- `/compare` → Property comparison page
- `/pricing` → Pricing page
**Key Insight:** The `(public)` route group is for unauthenticated users. **Agent profiles should follow the same pattern: `/agents/[id]`** under the `(public)` group.
---
## 2. EXISTING AGENT-RELATED CODE
### Agent Profile Type (Frontend)
**File:** `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;
}
```
### Existing Agent API Endpoints (Backend)
**File:** `apps/api/src/modules/agents/presentation/controllers/agents.controller.ts`
```typescript
// Endpoints:
GET /agents/me/dashboard # Agent dashboard (authenticated)
POST /agents/:agentId/recalculate-score # Recalculate quality score (admin)
// Returns:
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;
}
```
### Prisma Schema — Agent Model
**File:** `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])
}
```
**Related models:**
- `User` — Agent's user account (fullName, avatarUrl, phone, email, role, kycStatus)
- `Listing` — Properties agent represents (has `agentId` foreign key)
- `Lead` — Leads tracked by agent
---
## 3. AGENT API ENDPOINTS NEEDED FOR PUBLIC PROFILE
Based on the existing architecture, we need to create a **public endpoint** to fetch agent profile data:
### Proposed Endpoint
```http
GET /agents/:agentId/profile
```
**Response Structure (Public Profile DTO):**
```typescript
interface AgentPublicProfile {
id: string;
fullName: string;
avatarUrl: string | null;
// Agent-specific fields
licenseNumber: string | null;
agency: string | null;
qualityScore: number;
bio: string | null;
serviceAreas: string[];
isVerified: boolean;
// Stats
totalListings: number;
activeListings: number;
avgReviewRating: number;
totalReviews: number;
// Contact (optional, may require user preferences)
phone?: string;
// Timestamps
createdAt: string;
updatedAt: string;
}
```
**Related Endpoints Needed:**
```http
GET /listings?agentId=:agentId&status=ACTIVE # Agent's active listings
GET /reviews/stats?targetType=AGENT&targetId=:agentId # Agent reviews stats
GET /reviews?targetType=AGENT&targetId=:agentId&limit=10 # Recent agent reviews
```
These endpoints already exist and are public (no authentication required).
---
## 4. SHARED UI COMPONENTS & DESIGN PATTERNS
### Tailwind/Design System
**File:** `apps/web/tailwind.config.ts`
```typescript
// CSS Variables used (dark mode support)
colors: {
primary, primary-foreground
secondary, secondary-foreground
destructive, destructive-foreground
muted, muted-foreground
accent, accent-foreground
card, card-foreground
}
// Radius variable: var(--radius)
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
}
```
### Available UI Components
**File:** `apps/web/components/ui/`
- `button.tsx` — Styled button with variants
- `card.tsx` — Card (CardContent, etc.)
- `badge.tsx` — Badge with variants
- `input.tsx`, `label.tsx` — Form controls
- `dialog.tsx` — Modal dialog
- `tabs.tsx` — Tab navigation
- `table.tsx` — Data table
### Key Component Patterns
#### Badge Component
```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>
```
#### Card Pattern
```typescript
<Card>
<CardContent className="p-4">
{/* Content */}
</CardContent>
</Card>
```
#### Property Card (Listing Display) - REUSE THIS
**File:** `apps/web/components/search/property-card.tsx`
Shows a property with:
- Image gallery
- Price (formatted)
- Title & address
- Type, area, bedrooms badges
- Transaction type badge
**Reusable for:** Agent's listings display
---
## 5. STYLING & DESIGN PATTERNS
### Global CSS
**File:** `apps/web/app/globals.css`
Uses CSS variables for theming:
```css
--primary: hsl(var(--primary-h), var(--primary-s), var(--primary-l))
--background: hsl(...)
--card: hsl(...)
/* Dark mode support via [data-theme="dark"] */
```
### Typography
- Font: Inter (configured in tailwind.config.ts via CSS variable `--font-inter`)
- Heading levels: h1, h2, h3, h4
- Use classes: `text-lg font-bold`, `text-sm text-muted-foreground`
### Spacing
- Tailwind standard: `p-4`, `mt-8`, `gap-3`, etc.
- Card padding: `p-4`
- Section padding: `py-16`, `py-24` for hero sections
### Example Layout Pattern
```typescript
<section className="py-16 md:py-24">
<div className="mx-auto max-w-7xl px-4">
{/* Content */}
</div>
</section>
```
---
## 6. STATE MANAGEMENT & DATA FETCHING
### API Client Pattern
**File:** `apps/web/lib/api-client.ts`
```typescript
// Usage:
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 }),
};
// CSRF protection included automatically
```
### Server-Side Data Fetching Pattern
**File:** `apps/web/lib/listings-server.ts`
```typescript
// Example: fetch on server at build time or request time
export async function fetchListingById(id: string) {
try {
const res = await fetch(`${API_BASE_URL}/listings/${id}`, {
next: { revalidate: 3600 } // ISR: revalidate every 1 hour
});
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}
```
### Client-Side Data Fetching Pattern
```typescript
// In React component (using 'use client')
const [data, setData] = useState(null);
React.useEffect(() => {
apiClient
.get('/endpoint')
.then((res) => setData(res))
.catch(() => setError(true))
.finally(() => setLoading(false));
}, []);
```
### No Global State (Zustand)
- **Currently no Zustand stores** for agent data in codebase
- **Pattern:** Fetch data in page component → pass to child components
- Dashboard uses local useState for profile fetch
---
## 7. SEO PATTERNS & STRUCTURED DATA
### Metadata Generation Pattern
**File:** `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],
},
};
}
```
### JSON-LD Structured Data
**File:** `apps/web/components/seo/json-ld.tsx`
```typescript
// Example: RealEstateListing schema
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' },
// ... more properties
};
}
// Usage in page:
<JsonLd data={listingJsonLd} />
<JsonLd data={breadcrumbJsonLd} />
```
### For Agent Profile (Schema.org)
**Appropriate schema:** `LocalBusiness` or `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. EXISTING LISTING CARD COMPONENTS
### Property Card Component
**File:** `apps/web/components/search/property-card.tsx`
```typescript
interface PropertyCardProps {
listing: ListingDetail;
compact?: boolean;
}
// Displays:
// - Image gallery (with count badge)
// - Price (formatted)
// - Title & address
// - Badges: Transaction type, property type, area, bedrooms, bathrooms, direction
// - Compare button
```
### Used in:
- Home page featured listings
- Search results
- Comparison page
**For Agent Profile:** Can reuse this component to display agent's listings!
---
## 9. REVIEW & RATING COMPONENTS
### Review API Endpoints
**File:** `apps/api/src/modules/reviews/presentation/controllers/reviews.controller.ts`
```typescript
// Endpoints:
GET /reviews # List reviews by target (pagination)
GET /reviews/stats # Get aggregate rating stats
GET /reviews/me # Get authenticated user's reviews
POST /reviews # Create review (authenticated)
DELETE /reviews/:id # Delete own review
// Query params:
GET /reviews?targetType=AGENT&targetId=:id&page=1&limit=20
GET /reviews/stats?targetType=AGENT&targetId=:id
```
### Review DTO
```typescript
interface ReviewItemData {
id: string;
userId: string;
targetType: string;
targetId: string;
rating: number; // 1-5
comment: string | null;
createdAt: string;
// User info:
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;
};
}
```
### No Review Display Component Yet
- **Dashboard profile** shows `agentProfile.totalReviews` and `avgReviewRating`
- **Opportunity:** Create a reusable `ReviewCard` and `RatingStars` component for agent profile
---
## 10. TYPE DEFINITIONS & INTERFACES
### Core Types Used Frontend
```typescript
// From 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';
// From 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;
}
// From 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[];
// ... 15+ other properties
};
}
```
### Validation Schemas (Zod)
**File:** `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' },
// ... more
] as const;
export const LISTING_STATUSES = {
DRAFT: { label: 'Nháp', variant: 'secondary' },
ACTIVE: { label: 'Đang bán', variant: 'success' },
// ...
};
```
---
## 11. IMPLEMENTATION CHECKLIST FOR AGENT PROFILE PAGE
### Backend (NestJS API)
**New DTO & Interfaces:**
```
✓ CreateGetAgentPublicProfileQuery
✓ GetAgentPublicProfileHandler
✓ AgentPublicProfileDto (response)
✓ Update agent.repository.ts with method: getPublicProfile(agentId: string)
✓ Update prisma-agent.repository.ts to fetch via Prisma
```
**New Endpoint:**
```
✓ GET /agents/:agentId/profile (public, no auth)
✓ Returns: AgentPublicProfileDto
✓ Validates agentId exists
✓ Returns 404 if not found
```
**Leverage Existing:**
- Review queries: Already public
- Listings queries: Already public
- User profile: Linking to agent.userId
### Frontend (Next.js)
**New Files:**
```
✓ apps/web/app/[locale]/(public)/agents/ (directory)
✓ apps/web/app/[locale]/(public)/agents/[id]/ (directory)
✓ apps/web/app/[locale]/(public)/agents/[id]/page.tsx (server component)
✓ apps/web/app/[locale]/(public)/agents/[id]/layout.tsx (optional)
✓ apps/web/lib/agents-api.ts (API client)
✓ apps/web/lib/agents-server.ts (server-side fetch for ISR)
✓ apps/web/components/agents/ (new directory)
✓ 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
```
**Page Structure (follows listing pattern):**
```typescript
// [id]/page.tsx (Server Component)
export async function generateMetadata({ params }): Promise<Metadata> {
// Fetch agent
// Return title, description, OG image, canonical URL
}
export default async function AgentProfilePage({ params }) {
// Fetch agent profile (server-side, ISR)
// Render JsonLd breadcrumb
// Render JsonLd LocalBusiness or ProfessionalService
// Pass to client component
return <>
<JsonLd data={agentJsonLd} />
<AgentDetailClient agent={agent} />
</>
}
```
**Client Component:**
- Display agent info (name, avatar, bio, license, agency)
- Show quality score & badges
- Render reviews section
- Render listings section
- Contact/inquiry button (optional)
**API Client:**
```typescript
export const agentsApi = {
getById: (id: string) => apiClient.get<AgentPublicProfile>(`/agents/${id}/profile`),
};
```
---
## 12. KEY FILES TO REFERENCE/ADAPT
### Backend
| File | Purpose |
|------|---------|
| `apps/api/src/modules/agents/presentation/controllers/agents.controller.ts` | Add new public endpoint |
| `apps/api/src/modules/agents/domain/repositories/agent.repository.ts` | Add interface for public profile method |
| `apps/api/src/modules/agents/infrastructure/repositories/prisma-agent.repository.ts` | Implement public profile fetch |
| `apps/api/src/modules/agents/application/queries/` | Create new query handler for public profile |
| `prisma/schema.prisma` | Reference for Agent model |
### Frontend — Reference Examples
| File | Purpose | Reuse Pattern |
|------|---------|---|
| `apps/web/app/[locale]/(public)/listings/[id]/page.tsx` | Listing detail page | Use as template for agent page |
| `apps/web/components/search/property-card.tsx` | Property card | Reuse for agent's listings |
| `apps/web/lib/listings-api.ts` | Listings API client | Create similar agents-api.ts |
| `apps/web/lib/listings-server.ts` | Server-side fetch | Create similar agents-server.ts |
| `apps/web/components/seo/json-ld.tsx` | Structured data | Adapt for LocalBusiness schema |
| `apps/web/lib/currency.ts` | Price formatting | Reuse for listing prices |
| `apps/web/tailwind.config.ts` | Design system | Reference for styling |
---
## 13. SUMMARY OF KEY FINDINGS
### What Exists (Reusable)
✅ Agent model in Prisma with all needed fields
✅ API endpoints for listings, reviews (public)
✅ UI components: Card, Badge, Button, etc.
✅ Tailwind design system with dark mode
✅ SEO pattern with metadata generation & JSON-LD
✅ Image gallery component for listings
✅ PropertyCard component for listings display
✅ API client with CSRF protection
✅ Server-side data fetching with ISR pattern
### What Needs Building (Agent Profile)
🔨 `/agents/[id]` page (server component with metadata)
🔨 `AgentDetailClient` component (client-side rendering)
🔨 Public endpoint: `GET /agents/:agentId/profile`
🔨 Agent listings section (reuse PropertyCard)
🔨 Agent reviews section (fetch & display reviews)
🔨 Rating stars/aggregate display component
🔨 agents-api.ts (fetch agent profile)
🔨 agents-server.ts (server-side fetch for ISR)
🔨 JSON-LD LocalBusiness schema for agent
### Architecture Decisions
- **Routing:** Place at `apps/web/app/[locale]/(public)/agents/[id]/page.tsx`
- **Pattern:** Follow listing detail page pattern exactly
- **Metadata:** Use generateMetadata() server function
- **Components:** Split into Server Component (page) + Client Component (interactive)
- **SEO:** Include breadcrumb + LocalBusiness JSON-LD
- **Styling:** Use existing Tailwind tokens + components
- **Data Fetching:** Server-side fetch with ISR revalidation (3600s)
---
## 14. NEXT STEPS
1. **Design API DTO** for public agent profile
2. **Create backend query handler** for fetching public agent profile
3. **Create frontend API client** (agents-api.ts)
4. **Build page structure** following listing detail pattern
5. **Create agent detail client component** with sections
6. **Add reviews display section** with star ratings
7. **Add listings display section** reusing PropertyCard
8. **Generate JSON-LD** structured data for SEO
9. **Test ISR & metadata** generation
10. **Add international routes** (e.g., /en/agents/[id] via locale)