import json from collections import defaultdict from decimal import Decimal from django.db.models import Count, Q, Sum, Case, When, Value, DecimalField from apps.standards.models import StandardSource, StandardCategory, Standard, StandardCompliance class StandardsReportService: def __init__(self, hospital_id, source_code=None): self.hospital_id = hospital_id self.source_code = source_code def get_sources(self): return list(StandardSource.objects.filter(is_active=True).values("id", "name", "name_ar", "code")) def get_categories(self, source_code=None): sc = source_code or self.source_code qs = StandardCategory.objects.filter(source__isnull=False) if sc: qs = qs.filter(source__code=sc) return list(qs.values("id", "name", "name_ar", "source__code", "max_score", "order")) def get_summary(self): compliance_qs = StandardCompliance.objects.filter( hospital_id=self.hospital_id, standard__is_heading=False, standard__is_assessable=True, ) if self.source_code: compliance_qs = compliance_qs.filter(standard__source__code=self.source_code) total = compliance_qs.count() if total == 0: return {"total": 0, "met": 0, "partially_met": 0, "not_met": 0, "not_assessed": 0, "met_pct": 0, "partially_met_pct": 0, "not_met_pct": 0, "not_assessed_pct": 0} counts = compliance_qs.values("status").annotate(cnt=Count("id")) status_map = {c["status"]: c["cnt"] for c in counts} met = status_map.get("met", 0) partially_met = status_map.get("partially_met", 0) not_met = status_map.get("not_met", 0) not_assessed = status_map.get("not_assessed", 0) not_applicable = status_map.get("not_applicable", 0) return { "total": total, "met": met, "partially_met": partially_met, "not_met": not_met, "not_assessed": not_assessed, "not_applicable": not_applicable, "met_pct": round(met / total * 100, 1) if total else 0, "partially_met_pct": round(partially_met / total * 100, 1) if total else 0, "not_met_pct": round(not_met / total * 100, 1) if total else 0, "not_assessed_pct": round(not_assessed / total * 100, 1) if total else 0, } def get_category_breakdown(self): compliance_qs = StandardCompliance.objects.filter( hospital_id=self.hospital_id, standard__is_heading=False, standard__is_assessable=True, ) if self.source_code: compliance_qs = compliance_qs.filter(standard__source__code=self.source_code) categories = StandardCategory.objects.filter(source__isnull=False) if self.source_code: categories = categories.filter(source__code=self.source_code) result = [] for cat in categories.order_by("order", "name"): cat_compliance = compliance_qs.filter(standard__category=cat) total = cat_compliance.count() if total == 0: continue met = cat_compliance.filter(status="met").count() partially = cat_compliance.filter(status="partially_met").count() not_met = cat_compliance.filter(status="not_met").count() score_data = cat_compliance.filter(score__isnull=False).aggregate( total_score=Sum("score"), total_max=Sum("max_score") ) score = float(score_data["total_score"] or 0) max_score = float(score_data["total_max"] or float(cat.max_score) if cat.max_score else 0) result.append({ "id": str(cat.id), "name": cat.name, "name_ar": cat.name_ar, "total": total, "met": met, "partially_met": partially, "not_met": not_met, "met_pct": round(met / total * 100, 1) if total else 0, "score": score, "max_score": max_score, "score_pct": round(score / max_score * 100, 1) if max_score else 0, }) return result def get_status_distribution_chart(self): summary = self.get_summary() return { "labels": ["Met", "Partially Met", "Not Met", "Not Assessed"], "labels_ar": ["محقق", "محقق جزئياً", "غير محقق", "لم يتم التقييم"], "series": [summary["met"], summary["partially_met"], summary["not_met"], summary["not_assessed"]], "colors": ["#10b981", "#f59e0b", "#ef4444", "#94a3b8"], } def get_category_compliance_chart(self): breakdown = self.get_category_breakdown() if not breakdown: return None return { "categories": [b["name_ar"] or b["name"] for b in breakdown], "met_pct": [b["met_pct"] for b in breakdown], "series": [ {"name": "Met", "data": [b["met"] for b in breakdown]}, {"name": "Partially Met", "data": [b["partially_met"] for b in breakdown]}, {"name": "Not Met", "data": [b["not_met"] for b in breakdown]}, ], } def get_score_gauge_data(self): compliance_qs = StandardCompliance.objects.filter( hospital_id=self.hospital_id, standard__is_heading=False, standard__is_assessable=True, score__isnull=False, ) if self.source_code: compliance_qs = compliance_qs.filter(standard__source__code=self.source_code) if not compliance_qs.exists(): return None agg = compliance_qs.aggregate( total_score=Sum("score"), total_max=Sum("max_score"), ) total_score = float(agg["total_score"] or 0) total_max = float(agg["total_max"] or 0) pct = round(total_score / total_max * 100, 1) if total_max else 0 return { "score": total_score, "max_score": total_max, "pct": pct, } def get_corrective_actions(self): qs = StandardCompliance.objects.filter( hospital_id=self.hospital_id, corrective_action__gt="", ).select_related("standard", "standard__category", "standard__source") if self.source_code: qs = qs.filter(standard__source__code=self.source_code) return [ { "code": c.standard.code, "title": c.standard.title_ar or c.standard.title, "category": c.standard.category.name_ar or c.standard.category.name, "status": c.get_status_display(), "status_ar": c.status_ar, "corrective_action": c.corrective_action, "priority": c.get_priority_display() if c.priority else "", "priority_raw": c.priority, "target_date": c.target_date, } for c in qs.order_by("-priority", "standard__code") ] def get_standards_table(self, source_code=None, category_id=None): sc = source_code or self.source_code compliance_qs = StandardCompliance.objects.filter( hospital_id=self.hospital_id, ).select_related("standard", "standard__category", "standard__source") standards_qs = Standard.objects.filter( is_heading=False, is_assessable=True, is_active=True, ).select_related("source", "category", "parent_standard") if sc: standards_qs = standards_qs.filter(source__code=sc) compliance_qs = compliance_qs.filter(standard__source__code=sc) if category_id: standards_qs = standards_qs.filter(category_id=category_id) compliance_qs = compliance_qs.filter(standard__category_id=category_id) compliance_map = {c.standard_id: c for c in compliance_qs} result = [] for s in standards_qs.order_by("source", "category", "order_within_category", "code"): comp = compliance_map.get(s.id) if not comp: continue result.append({ "id": str(comp.id), "code": s.code, "title": s.title_ar or s.title, "category": s.category.name_ar or s.category.name, "source": s.source.name, "status": comp.get_status_display(), "status_ar": comp.status_ar, "status_raw": comp.status, "assessment_method": comp.standard.assessment_method_ar or comp.standard.get_assessment_method_display(), "recommendations": comp.recommendations, "score": float(comp.score) if comp.score else None, "max_score": float(comp.max_score) if comp.max_score else None, }) return result def get_all_chart_data(self): return json.dumps({ "status_distribution": self.get_status_distribution_chart(), "category_compliance": self.get_category_compliance_chart(), "score_gauge": self.get_score_gauge_data(), })