Files
goodgo-platform/CODEBASE_QUICK_REFERENCE.md
Ho Ngoc Hai 33c2e5ac1d feat(load-tests): add K6 coverage for search, admin, and MCP endpoints
Add three new K6 load test scripts to cover previously untested API surfaces:

- search-advanced.js: Combined geo + text + filter queries, paginated deep
  search, and sort variations against /search and /search/geo (300 peak VUs)
- admin.js: Moderation queue CRUD, bulk moderation, dashboard stats, audit
  logs, and user management endpoints (50 peak VUs)
- mcp.js: MCP server discovery, SSE connection, property-search tool calls,
  valuation/batch-valuation, and feature extraction (120 peak VUs)

Also updates README with new suite documentation, per-suite custom thresholds,
and adds the new suites to the CI workflow_dispatch selector.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 20:14:52 +07:00

11 KiB

GoodGo Platform - Quick Reference Guide

Pages (where to place new features)

  • Inquiry pages: apps/web/app/[locale]/(dashboard)/inquiries/
  • Lead pages: apps/web/app/[locale]/(dashboard)/leads/
  • Example pages: apps/web/app/[locale]/(dashboard)/listings/ (reference)

API Layer

  • Inquiry API: Create apps/web/lib/inquiries-api.ts
  • Lead API: Create apps/web/lib/leads-api.ts
  • Base client: apps/web/lib/api-client.ts ← reuse this

Components

  • UI base components: apps/web/components/ui/ (Button, Card, Badge, Table, Select, Input)
  • Domain components: apps/web/components/inquiries/, apps/web/components/leads/
  • Example domain component: apps/web/components/listings/listing-status-badge.tsx

Hooks

  • Create hooks: apps/web/lib/hooks/use-inquiries.ts, apps/web/lib/hooks/use-leads.ts
  • Example hook: apps/web/lib/hooks/use-listings.ts

Stores (if needed)

  • Location: apps/web/lib/ (e.g., inquiry-store.ts, lead-store.ts)
  • Example: apps/web/lib/comparison-store.ts

Backend API

  • Inquiries controller: apps/api/src/modules/inquiries/presentation/controllers/inquiries.controller.ts
  • Leads controller: apps/api/src/modules/leads/presentation/controllers/leads.controller.ts

🔌 Backend API Endpoints

Inquiries

POST   /api/v1/inquiries                   Create inquiry
GET    /api/v1/inquiries/listing/{id}      List by listing (paginated)
GET    /api/v1/inquiries/agent/me          List my inquiries (AGENT role)
PATCH  /api/v1/inquiries/{id}/read         Mark as read (AGENT role)

Leads

POST   /api/v1/leads                       Create lead (AGENT role)
GET    /api/v1/leads                       List leads (AGENT role, paginated)
GET    /api/v1/leads/stats                 Get stats (AGENT role)
PATCH  /api/v1/leads/{id}/status           Update status (AGENT role)
DELETE /api/v1/leads/{id}                  Delete lead (AGENT role)

🎨 Component Templates

List Page Template

'use client';
import { useQuery } from '@tanstack/react-query';
import { useTranslations } from 'next-intl';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Select } from '@/components/ui/select';

export default function InquiriesPage() {
  const t = useTranslations('inquiries');
  const [filters, setFilters] = useState({ page: 1, status: '' });
  
  const { data, isLoading } = useQuery({
    queryKey: ['inquiries', filters],
    queryFn: () => inquiriesApi.list(filters),
  });
  
  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <h1 className="text-2xl font-bold">{t('title')}</h1>
      </div>
      
      {/* Stats cards */}
      <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <StatCard label={t('total')} value={data?.total ?? 0} />
      </div>
      
      {/* Filters */}
      <div className="flex gap-3 flex-wrap">
        <Select value={filters.status} onChange={(e) => setFilters({...filters, status: e.target.value})}>
          <option value="">{t('allStatus')}</option>
          {/* ... status options */}
        </Select>
      </div>
      
      {/* Table */}
      {isLoading ? (
        <div className="flex justify-center"><Spinner /></div>
      ) : (
        <Card>
          <CardContent>
            <Table>
              <TableHeader>
                <TableRow>
                  <TableHead>{t('name')}</TableHead>
                  <TableHead>{t('status')}</TableHead>
                  <TableHead className="text-right">{t('actions')}</TableHead>
                </TableRow>
              </TableHeader>
              <TableBody>
                {data?.items.map(item => (
                  <TableRow key={item.id}>
                    <TableCell>{item.name}</TableCell>
                    <TableCell><Badge>{item.status}</Badge></TableCell>
                    <TableCell className="text-right">
                      <Button variant="ghost" size="sm">View</Button>
                    </TableCell>
                  </TableRow>
                ))}
              </TableBody>
            </Table>
          </CardContent>
        </Card>
      )}
    </div>
  );
}

API Service Template

// apps/web/lib/inquiries-api.ts
import { apiClient } from './api-client';

export interface InquiryDto {
  id: string;
  listingId: string;
  userId: string;
  message: string;
  isRead: boolean;
  createdAt: string;
}

export interface InquiryListResponse {
  items: InquiryDto[];
  total: number;
  page: number;
  limit: number;
}

export const inquiriesApi = {
  list: (params: { page?: number; limit?: number; status?: string }) =>
    apiClient.get<InquiryListResponse>('/inquiries', params),
  
  getById: (id: string) =>
    apiClient.get<InquiryDto>(`/inquiries/${id}`),
  
  markAsRead: (id: string) =>
    apiClient.patch(`/inquiries/${id}/read`, {}),
};

Hook Template

// apps/web/lib/hooks/use-inquiries.ts
import { useQuery } from '@tanstack/react-query';
import { inquiriesApi } from '@/lib/inquiries-api';

export const inquiriesKeys = {
  all: ['inquiries'] as const,
  list: (params: any) => ['inquiries', 'list', params] as const,
  detail: (id: string) => ['inquiries', 'detail', id] as const,
};

export function useInquiries(params = {}) {
  return useQuery({
    queryKey: inquiriesKeys.list(params),
    queryFn: () => inquiriesApi.list(params),
  });
}

export function useInquiry(id: string) {
  return useQuery({
    queryKey: inquiriesKeys.detail(id),
    queryFn: () => inquiriesApi.getById(id),
    enabled: !!id,
  });
}

Status Badge Component Template

// apps/web/components/inquiries/inquiry-status-badge.tsx
import { Badge } from '@/components/ui/badge';

const INQUIRY_STATUSES = {
  NEW: { label: 'Mới', variant: 'info' as const },
  READ: { label: 'Đã xem', variant: 'secondary' as const },
  REPLIED: { label: 'Đã trả lời', variant: 'success' as const },
};

export function InquiryStatusBadge({ status }: { status: string }) {
  const config = INQUIRY_STATUSES[status as keyof typeof INQUIRY_STATUSES] ?? {
    label: status,
    variant: 'outline' as const,
  };
  return <Badge variant={config.variant}>{config.label}</Badge>;
}

📝 Translations (i18n)

Add to apps/web/messages/vi.json and apps/web/messages/en.json:

{
  "inquiries": {
    "title": "Quản lý Liên hệ",
    "subtitle": "Xem và quản lý các liên hệ từ khách hàng",
    "allStatus": "Tất cả trạng thái",
    "new": "Mới",
    "read": "Đã xem",
    "replied": "Đã trả lời",
    "total": "Tổng liên hệ",
    "thisMonth": "Tháng này",
    "message": "Tin nhắn",
    "from": "Từ",
    "date": "Ngày tạo",
    "markAsRead": "Đánh dấu đã xem"
  },
  "leads": {
    "title": "Quản lý Khách hàng tiềm năng",
    "subtitle": "Theo dõi và quản lý khách hàng tiềm năng",
    "name": "Tên khách hàng",
    "phone": "Số điện thoại",
    "email": "Email",
    "source": "Nguồn",
    "score": "Điểm số",
    "status": "Trạng thái",
    "new": "Mới",
    "contacted": "Đã liên hệ",
    "qualified": "Đã xác nhận",
    "negotiating": "Đang thương lượng",
    "converted": "Chuyển đổi",
    "lost": "Mất"
  }
}

Usage in components:

const t = useTranslations('inquiries');
// or
const t = useTranslations('leads');

🎯 Styling Conventions

Color Classes

/* Status indicators */
.success { @apply text-green-600 bg-green-50 }
.warning { @apply text-yellow-600 bg-yellow-50 }
.info { @apply text-blue-600 bg-blue-50 }
.error { @apply text-red-600 bg-red-50 }

/* Typography */
.title { @apply text-2xl font-bold }
.subtitle { @apply text-muted-foreground text-sm }
.label { @apply text-xs text-muted-foreground uppercase }

/* Layout */
.card-grid { @apply grid gap-4 sm:grid-cols-2 lg:grid-cols-3 }
.flex-between { @apply flex items-center justify-between }

Responsive Breakpoints

// Mobile first
className="w-full"           // Mobile: full width
className="sm:w-1/2"         // 640px+: 50%
className="md:w-1/3"         // 768px+: 33%
className="lg:grid-cols-3"   // 1024px+: 3 columns

🔐 Authentication & Authorization

Protected Pages

// pages automatically protected by (dashboard) group
// which has JwtAuthGuard applied via middleware or layout

// For role-specific pages (AGENT only):
// Use guard directly or check in component
const { user } = useAuthStore();
if (!user?.roles.includes('AGENT')) {
  // redirect or show error
}

API Calls with Auth

// Automatically includes:
// - httpOnly cookies (JWT)
// - CSRF token from XSRF-TOKEN cookie
// - X-CSRF-Token header (POST/PATCH/DELETE)

const { data } = await inquiriesApi.list(); // Auth headers auto-included

🧪 Testing Patterns

See existing tests in __tests__ folders for reference:

  • apps/web/lib/__tests__/auth-store.spec.ts
  • apps/web/components/ui/__tests__/

Pre-Build Checklist

Before creating Inquiry & Lead pages:

  • Create API service files (inquiries-api.ts, leads-api.ts)
  • Create React Query hooks (use-inquiries.ts, use-leads.ts)
  • Create status badge components
  • Add translations to vi.json and en.json
  • Create page components under (dashboard) group
  • Test API endpoints with backend
  • Verify auth guards (JwtAuthGuard, RolesGuard)
  • Test pagination with query params
  • Test loading/error states
  • Test responsive design (mobile/tablet/desktop)
  • Add JSDoc comments to reusable functions
  • Test dark mode colors

📚 Key Files to Reference

REFERENCE PAGES:
- apps/web/app/[locale]/(dashboard)/listings/page.tsx ← Best example
- apps/web/app/[locale]/(dashboard)/dashboard/page.tsx ← Stats & cards

REFERENCE COMPONENTS:
- apps/web/components/listings/listing-status-badge.tsx ← Status badge pattern
- apps/web/components/search/filter-bar.tsx ← Filter pattern
- apps/web/components/ui/table.tsx ← Table pattern

REFERENCE HOOKS:
- apps/web/lib/hooks/use-listings.ts ← React Query pattern
- apps/web/lib/hooks/use-analytics.ts ← Complex data fetching

REFERENCE STORES:
- apps/web/lib/auth-store.ts ← Async actions pattern
- apps/web/lib/comparison-store.ts ← Persistence pattern

REFERENCE API:
- apps/web/lib/listings-api.ts ← API service pattern
- apps/web/lib/auth-api.ts ← Auth API pattern

REFERENCE LAYOUT:
- apps/web/app/[locale]/(dashboard)/layout.tsx ← Dashboard nav

REFERENCE VALIDATION:
- apps/web/lib/validations/listings.ts ← Zod schema pattern