/** * Derive "Phù hợp với ai" personas and compose a "Vì sao nên ở đây" * narrative from existing data (listing + neighborhood score + nearby POIs). * * Phase D of the listings-detail enhancement: purely client-side — no new * backend fields are needed. Admin-authored overrides can layer on top later * once we have a `suitableFor` / `whyThisLocation` column (Phase B). */ import { Baby, HeartPulse, Home, Laptop, Shield, TrainFront, Trees, TrendingUp, type LucideIcon, } from 'lucide-react'; import type { NearbyPOICategory } from './analytics-api'; import type { ListingDetail, NeighborhoodScoreResult } from './listings-api'; export type PersonaKey = | 'family_with_kids' | 'young_family' | 'commuter' | 'single_young' | 'nature_lover' | 'investor' | 'safety_first' | 'senior'; export interface Persona { key: PersonaKey; label: string; icon: LucideIcon; reason: string; } const PERSONA_LABELS: Record = { family_with_kids: { label: 'Gia đình có con nhỏ', icon: Baby }, young_family: { label: 'Gia đình trẻ', icon: Home }, commuter: { label: 'Người đi làm xa', icon: TrainFront }, single_young: { label: 'Người trẻ / độc thân', icon: Laptop }, nature_lover: { label: 'Yêu thiên nhiên', icon: Trees }, investor: { label: 'Nhà đầu tư', icon: TrendingUp }, safety_first: { label: 'Ưu tiên an ninh', icon: Shield }, senior: { label: 'Người lớn tuổi', icon: HeartPulse }, }; type POICountByCategory = Partial>; function countPOIs(pois: Array<{ category: NearbyPOICategory }>): POICountByCategory { const counts: POICountByCategory = {}; for (const p of pois) { counts[p.category] = (counts[p.category] ?? 0) + 1; } return counts; } export function derivePersonas( listing: ListingDetail, score: NeighborhoodScoreResult | null, pois: Array<{ category: NearbyPOICategory }>, ): Persona[] { const { property } = listing; const poiCount = countPOIs(pois); const out: Persona[] = []; // Gia đình có con nhỏ — cần trường học + 2+ phòng ngủ. if (score && score.educationScore >= 7 && (property.bedrooms ?? 0) >= 2) { const schools = poiCount.school ?? 0; out.push({ ...PERSONA_LABELS.family_with_kids, key: 'family_with_kids', reason: schools > 0 ? `Khu vực có ${schools} trường học gần & điểm giáo dục ${score.educationScore}/10.` : `Điểm giáo dục khu vực ${score.educationScore}/10.`, }); } // Gia đình trẻ — 2 PN + y tế tốt (mẹ và bé). if (property.bedrooms === 2 && score && score.healthcareScore >= 7) { const hospitals = poiCount.hospital ?? 0; out.push({ ...PERSONA_LABELS.young_family, key: 'young_family', reason: hospitals > 0 ? `Có ${hospitals} bệnh viện/phòng khám gần, y tế ${score.healthcareScore}/10.` : `Y tế khu vực đạt ${score.healthcareScore}/10.`, }); } // Người đi làm xa — gần metro (<1km) HOẶC điểm giao thông cao. const metroM = property.metroDistanceM; const transitCount = poiCount.transit ?? 0; if ((metroM != null && metroM <= 1000) || (score && score.transportScore >= 7) || transitCount >= 2) { out.push({ ...PERSONA_LABELS.commuter, key: 'commuter', reason: metroM != null && metroM <= 1000 ? `Cách metro chỉ ${metroM < 1000 ? `${metroM}m` : `${(metroM / 1000).toFixed(1)}km`}.` : `Giao thông ${score?.transportScore ?? '?'}/10, ${transitCount} điểm metro/bus gần.`, }); } // Người trẻ/độc thân — studio/1PN HOẶC apartment gần mua sắm/ăn uống. const bedrooms = property.bedrooms ?? 0; const restaurantCount = poiCount.restaurant ?? 0; if ( bedrooms <= 1 || (property.propertyType === 'APARTMENT' && score && score.shoppingScore >= 7 && restaurantCount >= 2) ) { out.push({ ...PERSONA_LABELS.single_young, key: 'single_young', reason: bedrooms <= 1 ? `Thiết kế ${bedrooms} phòng ngủ phù hợp ở một mình / cặp đôi trẻ.` : `Gần ${restaurantCount} nhà hàng/quán cafe, mua sắm ${score?.shoppingScore ?? '?'}/10.`, }); } // Yêu thiên nhiên — điểm môi trường cao hoặc có công viên gần. const parkCount = poiCount.park ?? 0; if ((score && score.greeneryScore >= 7) || parkCount >= 1) { out.push({ ...PERSONA_LABELS.nature_lover, key: 'nature_lover', reason: parkCount > 0 ? `Có ${parkCount} công viên gần, môi trường ${score?.greeneryScore ?? '?'}/10.` : `Môi trường khu vực ${score?.greeneryScore ?? '?'}/10.`, }); } // Ưu tiên an ninh. if (score && score.safetyScore >= 8) { out.push({ ...PERSONA_LABELS.safety_first, key: 'safety_first', reason: `An ninh khu vực đạt ${score.safetyScore}/10.`, }); } // Người lớn tuổi — gần y tế + không ồn ào (bedrooms >= 2, có elevator ngầm // nếu là chung cư cao tầng thì totalFloors >= 5 không phải walk-up). const hospitals = poiCount.hospital ?? 0; if (score && score.healthcareScore >= 8 && hospitals >= 2) { out.push({ ...PERSONA_LABELS.senior, key: 'senior', reason: `Có ${hospitals} bệnh viện gần, y tế ${score.healthcareScore}/10.`, }); } // Nhà đầu tư — SALE + transport cao + total score high → khu vực hot. if ( listing.transactionType === 'SALE' && score && score.totalScore >= 75 && score.transportScore >= 7 ) { out.push({ ...PERSONA_LABELS.investor, key: 'investor', reason: `Khu vực tổng điểm ${score.totalScore}/100 với giao thông ${score.transportScore}/10 — tiềm năng cho thuê tốt.`, }); } return out; } /** * Compose a short narrative highlighting the strongest 2-3 reasons to live * here. Returns null when there isn't enough signal to say anything useful. */ export function composeWhyThisLocation( listing: ListingDetail, score: NeighborhoodScoreResult | null, pois: Array<{ category: NearbyPOICategory }>, ): string | null { if (!score) return null; const { property } = listing; const poiCount = countPOIs(pois); const scoreEntries: Array<{ label: string; score: number; detail: string }> = [ { label: 'giáo dục', score: score.educationScore, detail: poiCount.school ? `${poiCount.school} trường học gần` : 'hệ thống trường đa dạng', }, { label: 'y tế', score: score.healthcareScore, detail: poiCount.hospital ? `${poiCount.hospital} bệnh viện trong 2km` : 'tiện ích y tế đầy đủ', }, { label: 'giao thông', score: score.transportScore, detail: property.metroDistanceM != null && property.metroDistanceM <= 1000 ? `cách metro chỉ ${property.metroDistanceM}m` : poiCount.transit ? `${poiCount.transit} điểm metro/bus gần` : 'dễ kết nối các khu vực khác', }, { label: 'mua sắm', score: score.shoppingScore, detail: poiCount.shopping ? `${poiCount.shopping} siêu thị/TTTM gần` : 'nhiều lựa chọn mua sắm', }, { label: 'môi trường', score: score.greeneryScore, detail: poiCount.park ? `${poiCount.park} công viên gần` : 'không gian xanh tốt', }, { label: 'an ninh', score: score.safetyScore, detail: 'khu dân cư yên tĩnh', }, ]; const highlights = scoreEntries .filter((e) => e.score >= 7) .sort((a, b) => b.score - a.score) .slice(0, 3); if (highlights.length === 0) return null; const sentences = highlights.map( (h) => `${h.label.charAt(0).toUpperCase()}${h.label.slice(1)} đạt ${h.score}/10 (${h.detail})`, ); return `${sentences.join('. ')}.`; }