Implements §10 Ten-Year Value Comparison
This commit is contained in:
@@ -1,52 +1,128 @@
|
||||
/**
|
||||
* TenYearValueChart — upfront vs illustrative 10-yr savings, per option (spec §10).
|
||||
* Savings bars only render when a premium was provided and conservative mode is off.
|
||||
* TenYearValueChart — 10-year value comparison panel (spec §10).
|
||||
*
|
||||
* Honors the full §10 brief:
|
||||
* - shows ASSUMPTIONS per tier (discount %, service life, hail positioning, value view)
|
||||
* - lets the customer TOGGLE discount scenarios (low / base / high)
|
||||
* - separates upfront cost from lifecycle value (breakeven + lifecycle delta)
|
||||
* - never claims a guaranteed discount; all savings labeled illustrative
|
||||
*
|
||||
* Savings/breakeven/lifecycle only render when a premium was provided and
|
||||
* conservative mode is off; otherwise the panel falls back to assumptions only.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts';
|
||||
|
||||
const PKG_LABEL = { GOOD: 'Good', BETTER: 'Better', BEST: 'Best' };
|
||||
const BAR_COLOR = { GOOD: '#a1a1aa', BETTER: '#3b82f6', BEST: '#fda913' };
|
||||
const SCENARIOS = [
|
||||
{ key: 'low', label: 'Low' },
|
||||
{ key: 'base', label: 'Base' },
|
||||
{ key: 'high', label: 'High' },
|
||||
];
|
||||
|
||||
const usd = (n) => `$${Number(n).toLocaleString('en-US')}`;
|
||||
|
||||
const TenYearValueChart = ({ valueModel }) => {
|
||||
const [scenario, setScenario] = useState('base');
|
||||
const showSavings = valueModel.premiumKnown && !valueModel.conservativeMode;
|
||||
|
||||
const data = valueModel.rows.map((r) => ({
|
||||
name: PKG_LABEL[r.type],
|
||||
type: r.type,
|
||||
Upfront: r.price.base,
|
||||
'Est. 10-yr savings': r.savings ? r.savings.base : 0,
|
||||
'Est. 10-yr savings': r.savings ? r.savings[scenario] : 0,
|
||||
}));
|
||||
|
||||
const fmt = (n) => `$${(n / 1000).toFixed(0)}k`;
|
||||
const fmtK = (n) => `$${(n / 1000).toFixed(0)}k`;
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-zinc-900 p-4">
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white text-sm mb-3">10-year value comparison</h4>
|
||||
<div style={{ width: '100%', height: 220 }}>
|
||||
<div className="flex items-center justify-between gap-2 mb-3 flex-wrap">
|
||||
<h4 className="font-semibold text-zinc-900 dark:text-white text-sm">10-year value comparison</h4>
|
||||
{showSavings && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[11px] text-zinc-400 mr-1">Discount scenario</span>
|
||||
{SCENARIOS.map((s) => (
|
||||
<button
|
||||
key={s.key}
|
||||
type="button"
|
||||
onClick={() => setScenario(s.key)}
|
||||
className={`px-2.5 py-1 rounded-lg text-[11px] font-semibold transition-all
|
||||
${scenario === s.key
|
||||
? 'bg-amber-500 text-white shadow'
|
||||
: 'bg-zinc-100 dark:bg-white/5 text-zinc-500 dark:text-zinc-400 hover:text-amber-600'}`}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Upfront vs illustrative savings */}
|
||||
<div style={{ width: '100%', height: 200 }}>
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
<XAxis dataKey="name" tick={{ fontSize: 12 }} stroke="#a1a1aa" />
|
||||
<YAxis tickFormatter={fmt} tick={{ fontSize: 11 }} stroke="#a1a1aa" width={44} />
|
||||
<YAxis tickFormatter={fmtK} tick={{ fontSize: 11 }} stroke="#a1a1aa" width={44} />
|
||||
<Tooltip
|
||||
formatter={(v) => `$${Number(v).toLocaleString('en-US')}`}
|
||||
formatter={(v) => usd(v)}
|
||||
contentStyle={{ borderRadius: 12, border: '1px solid #e4e4e7', fontSize: 12 }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
<Bar dataKey="Upfront" radius={[4, 4, 0, 0]}>
|
||||
{data.map((d) => <Cell key={d.type} fill={BAR_COLOR[d.type]} fillOpacity={0.85} />)}
|
||||
</Bar>
|
||||
{valueModel.premiumKnown && !valueModel.conservativeMode && (
|
||||
{showSavings && (
|
||||
<Bar dataKey="Est. 10-yr savings" radius={[4, 4, 0, 0]} fill="#22c55e" fillOpacity={0.8} />
|
||||
)}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<p className="text-[11px] text-zinc-400 mt-2">
|
||||
{valueModel.premiumKnown
|
||||
? 'Savings shown are illustrative and depend on carrier approval, policy terms, and documentation.'
|
||||
: 'Add your insurance premium earlier in the wizard to see an illustrative 10-year savings scenario.'}
|
||||
|
||||
{/* Assumptions + lifecycle metrics per tier */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5 mt-4">
|
||||
{valueModel.rows.map((r) => (
|
||||
<div key={r.type} className="rounded-xl border border-zinc-200 dark:border-white/10 p-3">
|
||||
<div className="text-xs font-bold uppercase tracking-wide text-zinc-500 dark:text-zinc-400 mb-2">{PKG_LABEL[r.type]}</div>
|
||||
<Metric label="Upfront (base)" value={usd(r.price.base)} />
|
||||
<Metric label="Discount assumption" value={`${r.discountScenario[scenario] ?? 0}%`} />
|
||||
<Metric label="Service life" value={`${r.serviceLifeYears}+ yrs`} />
|
||||
<Metric label="Hail resistance" value={r.hailResistance} />
|
||||
<Metric label="10-yr value view" value={r.valueView} muted />
|
||||
{showSavings && (
|
||||
<>
|
||||
<div className="border-t border-zinc-100 dark:border-white/5 my-2" />
|
||||
<Metric label="Est. 10-yr savings" value={r.savings ? usd(r.savings[scenario]) : '—'} accent />
|
||||
{/* Breakeven only when pricing AND discount known (spec §10 display rule). */}
|
||||
<Metric label="Upgrade breakeven" value={r.breakeven[scenario] != null ? `${r.breakeven[scenario]} yrs` : '—'} />
|
||||
<Metric label="Lifecycle value delta" value={usd(r.lifecycleDelta[scenario])} hint="model estimate" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-zinc-400 mt-3 leading-relaxed">
|
||||
{showSavings
|
||||
? 'Savings scenarios are illustrative model estimates — not guaranteed. They depend on carrier approval, policy terms, state rules, and documentation. Lifecycle value delta is a model estimate, not a promise.'
|
||||
: valueModel.conservativeMode
|
||||
? 'Savings scenarios are hidden. Figures show upfront cost and durability assumptions only.'
|
||||
: 'Add your insurance premium earlier in the wizard to see illustrative 10-year savings, breakeven, and lifecycle value scenarios.'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Metric = ({ label, value, accent, muted, hint }) => (
|
||||
<div className="flex items-baseline justify-between gap-2 py-0.5">
|
||||
<span className="text-[11px] text-zinc-400">{label}</span>
|
||||
<span className={`text-xs font-semibold text-right ${accent ? 'text-emerald-600 dark:text-emerald-400' : muted ? 'text-zinc-500 dark:text-zinc-400' : 'text-zinc-800 dark:text-zinc-100'}`}>
|
||||
{value}
|
||||
{hint && <span className="block text-[9px] font-normal text-zinc-400 italic">{hint}</span>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TenYearValueChart;
|
||||
|
||||
Reference in New Issue
Block a user