import random import uuid from datetime import timedelta from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone from apps.accounts.models import User from apps.complaints.models import Complaint, ComplaintCategory, ComplaintUpdate, Inquiry, InquiryUpdate from apps.organizations.models import Hospital, Department, Staff from apps.observations.models import Observation, ObservationCategory, ObservationNote from apps.px_sources.models import PXSource ENGLISH_COMPLAINTS_STAFF = [ { "title": "Rude behavior from nurse during shift", "description": "I was extremely disappointed by the rude behavior of the nurse {staff_name} during the night shift on {date}. She was dismissive and unprofessional when I asked for pain medication.", "category": "staff_behavior", "severity": "critical", "priority": "urgent", }, { "title": "Physician misdiagnosed my condition", "description": "Dr. {staff_name} misdiagnosed my condition and prescribed wrong medication. I had to suffer for 3 more days before another doctor caught the error.", "category": "clinical_care", "severity": "critical", "priority": "urgent", }, { "title": "Nurse ignored call button for over 30 minutes", "description": "Despite pressing the call button multiple times, nurse {staff_name} did not respond for over 30 minutes. When she finally arrived, she was annoyed and unhelpful.", "category": "staff_behavior", "severity": "high", "priority": "high", }, { "title": "Physician did not explain treatment plan clearly", "description": "Dr. {staff_name} did not take the time to explain my diagnosis or treatment plan. He was rushing and seemed impatient with my questions.", "category": "clinical_care", "severity": "high", "priority": "high", }, { "title": "Nurse made medication error", "description": "Nurse {staff_name} attempted to give me medication meant for another patient. I only noticed because the name on the label was different. This is a serious safety concern.", "category": "clinical_care", "severity": "critical", "priority": "urgent", }, { "title": "Admin staff was unhelpful with billing inquiry", "description": "The administrative staff member {staff_name} was extremely unhelpful when I asked questions about my bill. She was dismissive and refused to explain the charges properly.", "category": "communication", "severity": "medium", "priority": "medium", }, ] ENGLISH_COMPLAINTS_GENERAL = [ { "title": "Long wait time in emergency room", "description": "I had to wait over 4 hours in the emergency room despite being in severe pain. The lack of attention and delay in treatment was unacceptable.", "category": "wait_time", "severity": "high", "priority": "high", }, { "title": "Room was not clean upon admission", "description": "When I was admitted to my room, it was not properly cleaned. There was dust on the surfaces and the bathroom was not sanitary.", "category": "facility", "severity": "medium", "priority": "medium", }, { "title": "Air conditioning not working properly", "description": "The air conditioning in my room was not working for 2 days. Despite multiple complaints to staff, nothing was done.", "category": "facility", "severity": "medium", "priority": "medium", }, { "title": "Billing statement has incorrect charges", "description": "My billing statement contains charges for procedures and medications I never received. I have tried to resolve this issue multiple times.", "category": "billing", "severity": "high", "priority": "high", }, { "title": "Difficulty getting prescription refills", "description": "Getting prescription refills has been extremely difficult. The process is unclear and there is poor communication between the pharmacy and doctors.", "category": "communication", "severity": "medium", "priority": "medium", }, { "title": "Food quality has declined", "description": "The quality of hospital food has significantly declined. Meals are often cold, not appetizing, and don't meet dietary requirements.", "category": "facility", "severity": "medium", "priority": "medium", }, { "title": "Poor discharge process and instructions", "description": "The discharge process was chaotic. I was given contradictory instructions about my medication and follow-up care. Nobody explained the next steps clearly.", "category": "communication", "severity": "high", "priority": "high", }, ] ARABIC_COMPLAINTS_STAFF = [ { "title": "سلوك غير مهذب من الممرضة أثناء المناوبة", "description": "كنت محبطاً جداً من السلوك غير المهذب للممرضة {staff_name} خلال المناوبة الليلية في {date}. كانت متجاهلة وغير مهنية عندما طلبت دواء للم.", "category": "staff_behavior", "severity": "critical", "priority": "urgent", }, { "title": "الطبيب تشخص خطأ في حالتي", "description": "تشخص د. {staff_name} خطأ في حالتي ووصف دواء خاطئ. اضطررت للمعاناة لمدة 3 أيام إضافية قبل أن يكتشف طبيب آخر الخطأ.", "category": "clinical_care", "severity": "critical", "priority": "urgent", }, { "title": "الممرضة تجاهلت زر الاستدعاء لأكثر من 30 دقيقة", "description": "على الرغم من الضغط على زر الاستدعاء عدة مرات، لم تستجب الممرضة {staff_name} لأكثر من 30 دقيقة.", "category": "staff_behavior", "severity": "high", "priority": "high", }, { "title": "الطبيب لم يوضح خطة العلاج بوضوح", "description": "د. {staff_name} لم يأخذ الوقت لتوضيح تشخيصي أو خطة العلاج. كان يتسرع ويبدو متضايقاً من أسئلتي.", "category": "clinical_care", "severity": "high", "priority": "high", }, { "title": "الممرضة ارتكبت خطأ في الدواء", "description": "حاولت الممرضة {staff_name} إعطائي دواء مخصص لمريض آخر. لاحظت ذلك فقط لأن الاسم على الملصق مختلف.", "category": "clinical_care", "severity": "critical", "priority": "urgent", }, { "title": "موظف الإدارة كان غير مفيد في استفسار الفوترة", "description": "كان موظف الإدارة {staff_name} غير مفيد جداً عندما سألت عن فاتورتي. كان متجاهلاً ورفض توضيح الرسوم بشكل صحيح.", "category": "communication", "severity": "medium", "priority": "medium", }, ] ARABIC_COMPLAINTS_GENERAL = [ { "title": "وقت انتظار طويل في الطوارئ", "description": "اضطررت للانتظار أكثر من 4 ساعات في غرفة الطوارئ رغم أنني كنت أعاني من ألم شديد. عدم الانتباه والتأخير في العلاج غير مقبول.", "category": "wait_time", "severity": "high", "priority": "high", }, { "title": "الغرفة لم تكن نظيفة عند القبول", "description": "عندما تم قبولي في غرفتي، لم تكن نظيفة بشكل صحيح. كان هناك غبار على الأسطح وحمام غير صحي.", "category": "facility", "severity": "medium", "priority": "medium", }, { "title": "التكييف لا يعمل بشكل صحيح", "description": "لم يكن التكييف في غرفتي يعمل لمدة يومين. على الرغم من شكاوى متعددة للموظفين، لم يتم فعل شيء.", "category": "facility", "severity": "medium", "priority": "medium", }, { "title": "كشف الفاتورة يحتوي على رسوم غير صحيحة", "description": "كشف فاتورتي يحتوي على رسوم لإجراءات وأدوية لم أتلقها أبداً. حاولت حل هذه المشكلة عدة مرات لكن لم أتلق أي مساعدة.", "category": "billing", "severity": "high", "priority": "high", }, { "title": "صعوبة الحصول على وصفات طبية", "description": "الحصول على وصفات طبية كان صعباً للغاية. العملية غير واضحة وهناك تواصل سيء بين الصيدلية والأطباء.", "category": "communication", "severity": "medium", "priority": "medium", }, { "title": "جودة الطعام انخفضت", "description": "جودة طعام المستشفى انخفضت بشكل كبير. الوجبات غالباً باردة وغير شهية ولا تلبي المتطلبات الغذائية.", "category": "facility", "severity": "medium", "priority": "medium", }, { "title": "عملية الخروج فوضوية وغير واضحة", "description": "كانت عملية الخروج فوضوية. أعطيت تعليمات متناقضة حول الدواء والمتابعة. لم يشرح أحد الخطوات التالية بوضوح.", "category": "communication", "severity": "high", "priority": "high", }, ] PATIENT_NAMES_EN = [ "John Smith", "Sarah Johnson", "Ahmed Al-Rashid", "Fatima Hassan", "Michael Brown", "Layla Al-Otaibi", "David Wilson", "Nora Al-Dosari", "James Taylor", "Aisha Al-Qahtani", ] PATIENT_NAMES_AR = [ "محمد العتيبي", "فاطمة الدوسري", "أحمد القحطاني", "سارة الشمري", "خالد الحربي", "نورة المطيري", "عبدالله العنزي", "مريم الزهراني", "سعود الشهري", "هند السالم", ] INQUIRY_TITLES = [ {"en": "Question about appointment booking", "ar": "سؤال عن حجز موعد", "category": "appointment"}, {"en": "Inquiry about insurance coverage", "ar": "استفسار عن التغطية التأمينية", "category": "billing"}, {"en": "Request for medical records", "ar": "طلب ملف طبي", "category": "medical_records"}, {"en": "Information about hospital services", "ar": "معلومات عن خدمات المستشفى", "category": "general"}, {"en": "Question about doctor availability", "ar": "سؤال عن توفر الأطباء", "category": "appointment"}, {"en": "Inquiry about test results", "ar": "استفسار عن نتائج الفحوصات", "category": "medical_records"}, {"en": "Request for price list", "ar": "طلب قائمة الأسعار", "category": "billing"}, {"en": "Question about visiting hours", "ar": "سؤال عن مواعيد الزيارة", "category": "general"}, {"en": "Inquiry about specialized treatment", "ar": "استفسار عن علاج تخصصي", "category": "general"}, {"en": "Request for second opinion", "ar": "طلب رأي ثاني", "category": "general"}, {"en": "Question about discharge process", "ar": "سؤال عن عملية الخروج", "category": "general"}, { "en": "Inquiry about medication side effects", "ar": "استفسار عن الآثار الجانبية للأدوية", "category": "medical_records", }, {"en": "Request for dietary information", "ar": "طلب معلومات غذائية", "category": "general"}, {"en": "Question about transportation", "ar": "سؤال عن وسائل النقل", "category": "general"}, {"en": "Inquiry about follow-up appointments", "ar": "استفسار عن مواعيد المتابعة", "category": "appointment"}, {"en": "Inquiry about patient rights", "ar": "استفسار عن حقوق المرضى", "category": "general"}, {"en": "Question about hospital policies", "ar": "سؤال عن سياسات المستشفى", "category": "general"}, { "en": "Inquiry about international patient services", "ar": "استفسار عن خدمات المرضى الدوليين", "category": "general", }, ] OBSERVATION_TEMPLATES = [ { "title_en": "Hand hygiene not followed before patient contact", "title_ar": "عدم الالتزام بالنظافة اليدوية قبل ملامسة المريض", "severity": "high", "description_en": "Observed healthcare worker entering patient room without performing hand hygiene.", "description_ar": "لوح عامل رعاية صحية يدخل غرفة المريض دون أداء النظافة اليدوية.", }, { "title_en": "PPE not properly worn in isolation area", "title_ar": "عدم ارتداء معدات الوقاية بشكل صحيح في منطقة العزل", "severity": "critical", "description_en": "Staff in isolation area not wearing proper PPE including masks and gowns.", "description_ar": "الموظفون في منطقة العزل لا يرتدون معدات الوقاية المناسبة بما في ذلك الأقنعة والأرواب.", }, { "title_en": "Medication storage at incorrect temperature", "title_ar": "تخزين الأدوية في درجة حرارة غير صحيحة", "severity": "high", "description_en": "Found medication refrigerator not maintaining required temperature range.", "description_ar": "ثبت أن ثلاجة الأدوية لا تحافظ على نطاق درجة الحرارة المطلوب.", }, { "title_en": "Patient identification bands missing", "title_ar": "عدم وجود أساور تعريف المرضى", "severity": "critical", "description_en": "Multiple patients in ward found without proper identification bands.", "description_ar": "وجد عدة مرضى في الجناح بدون أساور تعريف مناسبة.", }, { "title_en": "Expired supplies found in treatment room", "title_ar": "العثور على مستلزمات منتهية الصلاحية في غرفة العلاج", "severity": "medium", "description_en": "Several medical supplies past their expiration date found during routine check.", "description_ar": "العثور على عدة مستلزمات طبية منتهية الصلاحية أثناء الفحص الروتيني.", }, { "title_en": "Fall risk assessment not completed", "title_ar": "لم يتم إكمال تقييم خطر السقوط", "severity": "high", "description_en": "Elderly patient admitted without documented fall risk assessment.", "description_ar": "تم قبول مريض مسن بدون تقييم موثق لخطر السقوط.", }, { "title_en": "Cleanliness issue in outpatient waiting area", "title_ar": "مشكلة نظافة في منطقة انتظار العيادات الخارجية", "severity": "low", "description_en": "Waiting area floor and seating not properly cleaned between patient visits.", "description_ar": "أرضية ومنطقة الجلوس في منطقة الانتظار لم يتم تنظيفها بشكل صحيح بين زيارات المرضى.", }, { "title_en": "Incorrect waste segregation observed", "title_ar": "لوح سوء فرز النفايات", "severity": "medium", "description_en": "Medical and general waste being disposed in the same containers.", "description_ar": "التخلص من النفايات الطبية والعامة في نفس الحاويات.", }, { "title_en": "Emergency exit blocked by equipment", "title_ar": "مخرج الطوارئ مسدود بالمعدات", "severity": "critical", "description_en": "Emergency exit in corridor blocked by stored medical equipment.", "description_ar": "مخرج الطوارئ في الممر مسدود بالمعدات الطبية المخزنة.", }, { "title_en": "Improper IV line labeling", "title_ar": "وضع ملصقات غير صحيحة على خطوط الوريد", "severity": "high", "description_en": "IV lines not labeled with installation date and time as required.", "description_ar": "خطوط الوريد غير مُعلَّمة بتاريخ ووقت التركيب كما هو مطلوب.", }, { "title_en": "Fire extinguisher expired in nursing station", "title_ar": "طفاية حريق منتهية الصلاحية في محطة التمريض", "severity": "medium", "description_en": "Fire extinguisher past inspection date found at main nursing station.", "description_ar": "طفاية حريق تجاوزت تاريخ الفحص في محطة التمريض الرئيسية.", }, { "title_en": "Sharps container overfilled", "title_ar": "حاوية الأدوات الحادة ممتلئة", "severity": "high", "description_en": "Sharps container in procedure room filled beyond the safe fill line.", "description_ar": "حاوية الأدوات الحادة في غرفة الإجراءات ممتلئة فوق خط الملء الآمن.", }, { "title_en": "Patient left unattended in corridor", "title_ar": "مريض متروك دون مراقبة في الممر", "severity": "medium", "description_en": "Patient on wheelchair found alone in corridor for extended period without staff nearby.", "description_ar": "وجد مريض على كرسي متحرك بمفرده في الممر لفترة طويلة بدون موظفين في الجوار.", }, { "title_en": "Infection control sign missing from isolation room", "title_ar": "لافتحة مكافحة العدوى مفقودة من غرفة العزل", "severity": "medium", "description_en": "Required infection control signage not displayed at isolation room entrance.", "description_ar": "لافتحات مكافحة العدوى المطلوبة غير معروضة عند مدخل غرفة العزل.", }, { "title_en": "Oxygen supply equipment not regularly checked", "title_ar": "معدات إمداد الأكسجين لا يتم فحصها بانتظام", "severity": "high", "description_en": "Documentation shows oxygen supply equipment has not been inspected per schedule.", "description_ar": "الوثائق تظهر أن معدات إمداد الأكسجين لم يتم فحصها وفق الجدول.", }, ] RESOLUTION_TEXTS = [ "The issue has been investigated and resolved. Corrective actions have been implemented and staff have been briefed on proper procedures.", "Patient was contacted and offered a sincere apology. Compensation was provided and process improvements were made to prevent recurrence.", "The complaint was investigated by the department manager. The staff member received additional training and the issue has been fully resolved.", "After thorough review, the matter has been addressed. New protocols have been established to ensure this does not happen again.", "The issue was resolved after coordination between departments. An action plan has been implemented and monitoring is in place.", "Staff counseling was provided and the workflow has been updated. Patient expressed satisfaction with the resolution.", "Management reviewed the case and implemented systemic changes. All involved staff were briefed and monitoring continues.", ] class Command(BaseCommand): help = "Seed sample complaints, observations, and inquiries with varied statuses and dates" def add_arguments(self, parser): parser.add_argument("--complaints", type=int, default=30, help="Number of complaints (default: 30)") parser.add_argument("--observations", type=int, default=20, help="Number of observations (default: 20)") parser.add_argument("--inquiries", type=int, default=15, help="Number of inquiries (default: 15)") parser.add_argument("--hospital-code", type=str, help="Specific hospital code") parser.add_argument("--months-back", type=int, default=6, help="How far back to spread dates (default: 6)") parser.add_argument("--dry-run", action="store_true", help="Preview without creating") parser.add_argument("--clear", action="store_true", help="Delete existing sample data first") def handle(self, *args, **options): self.dry_run = options["dry_run"] self.months_back = options["months_back"] self.cutoff_date = timezone.now() - timedelta(days=self.months_back * 30) self.stdout.write(f"\n{'=' * 60}") self.stdout.write("Sample Data Seeding Command") self.stdout.write(f"{'=' * 60}\n") hospitals = self._get_hospitals(options["hospital_code"]) if not hospitals: return px_coordinators = User.objects.filter(groups__name="PX Coordinator", is_active=True) if not px_coordinators.exists(): self.stdout.write( self.style.WARNING("No PX Coordinator users found. Unassigned items will have no assignee.") ) px_coordinators = User.objects.filter(groups__name="Hospital Admin", is_active=True) all_staff = Staff.objects.filter(status="active") complaint_categories = ComplaintCategory.objects.filter(is_active=True) obs_categories = ObservationCategory.objects.filter(is_active=True) sources = PXSource.objects.filter(is_active=True) if not sources.exists(): self._ensure_pxsources() sources = PXSource.objects.filter(is_active=True) if not obs_categories.exists(): self.stdout.write( self.style.WARNING( "No observation categories found. Run setup_dev_environment or seed_observation_categories first." ) ) return if self.dry_run: self.stdout.write(self.style.WARNING("DRY RUN MODE\n")) self._print_config(options) if options["clear"]: self._clear_sample_data() complaints_created = self._seed_complaints( options["complaints"], hospitals, px_coordinators, all_staff, complaint_categories, sources ) observations_created = self._seed_observations( options["observations"], hospitals, px_coordinators, obs_categories ) inquiries_created = self._seed_inquiries(options["inquiries"], hospitals, px_coordinators, sources) self._print_summary(complaints_created, observations_created, inquiries_created) def _get_hospitals(self, hospital_code): if hospital_code: hospitals = Hospital.objects.filter(code=hospital_code) if not hospitals.exists(): self.stdout.write(self.style.ERROR(f"Hospital with code '{hospital_code}' not found")) return [] else: hospitals = Hospital.objects.filter(status="active") if not hospitals.exists(): self.stdout.write(self.style.ERROR("No active hospitals found.")) return [] self.stdout.write(self.style.SUCCESS(f"Hospitals: {hospitals.count()}")) return list(hospitals) def _ensure_pxsources(self): for key, (name_en, name_ar) in PX_SOURCE_MAP.items(): PXSource.objects.get_or_create(name_en=name_en, defaults={"name_ar": name_ar, "is_active": True}) def _print_config(self, options): self.stdout.write("Configuration:") self.stdout.write(f" Complaints: {options['complaints']}") self.stdout.write(f" Observations: {options['observations']}") self.stdout.write(f" Inquiries: {options['inquiries']}") self.stdout.write(f" Date spread: Last {options['months_back']} months") self.stdout.write(f" Dry run: {self.dry_run}") self.stdout.write("") def _clear_sample_data(self): if self.dry_run: self.stdout.write(self.style.WARNING("Would clear sample data (dry run)")) return c = Complaint.objects.filter(reference_number__startswith="SEED-").count() o = Observation.objects.filter(tracking_code__startswith="SEED-").count() i = Inquiry.objects.filter(subject__startswith="[SEED]").count() Complaint.objects.filter(reference_number__startswith="SEED-").delete() Observation.objects.filter(tracking_code__startswith="SEED-").delete() Inquiry.objects.filter(subject__startswith="[SEED]").delete() self.stdout.write(self.style.SUCCESS(f"Cleared: {c} complaints, {o} observations, {i} inquiries")) def _random_date(self, max_days_ago=None, min_days_ago=None): if max_days_ago is None: max_days_ago = self.months_back * 30 if min_days_ago is None: min_days_ago = 0 days_ago = random.randint(min_days_ago, max(min_days_ago, self.months_back * 30)) return timezone.now() - timedelta(days=days_ago) def _seed_complaints(self, count, hospitals, px_coordinators, all_staff, categories, sources): self.stdout.write("\n--- Complaints ---") complaint_statuses = [ ("open", 0.15), ("in_progress", 0.20), ("contacted", 0.10), ("partially_resolved", 0.08), ("resolved", 0.17), ("closed", 0.20), ("contacted_no_response", 0.05), ("cancelled", 0.05), ] statuses, weights = zip(*complaint_statuses) created = [] for i in range(count): hospital = random.choice(hospitals) status = random.choices(statuses, weights=weights, k=1)[0] is_arabic = random.random() < 0.70 if random.random() < 0.6 and all_staff.exists(): hospital_staff = all_staff.filter(hospital=hospital) staff_member = random.choice(hospital_staff) if hospital_staff.exists() else random.choice(all_staff) templates = ARABIC_COMPLAINTS_STAFF if is_arabic else ENGLISH_COMPLAINTS_STAFF else: staff_member = None templates = ARABIC_COMPLAINTS_GENERAL if is_arabic else ENGLISH_COMPLAINTS_GENERAL template = random.choice(templates) description = template["description"] if staff_member: name = ( f"{staff_member.first_name_ar} {staff_member.last_name_ar}" if is_arabic else f"{staff_member.first_name} {staff_member.last_name}" ) description = description.format(staff_name=name, date=self._random_date(60).strftime("%Y-%m-%d")) days_ago = self._days_for_status(status)[0] created_at = self._random_date(days_ago, days_ago // 2) category = random.choice(categories) if categories.exists() else None source = random.choice(sources) if sources.exists() else None dept = staff_member.department if staff_member else None ref = f"SEED-{hospital.code}-{str(uuid.uuid4())[:8].upper()}" patient_names = PATIENT_NAMES_AR if is_arabic else PATIENT_NAMES_EN contact_name = random.choice(patient_names) if self.dry_run: self.stdout.write(f" Would create: [{status}] {template['title'][:60]}") created.append({"status": status, "lang": "ar" if is_arabic else "en"}) continue complaint = Complaint( reference_number=ref, hospital=hospital, department=dept, category=category, title=template["title"], description=description, severity=template["severity"], priority=template["priority"], source=source, status=status, contact_name=contact_name, contact_phone=f"+9665{random.randint(10000000, 99999999)}", created_at=created_at, updated_at=created_at, ) if staff_member: complaint.staff = staff_member if status not in ("open",): if status in ("open",): coordinator = random.choice(px_coordinators) if px_coordinators.exists() else None complaint.assigned_to = coordinator complaint.assigned_at = created_at + timedelta(minutes=random.randint(5, 60)) else: complaint.assigned_to = random.choice(px_coordinators) if px_coordinators.exists() else None complaint.assigned_at = created_at + timedelta(minutes=random.randint(5, 60)) complaint.activated_at = created_at + timedelta(minutes=random.randint(30, 120)) if status in ("resolved", "closed"): resolved_days = max(1, days_ago // 2) complaint.resolved_at = created_at + timedelta(days=resolved_days) complaint.resolved_by = complaint.assigned_to complaint.resolution = random.choice(RESOLUTION_TEXTS) if status == "closed": closed_days = max(1, days_ago // 3) complaint.closed_at = created_at + timedelta(days=closed_days) complaint.closed_by = complaint.assigned_to complaint.save() self._create_complaint_timeline(complaint, created_at, days_ago) created.append(complaint) self.stdout.write(self.style.SUCCESS(f"Created {len(created)} complaints")) self._print_status_breakdown([c.status for c in created]) return created def _days_for_status(self, status): mapping = { "open": (0, 7), "in_progress": (1, 30), "contacted": (2, 20), "partially_resolved": (10, 40), "resolved": (15, 60), "closed": (30, 180), "contacted_no_response": (10, 45), "cancelled": (20, 90), } return mapping.get(status, (1, 30)) def _create_complaint_timeline(self, complaint, created_at, days_ago): ComplaintUpdate.objects.create( complaint=complaint, update_type="note", message="Complaint received and registered", created_by=None, created_at=created_at, ) if complaint.status in ( "in_progress", "contacted", "partially_resolved", "resolved", "closed", "contacted_no_response", ): ComplaintUpdate.objects.create( complaint=complaint, update_type="status_change", message=f"Complaint activated and assigned to {complaint.assigned_to.get_full_name() if complaint.assigned_to else 'PX Coordinator'}", old_status="open", new_status="in_progress", created_by=complaint.assigned_to, created_at=complaint.assigned_at if complaint.assigned_at else created_at + timedelta(minutes=5), ) if complaint.status in ("resolved", "closed"): ComplaintUpdate.objects.create( complaint=complaint, update_type="status_change", message="Complaint resolved after investigation. Corrective actions taken.", old_status="in_progress", new_status="resolved", created_by=complaint.resolved_by, created_at=complaint.resolved_at if complaint.resolved_at else created_at, ) if complaint.status == "closed": ComplaintUpdate.objects.create( complaint=complaint, update_type="status_change", message="Complaint closed after verification.", old_status="resolved", new_status="closed", created_by=complaint.closed_by, created_at=complaint.closed_at if complaint.closed_at else created_at, ) if complaint.status == "contacted_no_response": ComplaintUpdate.objects.create( complaint=complaint, update_type="note", message="Staff member has not responded to explanation request. Follow-up required.", created_by=complaint.assigned_to, created_at=created_at + timedelta(days=max(1, days_ago // 2)), ) def _seed_observations(self, count, hospitals, px_coordinators, obs_categories): self.stdout.write("\n--- Observations ---") obs_statuses = [ ("new", 0.15), ("triaged", 0.10), ("assigned", 0.10), ("in_progress", 0.15), ("contacted", 0.05), ("contacted_no_response", 0.02), ("resolved", 0.15), ("closed", 0.20), ("rejected", 0.05), ("duplicate", 0.03), ] statuses, weights = zip(*obs_statuses) created = [] for i in range(count): hospital = random.choice(hospitals) status = random.choices(statuses, weights=weights, k=1)[0] template = random.choice(OBSERVATION_TEMPLATES) is_arabic = random.random() < 0.70 days_ago = self._obs_days_for_status(status)[0] created_at = self._random_date(days_ago, days_ago // 2) tracking_code = f"SEED-{hospital.code}-{str(uuid.uuid4())[:6].upper()}" category = random.choice(obs_categories) if obs_categories.exists() else None title = template["title_ar"] if is_arabic else template["title_en"] description = template["description_ar"] if is_arabic else template["description_en"] if self.dry_run: self.stdout.write(f" Would create: [{status}] {title[:60]}") created.append({"status": status}) continue obs = Observation( hospital=hospital, tracking_code=tracking_code, title=title, description=description, severity=template["severity"], category=category, status=status, source="staff_portal", incident_datetime=created_at, reporter_name=random.choice(PATIENT_NAMES_AR if is_arabic else PATIENT_NAMES_EN), created_at=created_at, updated_at=created_at, ) if status not in ("new",): obs.assigned_to = random.choice(px_coordinators) if px_coordinators.exists() else None if status in ("resolved", "closed"): obs.resolved_at = created_at + timedelta(days=max(1, days_ago // 2)) obs.resolved_by = obs.assigned_to obs.resolution_notes = random.choice(RESOLUTION_TEXTS) if status == "closed": obs.closed_at = created_at + timedelta(days=max(1, days_ago // 3)) obs.closed_by = obs.assigned_to obs.save() self._create_observation_notes(obs, created_at, days_ago, is_arabic) created.append(obs) self.stdout.write(self.style.SUCCESS(f"Created {len(created)} observations")) self._print_status_breakdown([o.status for o in created]) return created def _obs_days_for_status(self, status): mapping = { "new": (0, 7), "triaged": (3, 14), "assigned": (5, 20), "in_progress": (10, 30), "contacted": (5, 15), "contacted_no_response": (10, 30), "resolved": (15, 60), "closed": (30, 180), "rejected": (10, 45), "duplicate": (20, 90), } return mapping.get(status, (1, 30)) def _create_observation_notes(self, obs, created_at, days_ago, is_arabic): if is_arabic: notes = { "new": "ملاحظة جديدة مسجلة وبانتظار المراجعة.", "triaged": "تم تصنيف الملاحظة وتحديد أولوية المعالجة.", "in_progress": "الملاحظة قيد التحقيق حالياً.", "resolved": "تم حل الملاحظة واتخاذ الإجراءات التصحيحية.", "closed": "تم إغلاق الملاحظة بعد التحقق.", "rejected": "تم رفض الملاحظة بعد المراجعة.", "duplicate": "تم تحديد هذه الملاحظة كنسخة مكررة.", "contacted": "تم التواصل مع القسم المعني لمتابعة الملاحظة.", "contacted_no_response": "لم يتم الرد من القسم المعني. مطلوب متابعة.", "assigned": "تم تعيين الملاحظة لمسؤول للمعالجة.", } else: notes = { "new": "New observation registered and pending review.", "triaged": "Observation triaged and priority level assigned.", "in_progress": "Observation is currently under investigation.", "resolved": "Observation resolved with corrective actions taken.", "closed": "Observation closed after verification.", "rejected": "Observation rejected after review.", "duplicate": "Observation marked as duplicate of an existing one.", "contacted": "Department contacted for follow-up on observation.", "contacted_no_response": "No response from department. Follow-up required.", "assigned": "Observation assigned for investigation and resolution.", } ObservationNote.objects.create( observation=obs, note=notes.get(obs.status, "Observation created."), created_by=obs.assigned_to, created_at=created_at, ) if obs.status in ("resolved", "closed"): ObservationNote.objects.create( observation=obs, note=random.choice(RESOLUTION_TEXTS)[:200], created_by=obs.resolved_by, created_at=obs.resolved_at if obs.resolved_at else created_at, ) def _seed_inquiries(self, count, hospitals, px_coordinators, sources): self.stdout.write("\n--- Inquiries ---") inquiry_statuses = [ ("open", 0.15), ("in_progress", 0.20), ("contacted", 0.10), ("contacted_no_response", 0.05), ("resolved", 0.25), ("closed", 0.25), ] statuses, weights = zip(*inquiry_statuses) created = [] for i in range(count): hospital = random.choice(hospitals) status = random.choices(statuses, weights=weights, k=1)[0] is_arabic = random.random() < 0.70 template = random.choice(INQUIRY_TITLES) days_ago = self._inquiry_days_for_status(status)[0] created_at = self._random_date(days_ago, days_ago // 2) subject = template["ar"] if is_arabic else f"[SEED] {template['en']}" category = template["category"] message = f"This is a {category} inquiry regarding {template['en'].lower()}. The patient is requesting information and assistance with their healthcare needs at the hospital." if is_arabic: message = f"هذا استفسار {template['ar']} يتعلق بطلب معلومات ومساعدة في الاحتياجات الصحية." source = random.choice(sources) if sources.exists() else None if self.dry_run: self.stdout.write(f" Would create: [{status}] {template['en'][:60]}") created.append({"status": status}) continue inquiry = Inquiry( hospital=hospital, subject=subject, message=message, category=category, status=status, source=source, contact_name=random.choice(PATIENT_NAMES_AR if is_arabic else PATIENT_NAMES_EN), contact_phone=f"+9665{random.randint(10000000, 99999999)}", created_at=created_at, updated_at=created_at, ) if status not in ("open",): inquiry.assigned_to = random.choice(px_coordinators) if px_coordinators.exists() else None inquiry.assigned_at = created_at + timedelta(minutes=random.randint(5, 60)) if status in ("resolved", "closed"): inquiry.response = random.choice(RESOLUTION_TEXTS) if status == "closed": pass inquiry.save() self._create_inquiry_updates(inquiry, created_at, days_ago) created.append(inquiry) self.stdout.write(self.style.SUCCESS(f"Created {len(created)} inquiries")) self._print_status_breakdown([i.status for i in created]) return created def _inquiry_days_for_status(self, status): mapping = { "open": (0, 7), "in_progress": (1, 30), "contacted": (2, 20), "contacted_no_response": (10, 30), "resolved": (15, 90), "closed": (30, 180), } return mapping.get(status, (1, 30)) def _create_inquiry_updates(self, inquiry, created_at, days_ago): InquiryUpdate.objects.create( inquiry=inquiry, update_type="note", message="Inquiry received and registered", created_by=None, created_at=created_at, ) if inquiry.status in ("in_progress", "contacted", "contacted_no_response", "resolved", "closed"): InquiryUpdate.objects.create( inquiry=inquiry, update_type="note", message=f"Inquiry assigned to {inquiry.assigned_to.get_full_name() if inquiry.assigned_to else 'PX Coordinator'} for handling.", created_by=inquiry.assigned_to, created_at=inquiry.assigned_at if inquiry.assigned_at else created_at + timedelta(minutes=5), ) if inquiry.status in ("resolved", "closed"): InquiryUpdate.objects.create( inquiry=inquiry, update_type="note", message="Inquiry resolved. Response sent to the inquirer.", created_by=inquiry.assigned_to, created_at=created_at + timedelta(days=max(1, days_ago // 2)), ) if inquiry.status == "closed": InquiryUpdate.objects.create( inquiry=inquiry, update_type="note", message="Inquiry closed after follow-up confirmation.", created_by=inquiry.assigned_to, created_at=created_at + timedelta(days=max(1, days_ago // 3)), ) def _print_status_breakdown(self, statuses_list): from collections import Counter counts = Counter(statuses_list) for status, count in sorted(counts.items()): self.stdout.write(f" {status}: {count}") self.stdout.write("") def _print_summary(self, complaints, observations, inquiries): self.stdout.write(f"\n{'=' * 60}") self.stdout.write("Summary:") self.stdout.write(f" Complaints: {len(complaints)}") self.stdout.write(f" Observations: {len(observations)}") self.stdout.write(f" Inquiries: {len(inquiries)}") self.stdout.write(f" Total: {len(complaints) + len(observations) + len(inquiries)}") self.stdout.write(f"{'=' * 60}") if self.dry_run: self.stdout.write(self.style.WARNING("\nDRY RUN: No changes were made\n")) else: self.stdout.write(self.style.SUCCESS("\nSample data seeding completed successfully!\n")) PX_SOURCE_MAP = { "patient": ("Patient", "مريض"), "family": ("Family Member", "عضو العائلة"), "staff": ("Staff", "موظف"), "call_center": ("Call Center", "مركز الاتصال"), "online": ("Online Form", "نموذج عبر الإنترنت"), "in_person": ("In Person", "شخصياً"), "survey": ("Survey", "استبيان"), "social_media": ("Social Media", "وسائل التواصل الاجتماعي"), }