gendesign/frontend/src/components/site-finder/MarketTab.tsx
lekss361 5878596bc5 feat(layouts): PDF ТЗ endpoint + BestLayoutsBlock UI (#113 PR D)
Backend (~330 LOC):
- POST /api/v1/parcels/{cad_num}/best-layouts/pdf via WeasyPrint
- services/exporters/layout_tz_pdf.py — HTML template с XSS-escape (html.escape)
  для cad_num/parcel_address/rationale_text/time_window
- 5 unit tests (skip on Windows если WeasyPrint deps missing)

Frontend (~800 LOC):
- BestLayoutsBlock.tsx — DataQualityCard + TopLayoutsTable + RecommendationCard
  + PDF download button (Blob → revokeObjectURL)
- useBestLayouts.ts — TanStack useMutation hook
- types/best-layouts.ts — manual TS types
- MarketTab.tsx +4 LOC integration после competitors

Pre-push fixes per review:
- XSS escape user-data в PDF HTML
- sold_pct_of_supply — убрать double ×100 (backend уже 0..100)
- alert() → pdfError state
- isNaN → Number.isNaN
- 7 inline-styles → Tailwind utilities

Phase 2.1 complete. Phase 2.2 (layout_type/balcony_count via B2B Объектив
#52) — follow-up issue.
2026-05-16 12:38:23 +03:00

91 lines
2.7 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 { MarketTrendBlock } from "./MarketTrendBlock";
import { BestLayoutsBlock } from "./BestLayoutsBlock";
import { CompetitorTable } from "./CompetitorTable";
import { Pipeline24moBlock } from "./Pipeline24moBlock";
import { SuccessRecommendationBlock } from "./SuccessRecommendationBlock";
import { VelocityBlock } from "./VelocityBlock";
interface Props {
data: ParcelAnalysis;
}
export function MarketTab({ data }: Props) {
const hasTrend = "market_trend" in data;
const hasRecommendation = "success_recommendation" in data;
const hasPipeline = data.pipeline_24mo !== undefined;
const hasVelocity = "velocity" in data;
const hasAny =
hasTrend ||
hasRecommendation ||
hasPipeline ||
hasVelocity ||
data.competitors.length > 0;
return (
<div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
{/* D4 (#36) — Pipeline 24mo */}
{data.pipeline_24mo && <Pipeline24moBlock data={data.pipeline_24mo} />}
{/* Market trend */}
{hasTrend && (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 10,
padding: "14px 18px",
background: "#fff",
}}
>
<MarketTrendBlock trend={data.market_trend} />
</div>
)}
{/* D2 (#34) — Velocity-score */}
{hasVelocity && (
<div
style={{
border: "1px solid #e5e7eb",
borderRadius: 10,
padding: "14px 18px",
background: "#fff",
}}
>
<VelocityBlock velocity={data.velocity} />
</div>
)}
{/* Competitors */}
{data.competitors.length > 0 && (
<div>
<SectionLabel style={{ marginBottom: 12 }}>Конкуренты</SectionLabel>
<CompetitorTable
competitors={data.competitors}
districtName={data.district?.district_name}
/>
</div>
)}
{/* Success recommendation */}
{hasRecommendation && (
<div>
<SectionLabel style={{ marginBottom: 12 }}>
Что хорошо продаётся
</SectionLabel>
<SuccessRecommendationBlock
recommendation={data.success_recommendation}
/>
</div>
)}
{/* Issue #113 — data-driven ТЗ на проектирование */}
<BestLayoutsBlock cadNum={data.cad_num} />
{!hasAny && <EmptyState message="Рыночные данные недоступны" />}
</div>
);
}