diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index f0edeea..b579783 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -93,24 +93,26 @@ cd pathways_app && timeout 60 python -m reflex run 2>&1 | head -30 ## Phase 3: State Management ### 3.1 Core State Variables -- [ ] Define filter state variables in `AppState`: +- [x] Define filter state variables in `AppState`: - `initiated_filter_enabled: bool = False` - - `initiated_from: datetime` - - `initiated_to: datetime` + - `initiated_from_date: str = ""` (ISO date string) + - `initiated_to_date: str = ""` - `last_seen_filter_enabled: bool = True` - - `last_seen_from: datetime` (default: 6 months ago) - - `last_seen_to: datetime` (default: latest in dataset) - - `selected_drugs: List[str]` (default: all) - - `selected_indications: List[str]` (default: all) - - `selected_directorates: List[str]` (default: all) -- [ ] Define data state variables: - - `data_loaded: bool` - - `total_records: int` - - `last_updated: datetime` - - `filtered_data: pd.DataFrame` (or computed) -- [ ] Define UI state variables: - - `chart_loading: bool` - - `error_message: str` + - `last_seen_from_date: str` (default: 6 months ago, computed at class definition) + - `last_seen_to_date: str` (default: today, updated on data load) + - `selected_drugs: list[str] = []` (empty = all) + - `selected_indications: list[str] = []` (empty = all) + - `selected_directorates: list[str] = []` (empty = all) +- [x] Define data state variables: + - `data_loaded: bool = False` + - `total_records: int = 0` + - `last_updated: str = ""` (ISO timestamp) + - `raw_data: list[dict[str, Any]] = []` (list of dicts, Reflex-friendly) + - `latest_date_in_data: str = ""` (for "to" date defaults) +- [x] Define UI state variables: + - `chart_loading: bool = False` + - `error_message: str = ""` + - `current_chart: str = "icicle"` ### 3.2 Data Loading - [ ] Create `load_data()` method that reads from SQLite diff --git a/pathways_app/app_v2.py b/pathways_app/app_v2.py index e70c494..efd2ddb 100644 --- a/pathways_app/app_v2.py +++ b/pathways_app/app_v2.py @@ -5,6 +5,9 @@ Single-page dashboard with reactive filtering and real-time chart updates. Design reference: DESIGN_SYSTEM.md """ +from datetime import datetime, timedelta +from typing import Any + import reflex as rx from pathways_app.styles import ( @@ -41,24 +44,51 @@ class AppState(rx.State): Will be expanded in Phase 3 with full filter state and data management. """ - # Placeholder state variables (expanded in Phase 3) + # ========================================================================= + # Data State Variables + # ========================================================================= + + # Data loading status data_loaded: bool = False total_records: int = 0 chart_loading: bool = False error_message: str = "" + # Data freshness tracking + last_updated: str = "" # ISO format timestamp of last data load + + # Raw data storage - list of dicts (Reflex-friendly) + # Each dict represents a patient record with keys like: + # UPID, Drug Name, Intervention Date, Price Actual, Directory, etc. + raw_data: list[dict[str, Any]] = [] + + # Latest date in dataset (detected on load, used for "to" date defaults) + latest_date_in_data: str = "" + + # ========================================================================= + # UI State Variables + # ========================================================================= + # Placeholder for current chart type (for top bar tabs) current_chart: str = "icicle" + # ========================================================================= + # Filter State Variables + # ========================================================================= + # Filter toggle state initiated_filter_enabled: bool = False last_seen_filter_enabled: bool = True - # Date filter values (ISO format strings for simplicity) + # Date filter values (ISO format strings YYYY-MM-DD) + # Initiated filter: Defaults empty (filter is OFF by default) initiated_from_date: str = "" initiated_to_date: str = "" - last_seen_from_date: str = "" - last_seen_to_date: str = "" + + # Last Seen filter: Defaults to last 6 months (filter is ON by default) + # These will be updated on data load to use actual latest date + last_seen_from_date: str = (datetime.now() - timedelta(days=180)).strftime("%Y-%m-%d") + last_seen_to_date: str = datetime.now().strftime("%Y-%m-%d") # Available options for dropdowns (populated from data in Phase 3) available_drugs: list[str] = ["Drug A", "Drug B", "Drug C", "Drug D", "Drug E"] @@ -292,6 +322,33 @@ class AppState(rx.State): return "—" return f"{self.indication_match_rate:.0f}%" + @rx.var + def last_updated_display(self) -> str: + """Format last updated timestamp for display in top bar.""" + if not self.last_updated: + return "Never" + try: + # Parse ISO format timestamp + dt = datetime.fromisoformat(self.last_updated) + now = datetime.now() + diff = now - dt + + if diff.days == 0: + if diff.seconds < 60: + return "Just now" + if diff.seconds < 3600: + mins = diff.seconds // 60 + return f"{mins}m ago" + hours = diff.seconds // 3600 + return f"{hours}h ago" + if diff.days == 1: + return "Yesterday" + if diff.days < 7: + return f"{diff.days}d ago" + return dt.strftime("%d %b %Y") + except (ValueError, TypeError): + return "Unknown" + # ============================================================================= # Layout Components @@ -630,7 +687,7 @@ def top_bar() -> rx.Component: rx.text( rx.cond( AppState.data_loaded, - "Last refreshed: recently", + "Refreshed: " + AppState.last_updated_display, "Connecting...", ), font_size="11px",