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.
170 lines
5.4 KiB
TypeScript
170 lines
5.4 KiB
TypeScript
"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>
|
||
);
|
||
}
|