953 lines
45 KiB
Python
953 lines
45 KiB
Python
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", "وسائل التواصل الاجتماعي"),
|
|
}
|