refactor: rename pages directory to views
Renames the `app/pages` module to `app/views` to better reflect the purpose of the directory, aligning with conventional MVC naming conventions where view-related logic is housed under `views`.
This commit is contained in:
118
app/views/summary.py
Normal file
118
app/views/summary.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Financial summary dashboard tab."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from app.components import charts
|
||||
from app.pages._helpers import report_meta, safe
|
||||
from core.export import build_report_data
|
||||
from core.tei_client import AthenaAPIError, TEIClient
|
||||
|
||||
|
||||
def render(client: TEIClient, tool: dict) -> None:
|
||||
st.header("📊 Financial Summary")
|
||||
public_id = tool["id"]
|
||||
report = report_meta(client, tool)
|
||||
|
||||
try:
|
||||
summary = client.get_summary(public_id)
|
||||
except AthenaAPIError as e:
|
||||
if e.status_code == 404:
|
||||
st.info(
|
||||
"No summary yet — click **Recalculate** in the sidebar after "
|
||||
"filling in benefits and costs."
|
||||
)
|
||||
return
|
||||
st.error(f"Athena API error: {e.detail}")
|
||||
return
|
||||
|
||||
npv = float(summary.get("net_present_value") or summary.get("npv") or 0)
|
||||
roi = float(
|
||||
summary.get("roi_percentage")
|
||||
or summary.get("roi")
|
||||
or summary.get("roi_pct")
|
||||
or 0
|
||||
)
|
||||
payback = summary.get("payback_period_months", summary.get("payback_months"))
|
||||
bpv = float(summary.get("total_benefits_pv") or 0)
|
||||
cpv = float(summary.get("total_costs_pv") or 0)
|
||||
|
||||
cols = st.columns(5)
|
||||
cols[0].metric("NPV", f"${npv/1_000_000:,.1f}M")
|
||||
cols[1].metric("ROI", f"{roi:,.0f}%")
|
||||
cols[2].metric(
|
||||
"Payback",
|
||||
f"{float(payback):.1f} months" if payback is not None else "N/A",
|
||||
)
|
||||
cols[3].metric("Benefits PV", f"${bpv/1_000_000:,.1f}M")
|
||||
cols[4].metric("Costs PV", f"${cpv/1_000_000:,.1f}M")
|
||||
|
||||
st.divider()
|
||||
|
||||
# Build the yearly breakdown from the documented per-year summary keys
|
||||
# (benefits_year_N / costs_year_N) when no pre-built breakdown exists.
|
||||
yb = summary.get("yearly_breakdown") or []
|
||||
if not yb:
|
||||
n = 1
|
||||
while f"benefits_year_{n}" in summary or f"costs_year_{n}" in summary:
|
||||
b = float(summary.get(f"benefits_year_{n}") or 0)
|
||||
c = float(summary.get(f"costs_year_{n}") or 0)
|
||||
yb.append({"year": n, "benefits": b, "costs": c, "net": b - c})
|
||||
n += 1
|
||||
initial = float(summary.get("initial_costs") or 0)
|
||||
if yb:
|
||||
charts.cashflow(yb, initial_cost=initial)
|
||||
with st.expander("Cash flow table"):
|
||||
st.dataframe(yb, use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.caption("No yearly breakdown in this summary.")
|
||||
|
||||
# Scenario comparison — computed locally from current values
|
||||
with st.expander("Scenario analysis (conservative / moderate / aggressive)"):
|
||||
envelope = safe(
|
||||
build_report_data,
|
||||
client,
|
||||
public_id,
|
||||
include_scenarios=True,
|
||||
study_slug=report.get("name", ""),
|
||||
)
|
||||
if envelope and envelope.get("scenarios"):
|
||||
charts.scenario_bars(envelope["scenarios"])
|
||||
rows = [
|
||||
{
|
||||
"Scenario": k,
|
||||
"Benefits PV": float(v.get("total_benefits_pv") or 0),
|
||||
"Costs PV": float(v.get("total_costs_pv") or 0),
|
||||
"NPV": float(v.get("npv") or 0),
|
||||
"ROI %": float(v.get("roi_pct") or 0),
|
||||
"Payback (months)": (
|
||||
round(float(v.get("payback_months") or 0), 1)
|
||||
if v.get("payback_months") is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
for k, v in envelope["scenarios"].items()
|
||||
]
|
||||
st.dataframe(rows, use_container_width=True, hide_index=True)
|
||||
|
||||
# Export button
|
||||
st.divider()
|
||||
if st.button("📦 Build export envelope (JSON)"):
|
||||
envelope = safe(
|
||||
build_report_data,
|
||||
client,
|
||||
public_id,
|
||||
include_scenarios=True,
|
||||
study_slug=report.get("name", ""),
|
||||
)
|
||||
if envelope:
|
||||
import json
|
||||
|
||||
data = json.dumps(envelope, indent=2, default=str)
|
||||
st.download_button(
|
||||
"Download export.json",
|
||||
data=data,
|
||||
file_name=f"{public_id}_export.json",
|
||||
mime="application/json",
|
||||
)
|
||||
Reference in New Issue
Block a user