gendesign/frontend/src/components/site-finder/KpiCard.tsx
Light1YT 589b3bfdc3
All checks were successful
CI / frontend-tests (pull_request) Successful in 1m0s
CI / openapi-codegen-check (pull_request) Successful in 1m55s
CI / backend-tests (pull_request) Successful in 9m59s
CI / changes (pull_request) Successful in 6s
fix(site-finder): honest POI-score label + drop duplicate MOI column (audit #1871 P2)
Два disclosure-фикса + 3 consistency follow-up (по code-review).

invest_score = взвешенная POI-сумма (distance-decay + center-bonus, max-ref
40), НЕ калиброванный инвест-рейтинг (scorer.py — пустой стаб). Был мислейбл
«Инвест-балл». Формулу/SCORE_MAX_REFERENCE не трогаем — только честная подпись.
- backend parcels.py: score_explanation → «POI-скор (0–40, оценочный)...
  Не инвест-рейтинг...»
- KpiCard: новый optional caption prop (backward-compat).
- Section1ParcelInfo: KPI «Инвест-балл» → «POI-скор · 0–40» + caption «оценочный».
- legacy site-finder: scoreLabel → «POI-скор · {label} · оценочный».
- CompareTable: «Инвест-балл» → «POI-скор · 0–40» (тот же score, был мислейбл).
- landing app/page.tsx: «Score 0–100» → «POI-скор 0–40» (реальная шкала).
- Section1 scoreColor: пороги 0–100 → 0–40 калиброванные (_score_label:
  <5 плохо · 5–15 средне · 15–25 хорошо · ≥25 отлично) — раньше хороший
  ~30/40 участок рисовался red рядом с честным «0–40» лейблом.

MOI (months_of_inventory) математически КОРРЕКТЕН — по определению не зависит
от горизонта проекции (MOI = сток / месячный темп). Не баг. Но дублировался
колонкой на 4 горизонта в ForecastHorizonsBlock (116.6/116.6/117.6/117.6 —
выглядело сломанным). Убрал колонку; MOI остаётся 1 раз в Section6 key_numbers.
deficit_index (варьируется) сохранён. Подчищены unused balanced/DEFICIT_BALANCE_EPS.

138 frontend vitest passed, ruff/tsc/lint clean.

Refs #1871
2026-06-23 12:29:02 +05:00

79 lines
1.6 KiB
TypeScript

"use client";
type KpiColor = "neutral" | "amber" | "green" | "red" | "blue";
const BG: Record<KpiColor, string> = {
neutral: "#f9fafb",
amber: "#fef3c7",
green: "#dcfce7",
red: "#fee2e2",
blue: "#dbeafe",
};
const FG: Record<KpiColor, string> = {
neutral: "#374151",
amber: "#b45309",
green: "#15803d",
red: "#b91c1c",
blue: "#1d4ed8",
};
interface Props {
value: string;
label: string;
color?: KpiColor;
/**
* Опциональная подпись под значением (caption-токен 12/16, --fg-tertiary).
* Видимая (не hover) — для честных квалификаторов вроде «оценочный».
*/
caption?: string;
}
export function KpiCard({ value, label, color = "neutral", caption }: Props) {
return (
<div
style={{
padding: "12px 16px",
background: BG[color],
borderRadius: 10,
minWidth: 0,
}}
>
<div
style={{
fontSize: 20,
fontWeight: 700,
color: FG[color],
lineHeight: 1.2,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{value}
</div>
{caption ? (
<div
style={{
fontSize: 12,
lineHeight: "16px",
color: "var(--fg-tertiary)",
marginTop: 2,
}}
>
{caption}
</div>
) : null}
<div
style={{
fontSize: 11,
color: "#6b7280",
marginTop: 4,
lineHeight: 1.4,
}}
>
{label}
</div>
</div>
);
}