feat(fe): trader-style agent profile — TEC-3061

Refactors /agents/[id] from card-avatar layout to a data-dense
trading-floor style profile per TEC-3037 §5 mockup.

- Profile header: avatar, KYC badge, quality score, years exp, service areas
- KPI strip (5 cards): total listings, active, deals, avg price, rating
- Performance line chart (12m): published vs sold, derived from real listings
- Listings table (DataTable): sortable by price/area/views/inquiries, dense rows
- Reviews panel: EmptyState when none, ReviewRow cards otherwise
- Sticky right sidebar: contact card + quality donut + bio
- fetchAgentListings() server fn (agents-server.ts) via GET /listings?agentId
- SearchListingsParams.agentId added (listings-api.ts)
- page.tsx fetches listings in parallel with agent + reviews
- Test suite updated for new props (listings/listingsTotal) + new text copy
- Web unit tests: 82/82 files pass, 697/697 tests pass

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-21 03:46:19 +07:00
parent 27ba8412e1
commit 9cefd439db
5 changed files with 680 additions and 322 deletions

View File

@@ -6,6 +6,7 @@
*/
import type { AgentPublicProfile, AgentReviewStats, PaginatedReviews } from './agents-api';
import type { ListingDetail, PaginatedResult } from './listings-api';
const API_BASE_URL = process.env['NEXT_PUBLIC_API_URL'] || 'http://localhost:3001/api/v1';
@@ -65,3 +66,30 @@ export async function fetchAgentReviewStats(agentId: string): Promise<AgentRevie
return null;
}
}
/**
* Fetch listings managed by a given agent — server-only.
* Returns `{ data: [], total: 0 }` on error so callers degrade gracefully.
*/
export async function fetchAgentListings(
agentId: string,
page = 1,
limit = 50,
): Promise<{ data: ListingDetail[]; total: number }> {
try {
const qs = new URLSearchParams({
agentId,
page: String(page),
limit: String(limit),
});
const res = await fetch(`${API_BASE_URL}/listings?${qs.toString()}`, {
next: { revalidate: 300 },
});
if (!res.ok) return { data: [], total: 0 };
const result = (await res.json()) as PaginatedResult<ListingDetail>;
return { data: result.data, total: result.total };
} catch {
return { data: [], total: 0 };
}
}

View File

@@ -187,6 +187,8 @@ export interface SearchListingsParams {
minArea?: number;
maxArea?: number;
bedrooms?: number;
/** Filter by assigned agent ID */
agentId?: string;
page?: number;
limit?: number;
}