diff --git a/frontend/package.json b/frontend/package.json
index f79f586c..ad2b5521 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,7 @@
"echarts-for-react": "^3.0.6",
"leaflet": "^1.9.4",
"leaflet-draw": "^1.0.4",
+ "lucide-react": "^0.511.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
diff --git a/frontend/src/app/site-finder/analysis/[cad]/page.tsx b/frontend/src/app/site-finder/analysis/[cad]/page.tsx
index 0573e29a..6e1d6d47 100644
--- a/frontend/src/app/site-finder/analysis/[cad]/page.tsx
+++ b/frontend/src/app/site-finder/analysis/[cad]/page.tsx
@@ -1,72 +1,13 @@
"use client";
import { use } from "react";
-import Link from "next/link";
import { LayoutDashboard } from "lucide-react";
-// ── Placeholder components ─────────────────────────────────────────────────
-// Replaced in Wave 2–4 with real sections
+import { AnalysisSidebar } from "@/components/site-finder/analysis/AnalysisSidebar";
+import { AnalysisBreadcrumb } from "@/components/site-finder/analysis/AnalysisBreadcrumb";
+import { UserAvatar } from "@/components/site-finder/analysis/UserAvatar";
-function SidebarPlaceholder() {
- return (
-
- );
-}
+// ── Section placeholder (Wave 3–4 refactor will replace these) ─────────────────
interface SectionPlaceholderProps {
id: string;
@@ -145,7 +86,7 @@ export default function AnalysisPage({ params }: PageProps) {
flexDirection: "column",
}}
>
- {/* Header / breadcrumb */}
+ {/* ── Header ─────────────────────────────────────────────────────────── */}
- {/* TODO A4: replace with */}
-
- ← К карте
-
- ·
-
- {cad}
-
- ·
-
- TODO A4: район · площадь · анализ свежий N мин
-
- {/* TODO A4: справа */}
+
+
+ {/* Push avatar to the right */}
+
+
+
- {/* Body: sidebar + scrollable sections */}
+ {/* ── Body: sidebar + scrollable sections ───────────────────────────── */}
- {/* TODO A4: replace with
*/}
-
+
{/* Main scrollable content */}
- {/* TODO A5: Section 1 — HeadlineBar + 4 KPI + mini-map + EgrnPropertyTable + PoiList2Gis */}
+ {/* Section 1 — TODO A5 */}
- {/* TODO A6: Section 2 — NspdEngineeringNearbyBlock */}
+ {/* Section 2 — TODO A6 */}
- {/* TODO A7/A8/A9: Section 3 — sub-sections 3.1 / 3.2 / 3.3 + CompetitorTable */}
+ {/* Section 3 — TODO A7/A8/A9 */}
- {/* TODO A10: Section 4 — ScoreBreakdownPanel + GateVerdictBanner + geology/hydro/risk */}
+ {/* Sub-section anchor targets (invisible) for scrollspy / direct links */}
+
+
+
+
+ {/* Section 4 — TODO A10 */}
- {/* TODO A11: Section 5 — SeasonalWeatherBlock + air quality */}
+ {/* Section 5 — TODO A11 */}
diff --git a/frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx b/frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx
new file mode 100644
index 00000000..b72e33c9
--- /dev/null
+++ b/frontend/src/components/site-finder/analysis/AnalysisBreadcrumb.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import Link from "next/link";
+import { ChevronRight } from "lucide-react";
+
+interface AnalysisBreadcrumbProps {
+ cadNum: string;
+}
+
+export function AnalysisBreadcrumb({ cadNum }: AnalysisBreadcrumbProps) {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx b/frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx
new file mode 100644
index 00000000..50f3044f
--- /dev/null
+++ b/frontend/src/components/site-finder/analysis/AnalysisSidebar.tsx
@@ -0,0 +1,254 @@
+"use client";
+
+import React, { useEffect, useRef, useState } from "react";
+import { ExternalLink } from "lucide-react";
+
+// ── Types ─────────────────────────────────────────────────────────────────────
+
+interface SubSection {
+ id: string;
+ label: string;
+}
+
+interface NavSection {
+ id: string;
+ label: string;
+ sub?: SubSection[];
+}
+
+// ── Config ────────────────────────────────────────────────────────────────────
+
+const NAV_SECTIONS: NavSection[] = [
+ { id: "section-1", label: "1. Объект" },
+ { id: "section-2", label: "2. Земля и риски" },
+ {
+ id: "section-3",
+ label: "3. Рынок",
+ sub: [
+ { id: "section-3-1", label: "3.1 Настройки выборки" },
+ { id: "section-3-2", label: "3.2 Планировки" },
+ { id: "section-3-3", label: "3.3 Остатки и скорость" },
+ ],
+ },
+ { id: "section-4", label: "4. Инфраструктура" },
+ { id: "section-5", label: "5. Свежесть" },
+];
+
+// All section IDs in scroll order (for IntersectionObserver)
+const ALL_SECTION_IDS: string[] = NAV_SECTIONS.flatMap((s) =>
+ s.sub ? [s.id, ...s.sub.map((sub) => sub.id)] : [s.id],
+);
+
+// ── Component ─────────────────────────────────────────────────────────────────
+
+export function AnalysisSidebar() {
+ const [activeId, setActiveId] = useState
(ALL_SECTION_IDS[0]);
+ const observerRef = useRef(null);
+
+ // Scrollspy via IntersectionObserver
+ useEffect(() => {
+ const candidates = ALL_SECTION_IDS.map((id) =>
+ document.getElementById(id),
+ ).filter((el): el is HTMLElement => el !== null);
+
+ if (candidates.length === 0) return;
+
+ // Track which sections are visible; pick topmost visible one
+ const visible = new Set();
+
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ visible.add(entry.target.id);
+ } else {
+ visible.delete(entry.target.id);
+ }
+ });
+
+ // Pick the topmost section that is currently visible
+ const next = ALL_SECTION_IDS.find((id) => visible.has(id));
+ if (next) setActiveId(next);
+ },
+ {
+ root: null,
+ // Trigger when section top enters top 60% of viewport
+ rootMargin: "-8px 0px -40% 0px",
+ threshold: 0,
+ },
+ );
+
+ candidates.forEach((el) => observerRef.current!.observe(el));
+
+ return () => {
+ observerRef.current?.disconnect();
+ };
+ }, []);
+
+ function handleAnchorClick(
+ e: React.MouseEvent,
+ targetId: string,
+ ) {
+ e.preventDefault();
+ const el = document.getElementById(targetId);
+ if (el) {
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
+ }
+ setActiveId(targetId);
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/site-finder/analysis/UserAvatar.tsx b/frontend/src/components/site-finder/analysis/UserAvatar.tsx
new file mode 100644
index 00000000..38760a3c
--- /dev/null
+++ b/frontend/src/components/site-finder/analysis/UserAvatar.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Building2 } from "lucide-react";
+
+// ── Helpers ───────────────────────────────────────────────────────────────────
+
+function getStoredOrgId(): string | null {
+ // Guard against SSR — localStorage not available on server
+ if (typeof window === "undefined") return null;
+ try {
+ return localStorage.getItem("gd_org_id");
+ } catch {
+ return null;
+ }
+}
+
+function orgInitials(orgId: string): string {
+ // Build a 2-letter monogram from org ID string
+ const parts = orgId
+ .toUpperCase()
+ .replace(/[^A-ZА-Я0-9]/gu, " ")
+ .split(" ")
+ .filter(Boolean);
+ if (parts.length === 0) return "??";
+ if (parts.length === 1) return parts[0].slice(0, 2);
+ return parts[0][0] + parts[1][0];
+}
+
+// ── Component ─────────────────────────────────────────────────────────────────
+
+export function UserAvatar() {
+ const [orgId, setOrgId] = useState(null);
+
+ // Hydration-safe: read localStorage after mount
+ useEffect(() => {
+ setOrgId(getStoredOrgId());
+ }, []);
+
+ const displayLabel = orgId ?? "Demo Org";
+ const initials = orgId ? orgInitials(orgId) : "DO";
+
+ return (
+
+ {/* Avatar circle */}
+
+ {orgId ? (
+
+ {initials}
+
+ ) : (
+
+ )}
+
+
+ {/* Org name — hidden on narrow viewports via maxWidth trick */}
+
+ {displayLabel}
+
+
+ );
+}