summaryrefslogtreecommitdiff
path: root/frontend/components/prism
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/components/prism')
-rw-r--r--frontend/components/prism/ValuationCard.tsx190
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>
+ );
+}