gendesign/tradein-mvp/frontend/src/components/trade-in/OfferCard.tsx
lekss361 5f6b0d6d87 feat(tradein): white-label branded page for client «Практика» (#657)
?brand=praktika re-skin: useBrand hook (reads ?brand= slug, fetches
/api/v1/brand/{slug}), runtime CSS-var override of accent palette +
Manrope font (open replacement for paid HalvarMittel), client logo in
Topbar, ?brand passthrough on the PDF link. No-op when ?brand= absent.
Migration 084 fills the praktika brand row (#235F49 / #1FCECB, logo, footer).

Closes #657
2026-05-29 18:52:13 +03:00

198 lines
9.1 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";
/**
* OfferCard — Секция 4 «Оффер» из mockup tradein.html.
* Сравнение trade-in vs самостоятельная продажа + 4 преимущества + CTA.
*/
import type { AggregatedEstimate } from "@/types/trade-in";
import { API_BASE_URL } from "@/lib/api";
interface Props {
estimate: AggregatedEstimate;
/** #657 white-label: активный ?brand= slug — прокидывается в PDF endpoint. */
brandSlug?: string | null;
}
function fmtRub(v: number): string {
return Math.round(v).toLocaleString("ru-RU");
}
export function OfferCard({ estimate, brandSlug }: Props) {
// PDF endpoint (api/v1/trade_in.py) уже honor'ит ?brand= для white-label.
// Без бренда query пустой → стандартный PDF.
const pdfQuery = brandSlug ? `?brand=${encodeURIComponent(brandSlug)}` : "";
const basePrice = estimate.median_price_rub;
const baseMln = basePrice / 1_000_000;
// Расчёт издержек самостоятельной продажи (от рыночной цены)
const torgLo = Math.round(basePrice * 0.05);
const torgHi = Math.round(basePrice * 0.15);
const realtorLo = Math.round((basePrice - torgHi) * 0.02);
const realtorHi = Math.round((basePrice - torgLo) * 0.03);
const totalLo = torgLo + realtorLo + 90_000 + 19_250 + 4_000;
const totalHi = torgHi + realtorHi + 150_000 + 50_000 + 36_000;
const totalPct = (((totalLo + totalHi) / 2) / basePrice) * 100;
return (
<article className="card" id="section-offer">
<div className="card-head">
<div>
<div className="section-kicker">Секция 4 · Оффер</div>
<h2>Формирование выкупной стоимости</h2>
</div>
<div className="card-meta">
<div>
База расчёта: <b className="mono">{baseMln.toFixed(2)} млн </b>
</div>
<div style={{ marginTop: 4 }}>при стоимости квартиры в объявлении</div>
</div>
</div>
<table className="offer-tbl" aria-label="Сравнение trade-in и самостоятельной продажи">
<thead>
<tr>
<th>Статья расходов</th>
<th className="col-trade">Через Trade-In</th>
<th className="col-self">Самостоятельная продажа</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div className="item-name">Торг покупателей</div>
<div className="item-sub">515% от цены объявления</div>
</td>
<td className="col-trade">не применимо</td>
<td className="col-self">
{fmtRub(torgLo)} {fmtRub(torgHi)}
</td>
</tr>
<tr>
<td>
<div className="item-name">Услуги риелтора</div>
<div className="item-sub">23% от цены сделки</div>
</td>
<td className="col-trade">бесплатно</td>
<td className="col-self">
{fmtRub(realtorLo)} {fmtRub(realtorHi)}
</td>
</tr>
<tr>
<td>
<div className="item-name">Аренда жилья после сделки</div>
<div className="item-sub">1-к квартира на 3 месяца</div>
</td>
<td className="col-trade">бесплатно</td>
<td className="col-self">90 000 150 000 </td>
</tr>
<tr>
<td>
<div className="item-name">Юридическое сопровождение</div>
<div className="item-sub">проверка, договор, регистрация</div>
</td>
<td className="col-trade">бесплатно</td>
<td className="col-self">от 19 250 </td>
</tr>
<tr>
<td>
<div className="item-name">Расходы на рекламу</div>
<div className="item-sub">платные показы Циан / Авито, 3 мес</div>
</td>
<td className="col-trade">бесплатно</td>
<td className="col-self">4 000 36 000 </td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Общие финансовые потери</td>
<td className="col-trade"> 0 </td>
<td className="col-self">
{fmtRub(totalLo)} {fmtRub(totalHi)}
</td>
</tr>
</tfoot>
</table>
<div className="warn-banner">
<span className="ic">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</span>
<div>
Издержки при самостоятельной продаже сопоставимы со скидкой <b>{totalPct.toFixed(0)}%</b> от рыночной цены.{" "}
<b>Trade-In закрывает их полностью</b>, а цена в новостройке фиксируется на момент оценки.
</div>
</div>
<div className="advantages">
<div className="advantage">
<div className="adv-ic">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="7.5" cy="15.5" r="5.5" />
<path d="m21 2-9.6 9.6" />
<path d="m15.5 7.5 3 3L22 7l-3-3" />
</svg>
</div>
<h4>Экономия времени</h4>
<p>Берём на себя показы, переговоры и поиск покупателей. Менеджеру остаётся только подписать ДКП.</p>
</div>
<div className="advantage">
<div className="adv-ic">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" />
</svg>
</div>
<h4>Юридическая безопасность</h4>
<p>Проверяем чистоту документов, ЕГРН, прописанных и обременения. Исключаем риски.</p>
</div>
<div className="advantage">
<div className="adv-ic">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="19" y1="5" x2="5" y2="19" />
<circle cx="6.5" cy="6.5" r="2.5" />
<circle cx="17.5" cy="17.5" r="2.5" />
</svg>
</div>
<h4>Фиксированная цена новостройки</h4>
<p>Сохраняем планировку в каталоге по цене на момент оценки до закрытия сделки.</p>
</div>
<div className="advantage">
<div className="adv-ic">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="12" r="6" />
<circle cx="12" cy="12" r="2" />
</svg>
</div>
<h4>Гарантия цены</h4>
<p>Снимаем риск колебаний рынка выкупная стоимость зафиксирована в оффере на 30 дней.</p>
</div>
</div>
<div className="offer-cta">
<div className="offer-cta-text">
<b>Готовы отправить оффер клиенту?</b>
<br />
PDF-отчёт + персональный оффер по выкупной цене за 1 клик.
</div>
<div className="offer-cta-buttons">
<a
href={`${API_BASE_URL}/api/v1/trade-in/estimate/${estimate.estimate_id}/pdf${pdfQuery}`}
download={`trade-in-${estimate.estimate_id}.pdf`}
className="btn btn-ghost"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
Скачать PDF
</a>
</div>
</div>
</article>
);
}