"use client"; import { useEffect, useMemo, useState } from "react"; import { computeAdvancedDcf } from "@/lib/dcf"; import { fmtCurrency } from "@/lib/format"; import type { AdvancedDcfInputs as AdvancedDcfInputShape } from "@/lib/dcf"; const YEARS = 5; const PRESETS: Record = { bear: { baseRevenue: 1_000, revenueGrowth: [0.02, 0.02, 0.02, 0.02, 0.02], ebitdaMargin: [0.2, 0.2, 0.2, 0.2, 0.2], dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05], capexPctRevenue: [0.1, 0.1, 0.1, 0.1, 0.1], nwcPctDeltaRev: [0.03, 0.03, 0.03, 0.03, 0.03], taxRate: [0.28, 0.28, 0.28, 0.28, 0.28], }, base: { baseRevenue: 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], }, bull: { baseRevenue: 1_000, revenueGrowth: [0.1, 0.1, 0.1, 0.1, 0.1], ebitdaMargin: [0.32, 0.32, 0.32, 0.32, 0.32], dnaPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05], capexPctRevenue: [0.05, 0.05, 0.05, 0.05, 0.05], nwcPctDeltaRev: [0.015, 0.015, 0.015, 0.015, 0.015], taxRate: [0.21, 0.21, 0.21, 0.21, 0.21], }, }; const LABELS: Record = { baseRevenue: "Base Revenue", revenueGrowth: "Revenue Growth", ebitdaMargin: "EBITDA Margin", dnaPctRevenue: "D&A % Rev", capexPctRevenue: "CapEx % Rev", nwcPctDeltaRev: "ΔNWC % ΔRev", taxRate: "Tax Rate", }; const YEAR_LABELS = ["Y1", "Y2", "Y3", "Y4", "Y5"]; type Props = { inputs: AdvancedDcfInputShape; onChange: (inputs: AdvancedDcfInputShape) => void; wacc: number; terminalGrowth: number; sharesOutstanding: number; equityBridge: number; seedRevenue?: number | null; currency: string; }; function fmtRate(v: number) { return `${(v * 100).toFixed(1)}%`; } function parseRateInput(raw: string): number | null { const cleaned = raw.replace(/[^0-9.\-]/g, ""); const num = parseFloat(cleaned) / 100; return Number.isFinite(num) ? num : null; } function fmtNumber(n: number): string { return n.toLocaleString("en-US"); } function parseNumber(raw: string): number { const cleaned = raw.replace(/[^0-9.\-]/g, ""); const num = parseFloat(cleaned); return Number.isFinite(num) ? num : 0; } export function AdvancedDcfPanel({ inputs, onChange, wacc, terminalGrowth, sharesOutstanding, equityBridge, seedRevenue, currency, }: Props) { const baseRevenue = seedRevenue ?? 1_000; const presets = useMemo>( () => ({ bear: { ...PRESETS.bear, baseRevenue }, base: { ...PRESETS.base, baseRevenue }, bull: { ...PRESETS.bull, baseRevenue }, }), [baseRevenue] ); const computed = useMemo(() => { return computeAdvancedDcf(inputs, { wacc, terminalGrowth, projectionYears: YEARS, sharesOutstanding, equityBridge, }); }, [inputs, wacc, terminalGrowth, sharesOutstanding, equityBridge]); const price = computed && !("error" in computed) ? computed.intrinsicValuePerShare : null; const isInvalid = price != null && price < 0; function applyPreset(name: string) { const preset = presets[name]; if (preset) onChange({ ...preset }); } function updateScalar(field: "baseRevenue", value: number) { onChange({ ...inputs, [field]: value }); } function updateArray(field: Exclude, index: number, value: number) { const next = [...inputs[field]]; next[index] = value; onChange({ ...inputs, [field]: next }); } const [baseRevenueText, setBaseRevenueText] = useState(fmtNumber(inputs.baseRevenue)); useEffect(() => { setBaseRevenueText(fmtNumber(inputs.baseRevenue)); }, [inputs.baseRevenue]); return (
{Object.keys(PRESETS).map((name) => ( ))}
{/* Scalar field */}
{ const raw = e.target.value; setBaseRevenueText(raw); const num = parseNumber(raw); if (num !== inputs.baseRevenue) { updateScalar("baseRevenue", num); } }} onBlur={() => setBaseRevenueText(fmtNumber(inputs.baseRevenue))} />
{/* Array fields with year headers */}
{YEAR_LABELS.map((yl) => ( {yl} ))}
{(Object.keys(LABELS) as Array) .filter((key) => key !== "baseRevenue") .map((field) => (
{LABELS[field]} {inputs[field].map((value, idx) => ( { const num = parseRateInput(e.target.value); if (num !== null) updateArray(field, idx, num); }} /> ))}
))}
Advanced Intrinsic Value {isInvalid ? "Invalid assumptions" : price != null ? fmtCurrency(price, 2, currency) : computed && "error" in computed ? computed.error : "—"}
); }