feat: add data freshness indicator with relative time and patient count (Task 5.3)
This commit is contained in:
@@ -242,9 +242,9 @@ Drawer selection → update_drug_selection → app-state store → load_pathway_
|
|||||||
- **Checkpoint**: Loading spinner appears during data fetch, empty state shows message
|
- **Checkpoint**: Loading spinner appears during data fetch, empty state shows message
|
||||||
|
|
||||||
### 5.3 Data freshness indicator
|
### 5.3 Data freshness indicator
|
||||||
- [ ] Header shows: green dot + "{N} records" + "Last updated: {relative_time}"
|
- [x] Header shows: green dot + "{N} patients" + "Last updated: {relative_time}"
|
||||||
- [ ] Pull from `pathway_refresh_log` via `queries.load_initial_data()`
|
- [x] Pull from `pathway_refresh_log` via `queries.load_initial_data()` (uses total_patients from root node as fallback when source_row_count is 0)
|
||||||
- [ ] Format as relative time (e.g., "2h ago", "yesterday")
|
- [x] Format as relative time (e.g., "2h ago", "yesterday")
|
||||||
- **Checkpoint**: Header shows correct data freshness
|
- **Checkpoint**: Header shows correct data freshness
|
||||||
|
|
||||||
### 5.4 Remove Reflex + final validation
|
### 5.4 Remove Reflex + final validation
|
||||||
|
|||||||
@@ -1,7 +1,39 @@
|
|||||||
"""Callbacks for reference data loading and filter state management."""
|
"""Callbacks for reference data loading and filter state management."""
|
||||||
|
from datetime import datetime
|
||||||
from dash import Input, Output, State, callback, ctx, no_update
|
from dash import Input, Output, State, callback, ctx, no_update
|
||||||
|
|
||||||
|
|
||||||
|
def _format_relative_time(iso_timestamp: str) -> str:
|
||||||
|
"""Format an ISO timestamp as relative time (e.g., '2h ago', 'yesterday')."""
|
||||||
|
if not iso_timestamp:
|
||||||
|
return "Unknown"
|
||||||
|
try:
|
||||||
|
dt = datetime.fromisoformat(iso_timestamp)
|
||||||
|
now = datetime.now()
|
||||||
|
delta = now - dt
|
||||||
|
seconds = int(delta.total_seconds())
|
||||||
|
|
||||||
|
if seconds < 60:
|
||||||
|
return "just now"
|
||||||
|
minutes = seconds // 60
|
||||||
|
if minutes < 60:
|
||||||
|
return f"{minutes}m ago"
|
||||||
|
hours = minutes // 60
|
||||||
|
if hours < 24:
|
||||||
|
return f"{hours}h ago"
|
||||||
|
days = hours // 24
|
||||||
|
if days == 1:
|
||||||
|
return "yesterday"
|
||||||
|
if days < 7:
|
||||||
|
return f"{days}d ago"
|
||||||
|
if days < 30:
|
||||||
|
weeks = days // 7
|
||||||
|
return f"{weeks}w ago"
|
||||||
|
return dt.strftime("%d %b %Y")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return iso_timestamp[:10] if len(iso_timestamp) >= 10 else "Unknown"
|
||||||
|
|
||||||
|
|
||||||
def register_filter_callbacks(app):
|
def register_filter_callbacks(app):
|
||||||
"""Register reference data loading and filter state callbacks."""
|
"""Register reference data loading and filter state callbacks."""
|
||||||
|
|
||||||
@@ -16,10 +48,18 @@ def register_filter_callbacks(app):
|
|||||||
from dash_app.data.queries import load_initial_data
|
from dash_app.data.queries import load_initial_data
|
||||||
|
|
||||||
ref = load_initial_data()
|
ref = load_initial_data()
|
||||||
|
|
||||||
total = ref.get("total_records", 0)
|
total = ref.get("total_records", 0)
|
||||||
record_text = f"{total:,} records" if total else "Data loaded"
|
patients = ref.get("total_patients", 0)
|
||||||
|
if total:
|
||||||
|
record_text = f"{total:,} records"
|
||||||
|
elif patients:
|
||||||
|
record_text = f"{patients:,} patients"
|
||||||
|
else:
|
||||||
|
record_text = "Data loaded"
|
||||||
|
|
||||||
last_updated = ref.get("last_updated", "")
|
last_updated = ref.get("last_updated", "")
|
||||||
updated_text = last_updated[:10] if last_updated else "Unknown"
|
updated_text = _format_relative_time(last_updated)
|
||||||
|
|
||||||
return ref, record_text, updated_text
|
return ref, record_text, updated_text
|
||||||
|
|
||||||
|
|||||||
@@ -92,12 +92,24 @@ def load_initial_data(db_path: Path) -> dict:
|
|||||||
""")
|
""")
|
||||||
available_trusts = [row[0] for row in cursor.fetchall()]
|
available_trusts = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Total patients from default root node (fallback when source_row_count is empty)
|
||||||
|
total_patients = 0
|
||||||
|
if not total_records:
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT value FROM pathway_nodes
|
||||||
|
WHERE level = 0 AND date_filter_id = 'all_6mo' AND chart_type = 'directory'
|
||||||
|
LIMIT 1
|
||||||
|
""")
|
||||||
|
root_row = cursor.fetchone()
|
||||||
|
total_patients = (root_row[0] or 0) if root_row else 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available_drugs": available_drugs,
|
"available_drugs": available_drugs,
|
||||||
"available_directorates": available_directorates,
|
"available_directorates": available_directorates,
|
||||||
"available_indications": available_indications,
|
"available_indications": available_indications,
|
||||||
"available_trusts": available_trusts,
|
"available_trusts": available_trusts,
|
||||||
"total_records": total_records,
|
"total_records": total_records,
|
||||||
|
"total_patients": total_patients,
|
||||||
"last_updated": last_updated,
|
"last_updated": last_updated,
|
||||||
}
|
}
|
||||||
except sqlite3.Error as e:
|
except sqlite3.Error as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user