HH/apps/presentations/services.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

1260 lines
59 KiB
Python

import calendar
import json
import logging
from collections import defaultdict
from django.db.models import Count, Q, Avg, Sum
from django.utils import timezone
from apps.complaints.models import Complaint, ComplaintInvolvedStaff
from apps.core.ai_service import AIService
from apps.dashboard.services.complaint_quarterly_service import (
ComplaintQuarterlyService,
TIMELINE_BUCKETS,
)
from apps.organizations.models import Hospital
from .models import (
Presentation,
Slide,
PresentationTheme,
SlideLayout,
PresentationStatus,
)
logger = logging.getLogger(__name__)
QUARTER_MONTHS = {
1: [1, 2, 3],
2: [4, 5, 6],
3: [7, 8, 9],
4: [10, 11, 12],
}
class PresentationGeneratorService:
def __init__(self, hospital_id, year, quarter=None, created_by=None):
self.hospital_id = hospital_id
self.year = year
self.quarter = quarter
self.created_by = created_by
self.quarterly_service = ComplaintQuarterlyService(
hospital_id=hospital_id, year=year
)
self._data = {}
self._ai_insights = {}
def generate(self):
self._data = self._fetch_data()
self._ai_insights = self._generate_ai_insights()
period_label = self._period_label()
hospital = Hospital.objects.get(pk=self.hospital_id)
presentation = Presentation.objects.create(
title=f"Patient Experience Report — {period_label}",
subtitle=f"{hospital.name}",
description=f"Auto-generated complaints presentation for {period_label}",
theme=PresentationTheme.HEALTHCARE_MODERN,
status=PresentationStatus.PUBLISHED,
presentation_type="quarterly" if self.quarter else "yearly",
created_by=self.created_by,
hospital=hospital,
presentation_date=timezone.now().date(),
is_shared=True,
)
order = 0
order = self._create_cover_slide(presentation, order, hospital)
order = self._create_section_divider(
presentation, order, "01", "OVERVIEW", "Key Performance Indicators"
)
order = self._create_kpi_dashboard_slide(presentation, order)
order = self._create_monthly_chart_slide(presentation, order)
order = self._create_section_divider(
presentation, order, "02", "BREAKDOWN", "Complaint Breakdown & Sources"
)
order = self._create_source_chart_slide(presentation, order)
order = self._create_dept_chart_slide(presentation, order)
order = self._create_section_divider(
presentation, order, "03", "ESCALATION", "Escalated Complaints"
)
order = self._create_escalation_kpi_slide(presentation, order)
order = self._create_escalation_table_slide(presentation, order)
order = self._create_section_divider(
presentation, order, "04", "RESPONSE", "Response Rate Performance"
)
order = self._create_response_chart_slide(presentation, order)
order = self._create_response_days_slide(presentation, order)
order = self._create_section_divider(
presentation, order, "05", "INSIGHTS", "Classification & Insights"
)
order = self._create_classification_table_slide(presentation, order)
order = self._create_key_finding_slide(presentation, order)
order = self._create_analysis_slide(presentation, order)
order = self._create_employees_slide(presentation, order)
order = self._create_closing_slide(presentation, order, hospital)
return presentation
def _period_label(self):
if self.quarter:
return f"Q{self.quarter} {self.year}"
return str(self.year)
def _active_months(self):
if self.quarter:
return QUARTER_MONTHS[self.quarter]
return list(range(1, 13))
def _month_labels(self):
months = self._active_months()
return [calendar.month_abbr[m] for m in months]
def _fetch_data(self):
kpi = self.quarterly_service.get_kpi_data()
source = self.quarterly_service.get_source_breakdown()
location = self.quarterly_service.get_location_breakdown()
dept = self.quarterly_service.get_dept_type_breakdown()
escalated = self.quarterly_service.get_escalated_breakdown()
response = self.quarterly_service.get_response_rates()
chart = self.quarterly_service.get_chart_data()
months = self._active_months()
filtered_kpi_months = {m: kpi["months"][m] for m in months}
filtered_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 = filtered_kpi_totals
filtered_kpi_totals["resolution_rate"] = (
round(t["closed"] / t["total"], 4) if t["total"] > 0 else 0
)
filtered_kpi_totals["resolved_72h_rate"] = (
round(t["resolved_72h"] / t["total"], 4) if t["total"] > 0 else 0
)
filtered_source = {m: source[m] for m in months}
source_totals = {
"internal": sum(s["internal"] for s in filtered_source.values()),
"external": sum(s["external"] for s in filtered_source.values()),
"moh": sum(s["moh"] for s in filtered_source.values()),
"chi": sum(s["chi"] for s in filtered_source.values()),
"insurance": sum(s["insurance"] for s in filtered_source.values()),
"total": sum(s["total"] for s in filtered_source.values()),
}
filtered_location = {m: location[m] for m in months}
location_totals = {
"ip": sum(l["ip"] for l in filtered_location.values()),
"op": sum(l["op"] for l in filtered_location.values()),
"er": sum(l["er"] for l in filtered_location.values()),
}
filtered_dept = {m: dept[m] for m in months}
dept_totals = {
"medical": sum(d["medical"] for d in filtered_dept.values()),
"admin": sum(d["admin"] for d in filtered_dept.values()),
"nursing": sum(d["nursing"] for d in filtered_dept.values()),
"support": sum(d["support"] for d in filtered_dept.values()),
}
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_months": filtered_kpi_months,
"kpi_totals": filtered_kpi_totals,
"source": filtered_source,
"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 _generate_ai_insights(self):
d = self._data
kpi = d["kpi_totals"]
st = d["source_totals"]
esc = d["escalated"]
dt = d["dept_totals"]
data_summary = (
f"Period: {self._period_label()}\n"
f"Hospital ID: {self.hospital_id}\n"
f"Total Complaints: {kpi['total']}\n"
f"Resolution Rate: {kpi['resolution_rate']*100:.1f}% (Target: 95%)\n"
f"72h Resolution Rate: {kpi['resolved_72h_rate']*100:.1f}% (Target: 95%)\n"
f"Patient Satisfaction Rate: {d['satisfaction_rate']*100:.1f}% (Target: 80%)\n"
f"Source Breakdown: Internal {st['internal']}, MOH {st['moh']}, CCHI {st['chi']}, Insurance {st['insurance']}\n"
f"Department Types: Medical {dt['medical']}, Admin {dt['admin']}, Nursing {dt['nursing']}, Support {dt['support']}\n"
f"Escalated: {esc['total_escalated']} out of {esc['total_complaints']} ({esc['escalation_rate']*100:.1f}%)\n"
f"Monthly Trend: {d['monthly_totals']}\n"
)
prompt = (
"You are a healthcare quality analyst for a Saudi Arabian hospital. "
"Based on the following complaint data, generate a JSON response with exactly these keys:\n"
'- "key_finding": A 2-3 sentence quote summarizing the most critical finding.\n'
'- "analysis": A 4-5 sentence analytical narrative about trends and patterns.\n'
'- "recommendations": An array of 4-5 actionable recommendation strings.\n'
'- "next_steps": An array of 3 concrete next-step strings with timelines.\n'
'- "kpi_summary": A 1-2 sentence summary of the KPI dashboard performance.\n'
'- "monthly_insight": A 1-2 sentence insight about the monthly complaint trend.\n'
'- "source_insight": A 1-2 sentence insight about the complaint source distribution.\n'
'- "dept_insight": A 1-2 sentence insight about the department breakdown.\n'
'- "escalation_insight": A 1-2 sentence insight about escalated complaints.\n'
'- "response_insight": A 1-2 sentence insight about response time performance.\n'
'- "resolution_insight": A 1-2 sentence insight about average resolution time by department.\n'
'- "classification_insight": A 1-2 sentence insight about the complaint classification breakdown.\n\n'
f"Data:\n{data_summary}"
)
try:
response = AIService.chat_completion(
prompt=prompt,
system_prompt="You are a healthcare quality analyst. Respond only in valid JSON.",
response_format="json_object",
temperature=0.4,
max_tokens=1200,
)
insights = json.loads(response)
except Exception as e:
logger.warning(f"AI insights generation failed: {e}")
insights = {
"key_finding": f"A total of {kpi['total']} complaints were recorded in {self._period_label()}. The resolution rate stands at {kpi['resolution_rate']*100:.1f}% against a 95% target. Key areas requiring attention include response time optimization and escalation management.",
"analysis": f"The complaint data for {self._period_label()} shows {kpi['total']} total complaints with a resolution rate of {kpi['resolution_rate']*100:.1f}%. Medical complaints ({dt['medical']}) form the largest category, followed by administrative ({dt['admin']}). The escalation rate of {esc['escalation_rate']*100:.1f}% indicates significant management attention is required. Response time analysis reveals opportunities for improvement across all departments.",
"recommendations": [
"Implement a real-time complaint tracking dashboard for department heads",
"Set clear response time standards with escalation protocols for medical complaints",
"Provide customer service and communication skills training for all staff",
"Ensure internal complaints receive the same urgency as external ones",
"Schedule monthly review meetings with department heads to discuss complaint trends",
],
"next_steps": [
"Schedule a meeting with department heads within 2 weeks",
"Develop an action plan with specific timelines within 1 month",
"Plan for a follow-up report to assess improvement impact within 3 months",
],
"kpi_summary": f"The KPI dashboard for {self._period_label()} shows {kpi['total']} total complaints with a {kpi['resolution_rate']*100:.1f}% resolution rate and {kpi['resolved_72h_rate']*100:.1f}% resolved within 72 hours.",
"monthly_insight": f"Monthly complaint volumes averaged {round(kpi['total']/12)} per month across the reporting period, with seasonal variations observed.",
"source_insight": f"Internal sources accounted for {st['internal']} complaints while external government sources (MOH: {st['moh']}, CCHI: {st['chi']}) contributed significantly to the overall volume.",
"dept_insight": f"Medical departments received {dt['medical']} complaints, making it the highest category, followed by admin ({dt['admin']}) and nursing ({dt['nursing']}).",
"escalation_insight": f"A total of {esc['total_escalated']} complaints were escalated, representing {esc['escalation_rate']*100:.1f}% of all complaints, requiring senior management intervention.",
"response_insight": "Response time distribution shows the majority of complaints are addressed within the 72-hour target, though some categories require attention.",
"resolution_insight": "Average resolution times vary significantly across department types, with medical complaints typically taking longer to resolve.",
"classification_insight": f"Complaint classifications reveal key areas of concern across {dt['medical']+dt['admin']+dt['nursing']+dt['support']} total complaints, with patterns indicating systemic issues in specific categories.",
}
return insights
# ── Slide Builders ──────────────────────────────────────────
def _make_slide(self, presentation, order, layout, title, subtitle="", content=None, notes=""):
return Slide.objects.create(
presentation=presentation,
layout=layout,
order=order,
title=title,
subtitle=subtitle,
content=content or {},
speaker_notes=notes,
)
def _create_cover_slide(self, presentation, order, hospital):
self._make_slide(
presentation, order, SlideLayout.COVER,
title=f"Patient Experience Report",
subtitle=f"{self._period_label()}{hospital.name}",
content={
"prepared_by": str(self.created_by.get_full_name()) if self.created_by else "",
"hospital_name": hospital.name,
},
)
return order + 1
def _create_section_divider(self, presentation, order, number, label, title):
self._make_slide(
presentation, order, SlideLayout.SECTION_DIVIDER,
title=title,
content={"section_number": number, "section_label": label},
)
return order + 1
def _create_kpi_dashboard_slide(self, presentation, order):
kpi = self._data["kpi_totals"]
metrics = [
{
"label": "Total Complaints",
"value": str(kpi["total"]),
"icon": "bar-chart-3",
"description": self._period_label(),
},
{
"label": "Resolution Rate",
"value": f"{kpi['resolution_rate']*100:.1f}%",
"icon": "check-circle",
"change": "Target: 95%",
"change_direction": "neutral",
"variant": "success" if kpi["resolution_rate"] >= 0.95 else ("warning" if kpi["resolution_rate"] >= 0.9 else "danger"),
"description": f"{kpi['closed']} of {kpi['total']} complaints resolved",
},
{
"label": "72h Resolution Rate",
"value": f"{kpi['resolved_72h_rate']*100:.1f}%",
"icon": "clock",
"change": "Target: 95%",
"change_direction": "neutral",
"variant": "success" if kpi["resolved_72h_rate"] >= 0.95 else ("warning" if kpi["resolved_72h_rate"] >= 0.9 else "danger"),
"description": f"{kpi['resolved_72h']} resolved within 72 hours",
},
{
"label": "Patient Satisfaction",
"value": f"{self._data['satisfaction_rate']*100:.1f}%",
"icon": "heart",
"change": "Target: 80%",
"change_direction": "neutral",
"variant": "success" if self._data["satisfaction_rate"] >= 0.8 else ("warning" if self._data["satisfaction_rate"] >= 0.7 else "danger"),
"description": f"Based on {self._data['satisfaction_total']} responses",
},
]
kpi_summary = self._ai_insights.get("kpi_summary", "")
self._make_slide(
presentation, order, SlideLayout.KPI_DASHBOARD,
title=f"Q{self.quarter} {self.year} — At a Glance" if self.quarter else f"{self.year} — At a Glance",
subtitle=kpi_summary or "Key Performance Indicators",
content={"metrics": metrics},
notes=kpi_summary,
)
return order + 1
def _create_monthly_chart_slide(self, presentation, order):
labels = self._month_labels()
totals = self._data["monthly_totals"]
chart_config = {
"chart": {"type": "bar", "height": "100%", "toolbar": {"show": False}},
"series": [{"name": "Complaints", "data": totals}],
"xaxis": {"categories": labels, "labels": {"style": {"fontSize": "14px", "fontWeight": 600}}},
"colors": ["#005696"],
"plotOptions": {"bar": {"borderRadius": 6, "columnWidth": "50%"}},
"dataLabels": {"enabled": True, "style": {"fontSize": "14px", "fontWeight": 600}},
"grid": {"borderColor": "#e2e8f0", "strokeDashArray": 4},
"yaxis": {"labels": {"style": {"fontSize": "12px"}}},
}
total = sum(totals)
peak_idx = totals.index(max(totals)) if totals else 0
sidebar = [
{"label": "Total Period", "value": str(total)},
{"label": "Monthly Average", "value": str(round(total / len(totals))) if totals else "0"},
{"label": f"Peak Month", "value": labels[peak_idx] if labels else "", "description": f"{max(totals)} complaints"},
]
self._make_slide(
presentation, order, SlideLayout.CHART_METRICS,
title="Monthly Complaints Trend",
subtitle=f"Complaint volume by month — {self._period_label()}",
content={
"chart_config": chart_config,
"sidebar_metrics": sidebar,
"key_insight": self._ai_insights.get("monthly_insight", ""),
},
)
return order + 1
def _create_source_chart_slide(self, presentation, order):
st = self._data["source_totals"]
total = st["total"] or 1
source_items = [
("Internal", st["internal"]),
("MOH", st["moh"]),
("CCHI", st["chi"]),
("Insurance", st["insurance"]),
("Patients", st.get("patients", 0)),
]
active_items = [(name, val) for name, val in source_items if val > 0]
if not active_items:
active_items = [("No Data", 1)]
chart_config = {
"chart": {"type": "donut", "height": "100%"},
"series": [val for _, val in active_items],
"labels": [name for name, _ in active_items],
"colors": ["#005696", "#0EA5E9", "#14B8A6", "#F59E0B", "#FB7185"][:len(active_items)],
"legend": {"position": "bottom", "fontSize": "14px"},
"plotOptions": {
"pie": {"donut": {"size": "65%", "labels": {"show": True, "total": {"show": True, "label": "Total", "fontSize": "18px"}}}}
},
"dataLabels": {"enabled": True, "style": {"fontSize": "13px"}},
}
sidebar = [
{"label": name, "value": str(val), "description": f"{val/total*100:.1f}%"}
for name, val in active_items[:3]
]
self._make_slide(
presentation, order, SlideLayout.CHART_METRICS,
title="Complaint Sources — Internal vs External",
subtitle=self._ai_insights.get("source_insight", "Breakdown by complaint origin"),
content={
"chart_config": chart_config,
"sidebar_metrics": sidebar,
"key_insight": self._ai_insights.get("source_insight", ""),
},
)
return order + 1
def _create_dept_chart_slide(self, presentation, order):
dt = self._data["dept_totals"]
loc = self._data["location_totals"]
chart_config = {
"chart": {"type": "bar", "height": "100%", "toolbar": {"show": False}},
"series": [{"name": "Complaints", "data": [dt["medical"], dt["admin"], dt["nursing"], dt["support"]]}],
"xaxis": {"categories": ["Medical", "Admin", "Nursing", "Support Services"]},
"colors": ["#005696"],
"plotOptions": {"bar": {"borderRadius": 6, "distributed": True}},
"dataLabels": {"enabled": True, "style": {"fontSize": "14px", "fontWeight": 600}},
"colors": ["#005696", "#0EA5E9", "#14B8A6", "#F59E0B"],
"grid": {"borderColor": "#e2e8f0", "strokeDashArray": 4},
}
self._make_slide(
presentation, order, SlideLayout.FULL_CHART,
title="Breakdown by Department & Location",
subtitle=self._ai_insights.get("dept_insight", f"Medical {dt['medical']} · Admin {dt['admin']} · Nursing {dt['nursing']} · Support {dt['support']} | IP {loc['ip']} · OP {loc['op']} · ER {loc['er']}"),
content={"chart_config": chart_config, "footer_note": self._ai_insights.get("dept_insight", "")},
)
return order + 1
def _create_escalation_kpi_slide(self, presentation, order):
esc = self._data["escalated"]
by_cat = esc.get("by_category", {})
med_total = sum(by_cat.get("medical", {}).values()) if isinstance(by_cat.get("medical"), dict) else by_cat.get("medical", 0)
nonmed_total = sum(by_cat.get("non_medical", {}).values()) if isinstance(by_cat.get("non_medical"), dict) else by_cat.get("non_medical", 0)
nurs_total = sum(by_cat.get("nursing", {}).values()) if isinstance(by_cat.get("nursing"), dict) else by_cat.get("nursing", 0)
metrics = [
{"label": "Medical", "value": str(med_total), "icon": "stethoscope", "variant": "danger"},
{"label": "Non-Medical", "value": str(nonmed_total), "icon": "building", "variant": "warning"},
{"label": "Nursing", "value": str(nurs_total), "icon": "heart-pulse", "variant": "warning"},
{"label": "Escalation Rate", "value": f"{esc['escalation_rate']*100:.1f}%", "icon": "trending-up",
"description": f"{esc['total_escalated']} of {esc['total_complaints']}", "variant": "danger"},
]
self._make_slide(
presentation, order, SlideLayout.KPI_DASHBOARD,
title="Escalated Complaints Overview",
subtitle=self._ai_insights.get("escalation_insight", f"Total escalated: {esc['total_escalated']} complaints"),
content={"metrics": metrics},
notes=self._ai_insights.get("escalation_insight", ""),
)
return order + 1
def _create_escalation_table_slide(self, presentation, order):
esc = self._data["escalated"]
by_cat = esc.get("by_category", {})
headers = ["Department", "Count"]
rows = []
for cat_key, cat_label in [("medical", "Medical"), ("non_medical", "Non-Medical"), ("nursing", "Nursing")]:
cat_data = by_cat.get(cat_key, {})
if isinstance(cat_data, dict):
for dept_name, count in sorted(cat_data.items(), key=lambda x: -x[1]):
rows.append([f"{cat_label}{dept_name}", str(count)])
self._make_slide(
presentation, order, SlideLayout.DATA_TABLE,
title="Escalated Complaints — Detailed Breakdown",
subtitle="By department and domain type",
content={"headers": headers, "rows": rows},
)
return order + 1
def _create_response_chart_slide(self, presentation, order):
resp = self._data["response"]
chart_config = {
"chart": {"type": "bar", "height": "100%", "stacked": False, "toolbar": {"show": False}},
"series": [
{"name": "Internal", "data": [resp["internal"]["counts"][b] for b in TIMELINE_BUCKETS]},
{"name": "MOH", "data": [resp["moh"]["counts"][b] for b in TIMELINE_BUCKETS]},
{"name": "CCHI", "data": [resp["chi"]["counts"][b] for b in TIMELINE_BUCKETS]},
],
"xaxis": {"categories": TIMELINE_BUCKETS},
"colors": ["#005696", "#0EA5E9", "#14B8A6"],
"plotOptions": {"bar": {"borderRadius": 4, "columnWidth": "60%"}},
"dataLabels": {"enabled": True, "style": {"fontSize": "13px"}},
"grid": {"borderColor": "#e2e8f0", "strokeDashArray": 4},
"legend": {"position": "top", "fontSize": "14px"},
}
sidebar = [
{
"label": "Internal Total",
"value": str(resp["internal"]["total"]),
"description": f"{resp['internal']['rates'].get('72h+', 0)*100:.0f}% over 72h",
},
{
"label": "MOH Total",
"value": str(resp["moh"]["total"]),
"description": f"{resp['moh']['rates'].get('72h+', 0)*100:.0f}% over 72h",
},
{
"label": "CCHI Total",
"value": str(resp["chi"]["total"]),
"description": f"{resp['chi']['rates'].get('72h+', 0)*100:.0f}% over 72h",
},
]
self._make_slide(
presentation, order, SlideLayout.CHART_METRICS,
title="Response Time Distribution",
subtitle="Response rate by source — 24h / 48h / 72h / 72h+",
content={
"chart_config": chart_config,
"sidebar_metrics": sidebar,
"key_insight": self._ai_insights.get("response_insight", ""),
},
)
return order + 1
def _create_response_days_slide(self, presentation, order):
months = self._active_months()
qs = Complaint.objects.filter(
hospital_id=self.hospital_id,
created_at__year=self.year,
created_at__month__in=months,
status__in=["closed", "resolved"],
closed_at__isnull=False,
)
dept_days = defaultdict(list)
for c in qs.select_related("domain").iterator(chunk_size=2000):
if c.created_at and c.closed_at:
days = (c.closed_at - c.created_at).total_seconds() / 86400
dt = ""
if c.domain:
dt = c.domain.domain_type or ""
if dt in ("admin", "non_medical"):
dept_days["Non-Medical"].append(days)
elif dt == "nursing":
dept_days["Nursing"].append(days)
elif dt in ("support", "support_services"):
dept_days["Support Services"].append(days)
else:
dept_days["Medical"].append(days)
categories = []
values = []
for name in ["Medical", "Non-Medical", "Nursing", "Support Services"]:
days_list = dept_days.get(name, [0])
avg = round(sum(days_list) / len(days_list), 1) if days_list else 0
categories.append(name)
values.append(avg)
chart_config = {
"chart": {"type": "bar", "height": "100%", "toolbar": {"show": False}},
"series": [{"name": "Avg Days", "data": values}],
"xaxis": {"categories": categories},
"colors": ["#005696", "#0EA5E9", "#14B8A6", "#F59E0B"],
"plotOptions": {"bar": {"borderRadius": 6, "distributed": True}},
"dataLabels": {"enabled": True, "formatter": "function(v){return v+'d'}", "style": {"fontSize": "14px", "fontWeight": 600}},
"grid": {"borderColor": "#e2e8f0", "strokeDashArray": 4},
"yaxis": {"title": {"text": "Days"}, "labels": {"style": {"fontSize": "12px"}}},
}
self._make_slide(
presentation, order, SlideLayout.FULL_CHART,
title="Average Response Rate by Department",
subtitle=self._ai_insights.get("resolution_insight", "Mean response time in days"),
content={"chart_config": chart_config, "footer_note": self._ai_insights.get("resolution_insight", "")},
)
return order + 1
def _create_classification_table_slide(self, presentation, order):
months = self._active_months()
qs = Complaint.objects.filter(
hospital_id=self.hospital_id,
created_at__year=self.year,
created_at__month__in=months,
).select_related("domain", "category", "subcategory_obj", "classification_obj")
classification_counts = defaultdict(lambda: defaultdict(int))
domain_map = {}
for c in qs.iterator(chunk_size=2000):
domain_name = str(c.domain) if c.domain else "Uncategorized"
cat_name = str(c.category) if c.category else "Other"
domain_type = ""
if c.domain:
domain_type = c.domain.domain_type or ""
if domain_type in ("admin", "non_medical"):
group = "Non-Medical"
elif domain_type == "nursing":
group = "Nursing"
elif domain_type in ("support", "support_services"):
group = "Support Services"
else:
group = "Medical"
classification_counts[group][cat_name] += 1
headers = ["Group", "Category", "Count"]
rows = []
for group in ["Medical", "Non-Medical", "Nursing", "Support Services"]:
cats = classification_counts.get(group, {})
for cat_name, count in sorted(cats.items(), key=lambda x: -x[1]):
rows.append([group, cat_name, str(count)])
self._make_slide(
presentation, order, SlideLayout.DATA_TABLE,
title="Complaint Classification",
subtitle=self._ai_insights.get("classification_insight", f"Categories breakdown — {self._period_label()}"),
content={"headers": headers, "rows": rows},
notes=self._ai_insights.get("classification_insight", ""),
)
return order + 1
def _create_key_finding_slide(self, presentation, order):
finding = self._ai_insights.get("key_finding", "No key finding generated.")
self._make_slide(
presentation, order, SlideLayout.QUOTE,
title="Key Finding",
content={"quote": finding, "attribution": f"AI-Generated Insight — {self._period_label()}"},
)
return order + 1
def _create_analysis_slide(self, presentation, order):
analysis = self._ai_insights.get("analysis", "")
recs = self._ai_insights.get("recommendations", [])
right_bullets = [{"text": r} for r in recs]
self._make_slide(
presentation, order, SlideLayout.TWO_COLUMN,
title="Analysis & Recommendations",
content={
"left_title": "Analysis",
"left_body": [analysis],
"right_title": "Recommendations",
"right_bullets": right_bullets,
},
)
return order + 1
def _create_employees_slide(self, presentation, order):
months = self._active_months()
staff_qs = ComplaintInvolvedStaff.objects.filter(
complaint__hospital_id=self.hospital_id,
complaint__created_at__year=self.year,
complaint__created_at__month__in=months,
role="accused",
).select_related("staff", "staff__department", "complaint")
dept_staff = defaultdict(lambda: defaultdict(int))
for inv in staff_qs.iterator(chunk_size=5000):
staff_name = str(inv.staff) if inv.staff else "Unknown"
dept_name = str(inv.staff.department) if inv.staff and inv.staff.department else "Other"
dept_staff[dept_name][staff_name] += 1
members = []
for dept_name, staff_dict in sorted(dept_staff.items()):
for staff_name, count in sorted(staff_dict.items(), key=lambda x: -x[1]):
members.append({
"name": staff_name,
"role": dept_name,
"metric_value": str(count),
"metric_label": "Complaints",
"initials": staff_name[0].upper() if staff_name else "?",
})
if not members:
members.append({"name": "No employee-specific data", "role": "N/A", "initials": "-"})
self._make_slide(
presentation, order, SlideLayout.TEAM_GRID,
title="Employee-Specific Patient Complaints",
subtitle=f"Staff with complaints — {self._period_label()}",
content={"members": members},
)
return order + 1
def _create_closing_slide(self, presentation, order, hospital):
next_steps = self._ai_insights.get("next_steps", [])
contact = []
if hospital.email:
contact.append(hospital.email)
if hospital.phone:
contact.append(f"Phone: {hospital.phone}")
self._make_slide(
presentation, order, SlideLayout.CLOSING,
title="Thank You",
subtitle=" | ".join(next_steps) if next_steps else "Thank you for your attention.",
content={"contact": contact},
)
return order + 1
RATING_ROW_CLASSES = {
"high": "high",
"medium": "medium",
"low": "low",
"top": "top",
}
class DoctorRatingsReportService:
DOCTORS_PER_SLIDE = 18
def __init__(self, hospital_id, year, quarter=None, created_by=None):
self.hospital_id = hospital_id
self.year = year
self.quarter = quarter
self.created_by = created_by
self._data = {}
self._ai_insights = {}
self._dept_managers = {}
def generate(self):
self._data = self._fetch_data()
self._ai_insights = self._generate_ai_insights()
period_label = self._period_label()
hospital = Hospital.objects.get(pk=self.hospital_id)
presentation = Presentation.objects.create(
title=f"Doctors' Ratings Report — {period_label}",
subtitle=f"{hospital.name}",
description=f"Auto-generated doctor ratings presentation for {period_label}",
theme=PresentationTheme.HEALTHCARE_MODERN,
status=PresentationStatus.PUBLISHED,
presentation_type="doctor_ratings",
created_by=self.created_by,
hospital=hospital,
presentation_date=timezone.now().date(),
is_shared=True,
)
order = 0
order = self._create_cover_slide(presentation, order, hospital)
order = self._create_section_divider(presentation, order, "01", "OVERVIEW", "Rating Performance Summary")
order = self._create_kpi_dashboard_slide(presentation, order)
order = self._create_distribution_chart_slide(presentation, order)
order = self._create_monthly_trend_slide(presentation, order)
order = self._create_section_divider(presentation, order, "02", "METHODOLOGY", "Survey & Rating Categories")
order = self._create_overview_slide(presentation, order)
order = self._create_section_divider(presentation, order, "03", "DEPARTMENTS", "Department Ratings Breakdown")
order = self._create_department_slides(presentation, order)
order = self._create_section_divider(presentation, order, "04", "INSIGHTS", "Top Performers & AI Insights")
order = self._create_top_performers_slide(presentation, order)
order = self._create_key_finding_slide(presentation, order)
order = self._create_analysis_slide(presentation, order)
order = self._create_closing_slide(presentation, order, hospital)
return presentation
def _period_label(self):
if self.quarter:
return f"Q{self.quarter}.{self.year}"
return str(self.year)
def _active_months(self):
if self.quarter:
return QUARTER_MONTHS[self.quarter]
return list(range(1, 13))
def _month_labels(self):
months = self._active_months()
return [calendar.month_abbr[m] for m in months]
def _rating_row_class(self, avg, is_top=False):
if is_top:
return "top"
if avg >= 4.5:
return "high"
elif avg >= 3.0:
return "medium"
return "low"
def _get_dept_manager(self, dept_name):
if dept_name in self._dept_managers:
return self._dept_managers[dept_name]
from apps.organizations.models import Department
dept = Department.objects.filter(name__iexact=dept_name).first()
if dept and dept.manager:
name = dept.manager.get_full_name() or str(dept.manager)
self._dept_managers[dept_name] = name
return name
self._dept_managers[dept_name] = ""
return ""
def _fetch_data(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,
"month": r.month,
})
consolidated = {}
for sid, entries in doctor_ratings.items():
name = entries[0]["name"]
eid = entries[0]["employee_id"]
dept = entries[0]["department"]
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": name,
"employee_id": eid,
"department": dept,
"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_ratings = ratings_qs.filter(month=m)
m_avg = m_ratings.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 _generate_ai_insights(self):
o = self._data["overall"]
dept_count = len(self._data["by_department"])
top_dept = max(self._data["by_department"].items(), key=lambda x: max(d["avg_rating"] for d in x[1])) if self._data["by_department"] else (None, [])
top_dept_avg = max((d["avg_rating"] for d in top_dept[1]), default=0)
data_summary = (
f"Period: {self._period_label()}\n"
f"Hospital ID: {self.hospital_id}\n"
f"Total Physicians Rated: {o['total_doctors']}\n"
f"Total Surveys: {o['total_surveys']}\n"
f"Average Rating: {o['avg_rating']}/5.0\n"
f"Positive Ratings (>=4): {o['positive_pct']}%\n"
f"Negative Ratings (<3): {o['negative_pct']}%\n"
f"Rating Distribution: 1*={o['r1']} 2*={o['r2']} 3*={o['r3']} 4*={o['r4']} 5*={o['r5']}\n"
f"Departments: {dept_count}\n"
f"Highest Dept: {top_dept[0]} (avg {top_dept_avg:.2f})\n"
)
prompt = (
"You are a healthcare quality analyst for a Saudi Arabian hospital. "
"Based on the following doctor ratings data, generate a JSON response with exactly these keys:\n"
'- "key_finding": A 2-3 sentence quote summarizing the most critical finding.\n'
'- "analysis": A 4-5 sentence analytical narrative about trends and patterns.\n'
'- "recommendations": An array of 4-5 actionable recommendation strings.\n'
'- "next_steps": An array of 3 concrete next-step strings with timelines.\n'
'- "kpi_summary": A 1-2 sentence summary of the overall rating performance.\n'
'- "distribution_insight": A 1-2 sentence insight about the rating distribution.\n'
'- "trend_insight": A 1-2 sentence insight about monthly rating trends.\n'
'- "department_insight": A 1-2 sentence insight about department-level performance.\n\n'
f"Data:\n{data_summary}"
)
try:
response = AIService.chat_completion(
prompt=prompt,
system_prompt="You are a healthcare quality analyst. Respond only in valid JSON.",
response_format="json_object",
temperature=0.4,
max_tokens=1200,
)
insights = json.loads(response)
except Exception as e:
logger.warning(f"AI insights generation failed: {e}")
insights = {
"key_finding": f"Across {o['total_doctors']} physicians, the average patient rating is {o['avg_rating']}/5.0 based on {o['total_surveys']} surveys. {o['positive_pct']}% of ratings are positive (4-5 stars) while {o['negative_pct']}% are negative (1-2 stars).",
"analysis": f"The doctor ratings data for {self._period_label()} covers {o['total_doctors']} physicians across {dept_count} departments with an overall average of {o['avg_rating']}/5.0. The distribution shows {o['r5']} five-star and {o['r4']} four-star ratings, indicating generally positive patient experiences. Areas for improvement are identified by departments with lower averages.",
"recommendations": [
"Recognize top-performing physicians with the highest patient ratings quarterly",
"Provide targeted coaching for physicians with ratings below 3.0",
"Implement real-time patient feedback monitoring dashboards for department heads",
"Develop peer mentoring programs pairing high-rated with low-rated physicians",
"Schedule monthly department-level review meetings to discuss rating trends",
],
"next_steps": [
"Schedule recognition ceremony for top performers within 2 weeks",
"Develop individual improvement plans for low-rated physicians within 1 month",
"Plan follow-up analysis for next quarter within 3 months",
],
"kpi_summary": f"The hospital has {o['total_doctors']} rated physicians with an average rating of {o['avg_rating']}/5.0 from {o['total_surveys']} patient surveys.",
"distribution_insight": f"Rating distribution shows {o['r5']} five-star and {o['r4']} four-star ratings as the most common.",
"trend_insight": "Monthly rating trends indicate stable performance across the reporting period.",
"department_insight": f"Across {dept_count} departments, performance varies with opportunities for targeted improvement in lower-rated specialties.",
}
return insights
def _make_slide(self, presentation, order, layout, title, subtitle="", content=None, notes=""):
return Slide.objects.create(
presentation=presentation,
layout=layout,
order=order,
title=title,
subtitle=subtitle,
content=content or {},
speaker_notes=notes,
)
def _create_cover_slide(self, presentation, order, hospital):
self._make_slide(
presentation, order, SlideLayout.COVER,
title="Doctors' Ratings Report",
subtitle=f"{self._period_label()}{hospital.name}",
content={
"prepared_by": str(self.created_by.get_full_name()) if self.created_by else "",
"hospital_name": hospital.name,
"department": "Patient Experience Department",
},
)
return order + 1
def _create_section_divider(self, presentation, order, number, label, title):
self._make_slide(
presentation, order, SlideLayout.SECTION_DIVIDER,
title=title,
content={"section_number": number, "section_label": label},
)
return order + 1
def _create_kpi_dashboard_slide(self, presentation, order):
o = self._data["overall"]
metrics = [
{
"label": "Total Physicians",
"value": str(o["total_doctors"]),
"icon": "users",
"description": f"Across {len(self._data['by_department'])} departments",
},
{
"label": "Average Rating",
"value": f"{o['avg_rating']:.2f}",
"icon": "star",
"description": f"Out of {o['total_surveys']} surveys",
"variant": "success" if o["avg_rating"] >= 4.5 else ("warning" if o["avg_rating"] >= 3.5 else "danger"),
},
{
"label": "Positive (>=4*)",
"value": f"{o['positive_pct']}%",
"icon": "thumbs-up",
"description": f"{o['total_positive']} ratings",
"variant": "success" if o["positive_pct"] >= 80 else ("warning" if o["positive_pct"] >= 60 else "danger"),
},
{
"label": "Negative (<3*)",
"value": f"{o['negative_pct']}%",
"icon": "thumbs-down",
"description": f"{o['total_negative']} ratings",
"variant": "success" if o["negative_pct"] <= 10 else ("warning" if o["negative_pct"] <= 20 else "danger"),
},
]
self._make_slide(
presentation, order, SlideLayout.KPI_DASHBOARD,
title=f"{self._period_label()} — At a Glance",
subtitle=self._ai_insights.get("kpi_summary", "Key Performance Indicators"),
content={"metrics": metrics},
notes=self._ai_insights.get("kpi_summary", ""),
)
return order + 1
def _create_distribution_chart_slide(self, presentation, order):
o = self._data["overall"]
chart_config = {
"chart": {"type": "bar", "height": "100%", "toolbar": {"show": False}},
"series": [{"name": "Ratings", "data": [o["r1"], o["r2"], o["r3"], o["r4"], o["r5"]]}],
"xaxis": {
"categories": ["1 *", "2 *", "3 *", "4 *", "5 *"],
"labels": {"style": {"fontSize": "14px", "fontWeight": 600}},
},
"colors": ["#ED1A24", "#f97316", "#eab308", "#0B769F", "#0B769F"],
"plotOptions": {"bar": {"borderRadius": 6, "distributed": True}},
"dataLabels": {"enabled": True, "style": {"fontSize": "14px", "fontWeight": 600}},
"grid": {"borderColor": "#e2e8f0", "strokeDashArray": 4},
"yaxis": {"labels": {"style": {"fontSize": "12px"}}, "title": {"text": "Count"}},
}
total = o["total_surveys"] or 1
sidebar = [
{"label": "5* Ratings", "value": str(o["r5"]), "description": f"{round(o['r5']/total*100,1)}% of total"},
{"label": "4* Ratings", "value": str(o["r4"]), "description": f"{round(o['r4']/total*100,1)}% of total"},
{"label": "1-2* Ratings", "value": str(o["r1"] + o["r2"]), "description": f"{round((o['r1']+o['r2'])/total*100,1)}% of total"},
]
self._make_slide(
presentation, order, SlideLayout.CHART_METRICS,
title="Rating Distribution",
subtitle=f"How patients rate their doctors — {self._period_label()}",
content={
"chart_config": chart_config,
"sidebar_metrics": sidebar,
"key_insight": self._ai_insights.get("distribution_insight", ""),
},
)
return order + 1
def _create_monthly_trend_slide(self, presentation, order):
months = self._data["months"]
monthly_avg = self._data["monthly_avg"]
labels = [calendar.month_abbr[m] for m in months]
values = [monthly_avg[m] for m in months]
chart_config = {
"chart": {"type": "line", "height": "100%", "toolbar": {"show": False}},
"series": [{"name": "Avg Rating", "data": values}],
"xaxis": {
"categories": labels,
"labels": {"style": {"fontSize": "14px", "fontWeight": 600}},
},
"colors": ["#0B769F"],
"stroke": {"curve": "smooth", "width": 3},
"markers": {"size": 5},
"yaxis": {"min": 1, "max": 5, "tickAmount": 4, "labels": {"style": {"fontSize": "12px"}}},
"grid": {"borderColor": "#e2e8f0", "strokeDashArray": 4},
"annotations": {
"yaxis": [{
"y": 4.5,
"borderColor": "#3A7C22",
"strokeDashArray": 4,
"label": {"text": "Target: 4.5", "style": {"color": "#fff", "background": "#3A7C22"}},
}]
},
}
o = self._data["overall"]
sidebar = [
{"label": "Overall Avg", "value": f"{o['avg_rating']:.2f}"},
{"label": "Total Surveys", "value": str(o["total_surveys"])},
{"label": "Departments", "value": str(len(self._data["by_department"]))},
]
self._make_slide(
presentation, order, SlideLayout.CHART_METRICS,
title="Monthly Rating Trend",
subtitle=f"Average physician rating by month — {self._period_label()}",
content={
"chart_config": chart_config,
"sidebar_metrics": sidebar,
"key_insight": self._ai_insights.get("trend_insight", ""),
},
)
return order + 1
def _create_overview_slide(self, presentation, order):
o = self._data["overall"]
left_body = (
f"Ratings were collected from patients following each appointment, "
f"and data was compiled throughout {self._period_label()}. "
f"The report covers {o['total_doctors']} physicians across "
f"{len(self._data['by_department'])} departments, with a total of "
f"{o['total_surveys']} patient surveys. "
f"The average rating is calculated by multiplying each rating by its "
f"corresponding frequency, summing these products, and dividing by the "
f"total number of ratings."
)
right_bullets = [
{"text": "Blue (4.5 - 5.0): High Rating"},
{"text": "Black (3.0 - 4.4): Medium Rating"},
{"text": "Red (1.0 - 2.9): Low Rating"},
{"text": "Green: Special Highlight (highest score among peers in department)"},
]
self._make_slide(
presentation, order, SlideLayout.TWO_COLUMN,
title="Survey Overview & Rating Categories",
content={
"left_title": "Methodology",
"left_body": [left_body],
"right_title": "Rating Categories",
"right_bullets": right_bullets,
},
notes="Rating categories: Blue >= 4.5, Black 3.0-4.4, Red < 3.0, Green = department highest",
)
return order + 1
def _create_department_slides(self, presentation, order):
DOCTORS_PER_SLIDE = self.DOCTORS_PER_SLIDE
for dept_name, doctors in sorted(self._data["by_department"].items()):
max_avg = max((d["avg_rating"] for d in doctors), default=0)
manager_name = self._get_dept_manager(dept_name)
for i in range(0, len(doctors), DOCTORS_PER_SLIDE):
chunk = doctors[i:i + DOCTORS_PER_SLIDE]
headers = ["Doctor's ID", "Doctor's Name", "Total Ratings", "Average Ratings"]
rows = []
for d in chunk:
is_top = d["avg_rating"] == max_avg
row_class = self._rating_row_class(d["avg_rating"], is_top)
rows.append([
{"text": d["employee_id"], "row_bg": row_class},
{"text": d["name"], "row_bg": row_class, "font_weight": "600"},
{"text": str(d["total_surveys"]), "row_bg": row_class},
{"rating_bar": {"value": f"{d['avg_rating']:.2f}", "pct": round(d["avg_rating"] / 5 * 100)}, "row_bg": row_class},
])
slide_title = dept_name
slide_subtitle = f"{len(doctors)} physicians"
if manager_name:
slide_subtitle = f"Head of Department: {manager_name}"
if i > 0:
slide_subtitle += f" | Page {i // DOCTORS_PER_SLIDE + 1} of {(len(doctors) - 1) // DOCTORS_PER_SLIDE + 1}"
self._make_slide(
presentation, order, SlideLayout.DATA_TABLE,
title=slide_title,
subtitle=slide_subtitle,
content={"headers": headers, "rows": rows},
)
order += 1
return order
def _create_top_performers_slide(self, presentation, order):
all_docs = sorted(self._data["consolidated"].values(), key=lambda d: d["avg_rating"], reverse=True)
top = all_docs[:10]
members = []
for d in top:
members.append({
"name": d["name"],
"role": d["department"],
"metric_value": f"{d['avg_rating']:.2f}",
"metric_label": "Avg Rating",
"initials": d["name"][0].upper() if d["name"] else "?",
})
if not members:
members.append({"name": "No physician data", "role": "N/A", "initials": "-"})
self._make_slide(
presentation, order, SlideLayout.TEAM_GRID,
title="Top Rated Physicians",
subtitle=f"Highest rated doctors — {self._period_label()}",
content={"members": members},
)
return order + 1
def _create_key_finding_slide(self, presentation, order):
finding = self._ai_insights.get("key_finding", "No key finding generated.")
self._make_slide(
presentation, order, SlideLayout.QUOTE,
title="Key Finding",
content={"quote": finding, "attribution": f"AI-Generated Insight — {self._period_label()}"},
)
return order + 1
def _create_analysis_slide(self, presentation, order):
analysis = self._ai_insights.get("analysis", "")
recs = self._ai_insights.get("recommendations", [])
right_bullets = [{"text": r} for r in recs]
self._make_slide(
presentation, order, SlideLayout.TWO_COLUMN,
title="Analysis & Recommendations",
content={
"left_title": "Analysis",
"left_body": [analysis],
"right_title": "Recommendations",
"right_bullets": right_bullets,
},
)
return order + 1
def _create_closing_slide(self, presentation, order, hospital):
next_steps = self._ai_insights.get("next_steps", [])
contact = []
if hospital.email:
contact.append(hospital.email)
if hospital.phone:
contact.append(f"Phone: {hospital.phone}")
self._make_slide(
presentation, order, SlideLayout.CLOSING,
title="Thank You",
subtitle=" | ".join(next_steps) if next_steps else "Thank you for your attention.",
content={"contact": contact},
)
return order + 1