feat(sf-fe-a8): Section 3.2 Планировки + 3.3 Остатки/Velocity + CompetitorTable drawer pattern #357

Merged
lekss361 merged 1 commit from feat/sf-fe-a8-section3-2-3-3-competitors into main 2026-05-18 01:14:32 +00:00
Owner

Summary

SF Frontend Wave 3 §A8 (combined §A9 CompetitorTable redesign).

Files (2)

frontend/src/components/site-finder/analysis/Section3SettingsAndCompetitors.tsx:

  • Section32Layouts (3.2): wraps <BestLayoutsBlock cadNum={cad} />
  • Section33RemainVelocity (3.3): 3-col grid Pipeline24moBlock + VelocityBlock + MarketTrendBlock (each conditional on non-null field)
  • CompetitorDetailDrawer: <Drawer side="right" width="420px"/> + Badge (status/class) + KPI cells (distance / flat_count / district / ready_dt)
  • Section 3 wrapper: selectedCompetitor state, onRowClick → drawer (not navigate)

frontend/src/components/site-finder/CompetitorTable.tsx (existing):

  • onRowClick?: (competitor: ParcelAnalysisCompetitor) => void prop added
  • First column position: sticky; left: 0 (both th + td) — no bleed на horizontal scroll
  • Row click fires onRowClick, hover highlight только когда handler set

Real data flow

  • useParcelAnalyzeQuery (B5) — score_breakdown.competitors / best_layouts / pipeline_24mo / velocity / market_trend
  • Conditional render per field — graceful empty state без mock fallback
  • Canonical hook (lib/site-finder-api), НЕ duplicate

Constraints

  • TS strict, "use client" только interactive
  • CSS tokens (globals.css), Lucide icons, no emoji
  • pre-commit pass
  • Sections 1+2+3.1 wiring PRESERVED (AnalysisPageContent.tsx 0 lines changed)
  • No duplicate useParcelAnalyzeQuery hook

Verify post-deploy

  • /site-finder/analysis/66:41:0204016:10
    • Section 3.2 BestLayoutsBlock рендерится
    • Section 3.3 — 3-col grid (или empty-state per block)
    • Click competitor row → drawer slides from right с KPI
    • Escape / overlay click → drawer closes
    • Sections 1+2+3.1 НЕ regressed

Part of plan vault code/patterns/SiteFinder_Frontend_Migration_Plan_May18.md §A8 + §A9.

## Summary SF Frontend Wave 3 §A8 (combined §A9 CompetitorTable redesign). ## Files (2) **`frontend/src/components/site-finder/analysis/Section3SettingsAndCompetitors.tsx`**: - `Section32Layouts` (3.2): wraps `<BestLayoutsBlock cadNum={cad} />` - `Section33RemainVelocity` (3.3): 3-col grid `Pipeline24moBlock` + `VelocityBlock` + `MarketTrendBlock` (each conditional on non-null field) - `CompetitorDetailDrawer`: `<Drawer side="right" width="420px"/>` + Badge (status/class) + KPI cells (distance / flat_count / district / ready_dt) - Section 3 wrapper: `selectedCompetitor` state, `onRowClick` → drawer (not navigate) **`frontend/src/components/site-finder/CompetitorTable.tsx`** (existing): - `onRowClick?: (competitor: ParcelAnalysisCompetitor) => void` prop added - First column `position: sticky; left: 0` (both `th` + `td`) — no bleed на horizontal scroll - Row click fires `onRowClick`, hover highlight только когда handler set ## Real data flow - `useParcelAnalyzeQuery` (B5) — `score_breakdown.competitors` / `best_layouts` / `pipeline_24mo` / `velocity` / `market_trend` - Conditional render per field — graceful empty state без mock fallback - Canonical hook (lib/site-finder-api), НЕ duplicate ## Constraints - ✅ TS strict, "use client" только interactive - ✅ CSS tokens (globals.css), Lucide icons, no emoji - ✅ pre-commit pass - ✅ **Sections 1+2+3.1 wiring PRESERVED** (AnalysisPageContent.tsx 0 lines changed) - ✅ No duplicate `useParcelAnalyzeQuery` hook ## Verify post-deploy - `/site-finder/analysis/66:41:0204016:10` → - Section 3.2 BestLayoutsBlock рендерится - Section 3.3 — 3-col grid (или empty-state per block) - Click competitor row → drawer slides from right с KPI - Escape / overlay click → drawer closes - Sections 1+2+3.1 НЕ regressed Part of plan vault `code/patterns/SiteFinder_Frontend_Migration_Plan_May18.md` §A8 + §A9.
lekss361 added 1 commit 2026-05-18 01:09:19 +00:00
Author
Owner

Deep Code Review — verdict: APPROVE

Files reviewed: 2 (P2: 2) · +389/-57 · Risk: Low · Reversibility: Easy revert

Sub-component imports (all verified against current main)

  • BestLayoutsBlock({ cadNum })<BestLayoutsBlock cadNum={cad}/> matches Props.cadNum (line 651) ✓
  • Pipeline24moBlock({ data: Pipeline24mo }) — guarded by hasPipeline + non-null assert ✓
  • VelocityBlock({ velocity: Velocity | null | undefined }) — prop signature compatible with data.velocity (Velocity | null) ✓
  • MarketTrendBlock({ trend?: MarketTrend | null }) — direct pass through ✓
  • Drawer({ open, onClose, side, width, children }) — all 4 props passed, drawer is A11y-ready (focus trap + ESC + body scroll lock) ✓
  • Badge variants used (success | warning | info | neutral) all valid per BadgeVariant union ✓

Sticky column z-index

  • th first cell: position: sticky; left: 0; zIndex: 1; background: #f6f7f9 — overlays other th (default z=0) ✓
  • td first cell: position: sticky; left: 0; background: rowBg (matches dynamic row bg incl. sameDistrict highlight) — no bleed-through on horizontal scroll ✓
  • boxShadow: 2px 0 4px rgba(0,0,0,0.04) for separation cue ✓
  • Hover JS keeps sticky bg in sync (mouseEnter/Leave update both row and inherits via background: rowBg) ✓

Backward compat — CompetitorTable

  • onRowClick?: ... optional → sole legacy caller MarketTab.tsx:242-245 does not pass onRowClick and remains type-correct
  • Cursor & hover are conditional on onRowClick: legacy renders unchanged (no pointer, no hover highlight)
  • Sticky first column applies unconditionally — visually consistent in both contexts, no regression for legacy

Other findings

  • Anchor IDs preserved: section-3 / -3-1 / -3-2 / -3-3 (A4 scrollspy intact) ✓
  • Section 3.1 wiring + AnalysisPageContent untouched ✓
  • 3-col grid uses repeat(auto-fit, minmax(280px, 1fr)) — degrades gracefully at narrow widths ✓
  • Empty-state per block (conditional render) — no "undefined"/"[object Object]" leaks; isEmpty short-circuits with localized fallback ✓
  • "use client" present, Lucide icons only, no emoji, TS strict, no any
  • No new HTTP calls — reuses data: ParcelAnalysis from B5 ✓

Nits (non-blocking)

  • Ruler icon used for "Срок сдачи" — semantically Calendar would be more idiomatic
  • New inline hex (#e0e7ff, #f6f7f9) inside CompetitorTable.tsx — but these are scoped to a pre-existing legacy file already containing inline hex, not new violations introduced by this PR

Merging via squash.

## Deep Code Review — verdict: APPROVE **Files reviewed**: 2 (P2: 2) · **+389/-57** · **Risk**: Low · **Reversibility**: Easy revert ### Sub-component imports (all verified against current main) - `BestLayoutsBlock({ cadNum })` — `<BestLayoutsBlock cadNum={cad}/>` matches `Props.cadNum` (line 651) ✓ - `Pipeline24moBlock({ data: Pipeline24mo })` — guarded by `hasPipeline` + non-null assert ✓ - `VelocityBlock({ velocity: Velocity | null | undefined })` — prop signature compatible with `data.velocity` (`Velocity | null`) ✓ - `MarketTrendBlock({ trend?: MarketTrend | null })` — direct pass through ✓ - `Drawer({ open, onClose, side, width, children })` — all 4 props passed, drawer is A11y-ready (focus trap + ESC + body scroll lock) ✓ - `Badge` variants used (`success | warning | info | neutral`) all valid per `BadgeVariant` union ✓ ### Sticky column z-index - `th` first cell: `position: sticky; left: 0; zIndex: 1; background: #f6f7f9` — overlays other `th` (default z=0) ✓ - `td` first cell: `position: sticky; left: 0; background: rowBg` (matches dynamic row bg incl. `sameDistrict` highlight) — no bleed-through on horizontal scroll ✓ - `boxShadow: 2px 0 4px rgba(0,0,0,0.04)` for separation cue ✓ - Hover JS keeps sticky bg in sync (mouseEnter/Leave update both row and inherits via `background: rowBg`) ✓ ### Backward compat — CompetitorTable - `onRowClick?: ...` optional → sole legacy caller `MarketTab.tsx:242-245` does not pass `onRowClick` and remains type-correct - Cursor & hover are conditional on `onRowClick`: legacy renders unchanged (no pointer, no hover highlight) - Sticky first column applies unconditionally — visually consistent in both contexts, no regression for legacy ### Other findings - Anchor IDs preserved: `section-3 / -3-1 / -3-2 / -3-3` (A4 scrollspy intact) ✓ - Section 3.1 wiring + AnalysisPageContent untouched ✓ - 3-col grid uses `repeat(auto-fit, minmax(280px, 1fr))` — degrades gracefully at narrow widths ✓ - Empty-state per block (conditional render) — no "undefined"/"[object Object]" leaks; `isEmpty` short-circuits with localized fallback ✓ - "use client" present, Lucide icons only, no emoji, TS strict, no `any` ✓ - No new HTTP calls — reuses `data: ParcelAnalysis` from B5 ✓ ### Nits (non-blocking) - `Ruler` icon used for "Срок сдачи" — semantically `Calendar` would be more idiomatic - New inline hex (`#e0e7ff`, `#f6f7f9`) inside CompetitorTable.tsx — but these are scoped to a pre-existing legacy file already containing inline hex, not new violations introduced by this PR Merging via squash.
lekss361 merged commit 4d651f00ef into main 2026-05-18 01:14:32 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#357
No description provided.