HH/apps/presentations/data_sources.py
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

295 lines
11 KiB
Python

import logging
from collections import defaultdict
from django.db.models import Count, Avg, Sum
logger = logging.getLogger(__name__)
QUARTER_MONTHS = {
1: [1, 2, 3],
2: [4, 5, 6],
3: [7, 8, 9],
4: [10, 11, 12],
}
class BaseDataSource:
name = ""
label = ""
description = ""
available_keys = []
def __init__(self, hospital_id, year, quarter=None):
self.hospital_id = hospital_id
self.year = year
self.quarter = quarter
def _active_months(self):
if self.quarter:
return QUARTER_MONTHS[self.quarter]
return list(range(1, 13))
def fetch(self):
raise NotImplementedError
def get_data_summary(self, data):
raise NotImplementedError
class ComplaintsDataSource(BaseDataSource):
name = "complaints"
label = "Complaints Quarterly"
description = "Patient complaints data aggregated quarterly from ComplaintQuarterlyService"
available_keys = [
"kpi_totals", "source_totals", "location_totals", "dept_totals",
"escalated", "response", "monthly_totals", "monthly_resolution",
"satisfaction_rate", "satisfaction_total", "months",
]
def fetch(self):
from apps.dashboard.services.complaint_quarterly_service import (
ComplaintQuarterlyService,
TIMELINE_BUCKETS,
)
from apps.complaints.models import Complaint
svc = ComplaintQuarterlyService(hospital_id=self.hospital_id, year=self.year)
months = self._active_months()
kpi = svc.get_kpi_data()
source = svc.get_source_breakdown()
location = svc.get_location_breakdown()
dept = svc.get_dept_type_breakdown()
escalated = svc.get_escalated_breakdown()
response = svc.get_response_rates()
chart = svc.get_chart_data()
filtered_kpi_months = {m: kpi["months"][m] for m in months}
kpi_totals = {
"total": sum(r["total"] for r in filtered_kpi_months.values()),
"closed": sum(r["closed"] for r in filtered_kpi_months.values()),
"resolved_72h": sum(r["resolved_72h"] for r in filtered_kpi_months.values()),
"activated": sum(r["activated"] for r in filtered_kpi_months.values()),
}
t = kpi_totals
kpi_totals["resolution_rate"] = round(t["closed"] / t["total"], 4) if t["total"] > 0 else 0
kpi_totals["resolved_72h_rate"] = round(t["resolved_72h"] / t["total"], 4) if t["total"] > 0 else 0
source_totals = {
"internal": sum(source[m]["internal"] for m in months),
"external": sum(source[m]["external"] for m in months),
"moh": sum(source[m]["moh"] for m in months),
"chi": sum(source[m]["chi"] for m in months),
"insurance": sum(source[m]["insurance"] for m in months),
"total": sum(source[m]["total"] for m in months),
}
location_totals = {
"ip": sum(location[m]["ip"] for m in months),
"op": sum(location[m]["op"] for m in months),
"er": sum(location[m]["er"] for m in months),
}
dept_totals = {
"medical": sum(dept[m]["medical"] for m in months),
"admin": sum(dept[m]["admin"] for m in months),
"nursing": sum(dept[m]["nursing"] for m in months),
"support": sum(dept[m]["support"] for m in months),
}
monthly_totals = [filtered_kpi_months[m]["total"] for m in months]
monthly_resolution = [filtered_kpi_months[m]["resolution_rate"] for m in months]
satisfaction_qs = Complaint.objects.filter(
hospital_id=self.hospital_id,
created_at__year=self.year,
created_at__month__in=months,
satisfaction__isnull=False,
).exclude(satisfaction="")
sat_total = satisfaction_qs.count()
sat_satisfied = satisfaction_qs.filter(satisfaction="satisfied").count()
satisfaction_rate = round(sat_satisfied / sat_total, 4) if sat_total > 0 else 0
return {
"kpi_totals": kpi_totals,
"source_totals": source_totals,
"location_totals": location_totals,
"dept_totals": dept_totals,
"escalated": escalated,
"response": response,
"monthly_totals": monthly_totals,
"monthly_resolution": monthly_resolution,
"satisfaction_rate": satisfaction_rate,
"satisfaction_total": sat_total,
"months": months,
}
def get_data_summary(self, data):
kpi = data["kpi_totals"]
st = data["source_totals"]
dt = data["dept_totals"]
esc = data["escalated"]
return (
f"Report Type: Patient Experience / Complaints\n"
f"Total Complaints: {kpi['total']}\n"
f"Resolution Rate: {kpi['resolution_rate']*100:.1f}%\n"
f"72h Resolution Rate: {kpi['resolved_72h_rate']*100:.1f}%\n"
f"Patient Satisfaction: {data['satisfaction_rate']*100:.1f}%\n"
f"Sources: Internal {st['internal']}, MOH {st['moh']}, CCHI {st['chi']}\n"
f"Depts: Medical {dt['medical']}, Admin {dt['admin']}, Nursing {dt['nursing']}\n"
f"Escalated: {esc['total_escalated']} ({esc['escalation_rate']*100:.1f}%)\n"
)
class PhysicianRatingDataSource(BaseDataSource):
name = "physicians"
label = "Physician Ratings"
description = "Doctor ratings from PhysicianMonthlyRating aggregated data"
available_keys = [
"overall", "by_department", "consolidated", "monthly_avg", "months",
]
def fetch(self):
from apps.physicians.models import PhysicianMonthlyRating
months = self._active_months()
ratings_qs = PhysicianMonthlyRating.objects.filter(
staff__hospital_id=self.hospital_id,
year=self.year,
month__in=months,
).select_related("staff", "staff__department")
overall = ratings_qs.aggregate(
total_doctors=Count("staff", distinct=True),
total_surveys=Sum("total_surveys"),
avg_rating=Avg("average_rating"),
total_positive=Sum("positive_count"),
total_neutral=Sum("neutral_count"),
total_negative=Sum("negative_count"),
total_r1=Sum("rating_1_count"),
total_r2=Sum("rating_2_count"),
total_r3=Sum("rating_3_count"),
total_r4=Sum("rating_4_count"),
total_r5=Sum("rating_5_count"),
)
total_surveys = overall["total_surveys"] or 0
total_positive = overall["total_positive"] or 0
total_negative = overall["total_negative"] or 0
avg_rating = overall["avg_rating"] or 0
doctor_ratings = defaultdict(list)
for r in ratings_qs:
dept_name = str(r.staff.department) if r.staff.department else "Uncategorized"
doctor_ratings[r.staff_id].append({
"name": r.staff.name or f"{r.staff.first_name} {r.staff.last_name}",
"employee_id": r.staff.employee_id or "",
"department": dept_name,
"avg_rating": float(r.average_rating),
"total_surveys": r.total_surveys,
"positive": r.positive_count,
"neutral": r.neutral_count,
"negative": r.negative_count,
"r1": r.rating_1_count,
"r2": r.rating_2_count,
"r3": r.rating_3_count,
"r4": r.rating_4_count,
"r5": r.rating_5_count,
})
consolidated = {}
for sid, entries in doctor_ratings.items():
total_s = sum(e["total_surveys"] for e in entries)
if total_s == 0:
continue
weighted_sum = sum(e["avg_rating"] * e["total_surveys"] for e in entries)
avg = round(weighted_sum / total_s, 2)
consolidated[sid] = {
"name": entries[0]["name"],
"employee_id": entries[0]["employee_id"],
"department": entries[0]["department"],
"avg_rating": avg,
"total_surveys": total_s,
"r1": sum(e["r1"] for e in entries),
"r2": sum(e["r2"] for e in entries),
"r3": sum(e["r3"] for e in entries),
"r4": sum(e["r4"] for e in entries),
"r5": sum(e["r5"] for e in entries),
"positive": sum(e["positive"] for e in entries),
"neutral": sum(e["neutral"] for e in entries),
"negative": sum(e["negative"] for e in entries),
}
by_department = defaultdict(list)
for doc in consolidated.values():
by_department[doc["department"]].append(doc)
for dept_docs in by_department.values():
dept_docs.sort(key=lambda d: d["avg_rating"], reverse=True)
monthly_avg = {}
for m in months:
m_avg = ratings_qs.filter(month=m).aggregate(a=Avg("average_rating"))["a"]
monthly_avg[m] = round(float(m_avg), 2) if m_avg else 0
return {
"overall": {
"total_doctors": overall["total_doctors"] or 0,
"total_surveys": total_surveys,
"avg_rating": round(float(avg_rating), 2),
"total_positive": total_positive,
"total_negative": total_negative,
"total_neutral": overall["total_neutral"] or 0,
"r1": overall["total_r1"] or 0,
"r2": overall["total_r2"] or 0,
"r3": overall["total_r3"] or 0,
"r4": overall["total_r4"] or 0,
"r5": overall["total_r5"] or 0,
"positive_pct": round(total_positive / total_surveys * 100, 1) if total_surveys else 0,
"negative_pct": round(total_negative / total_surveys * 100, 1) if total_surveys else 0,
},
"by_department": dict(by_department),
"consolidated": consolidated,
"monthly_avg": monthly_avg,
"months": months,
}
def get_data_summary(self, data):
o = data["overall"]
return (
f"Report Type: Physician / Doctor Ratings\n"
f"Total Physicians: {o['total_doctors']}\n"
f"Total Surveys: {o['total_surveys']}\n"
f"Average Rating: {o['avg_rating']}/5.0\n"
f"Positive (>=4): {o['positive_pct']}%\n"
f"Negative (<3): {o['negative_pct']}%\n"
f"Departments: {len(data['by_department'])}\n"
)
REPORT_DATA_SOURCES = {
"complaints": {
"label": "Complaints Quarterly",
"description": "Patient complaints data with KPIs, sources, departments, and response rates",
"source_class": ComplaintsDataSource,
"data_keys": ComplaintsDataSource.available_keys,
},
"physicians": {
"label": "Physician Ratings",
"description": "Doctor patient ratings with department breakdowns and rating distributions",
"source_class": PhysicianRatingDataSource,
"data_keys": PhysicianRatingDataSource.available_keys,
},
}
def get_data_source(source_key):
entry = REPORT_DATA_SOURCES.get(source_key)
if not entry:
raise ValueError(f"Unknown data source: {source_key}. Available: {list(REPORT_DATA_SOURCES.keys())}")
return entry["source_class"]
def get_data_source_choices():
return [(key, entry["label"]) for key, entry in REPORT_DATA_SOURCES.items()]