feat(ptica): close tab prototype-parity gaps (Сценарии 6.6 + Отчёты Параметры)
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

- Сценарии: 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:
Light1YT 2026-06-21 11:34:11 +05:00
parent 4a2ed52cb7
commit e8d0863996
3 changed files with 101 additions and 0 deletions

View file

@ -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 ──────────────────────────────────── */}

View file

@ -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>
</>
);
}

View file

@ -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>
);
}