= Object.fromEntries(
+ (top.data ?? []).map((r) => [r.developer_id, r.developer_name]),
+ );
+
+ const compareIds = focusedId
+ ? [focusedId, ...["5791_0", "5832_0"].filter((x) => x !== focusedId)]
+ : ["6208_0", "5791_0", "5832_0"];
+
+ return (
+ <>
+ {focused ? (
+
+
+
+
+
+
+ ) : null}
+
+
+
+
+
+
+ >
+ );
+}
+
+export default function DevelopersPage() {
+ return (
+ Загрузка…}>
+
+
+ );
+}
diff --git a/frontend/src/app/analytics/layout.tsx b/frontend/src/app/analytics/layout.tsx
new file mode 100644
index 00000000..96bcad05
--- /dev/null
+++ b/frontend/src/app/analytics/layout.tsx
@@ -0,0 +1,47 @@
+import Link from "next/link";
+
+import { AnalyticsNav } from "@/components/analytics/AnalyticsNav";
+
+export default function AnalyticsLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/frontend/src/app/analytics/page.tsx b/frontend/src/app/analytics/page.tsx
new file mode 100644
index 00000000..1df9aa92
--- /dev/null
+++ b/frontend/src/app/analytics/page.tsx
@@ -0,0 +1,154 @@
+"use client";
+
+import { DistrictTreemap } from "@/components/analytics/DistrictTreemap";
+import { KpiCard } from "@/components/analytics/KpiCard";
+import { MarketPulseChart } from "@/components/analytics/MarketPulseChart";
+import { PipelineChart } from "@/components/analytics/PipelineChart";
+import { QuartirographyChart } from "@/components/analytics/QuartirographyChart";
+import { Section } from "@/components/analytics/Section";
+import { YandexClassPie } from "@/components/analytics/YandexClassPie";
+import { useMarketPulse, useYandexListings } from "@/lib/analytics-api";
+
+export default function SverdlMarketPage() {
+ const pulse = useMarketPulse();
+ const yandex = useYandexListings();
+
+ const last = pulse.data?.[pulse.data.length - 1];
+ const first = pulse.data?.[0];
+ const totalDelta =
+ last && first && first.total_square_th_sqm && last.total_square_th_sqm
+ ? Math.round(
+ ((last.total_square_th_sqm - first.total_square_th_sqm) /
+ first.total_square_th_sqm) *
+ 100,
+ )
+ : null;
+ const soldDelta =
+ last && first && first.sold_perc != null && last.sold_perc != null
+ ? Math.round(last.sold_perc - first.sold_perc)
+ : null;
+ const priceDelta =
+ last && first && first.price_avg && last.price_avg
+ ? Math.round(((last.price_avg - first.price_avg) / first.price_avg) * 100)
+ : null;
+
+ return (
+ <>
+
+ 0 ? "+" : ""}${totalDelta}% за период`,
+ positive: totalDelta > 0,
+ }
+ : undefined
+ }
+ hint={last ? `${last.snapshot_date.slice(0, 7)} (DOM.РФ)` : undefined}
+ />
+ 0 ? "+" : ""}${soldDelta} пп`,
+ positive: soldDelta >= 0,
+ }
+ : undefined
+ }
+ hint="доля проданных площадей в стройке"
+ />
+ 0 ? "+" : ""}${priceDelta}% за период`,
+ positive: null,
+ }
+ : undefined
+ }
+ />
+
+
+
+
+
+
+
+
+ Рынок голосует деньгами за семейные квартиры (37% сделок — 80+ м²), а
+ предложение перекошено в сторону инвесторских студий и однушек (52%
+ портфеля). Дефицит средне-большого жилья — ниша для входа.
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/app/analytics/prinzip/page.tsx b/frontend/src/app/analytics/prinzip/page.tsx
new file mode 100644
index 00000000..5c9584c9
--- /dev/null
+++ b/frontend/src/app/analytics/prinzip/page.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import { useState } from "react";
+
+import { InsightCards } from "@/components/analytics/InsightCards";
+import { KpiCard } from "@/components/analytics/KpiCard";
+import { PrinzipDistrictsBar } from "@/components/analytics/PrinzipDistrictsBar";
+import { PrinzipGapBar } from "@/components/analytics/PrinzipGapBar";
+import { PrinzipQuartirographyPie } from "@/components/analytics/PrinzipQuartirographyPie";
+import { PrinzipVelocityChart } from "@/components/analytics/PrinzipVelocityChart";
+import { Section } from "@/components/analytics/Section";
+import { useDeveloperDetail } from "@/lib/analytics-api";
+
+const PRINZIP = "6208_0";
+const BENCH_OPTIONS: { id: string; name: string }[] = [
+ { id: "5791_0", name: "Брусника" },
+ { id: "5832_0", name: "Холдинг Форум-групп" },
+ { id: "5654_0", name: "КОРТРОС" },
+ { id: "8809_0", name: "Атлас Девелопмент" },
+ { id: "5904_0", name: "Эталон" },
+ { id: "5868_0", name: "Унистрой" },
+ { id: "5772_0", name: "Атомстройкомплекс" },
+];
+
+export default function PrinzipPage() {
+ const detail = useDeveloperDetail(PRINZIP);
+ const [benchmarks, setBenchmarks] = useState(["5791_0", "5832_0"]);
+
+ const toggle = (id: string) =>
+ setBenchmarks((prev) =>
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
+ );
+
+ const ids = [PRINZIP, ...benchmarks];
+ const names: Record = {
+ [PRINZIP]: "PRINZIP",
+ ...Object.fromEntries(BENCH_OPTIONS.map((o) => [o.id, o.name])),
+ };
+
+ const d = detail.data;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {BENCH_OPTIONS.map((o) => {
+ const on = benchmarks.includes(o.id);
+ return (
+ toggle(o.id)}
+ style={{
+ padding: "6px 10px",
+ fontSize: 12,
+ border: "1px solid #d1d5db",
+ borderRadius: 16,
+ background: on ? "#1d4ed8" : "#fff",
+ color: on ? "#fff" : "#374151",
+ cursor: "pointer",
+ }}
+ >
+ {o.name}
+
+ );
+ })}
+
+ }
+ >
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 7d7ca582..7dc4da1a 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
+import { Providers } from "./providers";
+
export const metadata: Metadata = {
title: "GenDesign",
description: "Generative Design + Site Finder",
@@ -19,7 +21,7 @@ export default function RootLayout({
"system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
}}
>
- {children}
+ {children}