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()]