"use client"; import { useMemo, useState } from "react"; import type { ValuationResponse, WaccResponse } from "@/types/api"; import { deltaClass, fmtCurrency, fmtPct } from "@/lib/format"; import { computeAdvancedDcf, computeDcf, computeSensitivityGrid, type AdvancedDcfInputs, } from "@/lib/dcf"; import { AdvancedDcfPanel } from "@/components/prism/AdvancedDcfPanel"; import { DcfResults } from "@/components/prism/DcfResults"; import { SensitivityTable } from "@/components/prism/SensitivityTable"; type Props = { data: ValuationResponse; waccData?: WaccResponse | null; seedRevenue?: number | null }; type DcfMode = "simple" | "advanced"; function pctVsCurrent(implied?: number | null, current?: number | null): number | null { if (implied == null || current == null || current === 0) return null; return (implied - current) / current; } function SummaryChip({ label, price, current, currency, accent = false, customised = false, }: { label: string; price?: number | null; current?: number | null; currency: string; accent?: boolean; customised?: boolean; }) { const pct = pctVsCurrent(price, current); return (
{label} {customised && } {price != null && price >= 0 ? fmtCurrency(price, 2, currency) : "—"} {pct != null && ( {fmtPct(pct, 1, true)} )}
); } function MultipleRow({ label, multiple, price, current, currency, }: { label: string; multiple?: number | null; price?: number | null; current?: number | null; currency: string; }) { const pct = pctVsCurrent(price, current); return (
{label} {multiple != null ? `${multiple.toFixed(1)}×` : "—"} {price != null ? fmtCurrency(price, 2, currency) : "—"} {pct != null ? fmtPct(pct, 1, true) : "—"}
); } function DCFSlider({ label, value, min, max, step, fmt, onChange, }: { label: string; value: number; min: number; max: number; step: number; fmt: (v: number) => string; onChange: (v: number) => void; }) { return (
{label} {fmt(value)}
onChange(parseFloat(e.target.value))} />
{fmt(min)} {fmt(max)}
); } function WaccNote({ waccData }: { waccData?: WaccResponse | null }) { if (!waccData?.available || waccData.wacc == null) return null; return (
Market-implied WACC {fmtPct(waccData.wacc)} β {waccData.beta.toFixed(2)} · Rf {fmtPct(waccData.risk_free_rate)} · ERP{" "} {fmtPct(waccData.equity_risk_premium)} · rd {fmtPct(waccData.cost_of_debt)} · t{" "} {fmtPct(waccData.tax_rate)}
); } export function ValuationCard({ data, waccData, seedRevenue }: Props) { const { dcf, ev_ebitda, ev_revenue, price_to_book, current_price } = data; const currency = data.currency || "USD"; // Slider defaults captured once from the initial API response const [defaults] = useState(() => ({ wacc: dcf.wacc, terminalGrowth: dcf.terminal_growth, projectionYears: dcf.projection_years, growthRate: dcf.growth_rate_used ?? 0.05, })); const [wacc, setWacc] = useState(defaults.wacc); const [terminalGrowth, setTerminalGrowth] = useState(defaults.terminalGrowth); const [projectionYears, setProjectionYears] = useState(defaults.projectionYears); const [growthRate, setGrowthRate] = useState(defaults.growthRate); const [mode, setMode] = useState("simple"); const [advancedInputs, setAdvancedInputs] = useState(() => ({ baseRevenue: seedRevenue ?? 1_000, revenueGrowth: [0.05, 0.05, 0.05, 0.05, 0.05], ebitdaMargin: [0.25, 0.25, 0.25, 0.25, 0.25], dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05], capexPctRevenue: [0.07, 0.07, 0.07, 0.07, 0.07], nwcPctDeltaRev: [0.02, 0.02, 0.02, 0.02, 0.02], taxRate: [0.21, 0.21, 0.21, 0.21, 0.21], })); const isCustomised = mode !== "simple" || wacc !== defaults.wacc || terminalGrowth !== defaults.terminalGrowth || projectionYears !== defaults.projectionYears || growthRate !== defaults.growthRate; function handleReset() { setMode("simple"); setWacc(defaults.wacc); setTerminalGrowth(defaults.terminalGrowth); setProjectionYears(defaults.projectionYears); setGrowthRate(defaults.growthRate); } // equityBridge = net_debt + preferred equity + minority interest. // Derived as enterprise_value - equity_value (both from API); stays fixed while sliders move. const equityBridge = dcf.enterprise_value != null && dcf.equity_value != null ? dcf.enterprise_value - dcf.equity_value : (dcf.net_debt ?? 0); const computed = useMemo(() => { if (!dcf.available || dcf.error || !data.shares_outstanding) return null; if (mode === "advanced") { return computeAdvancedDcf(advancedInputs, { wacc, terminalGrowth, projectionYears: advancedInputs.revenueGrowth.length, sharesOutstanding: data.shares_outstanding, equityBridge, }); } if (!dcf.base_fcf) return null; return computeDcf( { baseFcf: dcf.base_fcf, equityBridge, sharesOutstanding: data.shares_outstanding }, { wacc, terminalGrowth, projectionYears, growthRate } ); }, [mode, advancedInputs, wacc, terminalGrowth, projectionYears, growthRate, equityBridge, dcf, data.shares_outstanding]); const livePrice = computed && !("error" in computed) ? computed.intrinsicValuePerShare : null; const sensitivityMatrix = useMemo(() => { if (!dcf.available || dcf.error || !data.shares_outstanding) return null; if (mode === "simple" && dcf.sensitivity) return dcf.sensitivity; if (mode === "simple" && !dcf.base_fcf) return null; const shares = data.shares_outstanding; const grid = computeSensitivityGrid(wacc, terminalGrowth, (testWacc, testTerminalGrowth) => { if (mode === "simple") { if (!dcf.base_fcf) return null; const result = computeDcf( { baseFcf: dcf.base_fcf, equityBridge, sharesOutstanding: shares }, { wacc: testWacc, terminalGrowth: testTerminalGrowth, projectionYears, growthRate } ); return "error" in result ? null : result.intrinsicValuePerShare; } const result = computeAdvancedDcf(advancedInputs, { wacc: testWacc, terminalGrowth: testTerminalGrowth, projectionYears: advancedInputs.revenueGrowth.length, sharesOutstanding: shares, equityBridge, }); return "error" in result ? null : result.intrinsicValuePerShare; }); return { wacc: grid.wacc, terminal_growth: grid.terminalGrowth, implied_prices: grid.impliedPrices, }; }, [mode, dcf, advancedInputs, wacc, terminalGrowth, projectionYears, growthRate, equityBridge, data.shares_outstanding]); const hasMultiples = ev_ebitda.available || ev_revenue.available || price_to_book.available; return (
{/* Summary strip */}
Market Price {current_price != null ? fmtCurrency(current_price, 2, currency) : "—"}
{data.currency_warning && (

{data.currency_warning}

)} {/* DCF detail */}
Discounted Cash Flow
{dcf.available && !dcf.error && isCustomised && ( )}
{!dcf.available && (

Insufficient free cash flow history for DCF.

)} {dcf.available && dcf.error && (

{dcf.error}

)} {dcf.available && !dcf.error && (
{/* Left: assumptions */}
Assumptions
{mode === "simple" ? ( <> `${(v * 100).toFixed(1)}%`} onChange={setWacc} /> `${(v * 100).toFixed(2)}%`} onChange={setTerminalGrowth} /> `${(v * 100).toFixed(0)}%`} onChange={setGrowthRate} /> `${v} yr`} onChange={(v) => setProjectionYears(Math.round(v))} /> ) : ( )}
{/* Divider */}
{/* Right: results */}
)} {sensitivityMatrix && ( )}
{/* Multiples */} {hasMultiples && (
Multiples — at current market multiple
{ev_ebitda.available && ( )} {ev_revenue.available && ( )} {price_to_book.available && ( )}
)}
); }