"use client";
import { useMemo, useState } from "react";
import type { ValuationResponse } from "@/types/api";
import { deltaClass, fmtCurrency, fmtLarge, fmtPct } from "@/lib/format";
import { computeDcf } from "@/lib/dcf";
type Props = { data: ValuationResponse };
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,
accent = false,
customised = false,
}: {
label: string;
price?: number | null;
current?: number | null;
accent?: boolean;
customised?: boolean;
}) {
const pct = pctVsCurrent(price, current);
return (
{label}
{customised && }
{price != null ? fmtCurrency(price) : "—"}
{pct != null && (
{fmtPct(pct, 1, true)}
)}
);
}
function MultipleRow({
label,
multiple,
price,
current,
}: {
label: string;
multiple?: number | null;
price?: number | null;
current?: number | null;
}) {
const pct = pctVsCurrent(price, current);
return (
{label}
{multiple != null ? `${multiple.toFixed(1)}×` : "—"}
{price != null ? fmtCurrency(price) : "—"}
{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)}
);
}
export function ValuationCard({ data }: Props) {
const { dcf, ev_ebitda, ev_revenue, price_to_book, current_price } = data;
// 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 isCustomised =
wacc !== defaults.wacc ||
terminalGrowth !== defaults.terminalGrowth ||
projectionYears !== defaults.projectionYears ||
growthRate !== defaults.growthRate;
function handleReset() {
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 || !dcf.base_fcf || !data.shares_outstanding) return null;
return computeDcf(
{ baseFcf: dcf.base_fcf, equityBridge, sharesOutstanding: data.shares_outstanding },
{ wacc, terminalGrowth, projectionYears, growthRate }
);
}, [wacc, terminalGrowth, projectionYears, growthRate, equityBridge, dcf, data.shares_outstanding]);
const livePrice =
computed && !("error" in computed) ? computed.intrinsicValuePerShare : null;
const hasMultiples = ev_ebitda.available || ev_revenue.available || price_to_book.available;
return (
{/* Summary strip */}
Market Price
{current_price != null ? fmtCurrency(current_price) : "—"}
{/* 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: sliders */}
Assumptions
`${(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 */}
Results
{computed && "error" in computed ? (
{computed.error}
) : (
<>
Base FCF (TTM)
{dcf.base_fcf != null ? fmtLarge(dcf.base_fcf) : "—"}
Historical growth
{dcf.growth_rate_used != null ? fmtPct(dcf.growth_rate_used) : "—"}
FCF PV Sum
{computed ? fmtLarge(computed.fcfPvSum) : "—"}
Terminal Value PV
{computed ? fmtLarge(computed.terminalValuePv) : "—"}
Enterprise Value
{computed ? fmtLarge(computed.enterpriseValue) : "—"}
Net Debt
{dcf.net_debt != null ? fmtLarge(dcf.net_debt) : "—"}
Intrinsic Value
{livePrice != null ? fmtCurrency(livePrice) : "—"}
{data.shares_outstanding != null && (
{fmtLarge(data.shares_outstanding)} shares
)}
>
)}
)}
{/* Multiples */}
{hasMultiples && (
Multiples — at current market multiple
{ev_ebitda.available && (
)}
{ev_revenue.available && (
)}
{price_to_book.available && (
)}
)}
);
}