From ab699a136169e8661b4f22fbce023ca7f033ce18 Mon Sep 17 00:00:00 2001
From: Tyler Hoang <tyler@tylerhoang.xyz>
Date: Mon, 29 Jun 2026 01:41:24 -0700
Subject: feat: update ValuationPage and valuation cards

---
 frontend/components/prism/ValuationCard.tsx | 305 ++++++++++++++++++----------
 1 file changed, 194 insertions(+), 111 deletions(-)

(limited to 'frontend/components/prism/ValuationCard.tsx')

diff --git a/frontend/components/prism/ValuationCard.tsx b/frontend/components/prism/ValuationCard.tsx
index a1b80c3..4c00a0c 100644
--- a/frontend/components/prism/ValuationCard.tsx
+++ b/frontend/components/prism/ValuationCard.tsx
@@ -1,10 +1,20 @@
 "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";
+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 };
+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;
@@ -15,12 +25,14 @@ function SummaryChip({
   label,
   price,
   current,
+  currency,
   accent = false,
   customised = false,
 }: {
   label: string;
   price?: number | null;
   current?: number | null;
+  currency: string;
   accent?: boolean;
   customised?: boolean;
 }) {
@@ -31,7 +43,7 @@ function SummaryChip({
         {label}
         {customised && <span className="psm-val-chip-dot" />}
       </span>
-      <span className="psm-val-chip-price">{price != null ? fmtCurrency(price) : "—"}</span>
+      <span className="psm-val-chip-price">{price != null && price >= 0 ? fmtCurrency(price, 2, currency) : "—"}</span>
       {pct != null && (
         <span className={`psm-val-chip-pct ${deltaClass(pct)}`}>{fmtPct(pct, 1, true)}</span>
       )}
@@ -44,18 +56,20 @@ function MultipleRow({
   multiple,
   price,
   current,
+  currency,
 }: {
   label: string;
   multiple?: number | null;
   price?: number | null;
   current?: number | null;
+  currency: string;
 }) {
   const pct = pctVsCurrent(price, current);
   return (
     <div className="psm-val-mult-row">
       <span className="psm-val-mult-label">{label}</span>
       <span className="psm-val-mult-x">{multiple != null ? `${multiple.toFixed(1)}×` : "—"}</span>
-      <span className="psm-val-mult-price">{price != null ? fmtCurrency(price) : "—"}</span>
+      <span className="psm-val-mult-price">{price != null ? fmtCurrency(price, 2, currency) : "—"}</span>
       <span className={`psm-val-mult-pct ${pct != null ? deltaClass(pct) : "neutral"}`}>
         {pct != null ? fmtPct(pct, 1, true) : "—"}
       </span>
@@ -103,8 +117,24 @@ function DCFSlider({
   );
 }
 
-export function ValuationCard({ data }: Props) {
+function WaccNote({ waccData }: { waccData?: WaccResponse | null }) {
+  if (!waccData?.available || waccData.wacc == null) return null;
+  return (
+    <div className="psm-val-wacc-note">
+      <span className="psm-val-wacc-label">Market-implied WACC</span>
+      <span className="psm-val-wacc-value">{fmtPct(waccData.wacc)}</span>
+      <span className="psm-val-wacc-detail">
+        β {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)}
+      </span>
+    </div>
+  );
+}
+
+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(() => ({
@@ -118,14 +148,26 @@ export function ValuationCard({ data }: Props) {
   const [terminalGrowth, setTerminalGrowth] = useState(defaults.terminalGrowth);
   const [projectionYears, setProjectionYears] = useState(defaults.projectionYears);
   const [growthRate, setGrowthRate] = useState(defaults.growthRate);
+  const [mode, setMode] = useState<DcfMode>("simple");
+  const [advancedInputs, setAdvancedInputs] = useState<AdvancedDcfInputs>(() => ({
+    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);
@@ -140,16 +182,58 @@ export function ValuationCard({ data }: Props) {
       : (dcf.net_debt ?? 0);
 
   const computed = useMemo(() => {
-    if (!dcf.available || dcf.error || !dcf.base_fcf || !data.shares_outstanding) return null;
+    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 }
     );
-  }, [wacc, terminalGrowth, projectionYears, growthRate, equityBridge, dcf, data.shares_outstanding]);
+  }, [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 (
@@ -159,41 +243,65 @@ export function ValuationCard({ data }: Props) {
         <div className="psm-val-chip accent">
           <span className="psm-val-chip-label">Market Price</span>
           <span className="psm-val-chip-price">
-            {current_price != null ? fmtCurrency(current_price) : "—"}
+            {current_price != null ? fmtCurrency(current_price, 2, currency) : "—"}
           </span>
         </div>
         <SummaryChip
           label="DCF"
           price={livePrice}
           current={current_price}
+          currency={currency}
           customised={isCustomised}
         />
         <SummaryChip
           label="EV / EBITDA"
           price={ev_ebitda.available ? ev_ebitda.implied_price_per_share : null}
           current={current_price}
+          currency={currency}
         />
         <SummaryChip
           label="EV / Revenue"
           price={ev_revenue.available ? ev_revenue.implied_price_per_share : null}
           current={current_price}
+          currency={currency}
         />
         <SummaryChip
           label="P / Book"
           price={price_to_book.available ? price_to_book.implied_price_per_share : null}
           current={current_price}
+          currency={currency}
         />
       </div>
 
+      {data.currency_warning && (
+        <p className="psm-muted-copy psm-val-currency-note">{data.currency_warning}</p>
+      )}
+
       {/* DCF detail */}
       <div className="psm-val-section">
         <div className="psm-val-dcf-section-head">
           <span className="psm-eyebrow">Discounted Cash Flow</span>
-          {dcf.available && !dcf.error && isCustomised && (
-            <button className="psm-val-reset-btn" onClick={handleReset}>
-              Reset
-            </button>
-          )}
+          <div className="psm-val-mode-toggle">
+            <div className="psm-val-mode-segment">
+              <button
+                className={`psm-val-mode-btn${mode === "simple" ? " active" : ""}`}
+                onClick={() => setMode("simple")}
+              >
+                Simple
+              </button>
+              <button
+                className={`psm-val-mode-btn${mode === "advanced" ? " active" : ""}`}
+                onClick={() => setMode("advanced")}
+              >
+                Advanced
+              </button>
+            </div>
+            {dcf.available && !dcf.error && isCustomised && (
+              <button className="psm-val-reset-btn" onClick={handleReset}>
+                Reset
+              </button>
+            )}
+          </div>
         </div>
 
         {!dcf.available && (
@@ -204,112 +312,84 @@ export function ValuationCard({ data }: Props) {
         )}
         {dcf.available && !dcf.error && (
           <div className="psm-val-dcf-body">
-            {/* Left: sliders */}
+            {/* Left: assumptions */}
             <div className="psm-val-dcf-assumptions">
               <div className="psm-val-dcf-col-label">Assumptions</div>
-              <DCFSlider
-                label="WACC"
-                value={wacc}
-                min={0.03}
-                max={0.20}
-                step={0.005}
-                fmt={(v) => `${(v * 100).toFixed(1)}%`}
-                onChange={setWacc}
-              />
-              <DCFSlider
-                label="Terminal Growth"
-                value={terminalGrowth}
-                min={0}
-                max={0.06}
-                step={0.0025}
-                fmt={(v) => `${(v * 100).toFixed(2)}%`}
-                onChange={setTerminalGrowth}
-              />
-              <DCFSlider
-                label="FCF Growth"
-                value={growthRate}
-                min={0}
-                max={0.50}
-                step={0.01}
-                fmt={(v) => `${(v * 100).toFixed(0)}%`}
-                onChange={setGrowthRate}
-              />
-              <DCFSlider
-                label="Horizon"
-                value={projectionYears}
-                min={3}
-                max={15}
-                step={1}
-                fmt={(v) => `${v} yr`}
-                onChange={(v) => setProjectionYears(Math.round(v))}
-              />
+              <WaccNote waccData={waccData} />
+              {mode === "simple" ? (
+                <>
+                  <DCFSlider
+                    label="WACC"
+                    value={wacc}
+                    min={0.03}
+                    max={0.20}
+                    step={0.005}
+                    fmt={(v) => `${(v * 100).toFixed(1)}%`}
+                    onChange={setWacc}
+                  />
+                  <DCFSlider
+                    label="Terminal Growth"
+                    value={terminalGrowth}
+                    min={0}
+                    max={0.06}
+                    step={0.0025}
+                    fmt={(v) => `${(v * 100).toFixed(2)}%`}
+                    onChange={setTerminalGrowth}
+                  />
+                  <DCFSlider
+                    label="FCF Growth"
+                    value={growthRate}
+                    min={0}
+                    max={0.50}
+                    step={0.01}
+                    fmt={(v) => `${(v * 100).toFixed(0)}%`}
+                    onChange={setGrowthRate}
+                  />
+                  <DCFSlider
+                    label="Horizon"
+                    value={projectionYears}
+                    min={3}
+                    max={15}
+                    step={1}
+                    fmt={(v) => `${v} yr`}
+                    onChange={(v) => setProjectionYears(Math.round(v))}
+                  />
+                </>
+              ) : (
+                <AdvancedDcfPanel
+                  inputs={advancedInputs}
+                  onChange={setAdvancedInputs}
+                  wacc={wacc}
+                  terminalGrowth={terminalGrowth}
+                  sharesOutstanding={data.shares_outstanding ?? 0}
+                  equityBridge={equityBridge}
+                  seedRevenue={seedRevenue}
+                  currency={currency}
+                />
+              )}
             </div>
 
             {/* Divider */}
             <div className="psm-val-dcf-divider" />
 
             {/* Right: results */}
-            <div className="psm-val-dcf-results">
-              <div className="psm-val-dcf-col-label">Results</div>
-
-              {computed && "error" in computed ? (
-                <p className="psm-val-dcf-error">{computed.error}</p>
-              ) : (
-                <>
-                  <div className="psm-val-kv-list">
-                    <div className="psm-val-kv-row">
-                      <span className="psm-val-kv-label">Base FCF (TTM)</span>
-                      <span className="psm-val-kv-val">
-                        {dcf.base_fcf != null ? fmtLarge(dcf.base_fcf) : "—"}
-                      </span>
-                    </div>
-                    <div className="psm-val-kv-row">
-                      <span className="psm-val-kv-label">Historical growth</span>
-                      <span className="psm-val-kv-val">
-                        {dcf.growth_rate_used != null ? fmtPct(dcf.growth_rate_used) : "—"}
-                      </span>
-                    </div>
-                    <div className="psm-val-kv-row is-divider">
-                      <span className="psm-val-kv-label">FCF PV Sum</span>
-                      <span className="psm-val-kv-val">
-                        {computed ? fmtLarge(computed.fcfPvSum) : "—"}
-                      </span>
-                    </div>
-                    <div className="psm-val-kv-row">
-                      <span className="psm-val-kv-label">Terminal Value PV</span>
-                      <span className="psm-val-kv-val">
-                        {computed ? fmtLarge(computed.terminalValuePv) : "—"}
-                      </span>
-                    </div>
-                    <div className="psm-val-kv-row">
-                      <span className="psm-val-kv-label">Enterprise Value</span>
-                      <span className="psm-val-kv-val">
-                        {computed ? fmtLarge(computed.enterpriseValue) : "—"}
-                      </span>
-                    </div>
-                    <div className="psm-val-kv-row is-total">
-                      <span className="psm-val-kv-label">Net Debt</span>
-                      <span className="psm-val-kv-val">
-                        {dcf.net_debt != null ? fmtLarge(dcf.net_debt) : "—"}
-                      </span>
-                    </div>
-                  </div>
-                  <div className="psm-val-intrinsic">
-                    <span className="psm-val-intrinsic-label">Intrinsic Value</span>
-                    <span className="psm-val-intrinsic-price">
-                      {livePrice != null ? fmtCurrency(livePrice) : "—"}
-                    </span>
-                    {data.shares_outstanding != null && (
-                      <span className="psm-val-intrinsic-shares">
-                        {fmtLarge(data.shares_outstanding)} shares
-                      </span>
-                    )}
-                  </div>
-                </>
-              )}
-            </div>
+            <DcfResults
+              dcf={dcf}
+              computed={computed}
+              livePrice={livePrice}
+              sharesOutstanding={data.shares_outstanding ?? null}
+              currency={currency}
+            />
           </div>
         )}
+        {sensitivityMatrix && (
+          <SensitivityTable
+            matrix={sensitivityMatrix}
+            centerWacc={wacc}
+            centerTerminalGrowth={terminalGrowth}
+            currency={currency}
+          />
+        )}
       </div>
 
       {/* Multiples */}
@@ -325,6 +405,7 @@ export function ValuationCard({ data }: Props) {
                 multiple={ev_ebitda.multiple_used}
                 price={ev_ebitda.implied_price_per_share}
                 current={current_price}
+                currency={currency}
               />
             )}
             {ev_revenue.available && (
@@ -333,6 +414,7 @@ export function ValuationCard({ data }: Props) {
                 multiple={ev_revenue.multiple_used}
                 price={ev_revenue.implied_price_per_share}
                 current={current_price}
+                currency={currency}
               />
             )}
             {price_to_book.available && (
@@ -341,6 +423,7 @@ export function ValuationCard({ data }: Props) {
                 multiple={price_to_book.multiple_used}
                 price={price_to_book.implied_price_per_share}
                 current={current_price}
+                currency={currency}
               />
             )}
           </div>
-- 
cgit v1.3-2-g0d8e

