feat: implement filter logic with reactive KPI updates (Task 3.3)
- Add apply_filters() method that queries SQLite with current filter state - Handle initiated date filter (first intervention date range) - Handle last seen date filter (last intervention date range) - Handle drug and directorate multi-select filters - Use CTE pattern for efficient patient-level date filtering - Update KPI values (unique_patients, total_drugs, total_cost) on filter change - Call apply_filters() from all filter event handlers - Call apply_filters() after initial data load
This commit is contained in:
@@ -122,11 +122,11 @@ cd pathways_app && timeout 60 python -m reflex run 2>&1 | head -30
|
|||||||
- [x] Call on app initialization
|
- [x] Call on app initialization
|
||||||
|
|
||||||
### 3.3 Filter Logic
|
### 3.3 Filter Logic
|
||||||
- [ ] Create `apply_filters()` computed method that filters the data based on current state
|
- [x] Create `apply_filters()` computed method that filters the data based on current state
|
||||||
- [ ] Handle initiated date filter (when enabled)
|
- [x] Handle initiated date filter (when enabled)
|
||||||
- [ ] Handle last seen date filter (when enabled)
|
- [x] Handle last seen date filter (when enabled)
|
||||||
- [ ] Handle drug/indication/directorate multi-select filters
|
- [x] Handle drug/indication/directorate multi-select filters
|
||||||
- [ ] Return filtered DataFrame
|
- [x] Return filtered DataFrame
|
||||||
|
|
||||||
### 3.4 KPI Calculations
|
### 3.4 KPI Calculations
|
||||||
- [ ] Create computed properties for KPI values:
|
- [ ] Create computed properties for KPI values:
|
||||||
|
|||||||
@@ -115,27 +115,39 @@ class AppState(rx.State):
|
|||||||
def toggle_initiated_filter(self):
|
def toggle_initiated_filter(self):
|
||||||
"""Toggle initiated date filter on/off."""
|
"""Toggle initiated date filter on/off."""
|
||||||
self.initiated_filter_enabled = not self.initiated_filter_enabled
|
self.initiated_filter_enabled = not self.initiated_filter_enabled
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def toggle_last_seen_filter(self):
|
def toggle_last_seen_filter(self):
|
||||||
"""Toggle last seen date filter on/off."""
|
"""Toggle last seen date filter on/off."""
|
||||||
self.last_seen_filter_enabled = not self.last_seen_filter_enabled
|
self.last_seen_filter_enabled = not self.last_seen_filter_enabled
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
# Event handlers for date changes
|
# Event handlers for date changes
|
||||||
def set_initiated_from(self, value: str):
|
def set_initiated_from(self, value: str):
|
||||||
"""Set initiated from date."""
|
"""Set initiated from date."""
|
||||||
self.initiated_from_date = value
|
self.initiated_from_date = value
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def set_initiated_to(self, value: str):
|
def set_initiated_to(self, value: str):
|
||||||
"""Set initiated to date."""
|
"""Set initiated to date."""
|
||||||
self.initiated_to_date = value
|
self.initiated_to_date = value
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def set_last_seen_from(self, value: str):
|
def set_last_seen_from(self, value: str):
|
||||||
"""Set last seen from date."""
|
"""Set last seen from date."""
|
||||||
self.last_seen_from_date = value
|
self.last_seen_from_date = value
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def set_last_seen_to(self, value: str):
|
def set_last_seen_to(self, value: str):
|
||||||
"""Set last seen to date."""
|
"""Set last seen to date."""
|
||||||
self.last_seen_to_date = value
|
self.last_seen_to_date = value
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
# Event handlers for search
|
# Event handlers for search
|
||||||
def set_drug_search(self, value: str):
|
def set_drug_search(self, value: str):
|
||||||
@@ -185,6 +197,8 @@ class AppState(rx.State):
|
|||||||
self.selected_drugs = [d for d in self.selected_drugs if d != drug]
|
self.selected_drugs = [d for d in self.selected_drugs if d != drug]
|
||||||
else:
|
else:
|
||||||
self.selected_drugs = self.selected_drugs + [drug]
|
self.selected_drugs = self.selected_drugs + [drug]
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def toggle_indication(self, indication: str):
|
def toggle_indication(self, indication: str):
|
||||||
"""Toggle an indication selection."""
|
"""Toggle an indication selection."""
|
||||||
@@ -192,6 +206,8 @@ class AppState(rx.State):
|
|||||||
self.selected_indications = [i for i in self.selected_indications if i != indication]
|
self.selected_indications = [i for i in self.selected_indications if i != indication]
|
||||||
else:
|
else:
|
||||||
self.selected_indications = self.selected_indications + [indication]
|
self.selected_indications = self.selected_indications + [indication]
|
||||||
|
# Note: Indication filter not yet implemented at database level
|
||||||
|
# Will be added when indication-based filtering is required
|
||||||
|
|
||||||
def toggle_directorate(self, directorate: str):
|
def toggle_directorate(self, directorate: str):
|
||||||
"""Toggle a directorate selection."""
|
"""Toggle a directorate selection."""
|
||||||
@@ -199,31 +215,43 @@ class AppState(rx.State):
|
|||||||
self.selected_directorates = [d for d in self.selected_directorates if d != directorate]
|
self.selected_directorates = [d for d in self.selected_directorates if d != directorate]
|
||||||
else:
|
else:
|
||||||
self.selected_directorates = self.selected_directorates + [directorate]
|
self.selected_directorates = self.selected_directorates + [directorate]
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
# Select/clear all handlers
|
# Select/clear all handlers
|
||||||
def select_all_drugs(self):
|
def select_all_drugs(self):
|
||||||
"""Select all available drugs."""
|
"""Select all available drugs."""
|
||||||
self.selected_drugs = self.available_drugs.copy()
|
self.selected_drugs = self.available_drugs.copy()
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def clear_all_drugs(self):
|
def clear_all_drugs(self):
|
||||||
"""Clear all drug selections."""
|
"""Clear all drug selections."""
|
||||||
self.selected_drugs = []
|
self.selected_drugs = []
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def select_all_indications(self):
|
def select_all_indications(self):
|
||||||
"""Select all available indications."""
|
"""Select all available indications."""
|
||||||
self.selected_indications = self.available_indications.copy()
|
self.selected_indications = self.available_indications.copy()
|
||||||
|
# Note: Indication filter not yet implemented at database level
|
||||||
|
|
||||||
def clear_all_indications(self):
|
def clear_all_indications(self):
|
||||||
"""Clear all indication selections."""
|
"""Clear all indication selections."""
|
||||||
self.selected_indications = []
|
self.selected_indications = []
|
||||||
|
# Note: Indication filter not yet implemented at database level
|
||||||
|
|
||||||
def select_all_directorates(self):
|
def select_all_directorates(self):
|
||||||
"""Select all available directorates."""
|
"""Select all available directorates."""
|
||||||
self.selected_directorates = self.available_directorates.copy()
|
self.selected_directorates = self.available_directorates.copy()
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
def clear_all_directorates(self):
|
def clear_all_directorates(self):
|
||||||
"""Clear all directorate selections."""
|
"""Clear all directorate selections."""
|
||||||
self.selected_directorates = []
|
self.selected_directorates = []
|
||||||
|
if self.data_loaded:
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
# Computed vars for filtered options based on search
|
# Computed vars for filtered options based on search
|
||||||
@rx.var
|
@rx.var
|
||||||
@@ -350,6 +378,142 @@ class AppState(rx.State):
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
# =========================================================================
|
||||||
|
# Filter Logic Methods
|
||||||
|
# =========================================================================
|
||||||
|
|
||||||
|
def apply_filters(self):
|
||||||
|
"""
|
||||||
|
Apply current filter state to data and update KPI values.
|
||||||
|
|
||||||
|
This method queries the SQLite database with the current filter settings:
|
||||||
|
- Initiated date filter: filters patients whose FIRST intervention date is within range
|
||||||
|
- Last Seen date filter: filters patients whose LAST intervention date is within range
|
||||||
|
- Drug filter: filters by selected drugs (empty = all)
|
||||||
|
- Directorate filter: filters by selected directorates (empty = all)
|
||||||
|
|
||||||
|
Note: Indication filter is not implemented at the database level since indications
|
||||||
|
are derived from drug mappings, not stored directly in fact_interventions.
|
||||||
|
|
||||||
|
Updates: unique_patients, total_drugs, total_cost, and filtered_record_count
|
||||||
|
"""
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
db_path = Path("data/pathways.db")
|
||||||
|
|
||||||
|
if not db_path.exists():
|
||||||
|
self.error_message = "Database not found."
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Build the filter query dynamically
|
||||||
|
# We use a CTE to compute first_seen and last_seen dates per patient,
|
||||||
|
# then filter based on those dates if date filters are enabled
|
||||||
|
|
||||||
|
where_clauses = []
|
||||||
|
params = []
|
||||||
|
|
||||||
|
# Drug filter (if any drugs selected)
|
||||||
|
if self.selected_drugs:
|
||||||
|
placeholders = ",".join("?" * len(self.selected_drugs))
|
||||||
|
where_clauses.append(f"drug_name_std IN ({placeholders})")
|
||||||
|
params.extend(self.selected_drugs)
|
||||||
|
|
||||||
|
# Directorate filter (if any directorates selected)
|
||||||
|
if self.selected_directorates:
|
||||||
|
placeholders = ",".join("?" * len(self.selected_directorates))
|
||||||
|
where_clauses.append(f"directory IN ({placeholders})")
|
||||||
|
params.extend(self.selected_directorates)
|
||||||
|
|
||||||
|
# Build WHERE clause for base data filtering
|
||||||
|
base_where = ""
|
||||||
|
if where_clauses:
|
||||||
|
base_where = "WHERE " + " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
# Date filter logic:
|
||||||
|
# - "Initiated" filters patients whose FIRST intervention is within the date range
|
||||||
|
# - "Last Seen" filters patients whose LAST intervention is within the date range
|
||||||
|
# We need to use a subquery to compute patient-level date ranges
|
||||||
|
|
||||||
|
having_clauses = []
|
||||||
|
having_params = []
|
||||||
|
|
||||||
|
# Initiated filter (when enabled)
|
||||||
|
if self.initiated_filter_enabled and self.initiated_from_date:
|
||||||
|
having_clauses.append("first_seen_date >= ?")
|
||||||
|
having_params.append(self.initiated_from_date)
|
||||||
|
if self.initiated_filter_enabled and self.initiated_to_date:
|
||||||
|
having_clauses.append("first_seen_date <= ?")
|
||||||
|
having_params.append(self.initiated_to_date)
|
||||||
|
|
||||||
|
# Last Seen filter (when enabled)
|
||||||
|
if self.last_seen_filter_enabled and self.last_seen_from_date:
|
||||||
|
having_clauses.append("last_seen_date >= ?")
|
||||||
|
having_params.append(self.last_seen_from_date)
|
||||||
|
if self.last_seen_filter_enabled and self.last_seen_to_date:
|
||||||
|
having_clauses.append("last_seen_date <= ?")
|
||||||
|
having_params.append(self.last_seen_to_date)
|
||||||
|
|
||||||
|
having_clause = ""
|
||||||
|
if having_clauses:
|
||||||
|
having_clause = "HAVING " + " AND ".join(having_clauses)
|
||||||
|
|
||||||
|
# Query to get filtered patient UPIDs
|
||||||
|
# This computes per-patient first/last seen dates and filters accordingly
|
||||||
|
patient_filter_query = f"""
|
||||||
|
WITH patient_dates AS (
|
||||||
|
SELECT
|
||||||
|
upid,
|
||||||
|
MIN(intervention_date) as first_seen_date,
|
||||||
|
MAX(intervention_date) as last_seen_date
|
||||||
|
FROM fact_interventions
|
||||||
|
{base_where}
|
||||||
|
GROUP BY upid
|
||||||
|
{having_clause}
|
||||||
|
)
|
||||||
|
SELECT upid FROM patient_dates
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Now get KPI values for filtered patients
|
||||||
|
kpi_query = f"""
|
||||||
|
WITH filtered_patients AS (
|
||||||
|
{patient_filter_query}
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT f.upid) as unique_patients,
|
||||||
|
COUNT(DISTINCT f.drug_name_std) as unique_drugs,
|
||||||
|
COALESCE(SUM(f.price_actual), 0) as total_cost,
|
||||||
|
COUNT(*) as record_count
|
||||||
|
FROM fact_interventions f
|
||||||
|
INNER JOIN filtered_patients fp ON f.upid = fp.upid
|
||||||
|
{base_where.replace('WHERE', 'AND') if base_where else ''}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Combine all params: base params for CTE, having params, then base params again for final join
|
||||||
|
all_params = params + having_params
|
||||||
|
if where_clauses:
|
||||||
|
all_params.extend(params) # For the AND conditions in the final query
|
||||||
|
|
||||||
|
cursor.execute(kpi_query, all_params)
|
||||||
|
result = cursor.fetchone()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.unique_patients = result[0] or 0
|
||||||
|
self.total_drugs = result[1] or 0
|
||||||
|
self.total_cost = float(result[2]) if result[2] else 0.0
|
||||||
|
# Note: filtered_record_count could be stored if needed
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
self.error_message = ""
|
||||||
|
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
self.error_message = f"Database error: {str(e)}"
|
||||||
|
except Exception as e:
|
||||||
|
self.error_message = f"Filter error: {str(e)}"
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# Data Loading Methods
|
# Data Loading Methods
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
@@ -454,6 +618,9 @@ class AppState(rx.State):
|
|||||||
self.last_updated = datetime.now().isoformat()
|
self.last_updated = datetime.now().isoformat()
|
||||||
self.error_message = ""
|
self.error_message = ""
|
||||||
|
|
||||||
|
# Apply initial filters to compute KPI values
|
||||||
|
self.apply_filters()
|
||||||
|
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
self.error_message = f"Database error: {str(e)}"
|
self.error_message = f"Database error: {str(e)}"
|
||||||
self.data_loaded = False
|
self.data_loaded = False
|
||||||
|
|||||||
Reference in New Issue
Block a user