- NSPDClient: QUARTER_OPPORTUNITY_LAYERS (auction/scheme/free/future/oopt) + QuarterDump.opportunity field - nspd_sync: harvest_quarter accepts include_opportunity, denorm cols has_auction_parcels + opportunity_count in UPSERT - quarter_dump_lookup: - _get_opportunity_parcels (sort by distance, early-exit on count=0) - _get_red_lines (query existing dump.red_lines core path, layer='red_lines') - SQL 89_*: has_auction_parcels + opportunity_count + partial index - Pydantic: OpportunityParcel + RedLine schemas - /analyze: nspd_opportunity_parcels + nspd_red_lines fields - Frontend: NspdOpportunityBlock + NspdRedLinesBlock + LandTab integration - Tests: 28 total (11 PR1 + 17 PR2), all pass Red lines uses existing core harvest path (layer 879243 already in dump.red_lines from PR1+core) — no duplicate harvest, no red_lines_count_v2 column needed. Part of #94
145 lines
5 KiB
TypeScript
145 lines
5 KiB
TypeScript
"use client";
|
||
|
||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||
import { SectionLabel } from "@/components/ui/SectionLabel";
|
||
import { EmptyState } from "@/components/ui/EmptyState";
|
||
import { GeologyBlock } from "./GeologyBlock";
|
||
import { GeometrySuitabilityBlock } from "./GeometrySuitabilityBlock";
|
||
import { GeotechRiskBlock } from "./GeotechRiskBlock";
|
||
import { NeighborsBlock } from "./NeighborsBlock";
|
||
import { NspdZoningBlock } from "./NspdZoningBlock";
|
||
import { NspdZouitOverlapsBlock } from "./NspdZouitOverlapsBlock";
|
||
import { NspdEngineeringNearbyBlock } from "./NspdEngineeringNearbyBlock";
|
||
import { NspdRiskZonesBlock } from "./NspdRiskZonesBlock";
|
||
import { NspdOpportunityBlock } from "./NspdOpportunityBlock";
|
||
import { NspdRedLinesBlock } from "./NspdRedLinesBlock";
|
||
import { NspdFreshnessBadge } from "./NspdFreshnessBadge";
|
||
|
||
interface Props {
|
||
data: ParcelAnalysis;
|
||
}
|
||
|
||
export function LandTab({ data }: Props) {
|
||
const hasAny =
|
||
data.geotech_risk !== undefined ||
|
||
data.geology !== undefined ||
|
||
data.geometry_suitability !== undefined ||
|
||
data.neighbors_summary !== undefined ||
|
||
data.nspd_zoning !== undefined ||
|
||
data.nspd_zouit_overlaps !== undefined ||
|
||
data.nspd_engineering_nearby !== undefined ||
|
||
data.nspd_risk_zones !== undefined ||
|
||
(data.nspd_opportunity_parcels !== undefined &&
|
||
(data.nspd_opportunity_parcels?.length ?? 0) > 0) ||
|
||
(data.nspd_red_lines !== undefined &&
|
||
(data.nspd_red_lines?.length ?? 0) > 0);
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
|
||
{/* P2 (#46) — Соседи + overlap warning (hard warn если overlap) */}
|
||
{data.neighbors_summary && (
|
||
<NeighborsBlock data={data.neighbors_summary} />
|
||
)}
|
||
|
||
{/* P1 (#45) — Geometry suitability */}
|
||
{data.geometry_suitability !== undefined && (
|
||
<GeometrySuitabilityBlock data={data.geometry_suitability} />
|
||
)}
|
||
|
||
{/* Issue #202 — Зонирование (ПЗЗ) из НСПД */}
|
||
<div>
|
||
<div
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 8,
|
||
marginBottom: 10,
|
||
}}
|
||
>
|
||
<SectionLabel>Зонирование (ПЗЗ)</SectionLabel>
|
||
<NspdFreshnessBadge dump={data.nspd_dump} />
|
||
</div>
|
||
<NspdZoningBlock
|
||
data={data.nspd_zoning}
|
||
dump={data.nspd_dump}
|
||
cadNum={data.cad_num}
|
||
/>
|
||
</div>
|
||
|
||
{/* Issue #202 — ЗОУИТ пересечения */}
|
||
{data.nspd_zouit_overlaps !== undefined && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 10 }}>
|
||
Зоны с особыми условиями использования (ЗОУИТ)
|
||
</SectionLabel>
|
||
<NspdZouitOverlapsBlock overlaps={data.nspd_zouit_overlaps ?? []} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Issue #94 PR2 TIER 4 — Opportunity parcels */}
|
||
{(data.nspd_opportunity_parcels?.length ?? 0) > 0 && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 10 }}>
|
||
Возможности рядом (НСПД)
|
||
</SectionLabel>
|
||
<NspdOpportunityBlock
|
||
opportunityParcels={data.nspd_opportunity_parcels}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Issue #94 PR2 TIER 4 — Red lines */}
|
||
{(data.nspd_red_lines?.length ?? 0) > 0 && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 10 }}>
|
||
Красные линии застройки (НСПД)
|
||
</SectionLabel>
|
||
<NspdRedLinesBlock redLines={data.nspd_red_lines} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Issue #94 TIER 3 — Риск-зоны НСПД */}
|
||
{data.nspd_risk_zones !== undefined && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 10 }}>
|
||
Природные риски (НСПД)
|
||
</SectionLabel>
|
||
<NspdRiskZonesBlock riskZones={data.nspd_risk_zones} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Issue #202 — Инженерные объекты в 200 м */}
|
||
{data.nspd_engineering_nearby !== undefined && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 10 }}>
|
||
Инженерные объекты вблизи участка
|
||
</SectionLabel>
|
||
<NspdEngineeringNearbyBlock
|
||
nearby={data.nspd_engineering_nearby ?? []}
|
||
cadNum={data.cad_num}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Geotech risk */}
|
||
{data.geotech_risk !== undefined && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 12 }}>
|
||
Геотехнический риск
|
||
</SectionLabel>
|
||
<GeotechRiskBlock risk={data.geotech_risk} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Geology */}
|
||
{data.geology !== undefined && (
|
||
<div>
|
||
<SectionLabel style={{ marginBottom: 12 }}>Геология</SectionLabel>
|
||
<GeologyBlock geology={data.geology} />
|
||
</div>
|
||
)}
|
||
|
||
{!hasAny && <EmptyState message="Данные о земле и геологии недоступны" />}
|
||
</div>
|
||
);
|
||
}
|