SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
2.9 KB · 25 lines tsx
Raw Blame History
1import type { ToveliEstimate } from "@/lib/estimate";2import { fmtPrice, fmtPct } from "@/lib/format";3export function EstimateBreakdown({ e, price }: { e: ToveliEstimate; price?: number | null }) {4  const r = e.result;5  const w = (v: number | null) => (v && r.estimate ? Math.min(100, Math.max(0, ((v - r.low) / Math.max(1, r.high - r.low)) * 100)) : null);6  return (7    <div className="tv-card p-4">8      <div className="relative h-2 rounded-full bg-surface-2 mt-6 mb-7">9        <div className="absolute inset-y-0 rounded-full bg-brand-soft" style={{ left: 0, right: 0 }} />10        {[["Estimation", r.estimate, "bg-brand"], ...(r.modelEstimate ? [["Modèle", r.modelEstimate, "bg-ink-3"]] : []), ...(r.compsEstimate ? [["Comparables", r.compsEstimate, "bg-ink-3"]] : []), ...(price ? [["Prix demandé", price, "bg-info"]] : [])].map(([k, v, c]) => { const x = w(v as number); return x == null ? null : <span key={String(k)} className="absolute -top-5 text-[10px] text-ink-3 -translate-x-1/2 whitespace-nowrap" style={{ left: `${x}%` }}>{k}<span className={`block mx-auto mt-0.5 w-2.5 h-2.5 rounded-full ${c}`} /></span>; })}11        <span className="absolute -bottom-5 left-0 text-[11px] text-ink-3">{fmtPrice(r.low)}</span><span className="absolute -bottom-5 right-0 text-[11px] text-ink-3">{fmtPrice(r.high)}</span>12      </div>13      <dl className="grid sm:grid-cols-2 gap-x-6 gap-y-1 text-sm">14        {r.modelEstimate != null && <div className="flex justify-between border-b border-line py-1"><dt className="text-ink-3">Modèle hédonique (poids {Math.round(r.modelWeight * 100)} %)</dt><dd className="tabular-nums">{fmtPrice(r.modelEstimate)}</dd></div>}15        {r.compsEstimate != null && <div className="flex justify-between border-b border-line py-1"><dt className="text-ink-3">Comparables ajustés (poids {Math.round((1 - r.modelWeight) * 100)} %)</dt><dd className="tabular-nums">{fmtPrice(r.compsEstimate)}</dd></div>}16        <div className="flex justify-between border-b border-line py-1"><dt className="text-ink-3">Comparables retenus</dt><dd>{r.nCompsUsed}</dd></div>17        {r.compsDispersionPct != null && <div className="flex justify-between border-b border-line py-1"><dt className="text-ink-3">Dispersion des comparables</dt><dd>{r.compsDispersionPct} %</dd></div>}18        {e.roleUnit?.role_value != null && <div className="flex justify-between border-b border-line py-1"><dt className="text-ink-3">Valeur au rôle</dt><dd className="tabular-nums">{fmtPrice(e.roleUnit.role_value)} <span className="text-ink-3 text-xs">({fmtPct(r.estimate / e.roleUnit.role_value - 1)})</span></dd></div>}19        <div className="flex justify-between border-b border-line py-1"><dt className="text-ink-3">Largeur de la fourchette</dt><dd>{fmtPct((r.high - r.low) / r.estimate, 0, false)}</dd></div>20      </dl>21      <p className="mt-3 text-xs text-ink-3">Entrées : {e.method.inputs.join(" ; ")}.</p>22    </div>23  );24}25