HH/apps/complaints/management/commands/seed_complaints.py

571 lines
27 KiB
Python

"""
Management command to seed complaint data with bilingual support (English and Arabic)
"""
import random
import uuid
from datetime import timedelta
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from apps.accounts.models import User
from apps.complaints.models import Complaint, ComplaintCategory, ComplaintUpdate
from apps.organizations.models import Hospital, Department, Staff
from apps.px_sources.models import PXSource
# English complaint templates
ENGLISH_COMPLAINTS = {
'staff_mentioned': [
{
'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. Her attitude made my hospital experience very unpleasant.',
'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. This negligence is unacceptable and needs to be addressed immediately.',
'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. This level of neglect is unacceptable in a healthcare setting.',
'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. I felt dismissed and anxious about my treatment.',
'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 that needs immediate investigation.',
'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. This poor customer service reflects badly on the hospital.',
'category': 'communication',
'severity': 'medium',
'priority': 'medium'
},
{
'title': 'Nurse was compassionate and helpful',
'description': 'I want to express my appreciation for nurse {staff_name} who went above and beyond to make me comfortable during my stay. Her kind and caring demeanor made a difficult situation much more bearable.',
'category': 'staff_behavior',
'severity': 'low',
'priority': 'low'
},
{
'title': 'Physician provided excellent care',
'description': 'Dr. {staff_name} provided exceptional care and took the time to thoroughly explain my condition and treatment options. His expertise and bedside manner were outstanding.',
'category': 'clinical_care',
'severity': 'low',
'priority': 'low'
}
],
'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 for an emergency situation.',
'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. This is concerning for patient safety.',
'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. The room was uncomfortably hot which affected my recovery.',
'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 but have not received any assistance.',
'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. This has caused delays in my treatment.',
'category': 'communication',
'severity': 'medium',
'priority': 'medium'
},
{
'title': 'Parking is inadequate for visitors',
'description': 'There is very limited parking available for visitors. I had to circle multiple times to find a spot and was late for my appointment. This needs to be addressed.',
'category': 'facility',
'severity': 'low',
'priority': 'low'
},
{
'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. This affects patient satisfaction.',
'category': 'facility',
'severity': 'medium',
'priority': 'medium'
}
]
}
# Arabic complaint templates
ARABIC_COMPLAINTS = {
'staff_mentioned': [
{
'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'
},
{
'title': 'الممرضة كانت متعاطفة ومساعدة',
'description': 'أريد أن أعبر عن تقديري للممرضة {staff_name} التي بذلت ما هو أبعد من المتوقع لجعلي مرتاحاً خلال إقامتي. كلمتها اللطيفة والراعية جعلت الموقف الصعب أكثر قابلية للتحمل.',
'category': 'staff_behavior',
'severity': 'low',
'priority': 'low'
},
{
'title': 'الطبيب قدم رعاية ممتازة',
'description': 'قدم د. {staff_name} رعاية استثنائية وأخذ الوقت لتوضيح حالتي وخيارات العلاج بدقة. كانت خبرته وأسلوبه مع المرضى ممتازين.',
'category': 'clinical_care',
'severity': 'low',
'priority': 'low'
}
],
'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': 'low',
'priority': 'low'
},
{
'title': 'جودة الطعام انخفضت',
'description': 'جودة طعام المستشفى انخفضت بشكل كبير. الوجبات غالباً باردة وغير شهية ولا تلبي المتطلبات الغذائية. هذا يؤثر على رضا المرضى.',
'category': 'facility',
'severity': 'medium',
'priority': 'medium'
}
]
}
# Patient names for complaints
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 = [
'محمد العتيبي', 'فاطمة الدوسري', 'أحمد القحطاني', 'سارة الشمري',
'خالد الحربي', 'نورة المطيري', 'عبدالله العنزي', 'مريم الزهراني',
'سعود الشهري', 'هند السالم'
]
# Source mapping for PXSource
SOURCE_MAPPING = {
'patient': ('Patient', 'مريض'),
'family': ('Family Member', 'عضو العائلة'),
'staff': ('Staff', 'موظف'),
'call_center': ('Call Center', 'مركز الاتصال'),
'online': ('Online Form', 'نموذج عبر الإنترنت'),
'in_person': ('In Person', 'شخصياً'),
'survey': ('Survey', 'استبيان'),
'social_media': ('Social Media', 'وسائل التواصل الاجتماعي'),
}
# Categories mapping
CATEGORY_MAP = {
'clinical_care': 'الرعاية السريرية',
'staff_behavior': 'سلوك الموظفين',
'facility': 'المرافق والبيئة',
'wait_time': 'وقت الانتظار',
'billing': 'الفواتير',
'communication': 'التواصل',
'other': 'أخرى'
}
class Command(BaseCommand):
help = 'Seed complaint data with bilingual support (English and Arabic)'
def add_arguments(self, parser):
parser.add_argument(
'--count',
type=int,
default=10,
help='Number of complaints to create (default: 10)'
)
parser.add_argument(
'--arabic-percent',
type=int,
default=70,
help='Percentage of Arabic complaints (default: 70)'
)
parser.add_argument(
'--hospital-code',
type=str,
help='Target hospital code (default: all hospitals)'
)
parser.add_argument(
'--staff-mention-percent',
type=int,
default=60,
help='Percentage of staff-mentioned complaints (default: 60)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Preview without making changes'
)
parser.add_argument(
'--clear',
action='store_true',
help='Clear existing complaints first'
)
def handle(self, *args, **options):
count = options['count']
arabic_percent = options['arabic_percent']
hospital_code = options['hospital_code']
staff_mention_percent = options['staff_mention_percent']
dry_run = options['dry_run']
clear_existing = options['clear']
self.stdout.write(f"\n{'='*60}")
self.stdout.write("Complaint Data Seeding Command")
self.stdout.write(f"{'='*60}\n")
with transaction.atomic():
# Get hospitals
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. Please create hospitals first.")
)
return
self.stdout.write(
self.style.SUCCESS(f"Found {hospitals.count()} hospital(s)")
)
# Get all categories
all_categories = ComplaintCategory.objects.filter(is_active=True)
if not all_categories.exists():
self.stdout.write(
self.style.ERROR("No complaint categories found. Please run seed_complaint_configs first.")
)
return
# Get all staff
all_staff = Staff.objects.filter(status='active')
if not all_staff.exists():
self.stdout.write(
self.style.WARNING("No staff found. Staff-mentioned complaints will not have linked staff.")
)
# Ensure PXSource instances exist
self.ensure_pxsources()
# Display configuration
self.stdout.write("\nConfiguration:")
self.stdout.write(f" Total complaints to create: {count}")
arabic_count = int(count * arabic_percent / 100)
english_count = count - arabic_count
self.stdout.write(f" Arabic complaints: {arabic_count} ({arabic_percent}%)")
self.stdout.write(f" English complaints: {english_count} ({100-arabic_percent}%)")
staff_mentioned_count = int(count * staff_mention_percent / 100)
general_count = count - staff_mentioned_count
self.stdout.write(f" Staff-mentioned: {staff_mentioned_count} ({staff_mention_percent}%)")
self.stdout.write(f" General: {general_count} ({100-staff_mention_percent}%)")
self.stdout.write(f" Status: All OPEN")
self.stdout.write(f" Dry run: {dry_run}")
# Clear existing complaints if requested
if clear_existing:
if dry_run:
self.stdout.write(
self.style.WARNING(f"\nWould delete {Complaint.objects.count()} existing complaints")
)
else:
deleted_count = Complaint.objects.count()
Complaint.objects.all().delete()
self.stdout.write(
self.style.SUCCESS(f"\n✓ Deleted {deleted_count} existing complaints")
)
# Track created complaints
created_complaints = []
by_language = {'en': 0, 'ar': 0}
by_type = {'staff_mentioned': 0, 'general': 0}
# Create complaints
for i in range(count):
# Determine language (alternate based on percentage)
is_arabic = i < arabic_count
lang = 'ar' if is_arabic else 'en'
# Determine type (staff-mentioned vs general)
is_staff_mentioned = random.random() < (staff_mention_percent / 100)
complaint_type = 'staff_mentioned' if is_staff_mentioned else 'general'
# Select hospital (round-robin through available hospitals)
hospital = hospitals[i % len(hospitals)]
# Select staff if needed
staff_member = None
if is_staff_mentioned and all_staff.exists():
# Try to find staff from same hospital
hospital_staff = all_staff.filter(hospital=hospital)
if hospital_staff.exists():
staff_member = random.choice(hospital_staff)
else:
staff_member = random.choice(all_staff)
# Get complaint templates for language and type
templates = ARABIC_COMPLAINTS[complaint_type] if is_arabic else ENGLISH_COMPLAINTS[complaint_type]
template = random.choice(templates)
# Get category
category_code = template['category']
category = all_categories.filter(code=category_code).first()
# Prepare complaint data
complaint_data = self.prepare_complaint_data(
template=template,
staff_member=staff_member,
category=category,
hospital=hospital,
is_arabic=is_arabic,
i=i
)
if dry_run:
self.stdout.write(
f" Would create: {complaint_data['title']} ({lang.upper()}) - {complaint_type}"
)
created_complaints.append({
'title': complaint_data['title'],
'language': lang,
'type': complaint_type
})
else:
# Create complaint
complaint = Complaint.objects.create(**complaint_data)
# Create timeline entry
self.create_timeline_entry(complaint)
created_complaints.append(complaint)
# Track statistics
by_language[lang] += 1
by_type[complaint_type] += 1
# Summary
self.stdout.write("\n" + "="*60)
self.stdout.write("Summary:")
self.stdout.write(f" Total complaints created: {len(created_complaints)}")
self.stdout.write(f" Arabic: {by_language['ar']}")
self.stdout.write(f" English: {by_language['en']}")
self.stdout.write(f" Staff-mentioned: {by_type['staff_mentioned']}")
self.stdout.write(f" General: {by_type['general']}")
self.stdout.write("="*60 + "\n")
if dry_run:
self.stdout.write(self.style.WARNING("DRY RUN: No changes were made\n"))
else:
self.stdout.write(self.style.SUCCESS("Complaint seeding completed successfully!\n"))
def prepare_complaint_data(self, template, staff_member, category, hospital, is_arabic, i):
"""Prepare complaint data from template"""
# Generate description with staff name if applicable
description = template['description']
if staff_member:
staff_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=staff_name, date=timezone.now().date())
# Generate reference number
reference = self.generate_reference_number(hospital.code)
# Generate patient name
patient_names = PATIENT_NAMES_AR if is_arabic else PATIENT_NAMES_EN
patient_name = patient_names[i % len(patient_names)]
# Generate contact info
contact_method = random.choice(['email', 'phone', 'both'])
if contact_method == 'email':
email = f"patient{i}@example.com"
phone = ""
elif contact_method == 'phone':
email = ""
phone = f"+9665{random.randint(10000000, 99999999)}"
else:
email = f"patient{i}@example.com"
phone = f"+9665{random.randint(10000000, 99999999)}"
# Select source key
source_key = random.choice(list(SOURCE_MAPPING.keys()))
source_instance = self.get_source_instance(source_key)
# Get department (if staff member exists, use their department)
department = staff_member.department if staff_member else None
# Prepare complaint data
data = {
'reference_number': reference,
'hospital': hospital,
'department': department,
'category': category,
'title': template['title'],
'description': description,
'severity': template['severity'],
'priority': template['priority'],
'source': source_instance,
'status': 'open',
'contact_name': patient_name,
'contact_phone': phone,
'contact_email': email,
'staff': staff_member,
}
return data
def generate_reference_number(self, hospital_code):
"""Generate unique complaint reference number"""
short_uuid = str(uuid.uuid4())[:8].upper()
year = timezone.now().year
return f"CMP-{hospital_code}-{year}-{short_uuid}"
def create_timeline_entry(self, complaint):
"""Create initial timeline entry for complaint"""
ComplaintUpdate.objects.create(
complaint=complaint,
update_type='status_change',
old_status='',
new_status='open',
message='Complaint created and registered',
created_by=None # System-created
)
def ensure_pxsources(self):
"""Ensure all required PXSource instances exist"""
for source_key, (name_en, name_ar) in SOURCE_MAPPING.items():
PXSource.objects.get_or_create(
name_en=name_en,
defaults={
'name_ar': name_ar,
'description': f'{name_en} source for complaints and inquiries',
'is_active': True
}
)
def get_source_instance(self, source_key):
"""Get PXSource instance by source key"""
name_en, _ = SOURCE_MAPPING.get(source_key, ('Other', 'أخرى'))
try:
return PXSource.objects.get(name_en=name_en, is_active=True)
except PXSource.DoesNotExist:
# Fallback to first active source
return PXSource.objects.filter(is_active=True).first()