diff options
Diffstat (limited to 'frontend/components/prism/ValuationCard.tsx')
| -rw-r--r-- | frontend/components/prism/ValuationCard.tsx | 190 |
1 files changed, 190 insertions, 0 deletions
diff --git a/frontend/components/prism/ValuationCard.tsx b/frontend/components/prism/ValuationCard.tsx new file mode 100644 index 0000000..7329ff5 --- /dev/null +++ b/frontend/components/prism/ValuationCard.tsx @@ -0,0 +1,190 @@ +"use client"; +import type { ValuationResponse } from "@/types/api"; +import { deltaClass, fmtCurrency, fmtLarge, fmtPct } from "@/lib/format"; + +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, +}: { + label: string; + price?: number | null; + current?: number | null; + accent?: boolean; +}) { + const pct = pctVsCurrent(price, current); + return ( + <div className={`psm-val-chip${accent ? " accent" : ""}`}> + <span className="psm-val-chip-label">{label}</span> + <span className="psm-val-chip-price">{price != null ? fmtCurrency(price) : "—"}</span> + {pct != null && ( + <span className={`psm-val-chip-pct ${deltaClass(pct)}`}>{fmtPct(pct, 1, true)}</span> + )} + </div> + ); +} + +function MultipleRow({ + label, + multiple, + price, + current, +}: { + label: string; + multiple?: number | null; + price?: number | null; + current?: number | null; +}) { + 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-pct ${pct != null ? deltaClass(pct) : "neutral"}`}> + {pct != null ? fmtPct(pct, 1, true) : "—"} + </span> + </div> + ); +} + +export function ValuationCard({ data }: Props) { + const { dcf, ev_ebitda, ev_revenue, price_to_book, current_price } = data; + const dcfPrice = dcf.available && !dcf.error ? dcf.intrinsic_value_per_share : null; + const hasMultiples = ev_ebitda.available || ev_revenue.available || price_to_book.available; + + return ( + <section className="psm-card psm-val-card"> + {/* Summary strip */} + <div className="psm-val-strip"> + <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) : "—"} + </span> + </div> + <SummaryChip label="DCF" price={dcfPrice} current={current_price} /> + <SummaryChip + label="EV / EBITDA" + price={ev_ebitda.available ? ev_ebitda.implied_price_per_share : null} + current={current_price} + /> + <SummaryChip + label="EV / Revenue" + price={ev_revenue.available ? ev_revenue.implied_price_per_share : null} + current={current_price} + /> + <SummaryChip + label="P / Book" + price={price_to_book.available ? price_to_book.implied_price_per_share : null} + current={current_price} + /> + </div> + + {/* DCF detail */} + <div className="psm-val-section"> + <div className="psm-val-section-head"> + <span className="psm-eyebrow">Discounted Cash Flow</span> + <span className="psm-val-wacc-note"> + WACC {(dcf.wacc * 100).toFixed(1)}% · Terminal {(dcf.terminal_growth * 100).toFixed(1)}% + </span> + </div> + + {!dcf.available && ( + <p className="psm-muted-copy">Insufficient free cash flow history for DCF.</p> + )} + {dcf.available && dcf.error && ( + <p className="psm-val-dcf-error">{dcf.error}</p> + )} + {dcf.available && !dcf.error && ( + <div className="psm-val-dcf-body"> + <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">Enterprise Value</span> + <span className="psm-val-kv-val"> + {dcf.enterprise_value != null ? fmtLarge(dcf.enterprise_value) : "—"} + </span> + </div> + <div className="psm-val-kv-row"> + <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 className="psm-val-kv-row is-total"> + <span className="psm-val-kv-label">Equity Value</span> + <span className="psm-val-kv-val"> + {dcf.equity_value != null ? fmtLarge(dcf.equity_value) : "—"} + </span> + </div> + </div> + <div className="psm-val-intrinsic"> + <span className="psm-val-intrinsic-label">Intrinsic Value</span> + <span className="psm-val-intrinsic-price"> + {dcf.intrinsic_value_per_share != null ? fmtCurrency(dcf.intrinsic_value_per_share) : "—"} + </span> + {data.shares_outstanding != null && ( + <span className="psm-val-intrinsic-shares"> + {fmtLarge(data.shares_outstanding)} shares + </span> + )} + </div> + </div> + )} + </div> + + {/* Multiples */} + {hasMultiples && ( + <div className="psm-val-section"> + <div className="psm-val-section-head"> + <span className="psm-eyebrow">Multiples — at current market multiple</span> + </div> + <div className="psm-val-mult-list"> + {ev_ebitda.available && ( + <MultipleRow + label="EV / EBITDA" + multiple={ev_ebitda.multiple_used} + price={ev_ebitda.implied_price_per_share} + current={current_price} + /> + )} + {ev_revenue.available && ( + <MultipleRow + label="EV / Revenue" + multiple={ev_revenue.multiple_used} + price={ev_revenue.implied_price_per_share} + current={current_price} + /> + )} + {price_to_book.available && ( + <MultipleRow + label="P / Book" + multiple={price_to_book.multiple_used} + price={price_to_book.implied_price_per_share} + current={current_price} + /> + )} + </div> + </div> + )} + </section> + ); +} |
