From de07cb93b91e233c04a56e7aba866b0a88ca6bd4 Mon Sep 17 00:00:00 2001 From: lekss361 Date: Mon, 18 May 2026 04:23:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(sf-fe-a10):=20Section=204=20=C2=AB=D0=9E?= =?UTF-8?q?=D1=86=D0=B5=D0=BD=D0=BA=D0=B0=20=D1=83=D1=87=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Section4Estimate component wiring real backend data from useParcelAnalyzeQuery (canonical hook). Renders conditionally: HeadlineBar with score + label, GateVerdictBanner, 2-col ScoreBreakdownPanel + ScoreBreakdownStackedBar, Geology/Hydrology/ GeotechRisk grid, NspdZouitOverlapsBlock, SuccessRecommendationBlock. Replace placeholder in AnalysisPageContent.tsx; sections 1-3 and 5 preserved. --- .../analysis/[cad]/AnalysisPageContent.tsx | 11 +- .../site-finder/analysis/Section4Estimate.tsx | 257 ++++++++++++++++++ 2 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/site-finder/analysis/Section4Estimate.tsx diff --git a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx index 2a4f762b..70430f4d 100644 --- a/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx +++ b/frontend/src/app/site-finder/analysis/[cad]/AnalysisPageContent.tsx @@ -5,6 +5,7 @@ import Link from "next/link"; import { Section1ParcelInfo } from "@/components/site-finder/analysis/Section1ParcelInfo"; import { Section2NetworksUtilities } from "@/components/site-finder/analysis/Section2NetworksUtilities"; import { Section3SettingsAndCompetitors } from "@/components/site-finder/analysis/Section3SettingsAndCompetitors"; +import { Section4Estimate } from "@/components/site-finder/analysis/Section4Estimate"; import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; import type { ParcelAnalysis } from "@/types/site-finder"; @@ -127,14 +128,8 @@ export function AnalysisPageContent({ cad }: Props) { {/* Section 3 — IMPLEMENTED in A7 */} - {/* Section 4 placeholder — filled by A10 */} -
- -
+ {/* Section 4 — IMPLEMENTED in A10 */} + {/* Section 5 placeholder — filled by A11 */}
diff --git a/frontend/src/components/site-finder/analysis/Section4Estimate.tsx b/frontend/src/components/site-finder/analysis/Section4Estimate.tsx new file mode 100644 index 00000000..e4fab1f4 --- /dev/null +++ b/frontend/src/components/site-finder/analysis/Section4Estimate.tsx @@ -0,0 +1,257 @@ +"use client"; + +/** + * Section4Estimate — "4. Оценка участка" + * + * Layout: + * HeadlineBar (score + verdict label as title) + * GateVerdictBanner (conditional — gate_verdict field) + * 2-col grid (desktop): + * left: ScoreBreakdownPanel (top-3 plus/minus + by-group bar) + * right: ScoreBreakdownStackedBar (per-category contribution) + * Geology / Hydrology / GeotechRisk blocks (conditional — each field) + * NspdZouitOverlapsBlock (conditional — nspd_zouit_overlaps present) + * SuccessRecommendationBlock (conditional — success_recommendation present) + * + * Data: useParcelAnalyzeQuery (canonical) from @/lib/site-finder-api. + * All sections are conditional — graceful if backend field is absent. + */ + +import { HeadlineBar } from "@/components/ui/HeadlineBar"; +import { GateVerdictBanner } from "@/components/site-finder/GateVerdictBanner"; +import { ScoreBreakdownPanel } from "@/components/site-finder/ScoreBreakdownPanel"; +import { ScoreBreakdownStackedBar } from "@/components/site-finder/ScoreBreakdownStackedBar"; +import { GeologyBlock } from "@/components/site-finder/GeologyBlock"; +import { HydrologyBlock } from "@/components/site-finder/HydrologyBlock"; +import { GeotechRiskBlock } from "@/components/site-finder/GeotechRiskBlock"; +import { NspdZouitOverlapsBlock } from "@/components/site-finder/NspdZouitOverlapsBlock"; +import { SuccessRecommendationBlock } from "@/components/site-finder/SuccessRecommendationBlock"; +import { useParcelAnalyzeQuery } from "@/lib/site-finder-api"; +import type { ParcelAnalysis } from "@/types/site-finder"; +import type { FactorContribution, ScoreGroupTotal } from "@/types/site-finder"; + +// ── Props ───────────────────────────────────────────────────────────────────── + +interface Props { + cad: string; +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function buildHeadlineTitle(analysis: ParcelAnalysis): string { + const score = analysis.score.toFixed(1); + const label = analysis.score_label ?? ""; + if (label) { + return `Оценка: ${score} · ${label.toUpperCase()}`; + } + return `Оценка: ${score}`; +} + +function buildHeadlineSubtitle(analysis: ParcelAnalysis): string | undefined { + const parts: string[] = []; + + if (analysis.gate_verdict) { + parts.push(`МКД: ${analysis.gate_verdict.verdict_label}`); + } + + if (analysis.score_explanation) { + parts.push(analysis.score_explanation); + } else if (analysis.confidence_label) { + parts.push(`Уверенность данных: ${analysis.confidence_label}`); + } + + return parts.length > 0 ? parts.join(" · ") : undefined; +} + +// ── Section 4 skeleton (loading) ────────────────────────────────────────────── + +function Section4Skeleton() { + return ( +
+
+
+
+
+
+ ); +} + +// ── Main component ──────────────────────────────────────────────────────────── + +export function Section4Estimate({ cad }: Props) { + const { data, isLoading, error } = useParcelAnalyzeQuery(cad); + + if (isLoading) { + return ; + } + + if (error || !data) { + return ( +
+ {error instanceof Error + ? error.message + : "Ошибка загрузки секции Оценка"} +
+ ); + } + + // Cast to ParcelAnalysis which is the richer superset type with all optional fields. + const analysis = data as unknown as ParcelAnalysis; + + const headlineTitle = buildHeadlineTitle(analysis); + const headlineSubtitle = buildHeadlineSubtitle(analysis); + + // Breakdown data — may be absent on older backend deploys + const topPositives: FactorContribution[] = + analysis.score_top_3_positives ?? []; + const topNegatives: FactorContribution[] = + analysis.score_top_3_negatives ?? []; + const byGroup: ScoreGroupTotal[] = analysis.score_by_group ?? []; + const detailed: FactorContribution[] = + analysis.score_breakdown_detailed ?? []; + + const hasBreakdown = + topPositives.length > 0 || + topNegatives.length > 0 || + byGroup.length > 0 || + detailed.length > 0; + + const hasStackedBar = detailed.length > 0; + + const hasZouit = + analysis.nspd_zouit_overlaps !== undefined && + analysis.nspd_zouit_overlaps !== null; + + return ( +
+ {/* ── Headline bar ─────────────────────────────────────────────────── */} +
+ +
+ + {/* ── Gate verdict banner ──────────────────────────────────────────── */} + {analysis.gate_verdict && ( +
+ +
+ )} + + {/* ── Score breakdown: 2-col on desktop ───────────────────────────── */} + {hasBreakdown && ( +
+ + {hasStackedBar && } +
+ )} + + {/* ── Risk blocks grid ─────────────────────────────────────────────── */} + {(analysis.geology || analysis.hydrology || analysis.geotech_risk) && ( +
+ {analysis.geology && } + {analysis.hydrology && ( + + )} + {analysis.geotech_risk && ( + + )} +
+ )} + + {/* ── NSPD ZOUIT overlaps ──────────────────────────────────────────── */} + {hasZouit && ( +
+
+ Охранные зоны ЗОУИТ +
+ +
+ )} + + {/* ── Success recommendation ───────────────────────────────────────── */} + {analysis.success_recommendation !== undefined && ( +
+ +
+ )} +
+ ); +} -- 2.45.3