gendesign/frontend/src/components/site-finder/ptica/scenarios/ConfidencePanel.tsx
Light1YT b0736e3a45 feat(ptica): build Сценарии tab (§22 forecast) + real hero invest-score
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.
2026-06-20 15:48:54 +05:00

91 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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