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.
This commit is contained in:
parent
4a2ed52cb7
commit
e8d0863996
3 changed files with 101 additions and 0 deletions
|
|
@ -31,6 +31,7 @@ import { API_BASE_URL } from "@/lib/api";
|
|||
import { triggerDownload } from "@/lib/download";
|
||||
import { useParcelForecastQuery } from "@/lib/site-finder-api";
|
||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
||||
import { StatusRow } from "@/components/site-finder/ptica/drawers/DrawerPrimitives";
|
||||
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||||
|
||||
interface Props {
|
||||
|
|
@ -345,6 +346,19 @@ export function PticaReports({ cad, analysis }: Props) {
|
|||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ── Параметры — что войдёт в сгенерированный отчёт ─────────────── */}
|
||||
<hr className={styles.overlayDivider} />
|
||||
<h3 className={styles.cardTitle}>
|
||||
Параметры <span className={styles.cardTitleSub}>OPTIONS</span>
|
||||
</h3>
|
||||
<StatusRow label="Включить карту и слои" state="Да" tone="ok" />
|
||||
<StatusRow label="Включить финмодель" state="Да" tone="ok" />
|
||||
<StatusRow
|
||||
label="Включить прогноз §22"
|
||||
state={forecastReady ? "Включён" : "Готовится"}
|
||||
tone={forecastReady ? "ok" : "warn"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Report-section checklist ──────────────────────────────────── */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
* 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import { ScenarioCompareTable } from "./ScenarioCompareTable";
|
|||
import { ConfidencePanel } from "./ConfidencePanel";
|
||||
import { RecommendedProduct } from "./RecommendedProduct";
|
||||
import { ScoringTransparency } from "./ScoringTransparency";
|
||||
import { FutureSupply } from "./FutureSupply";
|
||||
|
||||
interface Props {
|
||||
cad: string;
|
||||
|
|
@ -148,6 +149,8 @@ export function PticaScenarios({ cad, horizon, onHorizonChange }: Props) {
|
|||
</section>
|
||||
|
||||
{report.scoring && <ScoringTransparency scoring={report.scoring} />}
|
||||
|
||||
<FutureSupply competitors={fm.future_competitors} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue