Dark cockpit Scenarios tab wired to useParcelForecastQuery (ForecastEnvelope): scenario cards (base/aggr/conserv), demand-vs-supply chart + horizons table, scenario compare table, confidence panel, recommended product (квартирография + target price + success-score), scoring transparency. Calm polling skeleton while the async forecast is pending — never an error. Hero invest-score/buy-signal now surface real values once the forecast resolves (scoring.overall×10; buy-signal from exec_summary.key_numbers.deficit_index, derived independent of the overall score), keeping the "после прогноза" placeholders while pending. advisory · §22 caption shown in both states. All forecast field paths verified against types/forecast.ts. Dark theme stays scoped under .pticaRoot[data-theme=dark]. TS strict, no any.
91 lines
2.9 KiB
TypeScript
91 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
/**
|
||
* ConfidencePanel — 6.3 «Уверенность прогноза». An overall conf-pill (from
|
||
* confidence.level) + a row per confidence.factors entry, each with a high/mid/
|
||
* low pill. The factors map carries a stray non-factor boolean (advisory_capped)
|
||
* in prod envelopes — narrowed out via a runtime guard (mirrors the light block).
|
||
*/
|
||
|
||
import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";
|
||
import type { ConfidenceFactor, ReportConfidence } from "@/types/forecast";
|
||
import { CONFIDENCE_RU, confidencePill } from "./scenario-helpers";
|
||
|
||
interface Props {
|
||
confidence: ReportConfidence;
|
||
}
|
||
|
||
const FACTOR_RU: Record<string, string> = {
|
||
deal_count: "Объём данных по сделкам",
|
||
analog_count: "Аналоги (ЖК)",
|
||
domrf_coverage: "Покрытие ДОМ.РФ",
|
||
history_months: "Глубина истории",
|
||
component: "Компонент",
|
||
};
|
||
|
||
const LEVEL_RANK: Record<ConfidenceFactor["level"], number> = {
|
||
high: 0,
|
||
medium: 1,
|
||
low: 2,
|
||
};
|
||
|
||
const PILL_CLASS = {
|
||
high: styles.confPillHigh,
|
||
medium: styles.confPillMedium,
|
||
low: styles.confPillLow,
|
||
} as const;
|
||
|
||
function isConfidenceFactor(v: unknown): v is ConfidenceFactor {
|
||
if (typeof v !== "object" || v === null) return false;
|
||
const o = v as Record<string, unknown>;
|
||
return (
|
||
typeof o.note === "string" &&
|
||
(o.level === "high" || o.level === "medium" || o.level === "low")
|
||
);
|
||
}
|
||
|
||
function factorLabel(key: string, factor: ConfidenceFactor): string {
|
||
if (factor.label) return factor.label;
|
||
if (FACTOR_RU[key]) return FACTOR_RU[key];
|
||
const m = key.match(/^component(?:_(\d+))?$/);
|
||
if (m) return m[1] ? `Компонент ${m[1]}` : "Компонент";
|
||
return key;
|
||
}
|
||
|
||
export function ConfidencePanel({ confidence }: Props) {
|
||
const level = confidence.level;
|
||
const drivers = Object.entries(confidence.factors)
|
||
.filter((entry): entry is [string, ConfidenceFactor] =>
|
||
isConfidenceFactor(entry[1]),
|
||
)
|
||
.sort((a, b) => LEVEL_RANK[a[1].level] - LEVEL_RANK[b[1].level]);
|
||
|
||
return (
|
||
<div className={styles.panel}>
|
||
<div className={styles.cardHead}>
|
||
<h3 className={styles.cardTitle}>Уверенность прогноза</h3>
|
||
{level && (
|
||
<span
|
||
className={`${styles.confPill} ${PILL_CLASS[confidencePill(level)]}`}
|
||
>
|
||
{CONFIDENCE_RU[level]}
|
||
</span>
|
||
)}
|
||
</div>
|
||
{drivers.length > 0 ? (
|
||
drivers.map(([key, factor]) => (
|
||
<div key={key} className={styles.confRow}>
|
||
<span className={styles.confName}>{factorLabel(key, factor)}</span>
|
||
<span
|
||
className={`${styles.confPill} ${PILL_CLASS[confidencePill(factor.level)]}`}
|
||
>
|
||
{CONFIDENCE_RU[factor.level]}
|
||
</span>
|
||
</div>
|
||
))
|
||
) : (
|
||
<p className={styles.panelHint}>Факторы достоверности недоступны.</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|