gendesign/frontend/src/components/site-finder/LandTab.tsx
lekss361 ed3c128528 feat(nspd): TIER 4 opportunity layers + red lines (#94 PR2 of 4)
- 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
2026-05-16 19:06:22 +03:00

145 lines
5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}