feat(ptica): Сценарии 6.6 + Отчёты «Параметры» (tab prototype-parity) #1864
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 { triggerDownload } from "@/lib/download";
|
||||||
import { useParcelForecastQuery } from "@/lib/site-finder-api";
|
import { useParcelForecastQuery } from "@/lib/site-finder-api";
|
||||||
import type { ParcelAnalysis } from "@/types/site-finder";
|
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";
|
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -345,6 +346,19 @@ export function PticaReports({ cad, analysis }: Props) {
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* ── Report-section checklist ──────────────────────────────────── */}
|
{/* ── 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 { ConfidencePanel } from "./ConfidencePanel";
|
||||||
import { RecommendedProduct } from "./RecommendedProduct";
|
import { RecommendedProduct } from "./RecommendedProduct";
|
||||||
import { ScoringTransparency } from "./ScoringTransparency";
|
import { ScoringTransparency } from "./ScoringTransparency";
|
||||||
|
import { FutureSupply } from "./FutureSupply";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
cad: string;
|
cad: string;
|
||||||
|
|
@ -148,6 +149,8 @@ export function PticaScenarios({ cad, horizon, onHorizonChange }: Props) {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{report.scoring && <ScoringTransparency scoring={report.scoring} />}
|
{report.scoring && <ScoringTransparency scoring={report.scoring} />}
|
||||||
|
|
||||||
|
<FutureSupply competitors={fm.future_competitors} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue