gendesign/frontend/src/components/analytics/ObjectInfraMap.tsx
lekss361 522ecca5c4 ship #13-16: multi-target Dockerfile, per-object drill-in, CRM pipeline,
442-sweep instructions
#16 Dockerfile: runner (lean, 1.6 GB) + runner-with-chromium (2.9 GB).
   Compose + deploy.yml pushes 2 GHCR images. Backend saves 1.3 GB.
#15 5 backend object endpoints + /analytics/objects/[id] page (gallery,
   POI table, sale chart) + sparkline-driven PrinzipObjectsTable.
#14 prinzip_leads/deals + 2 MVs. Import script with PII-hashing &
   --inspect-only mode. 3 funnel endpoints + Sankey/Monthly charts.
#13 README section with UI/CLI/SQL for 442-object sweep.
2026-04-27 19:32:37 +03:00

170 lines
5.4 KiB
TypeScript
Raw 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 { useMemo, useState } from "react";
import { useObjectInfrastructure } from "@/lib/analytics-api";
const CATEGORY_COLORS: Record<string, string> = {
Спорт: "#0a7a3a",
Продукты: "#1d4ed8",
Образование: "#c2410c",
Медицина: "#b3261e",
Развлечения: "#9333ea",
Новостройки: "#0891b2",
Транспорт: "#475569",
};
interface Props {
objId: number | string;
centerLat: number | null;
centerLon: number | null;
}
export function ObjectInfraMap({ objId, centerLat, centerLon }: Props) {
const [maxDist, setMaxDist] = useState(2000);
const [activeCat, setActiveCat] = useState<string | null>(null);
const { data, isLoading } = useObjectInfrastructure(objId, {
category: activeCat ?? undefined,
max_distance: maxDist,
});
const stats = useMemo(() => {
const byCat: Record<string, number> = {};
for (const p of data ?? []) {
const c = p.poi_category ?? "—";
byCat[c] = (byCat[c] ?? 0) + 1;
}
return Object.entries(byCat).sort((a, b) => b[1] - a[1]);
}, [data]);
return (
<div>
<div
style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 12 }}
>
{stats.map(([cat, n]) => (
<button
key={cat}
onClick={() => setActiveCat(activeCat === cat ? null : cat)}
style={{
padding: "6px 10px",
fontSize: 12,
border: "1px solid #d1d5db",
borderRadius: 16,
background:
activeCat === cat ? CATEGORY_COLORS[cat] || "#1d4ed8" : "#fff",
color: activeCat === cat ? "#fff" : "#374151",
cursor: "pointer",
}}
>
{cat} ({n})
</button>
))}
<select
value={maxDist}
onChange={(e) => setMaxDist(Number(e.target.value))}
style={{
padding: "6px 10px",
fontSize: 12,
border: "1px solid #d1d5db",
borderRadius: 16,
background: "#fff",
cursor: "pointer",
}}
>
<option value={500}> 500 м</option>
<option value={1000}> 1 км</option>
<option value={2000}> 2 км</option>
<option value={5000}> 5 км</option>
</select>
</div>
{isLoading ? (
<div style={{ padding: 24, color: "#5b6066" }}>Загрузка POI</div>
) : (
<div
style={{
border: "1px solid #e6e8ec",
borderRadius: 8,
overflow: "hidden",
position: "relative",
}}
>
<div
style={{
padding: 12,
fontSize: 13,
color: "#5b6066",
background: "#f9fafb",
}}
>
Найдено {data?.length ?? 0} POI в радиусе {maxDist} м.{" "}
{centerLat && centerLon
? `ЖК: ${centerLat.toFixed(4)}, ${centerLon.toFixed(4)}`
: "координаты ЖК не заданы"}
</div>
<table
style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}
>
<thead>
<tr style={{ background: "#f6f7f9" }}>
{["POI", "Категория", "Адрес", "Расстояние, м"].map((h) => (
<th
key={h}
style={{
padding: "8px 10px",
textAlign: "left",
borderBottom: "1px solid #e6e8ec",
fontWeight: 600,
}}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{(data ?? []).slice(0, 50).map((p, i) => (
<tr
key={`${p.poi_name}-${p.lat}-${p.lon}`}
style={{
borderBottom: "1px solid #eef0f3",
background: i % 2 ? "#fafbfc" : "#fff",
}}
>
<td style={{ padding: "6px 10px" }}>
<div style={{ fontWeight: 500 }}>{p.poi_name}</div>
{p.poi_subtitle ? (
<div style={{ fontSize: 11, color: "#73767e" }}>
{p.poi_subtitle}
</div>
) : null}
</td>
<td style={{ padding: "6px 10px" }}>
<span
style={{
background:
CATEGORY_COLORS[p.poi_category ?? ""] || "#9ca3af",
color: "#fff",
fontSize: 11,
padding: "2px 6px",
borderRadius: 3,
}}
>
{p.poi_category ?? "—"}
</span>
</td>
<td style={{ padding: "6px 10px", color: "#5b6066" }}>
{p.poi_address ?? "—"}
</td>
<td style={{ padding: "6px 10px", textAlign: "right" }}>
{p.distance_m ? Math.round(p.distance_m) : "—"}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}