diff --git a/backend/app/schemas/recommend.py b/backend/app/schemas/recommend.py index 0139e0e1..81236116 100644 --- a/backend/app/schemas/recommend.py +++ b/backend/app/schemas/recommend.py @@ -17,7 +17,9 @@ class RecommendMixInput(BaseModel): months_window: int = Field(default=12, ge=3, le=36) # Velocity / pricing scenario knobs (live-tuned client-side; backend just # ships base coefficients so frontend can recompute without round-trips). - price_factor: float = Field(default=1.0, ge=0.5, le=2.0) + # 0.01..3.0 = -99%..+200% к рынку. min=0.01 (а не 0) чтобы избежать + # деления на 0 / pf^elasticity = ∞ при «бесплатной» цене. + price_factor: float = Field(default=1.0, ge=0.01, le=3.0) target_months: int | None = Field(default=None, ge=3, le=120) diff --git a/frontend/src/app/analytics/recommend/page.tsx b/frontend/src/app/analytics/recommend/page.tsx index 7286a298..065a68bf 100644 --- a/frontend/src/app/analytics/recommend/page.tsx +++ b/frontend/src/app/analytics/recommend/page.tsx @@ -68,20 +68,9 @@ export default function RecommendPage() { }); }, [data, effectiveShares, input.area_total_m2]); - const totalRevenue = derivedRows.reduce( - (a, r) => a + (r.effective_revenue_rub ?? 0), - 0, - ); - const weightedAvgPrice = useMemo(() => { - let num = 0; - let den = 0; - for (const r of derivedRows) { - const w = r.effective_share_pct * r.area_avg_m2; - num += w * r.price_median_per_m2; - den += w; - } - return den > 0 ? num / den : null; - }, [derivedRows]); + // totalRevenue и weightedAvgPrice сейчас уже отображаются внутри + // RecommendVelocityPanel и headline summary — здесь больше не считаем, + // чтобы не дублировать вычисления и держать одну точку правды для KPI. const errMsg = mutation.error ? mutation.error instanceof Error @@ -190,6 +179,21 @@ export default function RecommendPage() { /> + {/* Mix sliders — сразу под KPI/слайдером цены, чтобы при движении + было видно как меняются метрики и графики live. */} +
+ b.bucket)} + shares={effectiveShares} + recommended={recommendedShares} + onChange={setOverrideShares} + onReset={() => setOverrideShares(null)} + /> +
+ {/* Liquidity chart — кумулятивная кривая продаж */} {hasAllocation ? (
) : null} + {/* District-specific KPIs — что отличается между районами: + competitors, sale_graph покрытие, district/class факторы. + Раньше тут были city-wide дубли (Сделок 3815 / Weighted / + Выручка) — все одинаковые независимо от района. */}
- - - + + +
b.bucket)} @@ -263,19 +286,6 @@ export default function RecommendPage() {
) : null} -
- b.bucket)} - shares={effectiveShares} - recommended={recommendedShares} - onChange={setOverrideShares} - onReset={() => setOverrideShares(null)} - /> -
-
) : null} - {/* Price factor slider */} + {/* Price factor slider — 0.01..3.0 (от -99% до +200%). + min=0.01 (а не 0) чтобы избежать pf^elasticity = ∞ при свободной + цене. */}
Цена ± к рынку onPriceFactorChange(Number(e.target.value))} @@ -224,7 +226,7 @@ export function RecommendVelocityPanel({ {requiredPct}% к рынку).{" "}