hackathon_code4change / src /dashboard /pages /5_Scheduled_Cases_Explorer.py
RoyAalekh's picture
enhancements, added view for scehduled cases as tickets
9eaac57
"""Ticket Explorer (Post-Run)
Browse simulation runs as a CMS of tickets (cases). After a run finishes,
we build compact Parquet artifacts from events.csv and render case timelines.
"""
from __future__ import annotations
from pathlib import Path
import streamlit as st
import polars as pl
import pandas as pd
import plotly.express as px
from src.dashboard.utils.ticket_views import build_ticket_views, load_ticket_views
from src.config.paths import get_runs_base, list_run_dirs
st.set_page_config(page_title="Ticket Explorer", page_icon="tickets", layout="wide")
st.title("Scheduled Cases Explorer (Post-Run)")
st.caption(
"Inspect each case as a ticket with a full audit trail after the simulation run."
)
def _list_runs(base: Path) -> list[Path]:
if not base.exists():
return []
# run dirs are expected to be leaf directories under base
return sorted([p for p in base.iterdir() if p.is_dir()], reverse=True)
runs_base = get_runs_base()
run_dirs = list_run_dirs(runs_base)
if not run_dirs:
st.warning(f"No simulation runs found in {runs_base}. Run a simulation first.")
st.stop()
labels = [d.name for d in run_dirs]
idx = st.selectbox(
"Select a run", options=list(range(len(labels))), format_func=lambda i: labels[i]
)
run_dir = run_dirs[idx]
st.markdown(f"Run directory: `{run_dir}`")
col_a, col_b, col_c = st.columns([1, 1, 2])
with col_a:
if st.button(
"Rebuild ticket views", help="Recompute Parquet artifacts from events.csv"
):
build_ticket_views(run_dir)
st.success("Ticket views rebuilt")
st.rerun()
with col_b:
events_path = run_dir / "events.csv"
st.download_button(
"Download events.csv",
data=events_path.read_bytes() if events_path.exists() else b"",
file_name="events.csv",
mime="text/csv",
disabled=not events_path.exists(),
)
# Load views (build if missing)
journal_df, summary_df, spans_df = load_ticket_views(run_dir)
# Normalize to pandas for Streamlit controls
def _to_pandas(df):
if pl is not None and isinstance(df, pl.DataFrame):
return df.to_pandas()
return df
journal_pd: pd.DataFrame = _to_pandas(journal_df)
summary_pd: pd.DataFrame = _to_pandas(summary_df)
spans_pd: pd.DataFrame = _to_pandas(spans_df)
with st.sidebar:
st.header("Filters")
case_q = st.text_input("Search case_id contains")
types = sorted([x for x in summary_pd["case_type"].dropna().unique().tolist()])
sel_types = st.multiselect("Case types", options=types, default=[])
statuses = ["ACTIVE", "DISPOSED"]
sel_status = st.multiselect("Final status", options=statuses, default=[])
hearings_min, hearings_max = st.slider(
"Total hearings",
min_value=int(summary_pd.get("total_hearings", pd.Series([0])).min() or 0),
max_value=int(summary_pd.get("total_hearings", pd.Series([0])).max() or 0),
value=(0, int(summary_pd.get("total_hearings", pd.Series([0])).max() or 0)),
)
# Apply filters
filtered = summary_pd.copy()
if case_q:
filtered = filtered[
filtered["case_id"].astype(str).str.contains(case_q, case=False, na=False)
]
if sel_types:
filtered = filtered[filtered["case_type"].isin(sel_types)]
if sel_status:
filtered = filtered[filtered["final_status"].isin(sel_status)]
filtered = filtered[
(filtered.get("total_hearings", 0) >= hearings_min)
& (filtered.get("total_hearings", 0) <= hearings_max)
]
st.markdown("### Filtered Cases")
# Pagination
page_size = st.selectbox("Rows per page", [25, 50, 100], index=0)
total_rows = len(filtered)
page = st.number_input(
"Page",
min_value=1,
max_value=max(1, (total_rows - 1) // page_size + 1),
value=1,
step=1,
)
start, end = (page - 1) * page_size, min(page * page_size, total_rows)
st.caption(f"Showing {start + 1}{end} of {total_rows}")
cols_to_show = [
"case_id",
"case_type",
"final_status",
"current_stage",
"total_hearings",
"heard_count",
"adjourned_count",
"last_seen_date",
]
cols_to_show = [c for c in cols_to_show if c in filtered.columns]
st.dataframe(
filtered.iloc[start:end][cols_to_show], use_container_width=True, hide_index=True
)
st.markdown("### Scheduled Case event details")
sel_case = st.selectbox(
"Choose a case_id",
options=filtered["case_id"].tolist() if not filtered.empty else [],
)
if sel_case:
row = summary_pd[summary_pd["case_id"] == sel_case].iloc[0]
kpi1, kpi2, kpi3, kpi4 = st.columns(4)
with kpi1:
st.metric("Total hearings", int(row.get("total_hearings", 0)))
with kpi2:
st.metric("Heard", int(row.get("heard_count", 0)))
with kpi3:
st.metric("Adjourned", int(row.get("adjourned_count", 0)))
with kpi4:
st.metric("Status", str(row.get("final_status", "")))
# Journal slice
j = journal_pd[journal_pd["case_id"] == sel_case].copy()
j.sort_values(["date", "seq_no"], inplace=True)
# Export button
csv_bytes = j.to_csv(index=False).encode("utf-8")
st.download_button(
"Download this ticket's journal (CSV)",
data=csv_bytes,
file_name=f"{sel_case}_journal.csv",
mime="text/csv",
)
# Timeline table
st.subheader("Event journal")
show_cols = [
c
for c in [
"date",
"type",
"detail",
"stage",
"courtroom_id",
"priority_score",
"readiness_score",
"ripeness_status",
"days_since_hearing",
]
if c in j.columns
]
st.dataframe(j[show_cols].tail(100), use_container_width=True, hide_index=True)
# Stage spans chart (if available)
s = spans_pd[spans_pd["case_id"] == sel_case].copy()
if not s.empty:
s["start_date"] = pd.to_datetime(s["start_date"])
s["end_date"] = pd.to_datetime(s["end_date"])
fig = px.timeline(
s,
x_start="start_date",
x_end="end_date",
y="stage",
color="stage",
title="Stage spans",
)
fig.update_yaxes(autorange="reversed")
st.plotly_chart(fig, use_container_width=True)
else:
st.info("No stage change spans available for this ticket.")