gendesign/frontend/src/components/site-finder/NeighborsBlock.tsx
bot-backend 02b5f0b5d1
All checks were successful
Deploy / changes (push) Successful in 8s
Deploy / build-backend (push) Successful in 2m26s
Deploy / build-worker (push) Successful in 3m41s
Deploy / build-frontend (push) Successful in 4m2s
Deploy / deploy (push) Successful in 1m30s
feat(site-finder): max-info попапы — конкурент / §3-таблица / польз.точка / соседи (#2111) (#2113)
2026-06-30 13:45:43 +00:00

385 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import type { NeighborsSummary } from "@/types/site-finder";
interface Props {
data: NeighborsSummary;
}
function fmtPrice(n: number | null | undefined): string {
if (n == null) return "—";
if (n >= 1000) {
return `${(n / 1000).toFixed(0)} тыс ₽/м²`;
}
return `${n.toLocaleString("ru-RU")} ₽/м²`;
}
function fmtYearBuilt(y: number | null | undefined): string {
if (y == null || y === 0) return "—";
return String(y);
}
// Русский plural: 1 → "здание", 2-4 → "здания", 5+ → "зданий".
// Также корректно для 11-14 (zданий), 21 (здание), 22 (здания) etc.
function pluralBuildings(n: number): string {
const mod10 = n % 10;
const mod100 = n % 100;
if (mod100 >= 11 && mod100 <= 14) return "зданий";
if (mod10 === 1) return "здание";
if (mod10 >= 2 && mod10 <= 4) return "здания";
return "зданий";
}
export function NeighborsBlock({ data }: Props) {
const [expanded, setExpanded] = useState(false);
if (!data.data_available) {
return (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 10,
padding: "12px 16px",
background: "#f9fafb",
}}
>
<div
style={{
fontSize: 12,
fontWeight: 600,
color: "#6b7280",
textTransform: "uppercase",
letterSpacing: "0.05em",
marginBottom: 6,
}}
>
Соседи (100 м)
</div>
<div style={{ fontSize: 12, color: "#9ca3af" }}>
{data.note ?? "Данные о соседних зданиях недоступны"}
</div>
</div>
);
}
const count = data.count_buildings_100m ?? 0;
const hasOverlap = !!data.has_existing_buildings;
return (
<div
style={{
border: hasOverlap ? "1px solid #fca5a5" : "1px solid #e5e7eb",
background: "#fff",
borderRadius: 10,
padding: "14px 18px",
display: "flex",
flexDirection: "column",
gap: 12,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
}}
>
<span
style={{
fontSize: 11,
fontWeight: 700,
color: "#6b7280",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
Соседи (100 м)
</span>
<span
style={{
fontSize: 12,
color: "#374151",
fontVariantNumeric: "tabular-nums",
}}
>
{count} {pluralBuildings(count)}
</span>
</div>
{/* Overlap warning — hard warn */}
{hasOverlap &&
data.overlap_buildings &&
data.overlap_buildings.length > 0 && (
<div
style={{
padding: "10px 14px",
background: "#fee2e2",
border: "1px solid #fca5a5",
borderRadius: 8,
color: "#b91c1c",
fontSize: 13,
}}
>
<div style={{ fontWeight: 700, marginBottom: 4 }}>
На участке уже есть здания (overlap &gt;50 м²)
</div>
<ul
style={{
margin: 0,
paddingLeft: 18,
fontSize: 12,
lineHeight: 1.5,
}}
>
{data.overlap_buildings.map((b) => (
<li key={b.cad_num}>
{b.building_name ?? b.readable_address ?? b.cad_num}
{b.floors ? `, ${b.floors} эт.` : ""}
{b.overlap_m2 ? ` — пересечение ~${b.overlap_m2} м²` : ""}
</li>
))}
</ul>
<div style={{ marginTop: 6, fontSize: 11, fontStyle: "italic" }}>
Инвестиции невозможны без сноса.
</div>
</div>
)}
{/* Summary metrics */}
{count > 0 && (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))",
gap: 10,
fontSize: 12,
color: "#374151",
}}
>
<div>
<div style={{ color: "#9ca3af", fontSize: 11 }}>
Средн. этажность
</div>
<div
style={{
fontWeight: 600,
fontSize: 14,
fontVariantNumeric: "tabular-nums",
}}
>
{data.avg_floors_100m ?? "—"}
</div>
</div>
<div>
<div style={{ color: "#9ca3af", fontSize: 11 }}>Макс. этажей</div>
<div
style={{
fontWeight: 600,
fontSize: 14,
fontVariantNumeric: "tabular-nums",
}}
>
{data.max_floors_100m ?? "—"}
</div>
</div>
<div>
<div
style={{ color: "#9ca3af", fontSize: 11 }}
title="Медиана кадастровой стоимости / м² — proxy для «premium quarter»"
>
Медиана цены
</div>
<div
style={{
fontWeight: 600,
fontSize: 14,
fontVariantNumeric: "tabular-nums",
}}
>
{fmtPrice(data.median_cost_per_m2_100m)}
</div>
</div>
</div>
)}
{/* Toggle neighbor list */}
{data.neighbors && data.neighbors.length > 0 && (
<div>
<button
type="button"
onClick={() => setExpanded((e) => !e)}
aria-expanded={expanded}
style={{
background: "none",
border: "none",
padding: 0,
color: "#1d4ed8",
fontSize: 13,
cursor: "pointer",
fontWeight: 500,
}}
>
{expanded
? "Скрыть список"
: `Показать ${data.neighbors.length} ближайших`}
</button>
{expanded && (
<div
style={{
marginTop: 10,
maxHeight: 280,
overflowY: "auto",
border: "1px solid #f3f4f6",
borderRadius: 6,
}}
>
<table
style={{
width: "100%",
borderCollapse: "collapse",
fontSize: 12,
}}
>
<thead
style={{
background: "#f9fafb",
position: "sticky",
top: 0,
}}
>
<tr>
<th
style={{
padding: "6px 10px",
textAlign: "left",
color: "#6b7280",
fontWeight: 500,
}}
>
Здание
</th>
<th
style={{
padding: "6px 10px",
textAlign: "right",
color: "#6b7280",
fontWeight: 500,
}}
>
Эт.
</th>
<th
style={{
padding: "6px 10px",
textAlign: "right",
color: "#6b7280",
fontWeight: 500,
}}
>
Год
</th>
<th
style={{
padding: "6px 10px",
textAlign: "right",
color: "#6b7280",
fontWeight: 500,
}}
>
/м²
</th>
<th
style={{
padding: "6px 10px",
textAlign: "right",
color: "#6b7280",
fontWeight: 500,
}}
>
Дист.
</th>
</tr>
</thead>
<tbody>
{data.neighbors.map((n) => (
<tr
key={n.cad_num}
style={{ borderTop: "1px solid #f3f4f6" }}
>
<td style={{ padding: "6px 10px", color: "#374151" }}>
{n.building_name ?? n.readable_address ?? n.cad_num}
{(n.purpose || n.status) && (
<div
style={{
marginTop: 2,
fontSize: 11,
color: "#9ca3af",
lineHeight: 1.4,
}}
>
{n.purpose}
{n.purpose && n.status ? " · " : ""}
{n.status}
</div>
)}
</td>
<td
style={{
padding: "6px 10px",
textAlign: "right",
color: "#374151",
fontVariantNumeric: "tabular-nums",
}}
>
{n.floors ?? "—"}
</td>
<td
style={{
padding: "6px 10px",
textAlign: "right",
color: "#6b7280",
fontVariantNumeric: "tabular-nums",
}}
>
{fmtYearBuilt(n.year_built)}
</td>
<td
style={{
padding: "6px 10px",
textAlign: "right",
color: "#374151",
fontVariantNumeric: "tabular-nums",
}}
>
{fmtPrice(n.cost_per_m2)}
</td>
<td
style={{
padding: "6px 10px",
textAlign: "right",
color: "#6b7280",
fontVariantNumeric: "tabular-nums",
}}
>
{n.distance_m} м
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{data.note && (
<div style={{ fontSize: 11, color: "#9ca3af", fontStyle: "italic" }}>
{data.note}
</div>
)}
</div>
);
}