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
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:
parent
567549c3aa
commit
921c14e054
2 changed files with 7 additions and 130 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue