feat: add unit tests for featured listings, neighborhood scores + price history chart

- Add unit tests for FeatureListingHandler (6 tests) and ActivateFeaturedListingHandler (6 tests)
- Add unit tests for NeighborhoodScoreServiceImpl (5 tests) and GetNeighborhoodScoreHandler (2 tests)
- Add PriceHistoryChart component with recharts LineChart for listing detail page
- Wire up price history API client and integrate chart into listing detail view

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-16 18:21:44 +07:00
parent 0dda2bffdb
commit 8e9d021465
7 changed files with 560 additions and 14 deletions

View File

@@ -6,13 +6,14 @@ import * as React from 'react';
import { AddToCompareButton } from '@/components/comparison/add-to-compare-button';
import { ImageGallery } from '@/components/listings/image-gallery';
import { InquiryModal } from '@/components/listings/inquiry-modal';
import { PriceHistoryChart } from '@/components/listings/price-history-chart';
import { SocialShare } from '@/components/listings/social-share';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AiEstimateButton } from '@/components/valuation/ai-estimate-button';
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
import type { ListingDetail, NeighborhoodScoreResult } from '@/lib/listings-api';
import type { ListingDetail, NeighborhoodScoreResult, PriceHistoryItem } from '@/lib/listings-api';
import { listingsApi } from '@/lib/listings-api';
import { PROPERTY_TYPES, DIRECTIONS, TRANSACTION_TYPES } from '@/lib/validations/listings';
@@ -59,6 +60,7 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) {
const propertyTypeLabel = getLabel(PROPERTY_TYPES, property.propertyType);
const [inquiryOpen, setInquiryOpen] = React.useState(false);
const [neighborhoodScore, setNeighborhoodScore] = React.useState<NeighborhoodScoreResult | null>(null);
const [priceHistory, setPriceHistory] = React.useState<PriceHistoryItem[]>([]);
React.useEffect(() => {
if (!property.district || !property.city) return;
@@ -68,6 +70,13 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) {
.catch(() => {/* silently ignore — section simply won't render */});
}, [property.district, property.city]);
React.useEffect(() => {
listingsApi
.getPriceHistory(listing.id)
.then(setPriceHistory)
.catch(() => {/* silently ignore */});
}, [listing.id]);
return (
<div className="mx-auto max-w-6xl px-4 py-6">
{/* Breadcrumb */}
@@ -201,26 +210,55 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) {
</CardContent>
</Card>
{/* Neighborhood Score Radar Chart */}
{neighborhoodScore && (
{/* Price History Chart */}
{priceHistory.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Đánh giá khu vực</CardTitle>
<CardTitle>Lịch sử giá</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-3 flex items-center gap-2">
<span className="text-2xl font-bold text-primary">
{neighborhoodScore.totalScore.toFixed(1)}
</span>
<span className="text-sm text-muted-foreground">/10 điểm tổng</span>
</div>
<NeighborhoodRadarChart
categories={mapScoreToCategories(neighborhoodScore)}
height={300}
/>
<PriceHistoryChart data={priceHistory} />
</CardContent>
</Card>
)}
{/* Neighborhood Score Radar Chart */}
<Card>
<CardHeader>
<CardTitle>Đánh giá khu vực</CardTitle>
</CardHeader>
<CardContent>
{neighborhoodScore ? (
<>
<div className="mb-3 flex items-center gap-2">
<Badge
variant={
neighborhoodScore.totalScore > 7
? 'success'
: neighborhoodScore.totalScore >= 5
? 'warning'
: 'destructive'
}
className="px-3 py-1 text-lg font-bold"
>
{neighborhoodScore.totalScore.toFixed(1)}/10
</Badge>
<span className="text-sm text-muted-foreground">Điểm tổng khu vực</span>
</div>
<NeighborhoodRadarChart
categories={mapScoreToCategories(neighborhoodScore)}
height={300}
/>
</>
) : (
<div className="flex h-[200px] items-center justify-center rounded-lg bg-muted/50">
<p className="text-sm text-muted-foreground">
Chưa dữ liệu đánh giá khu vực này
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Sidebar */}