gendesign/frontend/src/components/site-finder/ptica/scenarios/FutureSupply.tsx
Light1YT e8d0863996
All checks were successful
CI / changes (pull_request) Successful in 7s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Successful in 57s
CI / openapi-codegen-check (pull_request) Successful in 1m54s
feat(ptica): close tab prototype-parity gaps (Сценарии 6.6 + Отчёты Параметры)
- Сценарии: add section 6.6 «будущее предложение и конкуренты» (FutureSupply)
  rendering report.future_market.future_competitors after ScoringTransparency
  (order 6.1→6.6). Columns ЖК/Класс/Квартир/Дистанция/Поглощение, real data,
  honest empty-state. Reuses .dtable.
- Отчёты: add «Параметры» panel (карта/финмодель/прогноз §22); §22 row tied to
  the real forecast-ready flag. Reuses StatusRow.
Verified on the all-tabs harness; no console errors. tokens-only, no fabrication.
2026-06-21 11:34:11 +05:00

84 lines
3.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.

/**
* FutureSupply — 6.6 «Будущее предложение и конкуренты». Renders
* §9.7 forward-looking competitors (report.future_market.future_competitors):
* one row per forecast competitor — ЖК / проект · класс · квартир · дистанция ·
* темп поглощения. Distinct from the market_now competitors block: these are the
* top-N projects ranked by `relevance_weight` at the horizon that will compete
* for the same demand.
*
* The prototype's «Срок сдачи» column has NO backing field in the §22 report, so
* it is dropped in favour of the real `velocity_per_month` (темп поглощения) —
* every value traces to a real forecast field. Rows are sorted by distance asc
* (closest competitors first). Nullable fields render «нет данных», never a
* fabricated 0. Empty/absent list → honest empty-state, NOT a hidden section.
*/
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
import type { FutureCompetitor } from "@/types/forecast";
import { fmtInt, fmtNum } from "./scenario-helpers";
interface Props {
competitors: FutureCompetitor[] | undefined;
}
/** Distance in metres → «445 м» / «1.20 км» (mirrors PticaMapInner.formatMeters). */
function formatMeters(m: number): string {
if (m >= 1000) return `${(m / 1000).toFixed(2)} км`;
return `${Math.round(m)} м`;
}
export function FutureSupply({ competitors }: Props) {
const rows = (competitors ?? [])
.slice()
.sort((a, b) => a.distance_m - b.distance_m);
return (
<>
<div className={styles.sectionLabel}>
6.6 · <b>будущее предложение и конкуренты</b>
</div>
<div className={styles.panel}>
{rows.length === 0 ? (
<div className={styles.emptyPanel}>
Нет данных по конкурирующему предложению
</div>
) : (
<>
<table className={styles.dtable}>
<thead>
<tr>
<th>ЖК / проект</th>
<th>Класс</th>
<th className={styles.thNum}>Квартир</th>
<th className={styles.thNum}>Дистанция</th>
<th className={styles.thNum}>Поглощение</th>
</tr>
</thead>
<tbody>
{rows.map((c) => (
<tr key={c.obj_id}>
<td>{c.comm_name ?? "нет данных"}</td>
<td>{c.obj_class ?? "нет данных"}</td>
<td className={styles.tdNum}>
{c.flats_total != null
? `${fmtInt(c.flats_total)} кв`
: "нет данных"}
</td>
<td className={styles.tdNum}>{formatMeters(c.distance_m)}</td>
<td className={styles.tdNum}>
{`${fmtNum(c.velocity_per_month, 1)} кв/мес`}
</td>
</tr>
))}
</tbody>
</table>
<p className={styles.panelHint}>
Конкурирующие проекты в горизонте прогноза, отранжированные по
близости. Поглощение темп продаж проекта (квартир в месяц).
</p>
</>
)}
</div>
</>
);
}