295 lines
11 KiB
Python
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()]
|