diff --git a/frontend/src/components/site-finder/ScoreCard.tsx b/frontend/src/components/site-finder/ScoreCard.tsx index 747e813b..1e8d1494 100644 --- a/frontend/src/components/site-finder/ScoreCard.tsx +++ b/frontend/src/components/site-finder/ScoreCard.tsx @@ -7,6 +7,7 @@ import type { ParcelAnalysisAirQuality, ParcelAnalysisWind, ParcelAnalysisWeather, + ParcelLocation, } from "@/types/site-finder"; import type { FeatureCollection } from "geojson"; @@ -16,6 +17,7 @@ import { SeasonalWeatherBlock } from "./SeasonalWeatherBlock"; import { HydrologyBlock } from "./HydrologyBlock"; import { GeotechRiskBlock } from "./GeotechRiskBlock"; import { IsochronesPanel } from "./IsochronesPanel"; +import { SuccessRecommendationBlock } from "./SuccessRecommendationBlock"; interface Props { data: ParcelAnalysis; @@ -481,6 +483,44 @@ function WeatherBlock({ ); } +// ── Center distance row ─────────────────────────────────────────────────────── + +function centerBonusColor(bonus: number): string { + if (bonus >= 3.0) return "#16a34a"; + if (bonus >= 1.5) return "#1d4ed8"; + return "#6b7280"; +} + +function CenterDistanceRow({ location }: { location: ParcelLocation }) { + const color = centerBonusColor(location.center_bonus); + return ( +
+ 📍 + + До центра ЕКБ:{" "} + + {location.distance_to_center_km.toFixed(1)} км + + + {location.center_bonus > 0 && ( + + (+{location.center_bonus.toFixed(1)} к score) + + )} +
+ ); +} + // ── ScoreCard ───────────────────────────────────────────────────────────────── export function ScoreCard({ data, onIsochronesResult }: Props) { @@ -570,6 +610,7 @@ export function ScoreCard({ data, onIsochronesResult }: Props) { {data.score_explanation} )} + {data.location && } @@ -727,6 +768,20 @@ export function ScoreCard({ data, onIsochronesResult }: Props) { )} + {/* Success recommendation */} + {"success_recommendation" in data && ( +
+ +
+ )} + {/* Seasonal weather */} {"seasonal_weather" in data && (
0.5) return "#16a34a"; + if (score >= 0) return "#d97706"; + return "#6b7280"; +} + +function successBg(score: number): string { + if (score > 0.5) return "#dcfce7"; + if (score >= 0) return "#fef3c7"; + return "#f3f4f6"; +} + +function formatPrice(v: number | null): string { + if (v == null) return "—"; + return (v / 1000).toFixed(0) + " тыс"; +} + +function formatArea(v: number | null): string { + if (v == null) return "—"; + return v.toFixed(0) + " м²"; +} + +function SuccessBadge({ bucket }: { bucket: SuccessRankingBucket }) { + const color = successColor(bucket.success_score); + const bg = successBg(bucket.success_score); + return ( +
+
+
+
+ + {bucket.success_score.toFixed(2)} + +
+ ); +} + +export function SuccessRecommendationBlock({ recommendation }: Props) { + if (!recommendation || recommendation.ranking.length === 0) { + return ( +
+ Недостаточно сделок в районе для рейтинга (мин 30) +
+ ); + } + + const { district, ranking, top_bucket, note } = recommendation; + const top5 = ranking.slice(0, 5); + + return ( +
+
+ Что хорошо продаётся в районе {district} +
+ +
+ + + + {[ + "Бакет", + "Сделок", + "Ср. цена ₽/м²", + "Ср. площадь м²", + "Успех", + ].map((h) => ( + + ))} + + + + {top5.map((row) => { + const isTop = row.bucket === top_bucket.bucket; + return ( + + + + + + + + ); + })} + +
+ {h} +
+ {isTop ? "⭐ " : ""} + {row.bucket} + + {row.n_deals} + + {formatPrice(row.avg_price_per_m2)} + + {formatArea(row.avg_area_m2)} + + +
+
+ +
{note}
+
+ ); +} diff --git a/frontend/src/types/site-finder.ts b/frontend/src/types/site-finder.ts index 343a595b..4fe5dee8 100644 --- a/frontend/src/types/site-finder.ts +++ b/frontend/src/types/site-finder.ts @@ -137,6 +137,28 @@ export interface MarketTrend { radius_km: number; } +export interface ParcelLocation { + distance_to_center_km: number; + center_bonus: number; + ekb_center: { lat: number; lon: number }; + note: string; +} + +export interface SuccessRankingBucket { + bucket: string; + success_score: number; + n_deals: number; + avg_price_per_m2: number | null; + avg_area_m2: number | null; +} + +export interface ParcelSuccessRecommendation { + district: string; + ranking: SuccessRankingBucket[]; + top_bucket: SuccessRankingBucket; + note: string; +} + export interface ParcelAnalysis { cad_num: string; source: "cad_quarter" | "cad_building"; @@ -159,6 +181,9 @@ export interface ParcelAnalysis { geotech_risk?: GeotechRisk; geology?: ParcelAnalysisGeology; isochrones_available?: boolean; + score_without_center?: number; + location?: ParcelLocation; + success_recommendation?: ParcelSuccessRecommendation | null; } export type PoiCategory =