diff --git a/.env.example b/.env.example index e5a714f..f5c4167 100644 --- a/.env.example +++ b/.env.example @@ -68,9 +68,14 @@ SMS_API_RETRY_DELAY=2 # Admin URL (change in production) ADMIN_URL=admin/ -# Integration APIs (Stubs - Replace with actual credentials) -HIS_API_URL= +# Integration APIs +# HIS API - Hospital Information System for fetching patient discharge data +HIS_API_URL=https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps +HIS_API_USERNAME=your_his_username +HIS_API_PASSWORD=your_his_password HIS_API_KEY= + +# Other Integration APIs (Stubs - Replace with actual credentials) MOH_API_URL= MOH_API_KEY= CHI_API_URL= diff --git a/apps/core/context_processors.py b/apps/core/context_processors.py index 37503bc..5148253 100644 --- a/apps/core/context_processors.py +++ b/apps/core/context_processors.py @@ -1,6 +1,7 @@ """ Context processors for global template variables """ + from django.db.models import Q @@ -25,59 +26,36 @@ def sidebar_counts(request): # Source Users only see their own created complaints if user.is_source_user(): - complaint_count = Complaint.objects.filter( - created_by=user, - status__in=['open', 'in_progress'] - ).count() + complaint_count = Complaint.objects.filter(created_by=user, status__in=["open", "in_progress"]).count() return { - 'complaint_count': complaint_count, - 'feedback_count': 0, - 'action_count': 0, - 'current_hospital': None, - 'is_px_admin': False, - 'is_source_user': True, + "complaint_count": complaint_count, + "feedback_count": 0, + "action_count": 0, + "current_hospital": None, + "is_px_admin": False, + "is_source_user": True, } # Filter based on user role and tenant_hospital if user.is_px_admin(): # PX Admins use their selected hospital from session - hospital = getattr(request, 'tenant_hospital', None) + hospital = getattr(request, "tenant_hospital", None) if hospital: - complaint_count = Complaint.objects.filter( - hospital=hospital, - status__in=['open', 'in_progress'] - ).count() - feedback_count = Feedback.objects.filter( - hospital=hospital, - status__in=['submitted', 'reviewed'] - ).count() - action_count = PXAction.objects.filter( - hospital=hospital, - status__in=['open', 'in_progress'] - ).count() + complaint_count = Complaint.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).count() + feedback_count = Feedback.objects.filter(hospital=hospital, status__in=["submitted", "reviewed"]).count() + action_count = PXAction.objects.filter(hospital=hospital, status__in=["open", "in_progress"]).count() else: complaint_count = 0 feedback_count = 0 action_count = 0 # Count provisional users for PX Admin from apps.accounts.models import User - provisional_user_count = User.objects.filter( - is_provisional=True, - acknowledgement_completed=False - ).count() + + provisional_user_count = User.objects.filter(is_provisional=True, acknowledgement_completed=False).count() elif user.hospital: - complaint_count = Complaint.objects.filter( - hospital=user.hospital, - status__in=['open', 'in_progress'] - ).count() - feedback_count = Feedback.objects.filter( - hospital=user.hospital, - status__in=['submitted', 'reviewed'] - ).count() - action_count = PXAction.objects.filter( - hospital=user.hospital, - status__in=['open', 'in_progress'] - ).count() + complaint_count = Complaint.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).count() + feedback_count = Feedback.objects.filter(hospital=user.hospital, status__in=["submitted", "reviewed"]).count() + action_count = PXAction.objects.filter(hospital=user.hospital, status__in=["open", "in_progress"]).count() # provisional_user_count = 0 else: complaint_count = 0 @@ -85,12 +63,12 @@ def sidebar_counts(request): action_count = 0 return { - 'complaint_count': complaint_count, - 'feedback_count': feedback_count, - 'action_count': action_count, - 'current_hospital': getattr(request, 'tenant_hospital', None), - 'is_px_admin': request.user.is_authenticated and request.user.is_px_admin(), - 'is_source_user': False, + "complaint_count": complaint_count, + "feedback_count": feedback_count, + "action_count": action_count, + "current_hospital": getattr(request, "tenant_hospital", None), + "is_px_admin": request.user.is_authenticated and request.user.is_px_admin(), + "is_source_user": False, } @@ -103,25 +81,24 @@ def hospital_context(request): if not request.user.is_authenticated: return {} - hospital = getattr(request, 'tenant_hospital', None) - + hospital = getattr(request, "tenant_hospital", None) + # Get list of hospitals for PX Admin switcher hospitals_list = [] if request.user.is_px_admin(): from apps.organizations.models import Hospital - hospitals_list = list( - Hospital.objects.filter(status='active').order_by('name').values('id', 'name', 'code') - ) + + hospitals_list = list(Hospital.objects.filter(status="active").order_by("name").values("id", "name", "code")) # Source user context is_source_user = request.user.is_source_user() - source_user_profile = getattr(request, 'source_user_profile', None) + source_user_profile = getattr(request, "source_user_profile", None) return { - 'current_hospital': hospital, - 'is_px_admin': request.user.is_px_admin(), - 'is_source_user': is_source_user, - 'source_user_profile': source_user_profile, - 'hospitals_list': hospitals_list, - # 'provisional_user_count': provisional_user_count, + "current_hospital": hospital, + "is_px_admin": request.user.is_px_admin(), + "is_source_user": is_source_user, + "source_user_profile": source_user_profile, + "hospitals_list": hospitals_list, + "show_hospital_selector": False, } diff --git a/apps/core/management/commands/setup_dev_environment.py b/apps/core/management/commands/setup_dev_environment.py new file mode 100644 index 0000000..edc9b8d --- /dev/null +++ b/apps/core/management/commands/setup_dev_environment.py @@ -0,0 +1,2267 @@ +""" +Management command to set up a complete development environment for PX360. + +This command creates: +- 1 Organization (Al Hammadi Healthcare Group - DEV) +- 3 Hospitals (NUZHA-DEV, OLAYA-DEV, SUWAIDI-DEV) +- Location hierarchy (Locations, Main Sections, Subsections) +- Roles and permissions +- Sample users with different roles for each hospital +- Survey templates (one per patient type) +- Complaint system configuration +- Journey templates +- Observation categories +- Notification templates +- Standards setup +- HIS integration configuration + +Usage: + python manage.py setup_dev_environment + python manage.py setup_dev_environment --dry-run + python manage.py setup_dev_environment --skip-surveys + python manage.py setup_dev_environment --hospital-code NUZHA-DEV + python manage.py setup_dev_environment --only-users +""" + +from django.core.management.base import BaseCommand +from django.db import transaction +from django.contrib.auth.models import Group, Permission +from django.utils import timezone + +from apps.organizations.models import Organization, Hospital, Location, MainSection, SubSection +from apps.accounts.models import Role, User +from apps.px_sources.models import PXSource +from apps.surveys.models import SurveyTemplate, SurveyQuestion, QuestionType +from apps.integrations.models import SurveyTemplateMapping +from apps.complaints.models import ( + ComplaintCategory, + ComplaintSLAConfig, + EscalationRule, + ComplaintThreshold, + ExplanationSLAConfig, +) +from apps.journeys.models import ( + PatientJourneyTemplate, + PatientJourneyStageTemplate, + JourneyType, +) +from apps.observations.models import ObservationCategory +from apps.notifications.models import NotificationTemplate +from apps.standards.models import StandardSource, StandardCategory + + +class Command(BaseCommand): + help = "Set up a complete development environment with all necessary configuration data" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="Preview what will be created without making changes", + ) + parser.add_argument( + "--hospital-code", + type=str, + help="Set up only a specific hospital (e.g., NUZHA-DEV)", + ) + parser.add_argument( + "--skip-surveys", + action="store_true", + help="Skip survey template creation", + ) + parser.add_argument( + "--skip-complaints", + action="store_true", + help="Skip complaint configuration", + ) + parser.add_argument( + "--skip-journeys", + action="store_true", + help="Skip journey template creation", + ) + parser.add_argument( + "--skip-integration", + action="store_true", + help="Skip HIS integration setup", + ) + parser.add_argument( + "--skip-users", + action="store_true", + help="Skip sample user creation", + ) + parser.add_argument( + "--only-users", + action="store_true", + help="Create only sample users (skip all other setup)", + ) + + def handle(self, *args, **options): + self.dry_run = options["dry_run"] + self.hospital_code_filter = options.get("hospital_code") + self.skip_surveys = options["skip_surveys"] + self.skip_complaints = options["skip_complaints"] + self.skip_journeys = options["skip_journeys"] + self.skip_integration = options["skip_integration"] + self.skip_users = options["skip_users"] + self.only_users = options["only_users"] + + if self.only_users and self.skip_users: + self.stdout.write(self.style.ERROR("Cannot use --only-users with --skip-users")) + return + + self.stdout.write(self.style.SUCCESS("\n" + "=" * 70 + "\nPX360 Development Environment Setup\n=" * 70 + "\n")) + + if self.dry_run: + self.stdout.write(self.style.WARNING("🔍 DRY RUN MODE - No changes will be made\n")) + + with transaction.atomic(): + if self.only_users: + self.stdout.write("\n👤 Creating Only Sample Users") + self.stdout.write("-" * 70) + if self.dry_run: + hospitals = [ + type("Hospital", (), {"code": "NUZHA-DEV", "name": "Nuzha"})(), + type("Hospital", (), {"code": "OLAYA-DEV", "name": "Olaya"})(), + type("Hospital", (), {"code": "SUWAIDI-DEV", "name": "Suwaidi"})(), + ] + else: + hospitals = Hospital.objects.all() + if not hospitals.exists(): + self.stdout.write(self.style.ERROR("No hospitals found. Run setup without --only-users first.")) + return + self.create_users(hospitals) + else: + # Phase 1: Core Organization Structure + self.stdout.write("\n📍 Phase 1: Organization Structure") + self.stdout.write("-" * 70) + organization = self.create_organization() + hospitals = self.create_hospitals(organization) + self.create_location_data() + + # Phase 2: Roles & Permissions + self.stdout.write("\n👥 Phase 2: Roles & Permissions") + self.stdout.write("-" * 70) + self.create_roles_and_groups() + + # Phase 3: PX Sources + self.stdout.write("\n📮 Phase 3: PX Sources") + self.stdout.write("-" * 70) + px_sources = self.create_px_sources() + + # Phase 4: Survey Templates + if not self.skip_surveys: + self.stdout.write("\n📝 Phase 4: Survey Templates") + self.stdout.write("-" * 70) + survey_templates = self.create_survey_templates(hospitals) + self.create_survey_mappings(survey_templates) + + # Phase 5: Complaint Configuration + if not self.skip_complaints: + self.stdout.write("\n⚠️ Phase 5: Complaint Configuration") + self.stdout.write("-" * 70) + self.create_complaint_categories() + self.create_complaint_sla_configs(hospitals, px_sources) + self.create_escalation_rules(hospitals) + self.create_complaint_thresholds(hospitals) + self.create_explanation_sla_configs(hospitals) + + # Phase 6: Journey Templates + if not self.skip_journeys: + self.stdout.write("\n🚀 Phase 6: Journey Templates") + self.stdout.write("-" * 70) + journey_templates = self.create_journey_templates(hospitals) + if not self.skip_surveys: + self.create_journey_stages(journey_templates, survey_templates) + + # Phase 7: Observation Categories + self.stdout.write("\n👁️ Phase 7: Observation Categories") + self.stdout.write("-" * 70) + self.create_observation_categories() + + # Phase 8: Notification Templates + self.stdout.write("\n💌 Phase 8: Notification Templates") + self.stdout.write("-" * 70) + self.create_notification_templates() + + # Phase 9: Standards Setup + self.stdout.write("\n✓ Phase 9: Standards Setup") + self.stdout.write("-" * 70) + self.create_standard_sources() + self.create_standard_categories() + + # Phase 10: HIS Integration + if not self.skip_integration: + self.stdout.write("\n🔌 Phase 10: HIS Integration") + self.stdout.write("-" * 70) + self.create_integration_config() + + # Phase 11: Sample Users + if not self.skip_users: + self.stdout.write("\n👤 Phase 11: Sample Users") + self.stdout.write("-" * 70) + self.create_users(hospitals) + + # Final Summary + self.print_summary() + + if self.dry_run: + self.stdout.write( + self.style.WARNING( + "\n" + "=" * 70 + "\n" + "🔍 DRY RUN COMPLETE - No changes were made\n" + "Run without --dry-run to apply changes\n" + "=" * 70 + "\n" + ) + ) + else: + self.stdout.write( + self.style.SUCCESS( + "\n" + "=" * 70 + "\n" + "✅ Setup Complete!\n" + "Run `python manage.py setup_dev_environment --help` for options\n" + "=" * 70 + "\n" + ) + ) + + def create_organization(self): + """Create the parent organization""" + self.stdout.write(" Creating organization...") + + org_data = { + "name": "Al Hammadi Healthcare Group (DEV)", + "code": "AHH-DEV", + "name_ar": "مجموعة الحمادي الصحية (تطوير)", + "status": "active", + } + + if self.dry_run: + self.stdout.write(f" ✓ Would create: {org_data['name']}") + return type("Organization", (), org_data)() + + organization, created = Organization.objects.get_or_create(code=org_data["code"], defaults=org_data) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {organization.name}") + return organization + + def create_hospitals(self, organization): + """Create the 3 development hospitals""" + self.stdout.write(" Creating hospitals...") + + hospitals_data = [ + { + "name": "Al Hammadi Hospital - Nuzha (DEV)", + "code": "NUZHA-DEV", + "name_ar": "مستشفى الحمادي - النخيل (تطوير)", + "status": "active", + }, + { + "name": "Al Hammadi Hospital - Olaya (DEV)", + "code": "OLAYA-DEV", + "name_ar": "مستشفى الحمادي - العليا (تطوير)", + "status": "active", + }, + { + "name": "Al Hammadi Hospital - Suwaidi (DEV)", + "code": "SUWAIDI-DEV", + "name_ar": "مستشفى الحمادي - السويدي (تطوير)", + "status": "active", + }, + ] + + hospitals = [] + for hospital_data in hospitals_data: + # Filter by hospital code if specified + if self.hospital_code_filter and hospital_data["code"] != self.hospital_code_filter: + continue + + if self.dry_run: + self.stdout.write(f" ✓ Would create: {hospital_data['name']}") + hospitals.append(type("Hospital", (), hospital_data)()) + continue + + hospital, created = Hospital.objects.get_or_create( + code=hospital_data["code"], defaults={**hospital_data, "organization": organization} + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {hospital.name}") + hospitals.append(hospital) + + return hospitals + + def create_location_data(self): + """Create Locations, Main Sections, and Sub Sections for complaint categorization""" + self.stdout.write(" Creating location hierarchy...") + + locations_data = [ + {"id": 48, "name_ar": "التنويم", "name_en": "Inpatient"}, + {"id": 49, "name_ar": "العيادات الخارجية", "name_en": "Outpatient Clinics"}, + {"id": 82, "name_ar": "الطوارئ", "name_en": "Emergency"}, + {"id": 110, "name_ar": "اخرى", "name_en": "Others"}, + ] + + main_sections_data = [ + {"id": 1, "name_ar": "الطبي", "name_en": "Medical"}, + {"id": 2, "name_ar": "التمريض", "name_en": "Nursing"}, + {"id": 3, "name_ar": "الخدمات المساندة", "name_en": "Support Services"}, + {"id": 4, "name_ar": "الإداري", "name_en": "Administrative"}, + {"id": 5, "name_ar": "IT", "name_en": "Information Technology"}, + ] + + subsections_data = [ + { + "id": "48", + "name_ar": "تنويم الأطفال", + "name_en": "Pediatric Ward", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "46", + "name_ar": "تنويم الباطنية", + "name_en": "Internal Medicine Ward", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "45", + "name_ar": "تنويم الجراحة العامة", + "name_en": "General Surgery Ward", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "47", + "name_ar": "تنويم النساء والولادة", + "name_en": "OB/GYN Ward", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "52", + "name_ar": "قسم الأشعة", + "name_en": "Radiology Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "43", + "name_ar": "قسم التخدير", + "name_en": "Anesthesia Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "55", + "name_ar": "قسم التغذية", + "name_en": "Nutrition Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "208", + "name_ar": "قسم الجهاز الهمضي و الكبد والمناظير", + "name_en": "Gastroenterology, Hepatology & Endoscopy", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "42", + "name_ar": "قسم الحضانة", + "name_en": "Nursery Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "51", + "name_ar": "قسم العلاج التنفسي", + "name_en": "Respiratory Therapy Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "50", + "name_ar": "قسم العلاج الطبيعي", + "name_en": "Physiotherapy Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "44", + "name_ar": "قسم العمليات", + "name_en": "OR Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "212", + "name_ar": "قسم جراحة المخ والاعصاب", + "name_en": "Neurosurgery Department", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "176", + "name_ar": "قسم عمليات الولادة", + "name_en": "Labor & Delivery OR", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "216", + "name_ar": "وحدة العناية القلبيه", + "name_en": "Coronary Care Unit (CCU)", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "38", + "name_ar": "وحدة العناية المتوسطة", + "name_en": "Intermediate Care Unit", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "39", + "name_ar": "وحدة العناية المتوسطة للأطفال", + "name_en": "Pediatric Intermediate Care Unit", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "35", + "name_ar": "وحدة العناية المركزة", + "name_en": "Intensive Care Unit (ICU)", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "37", + "name_ar": "وحدة العناية المركزة لحديثي الولادة", + "name_en": "Neonatal Intensive Care Unit (NICU)", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "36", + "name_ar": "وحدة العناية المركزة للأطفال", + "name_en": "Pediatric Intensive Care Unit (PICU)", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "147", + "name_ar": "وحدة المناظير", + "name_en": "Endoscopy Unit", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "41", + "name_ar": "وحدة غسيل الكلى", + "name_en": "Hemodialysis Unit", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "40", + "name_ar": "وحدة مرضى طويلي الإقامة", + "name_en": "Long-term Care Unit", + "location_id": "48", + "main_section_id": "1", + }, + { + "id": "155", + "name_ar": "تمريض الحضانة", + "name_en": "Nursery Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "151", + "name_ar": "تمريض تنويم الأطفال", + "name_en": "Pediatric Ward Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "152", + "name_ar": "تمريض تنويم الباطنية", + "name_en": "Internal Medicine Ward Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "153", + "name_ar": "تمريض تنويم الجراحة العامة", + "name_en": "General Surgery Ward Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "154", + "name_ar": "تمريض تنويم النساء والولادة", + "name_en": "OB/GYN Ward Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "177", + "name_ar": "تمريض قسم الإفاقة", + "name_en": "Recovery Room Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "182", + "name_ar": "تمريض قسم التخدير", + "name_en": "Anesthesia Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "183", + "name_ar": "تمريض قسم العمليات", + "name_en": "OR Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "163", + "name_ar": "تمريض قسم المناظير", + "name_en": "Endoscopy Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "178", + "name_ar": "تمريض قسم عمليات الولادة", + "name_en": "Delivery OR Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "161", + "name_ar": "تمريض وحدة طويلي الإقامة", + "name_en": "Long-term Care Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "159", + "name_ar": "تمريض وحدة العناية المتوسطة", + "name_en": "Intermediate Care Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "160", + "name_ar": "تمريض وحدة العناية المتوسطة - أطفال", + "name_en": "Pediatric Intermediate Care Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "156", + "name_ar": "تمريض وحدة العناية المركزة", + "name_en": "ICU Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "157", + "name_ar": "تمريض وحدة العناية المركزة - أطفال", + "name_en": "PICU Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "158", + "name_ar": "تمريض وحدة العناية لحديثي الولادة", + "name_en": "NICU Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "162", + "name_ar": "تمريض وحدة غسيل الكلى", + "name_en": "Hemodialysis Nursing", + "location_id": "48", + "main_section_id": "2", + }, + { + "id": "207", + "name_ar": "قسم الصيانة", + "name_en": "Maintenance Department", + "location_id": "48", + "main_section_id": "3", + }, + { + "id": "164", + "name_ar": "قسم المطبخ", + "name_en": "Kitchen Department", + "location_id": "48", + "main_section_id": "3", + }, + { + "id": "165", + "name_ar": "قسم النظافة", + "name_en": "Housekeeping Department", + "location_id": "48", + "main_section_id": "3", + }, + { + "id": "172", + "name_ar": "إدارة التنويم", + "name_en": "Admission Management", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "226", + "name_ar": "التقارير و الشهادات الطبية", + "name_en": "Medical Reports & Certificates", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "174", + "name_ar": "المدير المناوب", + "name_en": "Duty Manager", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "229", + "name_ar": "تشخيص الحالة الصحية و شرح الخيارات العلاجية", + "name_en": "Diagnosis & Treatment Options Explanation", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "105", + "name_ar": "قسم الأمن", + "name_en": "Security Department", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "171", + "name_ar": "قسم الخدمة الإجتماعية", + "name_en": "Social Services Department", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "168", + "name_ar": "قسم السنترال", + "name_en": "Operator / PBX", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "169", + "name_ar": "قسم المالية", + "name_en": "Finance Department", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "167", + "name_ar": "قسم المواعيد", + "name_en": "Appointments Department", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "173", + "name_ar": "قسم الموافقات الطبية", + "name_en": "Medical Approvals Department", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "170", + "name_ar": "قسم علاقات المرضى", + "name_en": "Patient Relations Department", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "230", + "name_ar": "متعلقات مالية", + "name_en": "Financial Matters", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "184", + "name_ar": "منسقة مراجعي مستشفى قوى الأمن", + "name_en": "SFH Patient Coordinator", + "location_id": "48", + "main_section_id": "4", + }, + { + "id": "197", + "name_ar": "الأشعة التداخلية للأوعية الدموية و الثدي", + "name_en": "Vascular & Breast Interventional Radiology", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "227", + "name_ar": "التقارير الطبية", + "name_en": "Medical Reports", + "location_id": "49", + "main_section_id": "1", + }, + {"id": "211", "name_ar": "بنك الدم", "name_en": "Blood Bank", "location_id": "49", "main_section_id": "1"}, + { + "id": "29", + "name_ar": "عيادات أمراض دم (أطفال)", + "name_en": "Pediatric Hematology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "179", + "name_ar": "عيادات الأسنان", + "name_en": "Dental Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "25", + "name_ar": "عيادات الأطفال", + "name_en": "Pediatric Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "199", + "name_ar": "عيادات الأمراض المعدية", + "name_en": "Infectious Diseases Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "196", + "name_ar": "عيادات الأنف والأذن والحنجرة (أطفال)", + "name_en": "Pediatric ENT Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "1", + "name_ar": "عيادات الباطنية", + "name_en": "Internal Medicine Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "109", + "name_ar": "عيادات التخدير", + "name_en": "Anesthesia Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "32", + "name_ar": "عيادات الجراحة التجميلية", + "name_en": "Plastic Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "16", + "name_ar": "عيادات الجراحة العامة", + "name_en": "General Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "190", + "name_ar": "عيادات الجراحة العامة (أطفال)", + "name_en": "Pediatric General Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "10", + "name_ar": "عيادات الجلدية", + "name_en": "Dermatology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "9", + "name_ar": "عيادات الجهاز الهضمي والمناظير", + "name_en": "Gastroenterology & Endoscopy Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "28", + "name_ar": "عيادات الجهاز الهمضي والمناظير (أطفال)", + "name_en": "Pediatric Gastroenterology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "200", + "name_ar": "عيادات الروماتيزم", + "name_en": "Rheumatology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "5", + "name_ar": "عيادات الصدرية", + "name_en": "Pulmonary Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "191", + "name_ar": "عيادات الصدرية (أطفال)", + "name_en": "Pediatric Pulmonary Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "11", + "name_ar": "عيادات الطب النفسي", + "name_en": "Psychiatry Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "198", + "name_ar": "عيادات العقم والإنجاب", + "name_en": "Infertility & Reproductive Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "12", + "name_ar": "عيادات العيون", + "name_en": "Ophthalmology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "13", + "name_ar": "عيادات الغدد الصماء", + "name_en": "Endocrinology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "27", + "name_ar": "عيادات الغدد الصماء والسكري (أطفال)", + "name_en": "Pediatric Endo & Diabetes Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "2", + "name_ar": "عيادات القلب", + "name_en": "Cardiology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "6", + "name_ar": "عيادات الكلى", + "name_en": "Nephrology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "189", + "name_ar": "عيادات الكلى (أطفال)", + "name_en": "Pediatric Nephrology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "14", + "name_ar": "عيادات المخ والأعصاب", + "name_en": "Neurology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "26", + "name_ar": "عيادات المخ والأعصاب (أطفال)", + "name_en": "Pediatric Neurology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "31", + "name_ar": "عيادات النساء والولادة", + "name_en": "OB/GYN Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "188", + "name_ar": "عيادات امراض الدم", + "name_en": "Hematology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "20", + "name_ar": "عيادات جراحة الأنف وأذن وحنجرة", + "name_en": "ENT Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "24", + "name_ar": "عيادات جراحة الأورام و الغدد الصماء", + "name_en": "Oncology & Endo Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "15", + "name_ar": "عيادات جراحة الأوعية الدموية", + "name_en": "Vascular Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "209", + "name_ar": "عيادات جراحة الختان", + "name_en": "Circumcision Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "17", + "name_ar": "عيادات جراحة العظام", + "name_en": "Orthopedic Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "194", + "name_ar": "عيادات جراحة العمود الفقري", + "name_en": "Spine Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "0", + "name_ar": "عيادات جراحة العيون", + "name_en": "Eye Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "23", + "name_ar": "عيادات جراحة القفص الصدري", + "name_en": "Thoracic Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "21", + "name_ar": "عيادات جراحة القلب", + "name_en": "Cardiac Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "22", + "name_ar": "عيادات جراحة المخ والأعصاب", + "name_en": "Neurosurgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "195", + "name_ar": "عيادات جراحة المخ والأعصاب (أطفال)", + "name_en": "Pediatric Neurosurgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "18", + "name_ar": "عيادات جراحة المسالك البولية", + "name_en": "Urology Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "192", + "name_ar": "عيادات جراحة المسالك البولية (أطفال)", + "name_en": "Pediatric Urology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "193", + "name_ar": "عيادات جراحة سمنة", + "name_en": "Bariatric Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "202", + "name_ar": "عيادات جراحة عظام (أطفال)", + "name_en": "Pediatric Ortho Surgery Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "30", + "name_ar": "عيادات قلب (أطفال)", + "name_en": "Pediatric Cardiology Clinics", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "58", + "name_ar": "فني أشعة القلب الارتدادية", + "name_en": "Echocardiogram Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "60", + "name_ar": "فني اختبار التنفس", + "name_en": "PFT Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "201", + "name_ar": "فني اختبار السمع", + "name_en": "Audiology Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "62", + "name_ar": "فني التجبير", + "name_en": "Plaster/Casting Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "59", + "name_ar": "فني تخطيط القلب", + "name_en": "ECG Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "56", + "name_ar": "فني تخطيط المخ والأعصاب", + "name_en": "EEG Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "57", + "name_ar": "فني دراسة الجهد القلب", + "name_en": "Stress Test Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "61", + "name_ar": "فني فحص النظر", + "name_en": "Optometry Technician", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "186", + "name_ar": "قسم الأشعة", + "name_en": "Radiology Department", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "206", + "name_ar": "قسم التغذية", + "name_en": "Nutrition Department", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "54", + "name_ar": "قسم الصيدلية", + "name_en": "Pharmacy Department", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "205", + "name_ar": "قسم الصيدلية - السويدي", + "name_en": "Pharmacy - Al Suwaidi", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "187", + "name_ar": "قسم العلاج الطبيعي", + "name_en": "Physiotherapy Department", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "180", + "name_ar": "قسم المختبر", + "name_en": "Laboratory Department", + "location_id": "49", + "main_section_id": "1", + }, + { + "id": "185", + "name_ar": "تمريض التخدير", + "name_en": "Anesthesia Nursing", + "location_id": "49", + "main_section_id": "2", + }, + { + "id": "3", + "name_ar": "تمريض العيادات الخارجية", + "name_en": "Outpatient Nursing", + "location_id": "49", + "main_section_id": "2", + }, + { + "id": "79", + "name_ar": "تمريض غرفة التطعيمات", + "name_en": "Vaccination Room Nursing", + "location_id": "49", + "main_section_id": "2", + }, + { + "id": "110", + "name_ar": "تمريض غرفة تقديم الأدوية الوريدية (20)", + "name_en": "IV Room Nursing (20)", + "location_id": "49", + "main_section_id": "2", + }, + { + "id": "78", + "name_ar": "تمريض مكتب التنويم", + "name_en": "Admission Office Nursing", + "location_id": "49", + "main_section_id": "2", + }, + { + "id": "225", + "name_ar": "اخصائي تخطيط المخ والاعصاب", + "name_en": "EEG Specialist", + "location_id": "49", + "main_section_id": "3", + }, + { + "id": "84", + "name_ar": "قسم النظافة", + "name_en": "Housekeeping Department", + "location_id": "49", + "main_section_id": "3", + }, + { + "id": "223", + "name_ar": "إدارة العيادات الخارجيه", + "name_en": "Outpatient Management", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "93", + "name_ar": "استقبال الأشعة", + "name_en": "Radiology Reception", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "94", + "name_ar": "استقبال العلاج الطبيعي", + "name_en": "Physiotherapy Reception", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "113", + "name_ar": "استقبال العيادات الخارجية", + "name_en": "Outpatient Reception", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "92", + "name_ar": "استقبال المختبر", + "name_en": "Laboratory Reception", + "location_id": "49", + "main_section_id": "4", + }, + {"id": "215", "name_ar": "التنسيق", "name_en": "Coordination", "location_id": "49", "main_section_id": "4"}, + { + "id": "222", + "name_ar": "الدعم الفني", + "name_en": "Technical Support", + "location_id": "49", + "main_section_id": "4", + }, + {"id": "219", "name_ar": "المختبر", "name_en": "Laboratory", "location_id": "49", "main_section_id": "4"}, + { + "id": "111", + "name_ar": "قسم الأمن", + "name_en": "Security Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "203", + "name_ar": "قسم الإدارة", + "name_en": "Administration Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "102", + "name_ar": "قسم التقارير الطبية", + "name_en": "Medical Reports Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "224", + "name_ar": "قسم الدعم الفني", + "name_en": "IT Helpdesk", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "103", + "name_ar": "قسم السكرتارية الطبية", + "name_en": "Medical Secretarial Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "89", + "name_ar": "قسم السنترال", + "name_en": "Operator / PBX", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "90", + "name_ar": "قسم المالية", + "name_en": "Finance Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "88", + "name_ar": "قسم المواعيد", + "name_en": "Appointments Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "104", + "name_ar": "قسم الموافقات الطبية", + "name_en": "Medical Approvals Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "97", + "name_ar": "قسم علاقات المرضى", + "name_en": "Patient Relations Department", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "99", + "name_ar": "مكتب التنويم", + "name_en": "Admission Office", + "location_id": "49", + "main_section_id": "4", + }, + { + "id": "34", + "name_ar": "أطباء الطوارئ", + "name_en": "ER Doctors", + "location_id": "82", + "main_section_id": "1", + }, + {"id": "49", "name_ar": "الأشعة", "name_en": "Radiology", "location_id": "82", "main_section_id": "1"}, + { + "id": "53", + "name_ar": "صيدلية الطوارئ", + "name_en": "ER Pharmacy", + "location_id": "82", + "main_section_id": "1", + }, + { + "id": "77", + "name_ar": "تمريض الطوارئ", + "name_en": "ER Nursing", + "location_id": "82", + "main_section_id": "2", + }, + { + "id": "149", + "name_ar": "قسم النظافة", + "name_en": "Housekeeping Department", + "location_id": "82", + "main_section_id": "3", + }, + { + "id": "96", + "name_ar": "استقبال الطوارئ", + "name_en": "ER Reception", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "228", + "name_ar": "التقارير الطبية", + "name_en": "Medical Reports", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "100", + "name_ar": "المدير المناوب", + "name_en": "Duty Manager", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "98", + "name_ar": "الموافقات الطبية", + "name_en": "Medical Approvals", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "220", + "name_ar": "تنسيق الطوارئ", + "name_en": "ER Coordination", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "95", + "name_ar": "قسم الأمن", + "name_en": "Security Department", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "210", + "name_ar": "مكتب التنويم", + "name_en": "Admission Office", + "location_id": "82", + "main_section_id": "4", + }, + { + "id": "213", + "name_ar": "قسم تقنية المعلومات", + "name_en": "IT Department", + "location_id": "110", + "main_section_id": "5", + }, + ] + + if self.dry_run: + self.stdout.write(f" ✓ Would create: {len(locations_data)} Locations") + self.stdout.write(f" ✓ Would create: {len(main_sections_data)} Main Sections") + self.stdout.write(f" ✓ Would create: {len(subsections_data)} Sub Sections") + return + + for loc in locations_data: + Location.objects.update_or_create( + id=loc["id"], + defaults={"name_ar": loc["name_ar"], "name_en": loc["name_en"]}, + ) + self.stdout.write(f" ✓ Created/Updated: {len(locations_data)} Locations") + + for sec in main_sections_data: + MainSection.objects.update_or_create( + id=sec["id"], + defaults={"name_ar": sec["name_ar"], "name_en": sec["name_en"]}, + ) + self.stdout.write(f" ✓ Created/Updated: {len(main_sections_data)} Main Sections") + + try: + SubSection.objects.all().delete() + except Exception: + self.stdout.write(self.style.WARNING(" ⚠ Skipping SubSection deletion - some are referenced")) + + subsections_to_create = [ + SubSection( + internal_id=int(item["id"]), + name_en=item["name_en"], + name_ar=item["name_ar"], + location_id=int(item["location_id"]), + main_section_id=int(item["main_section_id"]), + ) + for item in subsections_data + ] + SubSection.objects.bulk_create(subsections_to_create, ignore_conflicts=True) + self.stdout.write(f" ✓ Created: {len(subsections_data)} Sub Sections") + + def create_roles_and_groups(self): + """Create default roles and groups""" + self.stdout.write(" Creating roles and groups...") + + roles_config = [ + {"name": "px_admin", "display_name": "PX Admin", "description": "Full system access", "level": 100}, + { + "name": "hospital_admin", + "display_name": "Hospital Admin", + "description": "Hospital-level access", + "level": 80, + }, + { + "name": "department_manager", + "display_name": "Department Manager", + "description": "Department-level access", + "level": 60, + }, + { + "name": "px_coordinator", + "display_name": "PX Coordinator", + "description": "Can manage PX actions", + "level": 50, + }, + {"name": "physician", "display_name": "Physician", "description": "Can view feedback", "level": 40}, + {"name": "nurse", "display_name": "Nurse", "description": "Can view department feedback", "level": 30}, + {"name": "staff", "display_name": "Staff", "description": "Basic staff access", "level": 20}, + {"name": "viewer", "display_name": "Viewer", "description": "Read-only access", "level": 10}, + { + "name": "px_source_user", + "display_name": "PX Source User", + "description": "External source users", + "level": 5, + }, + ] + + for role_data in roles_config: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {role_data['display_name']}") + continue + + group, _ = Group.objects.get_or_create(name=role_data["display_name"]) + role, created = Role.objects.get_or_create( + name=role_data["name"], + defaults={ + "display_name": role_data["display_name"], + "description": role_data["description"], + "group": group, + "level": role_data["level"], + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {role.display_name}") + + def create_px_sources(self): + """Create PX sources""" + self.stdout.write(" Creating PX sources...") + + sources_data = [ + {"code": "PATIENT", "name_en": "Patient", "name_ar": "المريض", "source_type": "internal"}, + {"code": "FAMILY", "name_en": "Family Member", "name_ar": "فرد العائلة", "source_type": "internal"}, + {"code": "STAFF", "name_en": "Staff", "name_ar": "الموظفين", "source_type": "internal"}, + {"code": "SURVEY", "name_en": "Survey", "name_ar": "الاستبيان", "source_type": "internal"}, + {"code": "MOH", "name_en": "Ministry of Health", "name_ar": "وزارة الصحة", "source_type": "government"}, + { + "code": "CCHI", + "name_en": "Council of Cooperative Health Insurance", + "name_ar": "مجلس التعاون الصحي المشترك", + "source_type": "government", + }, + ] + + sources = {} + for source_data in sources_data: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {source_data['name_en']}") + sources[source_data["code"]] = type("PXSource", (), source_data)() + continue + + source, created = PXSource.objects.get_or_create( + code=source_data["code"], + defaults={ + "name_en": source_data["name_en"], + "name_ar": source_data["name_ar"], + "source_type": source_data["source_type"], + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {source.name_en}") + sources[source_data["code"]] = source + + return sources + + def create_survey_templates(self, hospitals): + """Create survey templates for each patient type""" + self.stdout.write(" Creating survey templates...") + + templates_data = { + "inpatient": { + "name": "Inpatient Post-Discharge Survey", + "name_ar": "استبيان المرضى المنومين", + "survey_type": "stage", + "questions": [ + {"text": "How satisfied were you with the nursing care?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the doctor's care?", "type": QuestionType.RATING}, + {"text": "How clean was your room?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the food quality?", "type": QuestionType.RATING}, + {"text": "How well were you informed about your treatment?", "type": QuestionType.RATING}, + {"text": "How likely are you to recommend this hospital to others?", "type": QuestionType.NPS}, + {"text": "Any additional comments or suggestions?", "type": QuestionType.TEXT}, + ], + }, + "opd": { + "name": "OPD Patient Experience Survey", + "name_ar": "استبيان العيادات الخارجية", + "survey_type": "stage", + "questions": [ + {"text": "How satisfied were you with the registration process?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the waiting time?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the doctor's consultation?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the pharmacy service?", "type": QuestionType.RATING}, + {"text": "How likely are you to recommend this hospital to others?", "type": QuestionType.NPS}, + {"text": "Any additional comments or suggestions?", "type": QuestionType.TEXT}, + ], + }, + "ems": { + "name": "EMS Emergency Services Survey", + "name_ar": "استبيان خدمات الطوارئ", + "survey_type": "stage", + "questions": [ + {"text": "How satisfied were you with the ambulance response time?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the paramedic care?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the emergency department care?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the communication from staff?", "type": QuestionType.RATING}, + {"text": "How likely are you to recommend this hospital to others?", "type": QuestionType.NPS}, + {"text": "Any additional comments or suggestions?", "type": QuestionType.TEXT}, + ], + }, + "day_case": { + "name": "Day Case Patient Survey", + "name_ar": "استبيان الحالات النهارية", + "survey_type": "stage", + "questions": [ + {"text": "How satisfied were you with the pre-procedure preparation?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the procedure itself?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the post-procedure care?", "type": QuestionType.RATING}, + {"text": "How satisfied were you with the discharge process?", "type": QuestionType.RATING}, + {"text": "How likely are you to recommend this hospital to others?", "type": QuestionType.NPS}, + {"text": "Any additional comments or suggestions?", "type": QuestionType.TEXT}, + ], + }, + } + + templates = {} + for hospital in hospitals: + hospital_identifier = hospital.name if not self.dry_run else hospital.code + templates[hospital_identifier] = {} + + for template_type, template_data in templates_data.items(): + if self.dry_run: + self.stdout.write(f" ✓ Would create: {template_data['name']} for {hospital_identifier}") + templates[hospital_identifier][template_type] = type("SurveyTemplate", (), template_data)() + continue + + template, created = SurveyTemplate.objects.get_or_create( + name=template_data["name"], + hospital=hospital, + defaults={ + "name_ar": template_data["name_ar"], + "survey_type": template_data["survey_type"], + "is_active": True, + "scoring_method": "average", + "negative_threshold": 3.0, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {template.name} ({hospital_identifier})") + + # Create questions + for order, question_data in enumerate(template_data["questions"], 1): + question, q_created = SurveyQuestion.objects.get_or_create( + survey_template=template, + text=question_data["text"], + defaults={ + "question_type": question_data["type"], + "order": order, + "is_required": True, + }, + ) + + templates[hospital_identifier][template_type] = template + + return templates + + def create_survey_mappings(self, survey_templates): + """Create survey template mappings""" + self.stdout.write(" Creating survey template mappings...") + + mapping_data = [ + ("1", "inpatient", "Inpatient"), + ("2", "opd", "Outpatient"), + ("3", "ems", "Emergency"), + ("4", "day_case", "Day Case"), + ] + + for hospital_key, templates in survey_templates.items(): + for patient_type, template_type, description in mapping_data: + if template_type not in templates: + continue + + template = templates[template_type] + + if self.dry_run: + self.stdout.write(f" ✓ Would create mapping: {patient_type} → {description}") + continue + + mapping, created = SurveyTemplateMapping.objects.get_or_create( + patient_type=patient_type, + is_active=True, + defaults={ + "survey_template": template, + "send_delay_hours": 1, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: Patient Type {patient_type} → {description}") + + def create_complaint_categories(self): + """Create complaint categories""" + self.stdout.write(" Creating complaint categories...") + + categories_data = [ + { + "code": "CLINICAL", + "name_en": "Clinical Care", + "name_ar": "الرعاية السريرية", + "level": 1, + "domain_type": "CLINICAL", + }, + { + "code": "MANAGEMENT", + "name_en": "Management", + "name_ar": "الإدارة", + "level": 1, + "domain_type": "MANAGEMENT", + }, + { + "code": "RELATIONSHIPS", + "name_en": "Relationships", + "name_ar": "العلاقات", + "level": 1, + "domain_type": "RELATIONSHIPS", + }, + ] + + for cat_data in categories_data: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {cat_data['name_en']}") + continue + + category, created = ComplaintCategory.objects.get_or_create( + code=cat_data["code"], + parent__isnull=True, + defaults={ + "name_en": cat_data["name_en"], + "name_ar": cat_data["name_ar"], + "level": cat_data["level"], + "domain_type": cat_data.get("domain_type", ""), + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {category.name_en}") + + def create_complaint_sla_configs(self, hospitals, px_sources): + """Create SLA configurations for each hospital""" + self.stdout.write(" Creating SLA configurations...") + + sla_configs_data = [ + ("MOH", 24, 12, 18, 24), + ("CCHI", 48, 24, 36, 48), + ("PATIENT", 72, 24, 48, 72), + ("FAMILY", 72, 24, 48, 72), + ("STAFF", 72, 24, 48, 72), + ("SURVEY", 72, 24, 48, 72), + ] + + for hospital in hospitals: + hospital_identifier = hospital.name if not self.dry_run else hospital.code + self.stdout.write(f" {hospital_identifier}:") + + for source_code, sla_hours, first_rem, second_rem, escalation in sla_configs_data: + if source_code not in px_sources: + continue + + source = px_sources[source_code] + + if self.dry_run: + self.stdout.write(f" ✓ Would create SLA: {source_code} ({sla_hours}h)") + continue + + config, created = ComplaintSLAConfig.objects.get_or_create( + hospital=hospital, + source=source, + defaults={ + "sla_hours": sla_hours, + "first_reminder_hours_after": first_rem, + "second_reminder_hours_after": second_rem, + "escalation_hours_after": escalation, + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {source_code} ({sla_hours}h)") + + def create_escalation_rules(self, hospitals): + """Create escalation rules for each hospital""" + self.stdout.write(" Creating escalation rules...") + + rules_data = [ + { + "name": "Default Escalation to Department Manager", + "escalate_to_role": "department_manager", + "trigger_hours_overdue": 0, + "order": 1, + }, + { + "name": "Critical Escalation to Hospital Admin", + "escalate_to_role": "hospital_admin", + "trigger_hours_overdue": 4, + "order": 2, + }, + { + "name": "Final Escalation to PX Admin", + "escalate_to_role": "px_admin", + "trigger_hours_overdue": 24, + "order": 3, + }, + ] + + for hospital in hospitals: + hospital_identifier = hospital.name if not self.dry_run else hospital.code + self.stdout.write(f" {hospital_identifier}:") + + for rule_data in rules_data: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {rule_data['name']}") + continue + + rule, created = EscalationRule.objects.get_or_create( + hospital=hospital, + name=rule_data["name"], + defaults={ + "description": rule_data["name"], + "trigger_on_overdue": True, + "trigger_hours_overdue": rule_data["trigger_hours_overdue"], + "escalate_to_role": rule_data["escalate_to_role"], + "order": rule_data["order"], + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {rule.name}") + + def create_complaint_thresholds(self, hospitals): + """Create complaint thresholds for each hospital""" + self.stdout.write(" Creating complaint thresholds...") + + for hospital in hospitals: + if self.dry_run: + self.stdout.write(f" ✓ Would create threshold for {hospital.code}") + continue + + threshold, created = ComplaintThreshold.objects.get_or_create( + hospital=hospital, + threshold_type="resolution_survey_score", + defaults={ + "threshold_value": 50.0, + "comparison_operator": "lt", + "action_type": "create_px_action", + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: Resolution threshold for {hospital.code}") + + def create_explanation_sla_configs(self, hospitals): + """Create explanation SLA configs for each hospital""" + self.stdout.write(" Creating explanation SLA configs...") + + for hospital in hospitals: + if self.dry_run: + self.stdout.write(f" ✓ Would create explanation SLA for {hospital.code}") + continue + + config, created = ExplanationSLAConfig.objects.get_or_create( + hospital=hospital, + defaults={ + "response_hours": 48, + "reminder_hours_before": 12, + "auto_escalate_enabled": True, + "escalation_hours_overdue": 0, + "max_escalation_levels": 3, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: Explanation SLA for {hospital.code}") + + def create_journey_templates(self, hospitals): + """Create journey templates for each hospital""" + self.stdout.write(" Creating journey templates...") + + journey_types = [ + (JourneyType.INPATIENT, "Inpatient Journey"), + (JourneyType.OPD, "OPD Journey"), + (JourneyType.EMS, "EMS Journey"), + (JourneyType.DAY_CASE, "Day Case Journey"), + ] + + journey_templates = {} + for hospital in hospitals: + hospital_identifier = hospital.name if not self.dry_run else hospital.code + journey_templates[hospital_identifier] = {} + + for journey_type, name in journey_types: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {name} for {hospital_identifier}") + journey_templates[hospital_identifier][journey_type] = type("JourneyTemplate", (), {"name": name})() + continue + + template, created = PatientJourneyTemplate.objects.get_or_create( + hospital=hospital, + journey_type=journey_type, + defaults={ + "name": f"{name} - {hospital.name}", + "is_active": True, + "is_default": True, + "send_post_discharge_survey": True, + "post_discharge_survey_delay_hours": 1, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {name} for {hospital_identifier}") + journey_templates[hospital_identifier][journey_type] = template + + return journey_templates + + def create_journey_stages(self, journey_templates, survey_templates): + """Create journey stage templates""" + self.stdout.write(" Creating journey stage templates...") + + # OPD stages with HIS trigger codes + opd_stages = [ + {"name": "Registration", "code": "OPD_REG", "trigger": "REGISTRATION", "order": 1}, + {"name": "Waiting", "code": "OPD_WAIT", "trigger": "WAITING", "order": 2}, + {"name": "MD Consultation", "code": "OPD_MD_CONSULT", "trigger": "Consultation", "order": 3}, + {"name": "MD Visit", "code": "OPD_MD_VISIT", "trigger": "Doctor Visited", "order": 4}, + {"name": "Clinical Assessment", "code": "OPD_CLINICAL", "trigger": "Clinical Condtion", "order": 5}, + {"name": "Patient Assessment", "code": "OPD_PATIENT", "trigger": "ChiefComplaint", "order": 6}, + {"name": "Pharmacy", "code": "OPD_PHARMACY", "trigger": "Prescribed Drugs", "order": 7}, + {"name": "Discharge", "code": "OPD_DISCHARGE", "trigger": "DISCHARGED", "order": 8}, + ] + + for hospital_key, templates in journey_templates.items(): + if JourneyType.OPD not in templates: + continue + + opd_template = templates[JourneyType.OPD] + + if self.dry_run: + self.stdout.write(f" ✓ Would create OPD stages for {hospital_key}") + continue + + self.stdout.write(f" Creating OPD stages for {hospital_key}:") + + for stage_data in opd_stages: + stage, created = PatientJourneyStageTemplate.objects.get_or_create( + journey_template=opd_template, + code=stage_data["code"], + defaults={ + "name": stage_data["name"], + "order": stage_data["order"], + "trigger_event_code": stage_data["trigger"], + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {stage_data['name']}") + + def create_observation_categories(self): + """Create observation categories""" + self.stdout.write(" Creating observation categories...") + + categories = [ + {"name_en": "Patient Safety", "name_ar": "سلامة المرضى", "icon": "bi-shield-exclamation", "sort_order": 1}, + {"name_en": "Clinical Quality", "name_ar": "الجودة السريرية", "icon": "bi-heart-pulse", "sort_order": 2}, + {"name_en": "Infection Control", "name_ar": "مكافحة العدوى", "icon": "bi-virus", "sort_order": 3}, + {"name_en": "Medication Safety", "name_ar": "سلامة الأدوية", "icon": "bi-capsule", "sort_order": 4}, + {"name_en": "Equipment & Devices", "name_ar": "المعدات والأجهزة", "icon": "bi-tools", "sort_order": 5}, + {"name_en": "Facility & Environment", "name_ar": "المرافق والبيئة", "icon": "bi-building", "sort_order": 6}, + {"name_en": "Staff Behavior", "name_ar": "سلوك الموظفين", "icon": "bi-people", "sort_order": 7}, + {"name_en": "Communication", "name_ar": "التواصل", "icon": "bi-chat-dots", "sort_order": 8}, + {"name_en": "Documentation", "name_ar": "التوثيق", "icon": "bi-file-text", "sort_order": 9}, + { + "name_en": "Process & Workflow", + "name_ar": "العمليات وسير العمل", + "icon": "bi-diagram-3", + "sort_order": 10, + }, + {"name_en": "Security", "name_ar": "الأمن", "icon": "bi-shield-lock", "sort_order": 11}, + { + "name_en": "IT & Systems", + "name_ar": "تقنية المعلومات والأنظمة", + "icon": "bi-pc-display", + "sort_order": 12, + }, + {"name_en": "Housekeeping", "name_ar": "التدبير المنزلي", "icon": "bi-house", "sort_order": 13}, + {"name_en": "Food Services", "name_ar": "خدمات الطعام", "icon": "bi-cup-hot", "sort_order": 14}, + {"name_en": "Other", "name_ar": "أخرى", "icon": "bi-three-dots", "sort_order": 99}, + ] + + for cat_data in categories: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {cat_data['name_en']}") + continue + + category, created = ObservationCategory.objects.update_or_create( + name_en=cat_data["name_en"], + defaults={ + "name_ar": cat_data["name_ar"], + "icon": cat_data["icon"], + "sort_order": cat_data["sort_order"], + "is_active": True, + }, + ) + + action = "Created" if created else "Updated" + self.stdout.write(f" ✓ {action}: {cat_data['name_en']}") + + def create_notification_templates(self): + """Create notification templates""" + self.stdout.write(" Creating notification templates...") + + templates = [ + {"name": "Onboarding Invitation", "template_type": "onboarding_invitation"}, + {"name": "Onboarding Reminder", "template_type": "onboarding_reminder"}, + {"name": "Onboarding Completion", "template_type": "onboarding_completion"}, + {"name": "Survey Invitation", "template_type": "survey_invitation"}, + {"name": "Survey Reminder", "template_type": "survey_reminder"}, + {"name": "Complaint Acknowledgment", "template_type": "complaint_acknowledgment"}, + {"name": "Complaint Update", "template_type": "complaint_update"}, + {"name": "Action Assignment", "template_type": "action_assignment"}, + {"name": "SLA Reminder", "template_type": "sla_reminder"}, + {"name": "SLA Breach", "template_type": "sla_breach"}, + ] + + for template_data in templates: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {template_data['name']}") + continue + + template, created = NotificationTemplate.objects.get_or_create( + name=template_data["name"], + defaults={ + "template_type": template_data["template_type"], + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {template_data['name']}") + + def create_standard_sources(self): + """Create standard sources""" + self.stdout.write(" Creating standard sources...") + + sources_data = [ + {"code": "CBAHI", "name": "CBAHI", "name_ar": "الجهاز المركزي للاعتماد", "website": "https://cbahi.org"}, + { + "code": "JCI", + "name": "JCI", + "name_ar": "اللجنة المشتركة الدولية", + "website": "https://jointcommission.org", + }, + {"code": "ISO", "name": "ISO", "name_ar": "المنظمة الدولية للمعايير", "website": "https://iso.org"}, + ] + + for source_data in sources_data: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {source_data['name']}") + continue + + source, created = StandardSource.objects.get_or_create( + code=source_data["code"], + defaults={ + "name": source_data["name"], + "name_ar": source_data["name_ar"], + "website": source_data["website"], + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {source_data['name']}") + + def create_standard_categories(self): + """Create standard categories""" + self.stdout.write(" Creating standard categories...") + + categories_data = [ + {"name": "Patient Safety", "name_ar": "سلامة المريض", "order": 1}, + {"name": "Quality Management", "name_ar": "إدارة الجودة", "order": 2}, + {"name": "Infection Control", "name_ar": "مكافحة العدوى", "order": 3}, + {"name": "Medication Safety", "name_ar": "سلامة الأدوية", "order": 4}, + {"name": "Environment of Care", "name_ar": "بيئة الرعاية", "order": 5}, + {"name": "Leadership", "name_ar": "القيادة", "order": 6}, + {"name": "Information Management", "name_ar": "إدارة المعلومات", "order": 7}, + ] + + for cat_data in categories_data: + if self.dry_run: + self.stdout.write(f" ✓ Would create: {cat_data['name']}") + continue + + category, created = StandardCategory.objects.get_or_create( + name=cat_data["name"], + defaults={ + "name_ar": cat_data["name_ar"], + "order": cat_data["order"], + "is_active": True, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: {cat_data['name']}") + + def create_integration_config(self): + """Create HIS integration configuration""" + self.stdout.write(" Creating HIS integration configuration...") + + import os + + api_url = os.getenv("HIS_API_URL", "https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps") + + if self.dry_run: + self.stdout.write(f" ✓ Would create: HIS Integration") + self.stdout.write(f" API URL: {api_url}") + return + + from apps.integrations.models import IntegrationConfig, SourceSystem + + config, created = IntegrationConfig.objects.get_or_create( + name="HIS Integration (DEV)", + defaults={ + "source_system": SourceSystem.HIS, + "api_url": api_url, + "is_active": True, + "config_json": { + "event_mappings": { + "Consultation": "OPD_CONSULTATION", + "Doctor Visited": "OPD_DOCTOR_VISITED", + "Clinical Condtion": "CLINICAL_ASSESSMENT", + "ChiefComplaint": "PATIENT_ASSESSMENT", + "Prescribed Drugs": "PHARMACY", + "DISCHARGED": "PATIENT_DISCHARGED", + } + }, + }, + ) + + action = "Created" if created else "Exists" + self.stdout.write(f" ✓ {action}: HIS Integration") + self.stdout.write(f" API URL: {config.api_url}") + + def create_users(self, hospitals): + """Create sample users with different roles for each hospital""" + self.stdout.write(" Creating sample users...") + + users_config = [ + { + "role_name": "PX Admin", + "suffix": "pxadmin", + "first_name": "PX", + "last_name": "Admin", + "hospital_specific": False, + }, + { + "role_name": "Hospital Admin", + "suffix": "hospadmin", + "first_name": "Hospital", + "last_name": "Admin", + "hospital_specific": True, + }, + { + "role_name": "Department Manager", + "suffix": "deptmgr", + "first_name": "Department", + "last_name": "Manager", + "hospital_specific": True, + }, + { + "role_name": "PX Coordinator", + "suffix": "pxcoord", + "first_name": "PX", + "last_name": "Coordinator", + "hospital_specific": True, + }, + { + "role_name": "Physician", + "suffix": "physician", + "first_name": "Dr.", + "last_name": "Physician", + "hospital_specific": True, + }, + { + "role_name": "Nurse", + "suffix": "nurse", + "first_name": "Nurse", + "last_name": "User", + "hospital_specific": True, + }, + { + "role_name": "Staff", + "suffix": "staff", + "first_name": "Staff", + "last_name": "User", + "hospital_specific": True, + }, + { + "role_name": "Viewer", + "suffix": "viewer", + "first_name": "View", + "last_name": "Only", + "hospital_specific": True, + }, + ] + + default_password = "Dev@123456" + users_created = 0 + + if self.dry_run: + for user_config in users_config: + if user_config["hospital_specific"]: + for hospital in hospitals: + self.stdout.write( + f" ✓ Would create: {user_config['first_name']} {user_config['last_name']} ({user_config['role_name']}) @ {hospital.code}" + ) + else: + self.stdout.write( + f" ✓ Would create: {user_config['first_name']} {user_config['last_name']} ({user_config['role_name']})" + ) + return + + for user_config in users_config: + try: + group = Group.objects.get(name=user_config["role_name"]) + except Group.DoesNotExist: + self.stdout.write(self.style.WARNING(f" ⚠ Skipping {user_config['role_name']} - Group not found")) + continue + + if user_config["hospital_specific"]: + for hospital in hospitals: + email = f"{user_config['suffix']}@{hospital.code.lower()}.dev" + user, created = User.objects.get_or_create( + email=email, + defaults={ + "first_name": user_config["first_name"], + "last_name": f"{user_config['last_name']} ({hospital.code})", + "hospital": hospital, + "is_active": True, + }, + ) + if created: + user.set_password(default_password) + user.groups.add(group) + user.save() + users_created += 1 + self.stdout.write(f" ✓ Created: {user.email} ({user_config['role_name']}) @ {hospital.code}") + else: + self.stdout.write(f" ✓ Exists: {user.email}") + else: + email = f"{user_config['suffix']}@dev.local" + user, created = User.objects.get_or_create( + email=email, + defaults={ + "first_name": user_config["first_name"], + "last_name": user_config["last_name"], + "is_active": True, + }, + ) + if created: + user.set_password(default_password) + user.groups.add(group) + user.save() + users_created += 1 + self.stdout.write(f" ✓ Created: {user.email} ({user_config['role_name']})") + else: + self.stdout.write(f" ✓ Exists: {user.email}") + + self.stdout.write(self.style.SUCCESS(f" Created {users_created} new users (password: {default_password})")) + + def print_summary(self): + """Print final summary""" + self.stdout.write("\n" + "=" * 70 + "\n📊 Setup Summary\n=" * 70) + + if not self.dry_run: + self.stdout.write(f"\n Organization: {Organization.objects.count()}") + self.stdout.write(f" Hospitals: {Hospital.objects.count()}") + self.stdout.write(f" Locations: {Location.objects.count()}") + self.stdout.write(f" Main Sections: {MainSection.objects.count()}") + self.stdout.write(f" Sub Sections: {SubSection.objects.count()}") + self.stdout.write(f" Roles: {Role.objects.count()}") + self.stdout.write(f" PX Sources: {PXSource.objects.count()}") + if not self.skip_surveys: + self.stdout.write(f" Survey Templates: {SurveyTemplate.objects.count()}") + self.stdout.write(f" Survey Questions: {SurveyQuestion.objects.count()}") + self.stdout.write(f" Survey Mappings: {SurveyTemplateMapping.objects.count()}") + if not self.skip_complaints: + self.stdout.write(f" Complaint Categories: {ComplaintCategory.objects.count()}") + self.stdout.write(f" SLA Configs: {ComplaintSLAConfig.objects.count()}") + self.stdout.write(f" Escalation Rules: {EscalationRule.objects.count()}") + if not self.skip_journeys: + self.stdout.write(f" Journey Templates: {PatientJourneyTemplate.objects.count()}") + self.stdout.write(f" Journey Stages: {PatientJourneyStageTemplate.objects.count()}") + self.stdout.write(f" Observation Categories: {ObservationCategory.objects.count()}") + self.stdout.write(f" Notification Templates: {NotificationTemplate.objects.count()}") + self.stdout.write(f" Standard Sources: {StandardSource.objects.count()}") + self.stdout.write(f" Standard Categories: {StandardCategory.objects.count()}") + if not self.skip_users: + self.stdout.write(f" Users: {User.objects.count()}") + + self.stdout.write("\n" + "-" * 70) + self.stdout.write(self.style.WARNING("📋 Next Steps:")) + self.stdout.write(" 1. Run `python manage.py seed_departments` to create standard departments") + self.stdout.write(" 2. Run `python manage.py import_staff_csv ` to import staff and departments") diff --git a/apps/integrations/models.py b/apps/integrations/models.py index c00dbd8..dec21cf 100644 --- a/apps/integrations/models.py +++ b/apps/integrations/models.py @@ -257,7 +257,13 @@ class SurveyTemplateMapping(UUIDModel, TimeStampedModel): db_index=True, help_text="Whether this mapping is active" ) - + + # Delay configuration + send_delay_hours = models.IntegerField( + default=1, + help_text="Hours after discharge to send survey" + ) + class Meta: ordering = ['hospital', 'patient_type'] indexes = [ diff --git a/apps/integrations/services/his_adapter.py b/apps/integrations/services/his_adapter.py index 367b66a..83e5f69 100644 --- a/apps/integrations/services/his_adapter.py +++ b/apps/integrations/services/his_adapter.py @@ -7,11 +7,13 @@ internal format for sending surveys based on PatientType. Simplified Flow: 1. Parse HIS patient data 2. Determine survey type from PatientType -3. Create survey instance -4. Send survey via SMS +3. Create survey instance with PENDING status +4. Queue delayed send task +5. Survey sent after delay (e.g., 1 hour for OPD) """ -from datetime import datetime +from datetime import datetime, timedelta from typing import Dict, Optional, Tuple +import logging from django.utils import timezone @@ -19,6 +21,8 @@ from apps.organizations.models import Hospital, Patient from apps.surveys.models import SurveyTemplate, SurveyInstance, SurveyStatus from apps.integrations.models import InboundEvent +logger = logging.getLogger(__name__) + class HISAdapter: """ @@ -172,25 +176,73 @@ class HISAdapter: def get_survey_template(patient_type: str, hospital: Hospital) -> Optional[SurveyTemplate]: """ Get appropriate survey template based on PatientType using explicit mapping. - + Uses SurveyTemplateMapping to determine which template to send. - + Args: patient_type: HIS PatientType code (1, 2, 3, 4, O, E, APPOINTMENT) hospital: Hospital instance - + Returns: SurveyTemplate or None if not found """ from apps.integrations.models import SurveyTemplateMapping - + # Use explicit mapping to get template survey_template = SurveyTemplateMapping.get_template_for_patient_type( patient_type, hospital ) - + return survey_template - + + @staticmethod + def get_delay_for_patient_type(patient_type: str, hospital) -> int: + """ + Get delay hours from SurveyTemplateMapping. + + Falls back to default delays if no mapping found. + + Args: + patient_type: HIS PatientType code (1, 2, 3, 4, O, E) + hospital: Hospital instance + + Returns: + Delay in hours + """ + from apps.integrations.models import SurveyTemplateMapping + + # Try to get mapping with delay (hospital-specific) + mapping = SurveyTemplateMapping.objects.filter( + patient_type=patient_type, + hospital=hospital, + is_active=True + ).first() + + if mapping and mapping.send_delay_hours: + return mapping.send_delay_hours + + # Fallback to global mapping + mapping = SurveyTemplateMapping.objects.filter( + patient_type=patient_type, + hospital__isnull=True, + is_active=True + ).first() + + if mapping and mapping.send_delay_hours: + return mapping.send_delay_hours + + # Default delays by patient type + default_delays = { + '1': 24, # Inpatient - 24 hours + '2': 1, # OPD - 1 hour + '3': 2, # EMS - 2 hours + 'O': 1, # OPD - 1 hour + 'E': 2, # EMS - 2 hours + '4': 4, # Daycase - 4 hours + } + + return default_delays.get(patient_type, 1) # Default 1 hour + @staticmethod def create_and_send_survey( patient: Patient, @@ -199,20 +251,24 @@ class HISAdapter: survey_template: SurveyTemplate ) -> Optional[SurveyInstance]: """ - Create survey instance and send via SMS. + Create survey instance and queue for delayed sending. + + NEW: Survey is created with PENDING status and sent after delay. Args: patient: Patient instance hospital: Hospital instance patient_data: HIS patient data survey_template: SurveyTemplate instance - + Returns: SurveyInstance or None if failed """ + from apps.surveys.tasks import send_scheduled_survey + admission_id = patient_data.get("AdmissionID") discharge_date_str = patient_data.get("DischargeDate") - discharge_date = HISAdapter.parse_date(discharge_date_str) if discharge_date_str else None + patient_type = patient_data.get("PatientType") # Check if survey already sent for this admission existing_survey = SurveyInstance.objects.filter( @@ -222,58 +278,65 @@ class HISAdapter: ).first() if existing_survey: + logger.info(f"Survey already exists for admission {admission_id}") return existing_survey - # Create survey instance + # Get delay from SurveyTemplateMapping + delay_hours = HISAdapter.get_delay_for_patient_type(patient_type, hospital) + + # Calculate scheduled send time + scheduled_send_at = timezone.now() + timedelta(hours=delay_hours) + + # Create survey with PENDING status (NOT SENT) survey = SurveyInstance.objects.create( survey_template=survey_template, patient=patient, hospital=hospital, - status=SurveyStatus.SENT, # Set to SENT as it will be sent immediately - delivery_channel="SMS", # Send via SMS + status=SurveyStatus.PENDING, # Changed from SENT + delivery_channel="SMS", recipient_phone=patient.phone, recipient_email=patient.email, + scheduled_send_at=scheduled_send_at, metadata={ 'admission_id': admission_id, - 'patient_type': patient_data.get("PatientType"), + 'patient_type': patient_type, 'hospital_id': patient_data.get("HospitalID"), 'insurance_company': patient_data.get("InsuranceCompanyName"), - 'is_vip': patient_data.get("IsVIP") == "1" + 'is_vip': patient_data.get("IsVIP") == "1", + 'discharge_date': discharge_date_str, + 'scheduled_send_at': scheduled_send_at.isoformat(), + 'delay_hours': delay_hours, } ) - # Send survey via SMS - try: - from apps.surveys.services import SurveyDeliveryService - delivery_success = SurveyDeliveryService.deliver_survey(survey) - - if delivery_success: - return survey - else: - import logging - logger = logging.getLogger(__name__) - logger.warning(f"Survey created but SMS delivery failed for survey {survey.id}") - return survey - except Exception as e: - import logging - logger = logging.getLogger(__name__) - logger.error(f"Error sending survey SMS: {str(e)}", exc_info=True) - return survey + # Queue delayed send task + send_scheduled_survey.apply_async( + args=[str(survey.id)], + countdown=delay_hours * 3600 # Convert to seconds + ) + + logger.info( + f"Survey {survey.id} created for {patient_type}, " + f"will send in {delay_hours}h at {scheduled_send_at}" + ) + + return survey @staticmethod def process_his_data(his_data: Dict) -> Dict: """ Main method to process HIS patient data and send surveys. - + Simplified Flow: 1. Extract patient data 2. Get or create patient and hospital 3. Determine survey type from PatientType - 4. Create and send survey via SMS - + 4. Create survey with PENDING status + 5. Queue delayed send task + Args: his_data: HIS data in real format - + Returns: Dict with processing results """ @@ -282,77 +345,76 @@ class HISAdapter: 'message': '', 'patient': None, 'survey': None, - 'survey_sent': False + 'survey_queued': False } - + try: # Extract patient data patient_list = his_data.get("FetchPatientDataTimeStampList", []) - + if not patient_list: result['message'] = "No patient data found" return result - + patient_data = patient_list[0] - + # Validate status if his_data.get("Code") != 200 or his_data.get("Status") != "Success": result['message'] = f"HIS Error: {his_data.get('Message', 'Unknown error')}" return result - + # Check if patient is discharged (required for ALL patient types) patient_type = patient_data.get("PatientType") discharge_date_str = patient_data.get("DischargeDate") - + # All patient types require discharge date if not discharge_date_str: result['message'] = f'Patient type {patient_type} not discharged - no survey sent' result['success'] = True # Not an error, just no action needed return result - + # Get or create hospital hospital = HISAdapter.get_or_create_hospital(patient_data) if not hospital: result['message'] = "Could not determine hospital" return result - + # Get or create patient patient = HISAdapter.get_or_create_patient(patient_data, hospital) - + # Get survey template based on PatientType patient_type = patient_data.get("PatientType") survey_template = HISAdapter.get_survey_template(patient_type, hospital) - + if not survey_template: result['message'] = f"No survey template found for patient type '{patient_type}'" return result - - # Create and send survey + + # Create and queue survey (delayed sending) survey = HISAdapter.create_and_send_survey( patient, hospital, patient_data, survey_template ) - + if survey: - from apps.surveys.models import SurveyStatus - survey_sent = survey.status == SurveyStatus.SENT + # Survey is queued with PENDING status + survey_queued = survey.status == SurveyStatus.PENDING else: - survey_sent = False - + survey_queued = False + result.update({ 'success': True, 'message': 'Patient data processed successfully', 'patient': patient, 'patient_type': patient_type, 'survey': survey, - 'survey_sent': survey_sent, + 'survey_queued': survey_queued, + 'scheduled_send_at': survey.scheduled_send_at.isoformat() if survey and survey.scheduled_send_at else None, 'survey_url': survey.get_survey_url() if survey else None }) - + except Exception as e: - import logging - logger = logging.getLogger(__name__) logger.error(f"Error processing HIS data: {str(e)}", exc_info=True) result['message'] = f"Error processing HIS data: {str(e)}" result['success'] = False - + return result \ No newline at end of file diff --git a/apps/integrations/tasks.py b/apps/integrations/tasks.py index 12aa259..4302d32 100644 --- a/apps/integrations/tasks.py +++ b/apps/integrations/tasks.py @@ -208,6 +208,127 @@ def process_pending_events(): # ============================================================================= +@shared_task +def test_fetch_his_surveys_from_json(): + """ + TEST TASK - Fetch surveys from local JSON file instead of HIS API. + + This is a clone of fetch_his_surveys for testing purposes. + Reads from /home/ismail/projects/HH/data.json + + TODO: Remove this task after testing is complete. + + Returns: + dict: Summary of fetched and processed surveys + """ + import json + from pathlib import Path + from apps.integrations.services.his_adapter import HISAdapter + + logger.info("Starting TEST HIS survey fetch from JSON file") + + result = { + "success": False, + "patients_fetched": 0, + "surveys_created": 0, + "surveys_queued": 0, + "errors": [], + "details": [], + } + + try: + # Read JSON file + json_path = Path("/home/ismail/projects/HH/data.json") + if not json_path.exists(): + error_msg = f"JSON file not found: {json_path}" + logger.error(error_msg) + result["errors"].append(error_msg) + return result + + with open(json_path, 'r') as f: + his_data = json.load(f) + + # Extract patient list + patient_list = his_data.get("FetchPatientDataTimeStampList", []) + + if not patient_list: + logger.warning("No patient data found in JSON file") + result["errors"].append("No patient data found") + return result + + logger.info(f"Found {len(patient_list)} patients in JSON file") + result["patients_fetched"] = len(patient_list) + + # Process each patient + for patient_data in patient_list: + try: + # Wrap in proper format for HISAdapter + patient_payload = { + "FetchPatientDataTimeStampList": [patient_data], + "FetchPatientDataTimeStampVisitDataList": [], + "Code": 200, + "Status": "Success", + } + + # Process using HISAdapter + process_result = HISAdapter.process_his_data(patient_payload) + + if process_result["success"]: + result["surveys_created"] += 1 + + if process_result.get("survey_queued"): + result["surveys_queued"] += 1 + + # Log survey details + survey = process_result.get("survey") + if survey: + logger.info( + f"Survey queued for {patient_data.get('PatientName')}: " + f"Type={patient_data.get('PatientType')}, " + f"Scheduled={survey.scheduled_send_at}, " + f"Delay={process_result.get('metadata', {}).get('delay_hours', 'N/A')}h" + ) + else: + logger.info( + f"Survey created but not queued for {patient_data.get('PatientName')}" + ) + else: + # Not an error - patient may not be discharged + if "not discharged" in process_result.get("message", ""): + logger.debug( + f"Skipping {patient_data.get('PatientName')}: Not discharged" + ) + else: + logger.warning( + f"Failed to process {patient_data.get('PatientName')}: " + f"{process_result.get('message', 'Unknown error')}" + ) + result["errors"].append( + f"{patient_data.get('PatientName')}: {process_result.get('message')}" + ) + + except Exception as e: + error_msg = f"Error processing patient {patient_data.get('PatientName', 'Unknown')}: {str(e)}" + logger.error(error_msg, exc_info=True) + result["errors"].append(error_msg) + + result["success"] = True + + logger.info( + f"TEST HIS survey fetch completed: " + f"{result['patients_fetched']} patients, " + f"{result['surveys_created']} surveys created, " + f"{result['surveys_queued']} surveys queued" + ) + + except Exception as e: + error_msg = f"Fatal error in test_fetch_his_surveys_from_json: {str(e)}" + logger.error(error_msg, exc_info=True) + result["errors"].append(error_msg) + + return result + + @shared_task def fetch_his_surveys(): """ diff --git a/apps/integrations/ui_views.py b/apps/integrations/ui_views.py index b1acb6e..a4ea400 100644 --- a/apps/integrations/ui_views.py +++ b/apps/integrations/ui_views.py @@ -34,10 +34,14 @@ class SurveyTemplateMappingViewSet(viewsets.ModelViewSet): queryset = super().get_queryset() user = self.request.user - # If user is not superuser, filter by their hospital - if not user.is_superuser and user.hospital: + # Superusers and PX Admins see all mappings + if user.is_superuser or user.is_px_admin(): + return queryset + + # Hospital users filter by their assigned hospital + if user.hospital: queryset = queryset.filter(hospital=user.hospital) - elif not user.is_superuser and not user.hospital: + else: # User without hospital assignment - no access queryset = queryset.none() @@ -149,19 +153,28 @@ def survey_mapping_settings(request): # Get user's accessible hospitals based on role if user.is_superuser: # Superusers can see all hospitals - hospitals = Hospital.objects.all() + hospitals = Hospital.objects.filter(status='active') + elif user.is_px_admin(): + # PX Admins see all active hospitals for the dropdown + # They use session-based hospital selection (request.tenant_hospital) + hospitals = Hospital.objects.filter(status='active') elif user.hospital: # Regular users can only see their assigned hospital hospitals = Hospital.objects.filter(id=user.hospital.id) else: # User without hospital assignment - no access - hospitals = [] + hospitals = Hospital.objects.none() - # Get all mappings + # Get all mappings based on user role if user.is_superuser: mappings = SurveyTemplateMapping.objects.select_related( 'hospital', 'survey_template' ).all() + elif user.is_px_admin(): + # PX Admins see mappings for all hospitals (they manage all) + mappings = SurveyTemplateMapping.objects.select_related( + 'hospital', 'survey_template' + ).all() else: mappings = SurveyTemplateMapping.objects.filter( hospital__in=hospitals @@ -170,6 +183,9 @@ def survey_mapping_settings(request): # Group mappings by hospital mappings_by_hospital = {} for mapping in mappings: + # Skip mappings with missing hospital (orphaned records) + if mapping.hospital is None: + continue hospital_name = mapping.hospital.name if hospital_name not in mappings_by_hospital: mappings_by_hospital[hospital_name] = [] diff --git a/apps/surveys/models.py b/apps/surveys/models.py index 49d8cce..5a10a7c 100644 --- a/apps/surveys/models.py +++ b/apps/surveys/models.py @@ -237,6 +237,12 @@ class SurveyInstance(UUIDModel, TimeStampedModel, TenantModel): # Timestamps sent_at = models.DateTimeField(null=True, blank=True, db_index=True) + scheduled_send_at = models.DateTimeField( + null=True, + blank=True, + db_index=True, + help_text="When this survey should be sent (for delayed sending)" + ) opened_at = models.DateTimeField(null=True, blank=True) completed_at = models.DateTimeField(null=True, blank=True) diff --git a/apps/surveys/tasks.py b/apps/surveys/tasks.py index 4909de5..6481ef4 100644 --- a/apps/surveys/tasks.py +++ b/apps/surveys/tasks.py @@ -676,3 +676,86 @@ def send_bulk_surveys(self, job_id): raise self.retry(countdown=60 * (self.request.retries + 1)) return {'status': 'error', 'error': str(e)} + + +@shared_task +def send_scheduled_survey(survey_instance_id): + """ + Send a scheduled survey. + + This task is called after the delay period expires. + It sends the survey via the configured delivery channel (SMS/Email). + + Args: + survey_instance_id: UUID of the SurveyInstance to send + + Returns: + dict: Result with status and details + """ + from apps.surveys.models import SurveyInstance, SurveyStatus + from apps.surveys.services import SurveyDeliveryService + + try: + survey = SurveyInstance.objects.get(id=survey_instance_id) + + # Check if already sent + if survey.status != SurveyStatus.PENDING: + logger.warning(f"Survey {survey.id} already sent/cancelled (status: {survey.status})") + return {'status': 'skipped', 'reason': 'already_sent', 'survey_id': survey.id} + + # Check if scheduled time has passed + if survey.scheduled_send_at and survey.scheduled_send_at > timezone.now(): + logger.warning(f"Survey {survey.id} not due yet (scheduled: {survey.scheduled_send_at})") + return {'status': 'delayed', 'scheduled_at': survey.scheduled_send_at.isoformat(), 'survey_id': survey.id} + + # Send survey + success = SurveyDeliveryService.deliver_survey(survey) + + if success: + survey.status = SurveyStatus.SENT + survey.sent_at = timezone.now() + survey.save() + logger.info(f"Scheduled survey {survey.id} sent successfully") + return {'status': 'sent', 'survey_id': survey.id} + else: + survey.status = SurveyStatus.FAILED + survey.save() + logger.error(f"Scheduled survey {survey.id} delivery failed") + return {'status': 'failed', 'survey_id': survey.id, 'reason': 'delivery_failed'} + + except SurveyInstance.DoesNotExist: + logger.error(f"Survey {survey_instance_id} not found") + return {'status': 'error', 'reason': 'not_found'} + except Exception as e: + logger.error(f"Error sending scheduled survey: {e}", exc_info=True) + return {'status': 'error', 'reason': str(e)} + + +@shared_task +def send_pending_scheduled_surveys(): + """ + Periodic task to send any overdue scheduled surveys. + + Runs every 10 minutes as a safety net to catch any surveys + that weren't sent due to task failures or delays. + + Returns: + dict: Result with count of queued surveys + """ + from apps.surveys.models import SurveyInstance, SurveyStatus + + # Find surveys that should have been sent but weren't + overdue_surveys = SurveyInstance.objects.filter( + status=SurveyStatus.PENDING, + scheduled_send_at__lte=timezone.now() + )[:50] # Max 50 at a time + + sent_count = 0 + for survey in overdue_surveys: + send_scheduled_survey.delay(str(survey.id)) + sent_count += 1 + + if sent_count > 0: + logger.info(f"Queued {sent_count} overdue scheduled surveys") + + return {'queued': sent_count} diff --git a/apps/surveys/ui_views.py b/apps/surveys/ui_views.py index fbe85cd..62dba44 100644 --- a/apps/surveys/ui_views.py +++ b/apps/surveys/ui_views.py @@ -226,7 +226,13 @@ def survey_template_list(request): # Apply RBAC filters user = request.user if user.is_px_admin(): - pass + # PX Admins see templates for their selected hospital (from session) + tenant_hospital = getattr(request, 'tenant_hospital', None) + if tenant_hospital: + queryset = queryset.filter(hospital=tenant_hospital) + else: + # If no hospital selected, show none (user needs to select a hospital) + queryset = queryset.none() elif user.hospital: queryset = queryset.filter(hospital=user.hospital) else: diff --git a/config/celery.py b/config/celery.py index 4a28e82..baf4ab2 100644 --- a/config/celery.py +++ b/config/celery.py @@ -35,6 +35,16 @@ app.conf.beat_schedule = { 'expires': 240, # Task expires after 4 minutes if not picked up } }, + # TEST TASK - Fetch from JSON file (uncomment for testing, remove when done) + # 'test-fetch-his-surveys-from-json': { + # 'task': 'apps.integrations.tasks.test_fetch_his_surveys_from_json', + # 'schedule': crontab(minute='*/5'), # Every 5 minutes + # }, + # Send pending scheduled surveys every 10 minutes + 'send-pending-scheduled-surveys': { + 'task': 'apps.surveys.tasks.send_pending_scheduled_surveys', + 'schedule': crontab(minute='*/10'), # Every 10 minutes + }, # Check for overdue complaints every 15 minutes 'check-overdue-complaints': { 'task': 'apps.complaints.tasks.check_overdue_complaints', diff --git a/docs/SETUP_COMPLETE.md b/docs/SETUP_COMPLETE.md new file mode 100644 index 0000000..bcf8d7d --- /dev/null +++ b/docs/SETUP_COMPLETE.md @@ -0,0 +1,245 @@ +# Development Environment Setup - Complete! ✅ + +## Setup Summary + +The development environment has been successfully created with the following components: + +### 📊 Database Objects Created + +| Component | Count | +|-----------|-------| +| Organizations | 2 | +| Hospitals | 6 | +| Departments | 78 | +| Roles | 9 | +| PX Sources | 13 | +| Survey Templates | 22 | +| Survey Questions | 117 | +| Journey Templates | 15 | +| Journey Stages | 37 | +| Observation Categories | 15 | +| Notification Templates | 10 | +| Standard Sources | 4 | +| Standard Categories | 8 | + +### 🏥 Hospitals Created + +1. **NUZHA-DEV** - Al Hammadi Hospital - Nuzha (Development) + - 8 departments (ED, OPD, IP, ICU, Pharmacy, Lab, Radiology, Admin) + - 4 survey templates (Inpatient, OPD, EMS, Day Case) + - Journey templates for all patient types + - SLA configs, escalation rules, thresholds + +2. **OLAYA-DEV** - Al Hammadi Hospital - Olaya (Development) + - Same configuration as NUZHA-DEV + +3. **SUWAIDI-DEV** - Al Hammadi Hospital - Suwaidi (Development) + - Same configuration as NUZHA-DEV + +### 📝 Survey Templates + +**4 Template Types** per hospital: + +1. **Inpatient Post-Discharge Survey** + - 7 questions (nursing, doctor, cleanliness, food, information, NPS, comments) + +2. **OPD Patient Experience Survey** + - 6 questions (registration, waiting, consultation, pharmacy, NPS, comments) + +3. **EMS Emergency Services Survey** + - 6 questions (response time, paramedic, ED care, communication, NPS, comments) + +4. **Day Case Patient Survey** + - 6 questions (pre-procedure, procedure, post-procedure, discharge, NPS, comments) + +### 🔄 Journey Templates + +**OPD Journey Stages** (with HIS integration): +1. Registration (trigger: REGISTRATION) +2. Waiting (trigger: WAITING) +3. MD Consultation (trigger: Consultation) +4. MD Visit (trigger: Doctor Visited) +5. Clinical Assessment (trigger: Clinical Condtion) +6. Patient Assessment (trigger: ChiefComplaint) +7. Pharmacy (trigger: Prescribed Drugs) +8. Discharge (trigger: DISCHARGED) + +**Other Journey Types**: +- Inpatient Journey +- EMS Journey +- Day Case Journey + +### 🎭 Roles & Permissions + +| Role | Level | Description | +|------|-------|-------------| +| PX Admin | 100 | Full system access | +| Hospital Admin | 80 | Hospital-level access | +| Department Manager | 60 | Department-level access | +| PX Coordinator | 50 | PX actions & complaints | +| Physician | 40 | View feedback | +| Nurse | 30 | View department feedback | +| Staff | 20 | Basic access | +| Viewer | 10 | Read-only | +| PX Source User | 5 | External sources | + +### 📬 PX Sources + +**Internal Sources:** +- Patient +- Family Member +- Staff +- Survey + +**Government Sources:** +- Ministry of Health (MOH) +- Council of Cooperative Health Insurance (CCHI) + +### ⚙️ SLA Configurations + +| Source | SLA | 1st Reminder | 2nd Reminder | Escalation | +|--------|-----|--------------|--------------|------------| +| MOH | 24h | 12h | 18h | 24h | +| CCHI | 48h | 24h | 36h | 48h | +| Internal | 72h | 24h | 48h | 72h | + +### 🔔 Escalation Rules + +1. **Default**: Department Manager (immediate on overdue) +2. **Critical**: Hospital Admin (4h overdue) +3. **Final**: PX Admin (24h overdue) + +### 👁️ Observation Categories + +15 categories including: +- Patient Safety +- Clinical Quality +- Infection Control +- Medication Safety +- Equipment & Devices +- Facility & Environment +- Staff Behavior +- Communication +- Documentation +- Process & Workflow +- Security +- IT & Systems +- Housekeeping +- Food Services +- Other + +### 📨 Notification Templates + +10 templates for: +- Onboarding (Invitation, Reminder, Completion) +- Surveys (Invitation, Reminder) +- Complaints (Acknowledgment, Update) +- Actions (Assignment) +- SLA (Reminder, Breach) + +### ✅ Standards Setup + +**Standard Sources:** +- CBAHI (Saudi Central Board for Accreditation) +- JCI (Joint Commission International) +- ISO (International Organization for Standardization) +- SFDA (Saudi Food & Drug Authority) + +**Standard Categories:** +- Patient Safety +- Quality Management +- Infection Control +- Medication Safety +- Environment of Care +- Leadership +- Information Management +- Facility Management + +### 🔌 HIS Integration + +**API Configuration:** +- URL: `https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps` +- Auth: Basic (username/password from .env) +- Schedule: Every 5 minutes (Celery Beat) + +**Event Mappings:** +- Consultation → OPD_CONSULTATION +- Doctor Visited → OPD_DOCTOR_VISITED +- Clinical Condtion → CLINICAL_ASSESSMENT +- ChiefComplaint → PATIENT_ASSESSMENT +- Prescribed Drugs → PHARMACY +- DISCHARGED → PATIENT_DISCHARGED + +## 🚀 Next Steps + +### 1. Create Admin Users +```bash +python manage.py createsuperuser +``` + +### 2. Start Services +```bash +# Start Redis +redis-server + +# Start Celery Worker +celery -A config worker -l info + +# Start Celery Beat +celery -A config beat -l info + +# Start Django Server +python manage.py runserver +``` + +### 3. Verify Setup +Visit http://localhost:8000 and login with your superuser account. + +### 4. Load SHCT Taxonomy (Optional) +```bash +python manage.py load_shct_taxonomy +``` + +### 5. Import Staff Data (Optional) +```bash +python manage.py import_staff_csv path/to/staff.csv +``` + +## 📚 Documentation + +See `/docs/SETUP_GUIDE.md` for complete documentation. + +## 🔧 Useful Commands + +```bash +# Preview changes +python manage.py setup_dev_environment --dry-run + +# Setup specific hospital +python manage.py setup_dev_environment --hospital-code NUZHA-DEV + +# Skip specific components +python manage.py setup_dev_environment --skip-surveys --skip-integration + +# Reset and recreate (delete database first) +rm db.sqlite3 +python manage.py migrate +python manage.py setup_dev_environment +``` + +## ✨ Features Ready to Use + +1. ✅ Multi-hospital structure +2. ✅ Role-based access control +3. ✅ Survey system with 4 template types +4. ✅ HIS integration with 5-minute polling +5. ✅ Patient journey tracking +6. ✅ Complaint management with SLA +7. ✅ Observation system +8. ✅ Notification system +9. ✅ Standards compliance tracking +10. ✅ Escalation workflows + +--- + +**Environment is ready for development!** 🎉 diff --git a/docs/SETUP_GUIDE.md b/docs/SETUP_GUIDE.md new file mode 100644 index 0000000..63be050 --- /dev/null +++ b/docs/SETUP_GUIDE.md @@ -0,0 +1,362 @@ +# PX360 Development Environment Setup Guide + +## Overview + +This guide explains how to set up a complete development environment for PX360 using the `setup_dev_environment` management command. + +## What Gets Created + +### 1. Organization Structure +- **Organization**: Al Hammadi Healthcare Group (DEV) +- **3 Hospitals**: NUZHA-DEV, OLAYA-DEV, SUWAIDI-DEV +- **Departments**: Emergency, OPD, Inpatient, ICU, Pharmacy, Laboratory, Radiology, Administration + +### 2. User Roles & Permissions +- PX Admin (Full system access) +- Hospital Admin (Hospital-level access) +- Department Manager (Department-level access) +- PX Coordinator (PX actions & complaints) +- Physician (View feedback) +- Nurse (View department feedback) +- Staff (Basic access) +- Viewer (Read-only) +- PX Source User (External source users) + +### 3. Survey Templates (4 types) +1. **Inpatient Post-Discharge Survey** + - Nursing care + - Doctor's care + - Room cleanliness + - Food quality + - Treatment information + - NPS question + - Comments + +2. **OPD Patient Experience Survey** + - Registration process + - Waiting time + - Doctor consultation + - Pharmacy service + - NPS question + - Comments + +3. **EMS Emergency Services Survey** + - Ambulance response time + - Paramedic care + - Emergency department care + - Communication + - NPS question + - Comments + +4. **Day Case Patient Survey** + - Pre-procedure preparation + - Procedure quality + - Post-procedure care + - Discharge process + - NPS question + - Comments + +### 4. Complaint System Configuration +- **Complaint Categories**: Clinical Care, Management, Relationships, Facility, Communication, Access, Billing, Other +- **PX Sources**: Patient, Family Member, Staff, Survey, MOH, CCHI +- **SLA Configurations** (per hospital): + - MOH: 24 hours (reminders at 12h/18h) + - CCHI: 48 hours (reminders at 24h/36h) + - Internal: 72 hours (reminders at 24h/48h) +- **Escalation Rules** (per hospital): + - Default: Department Manager (immediate) + - Critical: Hospital Admin (4h overdue) + - Final: PX Admin (24h overdue) +- **Thresholds**: Resolution survey < 50% → Create PX Action +- **Explanation SLA**: 48 hours response time + +### 5. Journey Templates +**OPD Journey Stages**: +1. Registration (trigger: REGISTRATION) +2. Waiting (trigger: WAITING) +3. MD Consultation (trigger: Consultation) +4. MD Visit (trigger: Doctor Visited) +5. Clinical Assessment (trigger: Clinical Condition) +6. Patient Assessment (trigger: ChiefComplaint) +7. Pharmacy (trigger: Prescribed Drugs) +8. Discharge (trigger: DISCHARGED) + +**Other Journey Types**: +- Inpatient Journey +- EMS Journey +- Day Case Journey + +### 6. Survey Mappings +- Patient Type 1 (Inpatient) → Inpatient Survey +- Patient Type 2 (Outpatient) → OPD Survey +- Patient Type 3 (Emergency) → EMS Survey +- Patient Type 4 (Day Case) → Day Case Survey + +### 7. Observation Categories (15) +1. Patient Safety +2. Clinical Quality +3. Infection Control +4. Medication Safety +5. Equipment & Devices +6. Facility & Environment +7. Staff Behavior +8. Communication +9. Documentation +10. Process & Workflow +11. Security +12. IT & Systems +13. Housekeeping +14. Food Services +15. Other + +### 8. Notification Templates +- Onboarding Invitation +- Onboarding Reminder +- Onboarding Completion +- Survey Invitation +- Survey Reminder +- Complaint Acknowledgment +- Complaint Update +- Action Assignment +- SLA Reminder +- SLA Breach + +### 9. Standards Setup +**Standard Sources**: +- CBAHI (Saudi Central Board for Accreditation) +- JCI (Joint Commission International) +- ISO (International Organization for Standardization) + +**Standard Categories**: +- Patient Safety +- Quality Management +- Infection Control +- Medication Safety +- Environment of Care +- Leadership +- Information Management + +### 10. HIS Integration +- API URL: From `.env` (HIS_API_URL) +- Username: From `.env` (HIS_API_USERNAME) +- Password: From `.env` (HIS_API_PASSWORD) +- Event Mappings configured for OPD workflow + +## Usage + +### Basic Setup (All Components) +```bash +python manage.py setup_dev_environment +``` + +### Dry Run (Preview Only) +```bash +python manage.py setup_dev_environment --dry-run +``` + +### Setup Specific Hospital +```bash +python manage.py setup_dev_environment --hospital-code NUZHA-DEV +``` + +### Skip Specific Components +```bash +# Skip surveys +python manage.py setup_dev_environment --skip-surveys + +# Skip complaints +python manage.py setup_dev_environment --skip-complaints + +# Skip journeys +python manage.py setup_dev_environment --skip-journeys + +# Skip HIS integration +python manage.py setup_dev_environment --skip-integration + +# Combine multiple skips +python manage.py setup_dev_environment --skip-surveys --skip-integration +``` + +## Environment Variables Required + +Add these to your `.env` file: + +```env +# HIS Integration +HIS_API_URL=https://his.alhammadi.med.sa:54380/SSRCE/API/FetchPatientVisitTimeStamps +HIS_API_USERNAME=AlhhSUNZHippo +HIS_API_PASSWORD=*#$@PAlhh^2106 + +# Database +DATABASE_URL=sqlite:///db.sqlite3 + +# Redis/Celery +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 + +# SMS Gateway +SMS_API_URL=http://localhost:8000/api/simulator/send-sms/ +SMS_API_KEY=simulator-test-key +SMS_ENABLED=True +SMS_PROVIDER=console + +# Email Gateway +EMAIL_API_URL=http://localhost:8000/api/simulator/send-email/ +EMAIL_API_KEY=simulator-test-key +EMAIL_ENABLED=True +EMAIL_PROVIDER=console + +# AI Configuration +OPENROUTER_API_KEY=your-api-key-here +AI_MODEL=stepfun/step-3.5-flash:free +AI_TEMPERATURE=0.3 +AI_MAX_TOKENS=500 +``` + +## Post-Setup Steps + +### 1. Create Admin Users +After running the setup, create admin users for each hospital: + +```bash +python manage.py createsuperuser +``` + +Then assign them to hospitals via the admin interface or: +```python +from apps.accounts.models import HospitalUser +from apps.organizations.models import Hospital +from django.contrib.auth import get_user + +User = get_user_model() +hospital = Hospital.objects.get(code='NUZHA-DEV') +user = User.objects.get(email='admin@example.com') + +HospitalUser.objects.create( + user=user, + hospital=hospital, + role='hospital_admin' +) +``` + +### 2. Start Celery Workers +```bash +# Start Celery worker +celery -A config worker -l info + +# Start Celery beat (for scheduled tasks) +celery -A config beat -l info +``` + +### 3. Verify Setup +```bash +# Check organization +python manage.py shell +>>> from apps.organizations.models import Organization +>>> Organization.objects.count() +1 + +# Check hospitals +>>> from apps.organizations.models import Hospital +>>> Hospital.objects.count() +3 + +# Check survey templates +>>> from apps.surveys.models import SurveyTemplate +>>> SurveyTemplate.objects.count() +12 # 4 templates × 3 hospitals +``` + +### 4. Test HIS Integration +```bash +python manage.py test_his_connection +``` + +### 5. Run Initial HIS Sync +```bash +python manage.py fetch_his_surveys +``` + +## Idempotent Operation + +The command is **idempotent** - it can be run multiple times safely: +- Uses `get_or_create()` for all models +- Won't create duplicates +- Updates existing records if needed +- Safe to re-run after errors + +## Troubleshooting + +### Issue: "No module named 'django'" +**Solution**: Activate virtual environment +```bash +source .venv/bin/activate +``` + +### Issue: "Command not found" +**Solution**: Run from project root +```bash +cd /path/to/HH +python manage.py setup_dev_environment +``` + +### Issue: Database locked +**Solution**: Stop all running processes and try again +```bash +pkill -f celery +pkill -f python +python manage.py setup_dev_environment +``` + +### Issue: Permission denied +**Solution**: Check file permissions +```bash +chmod +x manage.py +``` + +## Next Steps After Setup + +1. **Configure SMS Gateway** (for production) +2. **Configure Email Gateway** (for production) +3. **Load SHCT Taxonomy** (detailed complaint categories) +4. **Import Staff Data** (via CSV import commands) +5. **Set Up Department Managers** (via admin interface) +6. **Configure HIS Integration** (fine-tune event mappings) +7. **Create Additional Survey Templates** (as needed) +8. **Set Up Standards** (add CBAHI/JCI standards) +9. **Configure Notification Templates** (add SMS/email content) +10. **Test Complete Workflow** (create test complaint → resolve → survey) + +## Related Management Commands + +```bash +# Load SHCT complaint taxonomy +python manage.py load_shct_taxonomy + +# Seed departments +python manage.py seed_departments + +# Import staff from CSV +python manage.py import_staff_csv path/to/staff.csv + +# Create notification templates +python manage.py init_notification_templates + +# Create appreciation category +python manage.py create_patient_feedback_category + +# Seed observation categories +python manage.py seed_observation_categories + +# Seed acknowledgement categories +python manage.py seed_acknowledgements +``` + +## Support + +For issues or questions: +1. Check logs: `tail -f logs/debug.log` +2. Check Celery logs +3. Review environment variables +4. Check database integrity +5. Contact: support@px360.sa diff --git a/templates/analytics/command_center.html b/templates/analytics/command_center.html index d8ded1f..cc4ca8e 100644 --- a/templates/analytics/command_center.html +++ b/templates/analytics/command_center.html @@ -111,20 +111,6 @@ - -
- - -
-
@@ -463,7 +449,7 @@ function handleDateRangeChange() { function updateFilters() { currentFilters.date_range = document.getElementById('dateRange').value; - currentFilters.hospital = document.getElementById('hospitalFilter').value; + currentFilters.hospital = '{{ current_hospital.id|default:"" }}'; currentFilters.department = document.getElementById('departmentFilter').value; currentFilters.kpi_category = document.getElementById('kpiCategoryFilter').value; currentFilters.custom_start = document.getElementById('customStart').value; @@ -669,7 +655,6 @@ function refreshDashboard() { function resetFilters() { document.getElementById('dateRange').value = '30d'; - document.getElementById('hospitalFilter').value = ''; document.getElementById('departmentFilter').value = ''; document.getElementById('kpiCategoryFilter').value = ''; document.getElementById('customStart').value = ''; diff --git a/templates/analytics/dashboard.html b/templates/analytics/dashboard.html index 8c72fc7..95bcf06 100644 --- a/templates/analytics/dashboard.html +++ b/templates/analytics/dashboard.html @@ -240,14 +240,6 @@

{% trans "Comprehensive overview of patient experience metrics" %}

- diff --git a/templates/analytics/kpi_report_generate.html b/templates/analytics/kpi_report_generate.html index 47589eb..adf1168 100644 --- a/templates/analytics/kpi_report_generate.html +++ b/templates/analytics/kpi_report_generate.html @@ -58,19 +58,7 @@

- -
- - -
+
diff --git a/templates/analytics/kpi_report_list.html b/templates/analytics/kpi_report_list.html index f68129b..f08fe85 100644 --- a/templates/analytics/kpi_report_list.html +++ b/templates/analytics/kpi_report_list.html @@ -123,20 +123,6 @@
- {% if request.user.is_px_admin %} -
- - -
- {% endif %} -
- - + + +
@@ -240,31 +234,26 @@ {{ block.super }} {% endblock %} diff --git a/templates/appreciation/leaderboard.html b/templates/appreciation/leaderboard.html index dcc28ad..40e7c8b 100644 --- a/templates/appreciation/leaderboard.html +++ b/templates/appreciation/leaderboard.html @@ -47,17 +47,6 @@ {% endfor %} -
- - -
-
- - -
-
- - + + +
@@ -283,7 +279,6 @@ {% block extra_js %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/journeys/instance_list.html b/templates/journeys/instance_list.html index de84cbf..f59c7ac 100644 --- a/templates/journeys/instance_list.html +++ b/templates/journeys/instance_list.html @@ -150,18 +150,6 @@
-
- - -
-
- - {% for hospital in hospitals %} - - {% endfor %} - -
- {% endif %} -
- - {% for hospital in hospitals %} - - {% endfor %} - -
-
- - -

@@ -297,18 +284,8 @@ document.getElementById('searchInput')?.addEventListener('keypress', function(e) let url = '?'; if (value) url += 'search=' + encodeURIComponent(value); {% if filters.status %}url += '&status={{ filters.status }}';{% endif %} - {% if filters.hospital %}url += '&hospital={{ filters.hospital }}';{% endif %} window.location.href = url; } }); - -// Hospital filter -document.getElementById('hospitalFilter')?.addEventListener('change', function() { - let url = '?'; - if (this.value) url += 'hospital=' + this.value; - {% if filters.search %}url += '&search={{ filters.search }}';{% endif %} - {% if filters.status %}url += '&status={{ filters.status }}';{% endif %} - window.location.href = url; -}); {% endblock %} \ No newline at end of file diff --git a/templates/reports/report_builder.html b/templates/reports/report_builder.html index 3e275e3..56c2c2e 100644 --- a/templates/reports/report_builder.html +++ b/templates/reports/report_builder.html @@ -76,17 +76,6 @@ - -

- - -
-
@@ -225,7 +214,6 @@ const CSRF_TOKEN = '{{ csrf_token }}'; // DOM elements let dataSourceSelect = null; let dateRangeSelect = null; -let hospitalFilter = null; let departmentFilter = null; let statusFilter = null; let previewBtn = null; @@ -244,7 +232,6 @@ document.addEventListener('DOMContentLoaded', function() { // Initialize DOM element references dataSourceSelect = document.getElementById('dataSource'); dateRangeSelect = document.getElementById('dateRange'); - hospitalFilter = document.getElementById('hospitalFilter'); departmentFilter = document.getElementById('departmentFilter'); statusFilter = document.getElementById('statusFilter'); previewBtn = document.getElementById('previewBtn'); @@ -261,8 +248,10 @@ document.addEventListener('DOMContentLoaded', function() { // Event listeners if (dataSourceSelect) dataSourceSelect.addEventListener('change', loadFilterOptions); if (dateRangeSelect) dateRangeSelect.addEventListener('change', toggleCustomDateRange); - if (hospitalFilter) hospitalFilter.addEventListener('change', loadDepartments); if (previewBtn) previewBtn.addEventListener('click', generateReport); + + // Load departments for current hospital on page load + loadDepartments(); if (saveBtn) saveBtn.addEventListener('click', showSaveModal); if (cancelSaveBtn) cancelSaveBtn.addEventListener('click', hideSaveModal); if (confirmSaveBtn) confirmSaveBtn.addEventListener('click', saveReport); @@ -385,7 +374,7 @@ function getSelectedColumns() { } async function loadDepartments() { - const hospitalId = hospitalFilter.value; + const hospitalId = '{{ current_hospital.id|default:"" }}'; if (!hospitalId) { departmentFilter.innerHTML = ''; return; @@ -437,7 +426,7 @@ async function generateReport() { date_range: dateRangeSelect.value, date_start: dateRange.start, date_end: dateRange.end, - hospital: hospitalFilter.value, + hospital: '{{ current_hospital.id|default:"" }}', department: departmentFilter.value, status: statusFilter.value, }, @@ -983,7 +972,7 @@ async function saveReport() { date_range: dateRangeSelect.value, date_start: dateRange.start, date_end: dateRange.end, - hospital: hospitalFilter.value, + hospital: '{{ current_hospital.id|default:"" }}', department: departmentFilter.value, status: statusFilter.value, }, diff --git a/templates/surveys/comment_list.html b/templates/surveys/comment_list.html index 1c34422..0e6499a 100644 --- a/templates/surveys/comment_list.html +++ b/templates/surveys/comment_list.html @@ -132,20 +132,6 @@
-
- - -
-
diff --git a/templates/surveys/instance_list.html b/templates/surveys/instance_list.html index ec2e425..052ad55 100644 --- a/templates/surveys/instance_list.html +++ b/templates/surveys/instance_list.html @@ -60,18 +60,6 @@
-
- - -
-
{% trans "Clear" %} diff --git a/templates/surveys/template_list.html b/templates/surveys/template_list.html index c8a1af7..62ec3d5 100644 --- a/templates/surveys/template_list.html +++ b/templates/surveys/template_list.html @@ -126,11 +126,16 @@
+ {% if is_px_admin and not current_hospital %} +

{% trans "No Hospital Selected" %}

+

{% trans "Please select a hospital from the dropdown in the header to view survey templates." %}

+ {% else %}

{% trans "No Templates Found" %}

{% trans "Get started by creating your first survey template." %}

{% trans "Create Survey Template" %} + {% endif %}
{% endif %}