feat(trade-in): OfferCard — убрать редактируемую цену + ползунок торга, разбивка на дефолтах (#939)
All checks were successful
Deploy Trade-In / changes (push) Successful in 5s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m34s
Deploy Trade-In / deploy (push) Successful in 37s

Co-authored-by: bot-backend <bot-backend@gendsgn.local>
Co-committed-by: bot-backend <bot-backend@gendsgn.local>
This commit is contained in:
bot-backend 2026-05-31 22:31:20 +00:00 committed by bot-reviewer
parent 567549c3aa
commit 921c14e054
2 changed files with 7 additions and 130 deletions

View file

@ -2,9 +2,8 @@
/**
* OfferCard Секция 4 «Оффер» (commercial Brusnika p.4 «формирование
* выкупной стоимости»). Интерактивный КАЛЬКУЛЯТОР ВЫГОДЫ: web-преимущество
* над PDF менеджер двигает цену в объявлении / % торга и сразу видит, как
* меняются издержки самостоятельной продажи и «ваша выгода» через trade-in.
* выкупной стоимости»). Показывает разбивку издержек самостоятельной продажи
* на фиксированных дефолтных ставках (median_price_rub + DEFAULT_BARGAIN_PCT).
*
* NB: сама ВЫКУПНАЯ цена (сколько платит девелопер) НЕ рассчитывается это
* коммерческое решение «Практики» (нет параметров haircut/маржи/срока).
@ -14,11 +13,10 @@
* TODO(commercial): подставить реальные условия «Практики» в DEFAULT_OFFER_RATES
* (offer-rates.ts) единственное место для смены ставок.
*/
import { useMemo, useState } from "react";
import { useMemo } from "react";
import type { AggregatedEstimate } from "@/types/trade-in";
import { API_BASE_URL } from "@/lib/api";
import {
BARGAIN_SLIDER,
DEFAULT_OFFER_RATES,
computeBreakdown,
} from "./offer-rates";
@ -45,36 +43,12 @@ export function OfferCard({ estimate, brandSlug }: Props) {
// PDF endpoint (api/v1/trade_in.py) уже honor'ит ?brand= для white-label.
const pdfQuery = brandSlug ? `?brand=${encodeURIComponent(brandSlug)}` : "";
// ── Интерактивные входы ──
// asking median_price_rub — стартовая цена в объявлении (редактируемая).
const [askingPrice, setAskingPrice] = useState<number>(
Math.round(estimate.median_price_rub),
);
const [bargainPct, setBargainPct] = useState<number>(DEFAULT_BARGAIN_PCT);
// ── LIVE-пересчёт: входы → разбивка издержек (pure-функция в offer-rates.ts) ──
// ── Разбивка на фиксированных дефолтах ──
const fixedPrice = Math.round(estimate.median_price_rub);
const breakdown = useMemo(
() => computeBreakdown(askingPrice, bargainPct, DEFAULT_OFFER_RATES),
[askingPrice, bargainPct],
() => computeBreakdown(fixedPrice, DEFAULT_BARGAIN_PCT, DEFAULT_OFFER_RATES),
[fixedPrice],
);
const askingMln = askingPrice / 1_000_000;
function onPriceInput(e: React.ChangeEvent<HTMLInputElement>) {
// Только цифры: пользователь может стереть/набрать новую цену.
const digits = e.target.value.replace(/\D/g, "");
setAskingPrice(digits === "" ? 0 : Number(digits));
}
function resetPrice() {
setAskingPrice(Math.round(estimate.median_price_rub));
setBargainPct(DEFAULT_BARGAIN_PCT);
}
const isCustomPrice =
Math.round(estimate.median_price_rub) !== askingPrice ||
bargainPct !== DEFAULT_BARGAIN_PCT;
return (
<article className="card" id="section-offer">
<div className="card-head">
@ -93,55 +67,6 @@ export function OfferCard({ estimate, brandSlug }: Props) {
</div>
</div>
{/* ── Интерактивные контролы калькулятора ── */}
<div className="offer-calc-controls">
<div className="occ-field">
<label className="occ-label" htmlFor="occ-price">
Цена в объявлении
<span className="occ-hint">за сколько выставляете квартиру</span>
</label>
<div className="occ-price-input control-with-suffix">
<input
id="occ-price"
className="control mono"
inputMode="numeric"
value={askingPrice === 0 ? "" : fmtRub(askingPrice)}
onChange={onPriceInput}
aria-label="Цена в объявлении, рублей"
/>
<span className="suffix"></span>
</div>
<div className="occ-sub"> {askingMln.toFixed(2)} млн </div>
</div>
<div className="occ-field">
<label className="occ-label" htmlFor="occ-bargain">
Торг с покупателем
<span className="occ-hint">типично {pct(DEFAULT_OFFER_RATES.bargain.lo)}{pct(DEFAULT_OFFER_RATES.bargain.hi)}</span>
</label>
<input
id="occ-bargain"
className="occ-slider"
type="range"
min={BARGAIN_SLIDER.min}
max={BARGAIN_SLIDER.max}
step={BARGAIN_SLIDER.step}
value={bargainPct}
onChange={(e) => setBargainPct(Number(e.target.value))}
aria-label="Доля торга с покупателем"
/>
<div className="occ-sub">
<b className="mono">{pct(bargainPct)}</b> {fmtRub(askingPrice * bargainPct)}
</div>
</div>
{isCustomPrice && (
<button type="button" className="occ-reset" onClick={resetPrice}>
Сбросить к оценке
</button>
)}
</div>
{/* ── Прайм-блок: ваша выгода ── */}
<div className="offer-benefit">
<div className="ob-label">Ваша выгода через Trade-In</div>

View file

@ -1184,52 +1184,6 @@
OFFER Section (4)
============================================================ */
/* ── Interactive benefit-calculator controls ── */
.offer-calc-controls {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 20px;
align-items: end;
padding: 18px 22px;
background: var(--surface-2);
border-bottom: 1px solid var(--border);
}
.occ-field { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.occ-label {
font-size: 12px;
font-weight: 600;
color: var(--fg);
display: flex;
flex-direction: column;
gap: 2px;
}
.occ-label .occ-hint {
font-size: 11px;
font-weight: 400;
color: var(--muted-2);
}
.occ-price-input { width: 100%; }
.occ-sub { font-size: 11px; color: var(--muted); }
.occ-sub b { color: var(--fg); font-weight: 600; }
.occ-slider {
width: 100%;
margin: 8px 0 2px;
accent-color: var(--accent);
cursor: pointer;
}
.occ-reset {
align-self: end;
font-size: 12px;
padding: 9px 14px;
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface);
color: var(--muted);
cursor: pointer;
white-space: nowrap;
}
.occ-reset:hover { color: var(--fg); border-color: var(--accent); }
/* ── Prominent «Ваша выгода» block ── */
.offer-benefit {
margin: 18px 22px 4px;
@ -1428,8 +1382,6 @@
.offer-cta-buttons .btn { flex: 1; }
.card-head { flex-direction: column; align-items: flex-start; }
.card-head .card-meta { text-align: left; }
.offer-calc-controls { grid-template-columns: 1fr; gap: 16px; }
.occ-reset { align-self: stretch; text-align: center; }
.offer-benefit .ob-value { font-size: 24px; }
}