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) => (
+ |
+ {h}
+ |
+ ))}
+
+
+
+ {top5.map((row) => {
+ const isTop = row.bucket === top_bucket.bucket;
+ return (
+
+ |
+ {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 =