feat(sf-fe-a10): Section 4 «Оценка участка»
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.
This commit is contained in:
parent
4d651f00ef
commit
de07cb93b9
2 changed files with 260 additions and 8 deletions
|
|
@ -5,6 +5,7 @@ import Link from "next/link";
|
||||||
import { Section1ParcelInfo } from "@/components/site-finder/analysis/Section1ParcelInfo";
|
import { Section1ParcelInfo } from "@/components/site-finder/analysis/Section1ParcelInfo";
|
||||||
import { Section2NetworksUtilities } from "@/components/site-finder/analysis/Section2NetworksUtilities";
|
import { Section2NetworksUtilities } from "@/components/site-finder/analysis/Section2NetworksUtilities";
|
||||||
import { Section3SettingsAndCompetitors } from "@/components/site-finder/analysis/Section3SettingsAndCompetitors";
|
import { Section3SettingsAndCompetitors } from "@/components/site-finder/analysis/Section3SettingsAndCompetitors";
|
||||||
|
import { Section4Estimate } from "@/components/site-finder/analysis/Section4Estimate";
|
||||||
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
import { useParcelAnalyzeQuery } from "@/lib/site-finder-api";
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||||
|
|
||||||
|
|
@ -127,14 +128,8 @@ export function AnalysisPageContent({ cad }: Props) {
|
||||||
{/* Section 3 — IMPLEMENTED in A7 */}
|
{/* Section 3 — IMPLEMENTED in A7 */}
|
||||||
<Section3SettingsAndCompetitors cad={cad} data={analysis} />
|
<Section3SettingsAndCompetitors cad={cad} data={analysis} />
|
||||||
|
|
||||||
{/* Section 4 placeholder — filled by A10 */}
|
{/* Section 4 — IMPLEMENTED in A10 */}
|
||||||
<section id="section-4" style={{ scrollMarginTop: 72 }}>
|
<Section4Estimate cad={cad} />
|
||||||
<SectionPlaceholder
|
|
||||||
number="4"
|
|
||||||
title="Оценка участка"
|
|
||||||
note="Заполняется в A10: ScoreBreakdown, GateVerdict, риски"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Section 5 placeholder — filled by A11 */}
|
{/* Section 5 placeholder — filled by A11 */}
|
||||||
<section id="section-5" style={{ scrollMarginTop: 72 }}>
|
<section id="section-5" style={{ scrollMarginTop: 72 }}>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<section id="section-4" style={{ scrollMarginTop: 72 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-headline)",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: "14px 18px",
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 20,
|
||||||
|
width: "40%",
|
||||||
|
background: "rgba(255,255,255,0.1)",
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: 120,
|
||||||
|
background: "var(--bg-card-alt)",
|
||||||
|
border: "1px solid var(--border-soft)",
|
||||||
|
borderRadius: 12,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main component ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function Section4Estimate({ cad }: Props) {
|
||||||
|
const { data, isLoading, error } = useParcelAnalyzeQuery(cad);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Section4Skeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error || !data) {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="section-4"
|
||||||
|
style={{
|
||||||
|
scrollMarginTop: 72,
|
||||||
|
padding: "16px 20px",
|
||||||
|
background: "var(--danger-soft, #FEE2E2)",
|
||||||
|
border: "1px solid var(--danger, #B3261E)",
|
||||||
|
borderRadius: 12,
|
||||||
|
color: "var(--danger, #B3261E)",
|
||||||
|
fontSize: 13,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Ошибка загрузки секции Оценка"}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
||||||
|
<section id="section-4" style={{ scrollMarginTop: 72 }}>
|
||||||
|
{/* ── Headline bar ─────────────────────────────────────────────────── */}
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<HeadlineBar title={headlineTitle} subtitle={headlineSubtitle} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Gate verdict banner ──────────────────────────────────────────── */}
|
||||||
|
{analysis.gate_verdict && (
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<GateVerdictBanner verdict={analysis.gate_verdict} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Score breakdown: 2-col on desktop ───────────────────────────── */}
|
||||||
|
{hasBreakdown && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: hasStackedBar ? "1fr 1fr" : "1fr",
|
||||||
|
gap: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScoreBreakdownPanel
|
||||||
|
topPositives={topPositives}
|
||||||
|
topNegatives={topNegatives}
|
||||||
|
byGroup={byGroup}
|
||||||
|
detailed={detailed}
|
||||||
|
/>
|
||||||
|
{hasStackedBar && <ScoreBreakdownStackedBar breakdown={detailed} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Risk blocks grid ─────────────────────────────────────────────── */}
|
||||||
|
{(analysis.geology || analysis.hydrology || analysis.geotech_risk) && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
|
||||||
|
gap: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{analysis.geology && <GeologyBlock geology={analysis.geology} />}
|
||||||
|
{analysis.hydrology && (
|
||||||
|
<HydrologyBlock hydrology={analysis.hydrology} />
|
||||||
|
)}
|
||||||
|
{analysis.geotech_risk && (
|
||||||
|
<GeotechRiskBlock risk={analysis.geotech_risk} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── NSPD ZOUIT overlaps ──────────────────────────────────────────── */}
|
||||||
|
{hasZouit && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card)",
|
||||||
|
border: "1px solid var(--border-card)",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: "16px 20px",
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "var(--fg-secondary)",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.04em",
|
||||||
|
marginBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Охранные зоны ЗОУИТ
|
||||||
|
</div>
|
||||||
|
<NspdZouitOverlapsBlock
|
||||||
|
overlaps={analysis.nspd_zouit_overlaps ?? []}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Success recommendation ───────────────────────────────────────── */}
|
||||||
|
{analysis.success_recommendation !== undefined && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "var(--bg-card)",
|
||||||
|
border: "1px solid var(--border-card)",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: "16px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SuccessRecommendationBlock
|
||||||
|
recommendation={analysis.success_recommendation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue