feat(listings): phase A — surface usableAreaM2, floor/totalFloors, metroDistanceM
Some checks failed
E2E Tests / Playwright E2E (push) Failing after 9s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 46s
Security Scanning / Trivy Filesystem Scan (push) Has been cancelled
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 9s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m18s
Deploy / Build API Image (push) Failing after 28s
Deploy / Build Web Image (push) Failing after 12s
Deploy / Build AI Services Image (push) Failing after 10s
Security Scanning / Trivy Scan — Web Image (push) Failing after 31s
Deploy / Deploy to Staging (push) Has been cancelled
Deploy / Smoke Test Staging (push) Has been cancelled
Deploy / Rollback Staging (push) Has been cancelled
Deploy / Smoke Test Production (push) Has been cancelled
Deploy / Rollback Production (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Security Scanning / Security Gate (push) Has been cancelled
Security Scanning / Trivy Scan — AI Services Image (push) Has started running
Some checks failed
E2E Tests / Playwright E2E (push) Failing after 9s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 46s
Security Scanning / Trivy Filesystem Scan (push) Has been cancelled
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 9s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 1m18s
Deploy / Build API Image (push) Failing after 28s
Deploy / Build Web Image (push) Failing after 12s
Deploy / Build AI Services Image (push) Failing after 10s
Security Scanning / Trivy Scan — Web Image (push) Failing after 31s
Deploy / Deploy to Staging (push) Has been cancelled
Deploy / Smoke Test Staging (push) Has been cancelled
Deploy / Rollback Staging (push) Has been cancelled
Deploy / Smoke Test Production (push) Has been cancelled
Deploy / Rollback Production (push) Has been cancelled
Deploy / Deploy to Production (push) Has been cancelled
Security Scanning / Security Gate (push) Has been cancelled
Security Scanning / Trivy Scan — AI Services Image (push) Has started running
The Property table already stores usableAreaM2, floor, totalFloors, metroDistanceM and nearbyPOIs but the listing detail endpoint was dropping them. Add them to ListingDetailData + the Prisma read query, mirror the additions on the frontend ListingDetail type, and render them on the detail page: - Quick-specs bar now shows "Tầng X / Y" (floor/totalFloors) with a sensible fallback to `floors`, plus "Cách metro" when populated. - Details card adds rows: "Diện tích sử dụng", "Tầng / Tổng tầng" (merges floor + totalFloors), "Cách metro gần nhất" (formatted m/km). - New "transit" icon for the metro stat. Purely additive surfacing — no schema change, no migration. Listings missing these fields still render as before. Test fixture in listing-detail-client.spec.tsx extended with the new nullable fields so the type stays compatible. Phase A of 4 (Listings detail enhancement plan). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,13 +28,18 @@ export interface ListingDetailData {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
areaM2: number;
|
||||
usableAreaM2: number | null;
|
||||
bedrooms: number | null;
|
||||
bathrooms: number | null;
|
||||
floors: number | null;
|
||||
floor: number | null;
|
||||
totalFloors: number | null;
|
||||
direction: Direction | null;
|
||||
yearBuilt: number | null;
|
||||
legalStatus: string | null;
|
||||
amenities: unknown;
|
||||
nearbyPOIs: unknown;
|
||||
metroDistanceM: number | null;
|
||||
projectName: string | null;
|
||||
media: ListingMediaData[];
|
||||
};
|
||||
|
||||
@@ -62,13 +62,18 @@ export async function findByIdWithProperty(
|
||||
latitude: geo.latitude,
|
||||
longitude: geo.longitude,
|
||||
areaM2: listing.property.areaM2,
|
||||
usableAreaM2: listing.property.usableAreaM2,
|
||||
bedrooms: listing.property.bedrooms,
|
||||
bathrooms: listing.property.bathrooms,
|
||||
floors: listing.property.floors,
|
||||
floor: listing.property.floor,
|
||||
totalFloors: listing.property.totalFloors,
|
||||
direction: listing.property.direction,
|
||||
yearBuilt: listing.property.yearBuilt,
|
||||
legalStatus: listing.property.legalStatus,
|
||||
amenities: listing.property.amenities,
|
||||
nearbyPOIs: listing.property.nearbyPOIs,
|
||||
metroDistanceM: listing.property.metroDistanceM,
|
||||
projectName: listing.property.projectName,
|
||||
media: listing.property.media.map((m) => ({
|
||||
id: m.id,
|
||||
|
||||
@@ -82,13 +82,18 @@ function makeListing(overrides: Partial<ListingDetail> = {}): ListingDetail {
|
||||
district: 'Quận Bình Thạnh',
|
||||
city: 'Hồ Chí Minh',
|
||||
areaM2: 75,
|
||||
usableAreaM2: 68,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
floors: null,
|
||||
floor: null,
|
||||
totalFloors: null,
|
||||
direction: 'SOUTH',
|
||||
yearBuilt: 2020,
|
||||
legalStatus: 'Sổ hồng',
|
||||
amenities: ['Hồ bơi', 'Gym'],
|
||||
nearbyPOIs: null,
|
||||
metroDistanceM: null,
|
||||
projectName: 'Vinhomes Central Park',
|
||||
latitude: 10.7975,
|
||||
longitude: 106.721,
|
||||
|
||||
@@ -138,12 +138,29 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) {
|
||||
{property.bathrooms != null && (
|
||||
<QuickStat icon="bath" label="Phòng tắm" value={`${property.bathrooms}`} />
|
||||
)}
|
||||
{property.floors != null && (
|
||||
{property.floor != null && property.totalFloors != null && (
|
||||
<QuickStat icon="floors" label="Tầng" value={`${property.floor} / ${property.totalFloors}`} />
|
||||
)}
|
||||
{property.floor != null && property.totalFloors == null && (
|
||||
<QuickStat icon="floors" label="Tầng" value={`${property.floor}`} />
|
||||
)}
|
||||
{property.floor == null && property.floors != null && (
|
||||
<QuickStat icon="floors" label="Số tầng" value={`${property.floors}`} />
|
||||
)}
|
||||
{property.direction && (
|
||||
<QuickStat icon="compass" label="Hướng" value={getLabel(DIRECTIONS, property.direction) || ''} />
|
||||
)}
|
||||
{property.metroDistanceM != null && (
|
||||
<QuickStat
|
||||
icon="transit"
|
||||
label="Cách metro"
|
||||
value={
|
||||
property.metroDistanceM < 1000
|
||||
? `${property.metroDistanceM} m`
|
||||
: `${(property.metroDistanceM / 1000).toFixed(1)} km`
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
@@ -167,14 +184,39 @@ export function ListingDetailClient({ listing }: ListingDetailClientProps) {
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||
<InfoItem label="Loại BĐS" value={propertyTypeLabel || '---'} />
|
||||
<InfoItem label="Diện tích" value={`${property.areaM2} m\u00B2`} />
|
||||
<InfoItem label="Diện tích tim tường" value={`${property.areaM2} m²`} />
|
||||
<InfoItem
|
||||
label="Diện tích sử dụng"
|
||||
value={property.usableAreaM2 != null ? `${property.usableAreaM2} m²` : '---'}
|
||||
/>
|
||||
<InfoItem label="Phòng ngủ" value={property.bedrooms != null ? `${property.bedrooms}` : '---'} />
|
||||
<InfoItem label="Phòng tắm" value={property.bathrooms != null ? `${property.bathrooms}` : '---'} />
|
||||
<InfoItem label="Số tầng" value={property.floors != null ? `${property.floors}` : '---'} />
|
||||
<InfoItem
|
||||
label="Tầng / Tổng tầng"
|
||||
value={
|
||||
property.floor != null && property.totalFloors != null
|
||||
? `${property.floor} / ${property.totalFloors}`
|
||||
: property.floor != null
|
||||
? `${property.floor}`
|
||||
: property.floors != null
|
||||
? `${property.floors}`
|
||||
: '---'
|
||||
}
|
||||
/>
|
||||
<InfoItem label="Hướng" value={getLabel(DIRECTIONS, property.direction) || '---'} />
|
||||
<InfoItem label="Năm xây" value={property.yearBuilt ? `${property.yearBuilt}` : '---'} />
|
||||
<InfoItem label="Pháp lý" value={property.legalStatus || '---'} />
|
||||
<InfoItem label="Dự án" value={property.projectName || '---'} />
|
||||
<InfoItem
|
||||
label="Cách metro gần nhất"
|
||||
value={
|
||||
property.metroDistanceM != null
|
||||
? property.metroDistanceM < 1000
|
||||
? `${property.metroDistanceM} m`
|
||||
: `${(property.metroDistanceM / 1000).toFixed(1)} km`
|
||||
: '---'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -387,6 +429,11 @@ function QuickStat({ icon, label, value }: { icon: string; label: string; value:
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 9l3 3m0 0l3-3m-3 3V6m0 6l-3 3m3-3l3 3m-3-3v6" />
|
||||
</svg>
|
||||
),
|
||||
transit: (
|
||||
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -56,13 +56,18 @@ export interface ListingDetail {
|
||||
district: string;
|
||||
city: string;
|
||||
areaM2: number;
|
||||
usableAreaM2: number | null;
|
||||
bedrooms: number | null;
|
||||
bathrooms: number | null;
|
||||
floors: number | null;
|
||||
floor: number | null;
|
||||
totalFloors: number | null;
|
||||
direction: Direction | null;
|
||||
yearBuilt: number | null;
|
||||
legalStatus: string | null;
|
||||
amenities: string[] | null;
|
||||
nearbyPOIs: unknown;
|
||||
metroDistanceM: number | null;
|
||||
projectName: string | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
|
||||
Reference in New Issue
Block a user