""" IPython display helpers — KPI cards, formatted summary blocks, alerts. Functions are notebook-safe: they fall back to plain ``print`` when running outside Jupyter / when IPython is not available. """ from __future__ import annotations from typing import Any try: # pragma: no cover – IPython is a soft dep from IPython.display import HTML, display _IPY = True except Exception: # pragma: no cover _IPY = False def _money(value: Any, default: str = "—") -> str: try: v = float(value) except (TypeError, ValueError): return default if abs(v) >= 1_000_000_000: return f"${v/1_000_000_000:,.1f}B" if abs(v) >= 1_000_000: return f"${v/1_000_000:,.1f}M" if abs(v) >= 1_000: return f"${v/1_000:,.1f}K" return f"${v:,.0f}" def _pct(value: Any, default: str = "—") -> str: try: v = float(value) except (TypeError, ValueError): return default return f"{v:,.0f}%" def _months(value: Any, default: str = "N/A") -> str: if value is None: return default try: v = float(value) except (TypeError, ValueError): return default if v < 6: return f"<6 months ({v:.1f})" return f"{v:.1f} months" def kpi_cards(summary: dict, *, title: str | None = None) -> Any: """ Render a row of KPI cards (NPV, ROI, Payback, Benefits PV). In notebooks, returns/displays inline HTML. Outside IPython, prints a plain text version. """ npv = _money(summary.get("npv")) roi = _pct(summary.get("roi") or summary.get("roi_pct")) payback = _months(summary.get("payback_months")) benefits_pv = _money(summary.get("total_benefits_pv")) costs_pv = _money(summary.get("total_costs_pv")) if not _IPY: # pragma: no cover print(title or "TEI Summary") print(f" NPV: {npv} ROI: {roi} Payback: {payback}") print(f" Benefits PV: {benefits_pv} Costs PV: {costs_pv}") return None title_html = ( f'
' f"{title}
" if title else "" ) card_style = ( "flex:1;min-width:140px;padding:14px 18px;margin:4px;border-radius:8px;" "background:#f7f9fc;border:1px solid #e3e8ee;" ) label_style = "font-size:0.78em;color:#6b7480;text-transform:uppercase;letter-spacing:0.04em;" value_style = "font-size:1.6em;font-weight:600;color:#1a2540;margin-top:4px;" cards = [ ("NPV", npv), ("ROI", roi), ("Payback", payback), ("Benefits PV", benefits_pv), ("Costs PV", costs_pv), ] cards_html = "".join( f'
' f'
{label}
' f'
{value}
' f"
" for label, value in cards ) html = ( f'
{title_html}' f'
{cards_html}
' f"
" ) return display(HTML(html)) def summary_panel(summary: dict, *, title: str = "TEI Financial Summary") -> None: """Plain-text bordered summary block (mirrors the PDF Cash Flow Analysis).""" width = 60 print("═" * width) print(f" {title}") print("═" * width) print(f" Benefits PV : {_money(summary.get('total_benefits_pv')):>20}") print(f" Costs PV : {_money(summary.get('total_costs_pv')):>20}") print("─" * width) print(f" NPV : {_money(summary.get('npv')):>20}") roi_val = summary.get("roi") or summary.get("roi_pct") print(f" ROI : {_pct(roi_val):>20}") print(f" Payback : {_months(summary.get('payback_months')):>20}") print("═" * width) def alert(text: str, kind: str = "info") -> Any: """Coloured alert box for notebooks ('info', 'success', 'warning', 'error').""" colors = { "info": ("#0277bd", "#e1f5fe"), "success": ("#2e7d32", "#e8f5e9"), "warning": ("#ef6c00", "#fff3e0"), "error": ("#c62828", "#ffebee"), } fg, bg = colors.get(kind, colors["info"]) if not _IPY: # pragma: no cover print(f"[{kind.upper()}] {text}") return None html = ( f'
' f"{text}
" ) return display(HTML(html))