- Сценарии: 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.
84 lines
3.5 KiB
TypeScript
84 lines
3.5 KiB
TypeScript
/**
|
||
* 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>
|
||
</>
|
||
);
|
||
}
|