1528 lines
65 KiB
Python
1528 lines
65 KiB
Python
import os
|
|
import django
|
|
|
|
# Set up Django environment
|
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
|
|
django.setup()
|
|
|
|
# Django setup will be handled by manage.py when running this script
|
|
# Import Django-related modules after Django is set up
|
|
import random
|
|
from datetime import datetime, date, time, timedelta
|
|
from django.utils import timezone as django_timezone
|
|
from django.contrib.auth import get_user_model
|
|
from appointments.models import (
|
|
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
|
|
TelemedicineSession, AppointmentTemplate, WaitingList, WaitingListContactLog,
|
|
QueueConfiguration, QueueMetrics
|
|
)
|
|
from patients.models import PatientProfile
|
|
from core.models import Tenant
|
|
from hr.models import Employee, Department
|
|
import uuid
|
|
|
|
from accounts.models import User
|
|
|
|
# Saudi-specific healthcare data
|
|
SAUDI_SPECIALTIES = [
|
|
'FAMILY_MEDICINE', 'INTERNAL_MEDICINE', 'PEDIATRICS', 'CARDIOLOGY',
|
|
'DERMATOLOGY', 'ENDOCRINOLOGY', 'GASTROENTEROLOGY', 'NEUROLOGY',
|
|
'ONCOLOGY', 'ORTHOPEDICS', 'PSYCHIATRY', 'RADIOLOGY', 'SURGERY',
|
|
'UROLOGY', 'GYNECOLOGY', 'OPHTHALMOLOGY', 'ENT', 'EMERGENCY'
|
|
]
|
|
|
|
APPOINTMENT_TYPES = [
|
|
'CONSULTATION', 'FOLLOW_UP', 'PROCEDURE', 'SURGERY', 'DIAGNOSTIC',
|
|
'THERAPY', 'VACCINATION', 'SCREENING', 'EMERGENCY', 'TELEMEDICINE'
|
|
]
|
|
|
|
|
|
SAUDI_CHIEF_COMPLAINTS = [
|
|
'Chest pain and shortness of breath',
|
|
'Abdominal pain and nausea',
|
|
'Headache and dizziness',
|
|
'Back pain and stiffness',
|
|
'Fever and cough',
|
|
'Joint pain and swelling',
|
|
'Fatigue and weakness',
|
|
'Skin rash and itching',
|
|
'Diabetes follow-up',
|
|
'Hypertension monitoring',
|
|
'Regular health checkup',
|
|
'Vaccination appointment',
|
|
'Pre-operative consultation',
|
|
'Post-operative follow-up',
|
|
'Pregnancy consultation',
|
|
'Child wellness visit',
|
|
'Mental health consultation',
|
|
'Physical therapy session',
|
|
'Diagnostic imaging',
|
|
'Laboratory test follow-up'
|
|
]
|
|
|
|
SAUDI_LOCATIONS = [
|
|
'Main Building - Floor 1',
|
|
'Main Building - Floor 2',
|
|
'Main Building - Floor 3',
|
|
'Emergency Wing',
|
|
'Outpatient Clinic - Wing A',
|
|
'Outpatient Clinic - Wing B',
|
|
'Surgical Suite - Floor 4',
|
|
'Radiology Department',
|
|
'Laboratory Building',
|
|
'Pediatric Wing'
|
|
]
|
|
|
|
TELEMEDICINE_PLATFORMS = ['ZOOM', 'TEAMS', 'WEBEX', 'DOXY', 'CUSTOM']
|
|
|
|
|
|
def create_mock_providers(tenants):
|
|
"""Create mock healthcare providers for demonstration"""
|
|
providers = []
|
|
|
|
mock_providers_data = [
|
|
{'first_name': 'Ahmad', 'last_name': 'Al-Rashid', 'role': 'PHYSICIAN', 'specialty': 'CARDIOLOGY'},
|
|
{'first_name': 'Fatima', 'last_name': 'Al-Ghamdi', 'role': 'PHYSICIAN', 'specialty': 'PEDIATRICS'},
|
|
{'first_name': 'Mohammed', 'last_name': 'Al-Otaibi', 'role': 'PHYSICIAN', 'specialty': 'FAMILY_MEDICINE'},
|
|
{'first_name': 'Sarah', 'last_name': 'Al-Harbi', 'role': 'PHYSICIAN', 'specialty': 'INTERNAL_MEDICINE'},
|
|
{'first_name': 'Khalid', 'last_name': 'Al-Mutairi', 'role': 'SURGEON', 'specialty': 'SURGERY'},
|
|
{'first_name': 'Nora', 'last_name': 'Al-Zahrani', 'role': 'PHYSICIAN', 'specialty': 'RADIOLOGY'},
|
|
{'first_name': 'Omar', 'last_name': 'Al-Dawsari', 'role': 'PHYSICIAN', 'specialty': 'EMERGENCY'},
|
|
{'first_name': 'Layla', 'last_name': 'Al-Subai', 'role': 'PHYSICIAN', 'specialty': 'GYNECOLOGY'},
|
|
]
|
|
|
|
for tenant in tenants:
|
|
for provider_data in mock_providers_data:
|
|
try:
|
|
# Check if user already exists
|
|
email = f"{provider_data['first_name'].lower()}.{provider_data['last_name'].lower().replace('-', '')}@{tenant.domain}"
|
|
existing_user = User.objects.filter(email=email).first()
|
|
|
|
if not existing_user:
|
|
user = User.objects.create_user(
|
|
email=email,
|
|
first_name=provider_data['first_name'],
|
|
last_name=provider_data['last_name'],
|
|
role=provider_data['role'],
|
|
tenant=tenant,
|
|
is_active=True,
|
|
password='temp_password_123' # Should be changed in production
|
|
)
|
|
providers.append(user)
|
|
print(f"Created mock provider: {user.get_full_name()}")
|
|
else:
|
|
providers.append(existing_user)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating mock provider {provider_data['first_name']} {provider_data['last_name']}: {e}")
|
|
continue
|
|
|
|
return providers
|
|
|
|
|
|
def get_providers_for_tenant(tenants):
|
|
"""Get healthcare providers using multiple fallback strategies"""
|
|
providers = []
|
|
|
|
# First try: Look for users with clinical employee profiles
|
|
for tenant in tenants:
|
|
tenant_providers = User.objects.filter(
|
|
employee_profile__tenant=tenant,
|
|
employee_profile__department__department_type='CLINICAL',
|
|
employee_profile__employment_status='ACTIVE'
|
|
).select_related('employee_profile__department')
|
|
providers.extend(list(tenant_providers))
|
|
|
|
# Second try: If no clinical providers found, look for any employees with medical roles
|
|
if not providers:
|
|
for tenant in tenants:
|
|
tenant_providers = User.objects.filter(
|
|
tenant=tenant,
|
|
employee_profile__role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST', 'SURGEON']
|
|
).select_related('employee_profile')
|
|
providers.extend(list(tenant_providers))
|
|
|
|
# Third try: If still no providers, look for any active users with medical-sounding roles
|
|
if not providers:
|
|
for tenant in tenants:
|
|
tenant_providers = User.objects.filter(
|
|
tenant=tenant,
|
|
is_active=True,
|
|
employee_profile__role__icontains='PHYSICIAN'
|
|
)
|
|
providers.extend(list(tenant_providers))
|
|
|
|
# Fourth try: Create some mock providers if none exist
|
|
if not providers:
|
|
print("No existing providers found. Creating mock providers...")
|
|
providers = create_mock_providers(tenants)
|
|
|
|
return providers
|
|
|
|
|
|
def create_appointment_templates(tenants):
|
|
"""Create appointment templates for common appointment types"""
|
|
templates = []
|
|
|
|
template_configs = [
|
|
{
|
|
'name': 'Family Medicine Consultation',
|
|
'appointment_type': 'CONSULTATION',
|
|
'specialty': 'FAMILY_MEDICINE',
|
|
'duration_minutes': 30,
|
|
'advance_booking_days': 30,
|
|
'minimum_notice_hours': 24,
|
|
'pre_instructions': 'Please bring your medical history and current medications list.',
|
|
},
|
|
{
|
|
'name': 'Cardiology Follow-up',
|
|
'appointment_type': 'FOLLOW_UP',
|
|
'specialty': 'CARDIOLOGY',
|
|
'duration_minutes': 45,
|
|
'advance_booking_days': 60,
|
|
'minimum_notice_hours': 48,
|
|
'authorization_required': True,
|
|
'pre_instructions': 'Please bring your latest ECG and cardiac test results.',
|
|
},
|
|
{
|
|
'name': 'Pediatric Wellness Visit',
|
|
'appointment_type': 'SCREENING',
|
|
'specialty': 'PEDIATRICS',
|
|
'duration_minutes': 45,
|
|
'advance_booking_days': 90,
|
|
'minimum_notice_hours': 24,
|
|
'pre_instructions': 'Please bring your child\'s vaccination record.',
|
|
},
|
|
{
|
|
'name': 'Surgical Consultation',
|
|
'appointment_type': 'CONSULTATION',
|
|
'specialty': 'SURGERY',
|
|
'duration_minutes': 60,
|
|
'advance_booking_days': 45,
|
|
'minimum_notice_hours': 72,
|
|
'authorization_required': True,
|
|
'pre_instructions': 'Please bring all relevant imaging studies and lab results.',
|
|
},
|
|
{
|
|
'name': 'Telemedicine Consultation',
|
|
'appointment_type': 'TELEMEDICINE',
|
|
'specialty': 'FAMILY_MEDICINE',
|
|
'duration_minutes': 20,
|
|
'advance_booking_days': 14,
|
|
'minimum_notice_hours': 4,
|
|
'pre_instructions': 'Please ensure you have a stable internet connection and privacy.',
|
|
},
|
|
{
|
|
'name': 'Emergency Consultation',
|
|
'appointment_type': 'EMERGENCY',
|
|
'specialty': 'EMERGENCY',
|
|
'duration_minutes': 60,
|
|
'advance_booking_days': 1,
|
|
'minimum_notice_hours': 1,
|
|
'pre_instructions': 'Please come immediately if experiencing emergency symptoms.',
|
|
},
|
|
{
|
|
'name': 'Diagnostic Imaging',
|
|
'appointment_type': 'DIAGNOSTIC',
|
|
'specialty': 'RADIOLOGY',
|
|
'duration_minutes': 30,
|
|
'advance_booking_days': 21,
|
|
'minimum_notice_hours': 24,
|
|
'pre_instructions': 'Please follow preparation instructions provided.',
|
|
},
|
|
{
|
|
'name': 'Physical Therapy Session',
|
|
'appointment_type': 'THERAPY',
|
|
'specialty': 'ORTHOPEDICS',
|
|
'duration_minutes': 60,
|
|
'advance_booking_days': 30,
|
|
'minimum_notice_hours': 24,
|
|
'pre_instructions': 'Please wear comfortable clothing suitable for exercise.',
|
|
},
|
|
]
|
|
|
|
for tenant in tenants:
|
|
for config in template_configs:
|
|
try:
|
|
template = AppointmentTemplate.objects.create(
|
|
tenant=tenant,
|
|
name=config['name'],
|
|
description=f"Standard {config['name'].lower()} appointment template",
|
|
appointment_type=config['appointment_type'],
|
|
specialty=config['specialty'],
|
|
duration_minutes=config['duration_minutes'],
|
|
advance_booking_days=config['advance_booking_days'],
|
|
minimum_notice_hours=config['minimum_notice_hours'],
|
|
insurance_verification_required=config.get('insurance_verification_required', False),
|
|
authorization_required=config.get('authorization_required', False),
|
|
pre_appointment_instructions=config['pre_instructions'],
|
|
post_appointment_instructions="Please follow up as instructed by your healthcare provider.",
|
|
required_forms=[
|
|
"Patient Registration Form",
|
|
"Medical History Form",
|
|
"Insurance Information Form"
|
|
],
|
|
is_active=True,
|
|
created_at=django_timezone.now() - timedelta(days=random.randint(30, 180)),
|
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30))
|
|
)
|
|
templates.append(template)
|
|
except Exception as e:
|
|
print(f"Error creating template {config['name']} for {tenant.name}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(templates)} appointment templates")
|
|
return templates
|
|
|
|
|
|
def create_waiting_queues(tenants):
|
|
"""Create waiting queues for different departments and specialties"""
|
|
queues = []
|
|
|
|
queue_configs = [
|
|
{
|
|
'name': 'Emergency Department Queue',
|
|
'queue_type': 'EMERGENCY',
|
|
'specialty': 'EMERGENCY',
|
|
'location': 'Emergency Wing',
|
|
'max_queue_size': 100,
|
|
'average_service_time': 45,
|
|
'priority_weights': {'EMERGENCY': 10, 'STAT': 8, 'URGENT': 5, 'ROUTINE': 1}
|
|
},
|
|
{
|
|
'name': 'Family Medicine Queue',
|
|
'queue_type': 'SPECIALTY',
|
|
'specialty': 'FAMILY_MEDICINE',
|
|
'location': 'Outpatient Clinic - Wing A',
|
|
'max_queue_size': 50,
|
|
'average_service_time': 30,
|
|
'priority_weights': {'STAT': 8, 'URGENT': 5, 'ROUTINE': 1}
|
|
},
|
|
{
|
|
'name': 'Cardiology Queue',
|
|
'queue_type': 'SPECIALTY',
|
|
'specialty': 'CARDIOLOGY',
|
|
'location': 'Main Building - Floor 2',
|
|
'max_queue_size': 30,
|
|
'average_service_time': 45,
|
|
'priority_weights': {'STAT': 8, 'URGENT': 5, 'ROUTINE': 1}
|
|
},
|
|
{
|
|
'name': 'Pediatrics Queue',
|
|
'queue_type': 'SPECIALTY',
|
|
'specialty': 'PEDIATRICS',
|
|
'location': 'Pediatric Wing',
|
|
'max_queue_size': 40,
|
|
'average_service_time': 35,
|
|
'priority_weights': {'STAT': 8, 'URGENT': 5, 'ROUTINE': 1}
|
|
},
|
|
{
|
|
'name': 'Surgery Consultation Queue',
|
|
'queue_type': 'SPECIALTY',
|
|
'specialty': 'SURGERY',
|
|
'location': 'Main Building - Floor 3',
|
|
'max_queue_size': 25,
|
|
'average_service_time': 60,
|
|
'priority_weights': {'STAT': 8, 'URGENT': 5, 'ROUTINE': 1}
|
|
},
|
|
{
|
|
'name': 'Radiology Queue',
|
|
'queue_type': 'PROCEDURE',
|
|
'specialty': 'RADIOLOGY',
|
|
'location': 'Radiology Department',
|
|
'max_queue_size': 35,
|
|
'average_service_time': 25,
|
|
'priority_weights': {'STAT': 8, 'URGENT': 5, 'ROUTINE': 1}
|
|
}
|
|
]
|
|
|
|
# Operating hours for queues (Saudi working hours)
|
|
operating_hours = {
|
|
'sunday': {'start': '08:00', 'end': '20:00'},
|
|
'monday': {'start': '08:00', 'end': '20:00'},
|
|
'tuesday': {'start': '08:00', 'end': '20:00'},
|
|
'wednesday': {'start': '08:00', 'end': '20:00'},
|
|
'thursday': {'start': '08:00', 'end': '20:00'},
|
|
'friday': {'start': '14:00', 'end': '20:00'}, # After Friday prayers
|
|
'saturday': {'start': '08:00', 'end': '16:00'} # Half day
|
|
}
|
|
|
|
for tenant in tenants:
|
|
for config in queue_configs:
|
|
try:
|
|
queue = WaitingQueue.objects.create(
|
|
tenant=tenant,
|
|
name=config['name'],
|
|
description=f"Patient waiting queue for {config['specialty'].replace('_', ' ').title()}",
|
|
queue_type=config['queue_type'],
|
|
specialty=config['specialty'],
|
|
location=config['location'],
|
|
max_queue_size=config['max_queue_size'],
|
|
average_service_time_minutes=config['average_service_time'],
|
|
priority_weights=config['priority_weights'],
|
|
is_active=True,
|
|
is_accepting_patients=True,
|
|
operating_hours=operating_hours,
|
|
created_at=django_timezone.now() - timedelta(days=random.randint(30, 180)),
|
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30))
|
|
)
|
|
queues.append(queue)
|
|
except Exception as e:
|
|
print(f"Error creating queue {config['name']} for {tenant.name}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(queues)} waiting queues")
|
|
return queues
|
|
|
|
|
|
def create_slot_availability(tenants, days_ahead=30):
|
|
"""Create provider availability slots"""
|
|
slots = []
|
|
|
|
# Get healthcare providers using the improved strategy
|
|
providers = get_providers_for_tenant(tenants)
|
|
|
|
if not providers:
|
|
print("No clinical providers found. Skipping slot creation.")
|
|
return slots
|
|
|
|
print(f"Found {len(providers)} providers for slot creation")
|
|
|
|
# Time slots configuration
|
|
time_slots = {
|
|
'MORNING': [
|
|
(time(8, 0), time(8, 30)),
|
|
(time(8, 30), time(9, 0)),
|
|
(time(9, 0), time(9, 30)),
|
|
(time(9, 30), time(10, 0)),
|
|
(time(10, 0), time(10, 30)),
|
|
(time(10, 30), time(11, 0)),
|
|
(time(11, 0), time(11, 30)),
|
|
(time(11, 30), time(12, 0)),
|
|
],
|
|
'AFTERNOON': [
|
|
(time(14, 0), time(14, 30)),
|
|
(time(14, 30), time(15, 0)),
|
|
(time(15, 0), time(15, 30)),
|
|
(time(15, 30), time(16, 0)),
|
|
(time(16, 0), time(16, 30)),
|
|
(time(16, 30), time(17, 0)),
|
|
(time(17, 0), time(17, 30)),
|
|
(time(17, 30), time(18, 0)),
|
|
],
|
|
'EVENING': [
|
|
(time(19, 0), time(19, 30)),
|
|
(time(19, 30), time(20, 0)),
|
|
]
|
|
}
|
|
|
|
start_date = django_timezone.now().date()
|
|
|
|
for provider in providers:
|
|
tenant = provider.tenant
|
|
|
|
# Get department if available, otherwise use a default
|
|
department = None
|
|
if hasattr(provider, 'employee_profile') and provider.employee_profile:
|
|
department = provider.employee_profile.department
|
|
|
|
# Determine specialty based on role or department
|
|
specialty_mapping = {
|
|
'CARDIOLOGY': 'CARDIOLOGY',
|
|
'PEDIATRICS': 'PEDIATRICS',
|
|
'SURGERY': 'SURGERY',
|
|
'RADIOLOGY': 'RADIOLOGY',
|
|
'EMERGENCY': 'EMERGENCY',
|
|
'OBSTETRICS': 'GYNECOLOGY',
|
|
'ORTHOPEDICS': 'ORTHOPEDICS'
|
|
}
|
|
|
|
if department and hasattr(department, 'department_code'):
|
|
specialty = specialty_mapping.get(department.department_code, 'FAMILY_MEDICINE')
|
|
elif hasattr(provider, 'role') and provider.role:
|
|
if 'SURGEON' in provider.role:
|
|
specialty = 'SURGERY'
|
|
elif 'RADIOLOGY' in provider.role:
|
|
specialty = 'RADIOLOGY'
|
|
else:
|
|
specialty = 'FAMILY_MEDICINE'
|
|
else:
|
|
specialty = 'FAMILY_MEDICINE'
|
|
|
|
# Create slots for the next days_ahead days
|
|
for day_offset in range(days_ahead):
|
|
slot_date = start_date + timedelta(days=day_offset)
|
|
weekday = slot_date.weekday()
|
|
|
|
# Skip Fridays for most providers (except emergency)
|
|
if weekday == 4 and specialty != 'EMERGENCY':
|
|
continue
|
|
|
|
# Determine working periods for the day
|
|
working_periods = ['MORNING']
|
|
if weekday != 5: # Not Saturday (half day)
|
|
working_periods.append('AFTERNOON')
|
|
if specialty == 'EMERGENCY':
|
|
working_periods.append('EVENING')
|
|
|
|
# Random availability (not all providers work every day)
|
|
if random.random() < 0.8: # 80% chance provider is available
|
|
for period in working_periods:
|
|
# Select some slots from the period
|
|
available_slots = random.sample(
|
|
time_slots[period],
|
|
random.randint(2, min(6, len(time_slots[period])))
|
|
)
|
|
|
|
for start_time, end_time in available_slots:
|
|
duration = int((datetime.combine(date.today(), end_time) -
|
|
datetime.combine(date.today(), start_time)).total_seconds() / 60)
|
|
|
|
# Determine slot configuration
|
|
max_appointments = 1
|
|
if specialty in ['FAMILY_MEDICINE', 'PEDIATRICS']:
|
|
max_appointments = random.choice([1, 2])
|
|
|
|
availability_type = 'REGULAR'
|
|
if period == 'EVENING':
|
|
availability_type = 'EXTENDED'
|
|
if specialty == 'EMERGENCY':
|
|
availability_type = 'EMERGENCY'
|
|
|
|
supports_telemedicine = random.choice([True, False]) if specialty != 'SURGERY' else False
|
|
|
|
# Get location from department or use default
|
|
location = 'Main Building - Floor 1'
|
|
if department and hasattr(department, 'location') and department.location:
|
|
location = department.location
|
|
else:
|
|
location = random.choice(SAUDI_LOCATIONS)
|
|
|
|
try:
|
|
slot = SlotAvailability.objects.create(
|
|
tenant=tenant,
|
|
provider=provider,
|
|
date=slot_date,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
duration_minutes=duration,
|
|
availability_type=availability_type,
|
|
max_appointments=max_appointments,
|
|
booked_appointments=0,
|
|
location=location,
|
|
room_number=f"Room {random.randint(101, 350)}",
|
|
specialty=specialty,
|
|
appointment_types=random.sample(APPOINTMENT_TYPES, random.randint(3, 6)),
|
|
supports_telemedicine=supports_telemedicine,
|
|
telemedicine_only=False,
|
|
is_active=True,
|
|
is_blocked=False,
|
|
created_at=django_timezone.now() - timedelta(days=random.randint(1, 30)),
|
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7))
|
|
)
|
|
slots.append(slot)
|
|
except Exception as e:
|
|
print(f"Error creating slot for {provider.get_full_name()}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(slots)} availability slots")
|
|
return slots
|
|
|
|
|
|
def create_appointment_requests(tenants, slots, days_back=14, appointments_per_day=50):
|
|
"""Create appointment requests with various statuses"""
|
|
appointments = []
|
|
|
|
# Get patients
|
|
patients = list(PatientProfile.objects.filter(tenant__in=tenants))
|
|
if not patients:
|
|
print("No patients found. Skipping appointment creation.")
|
|
return appointments
|
|
|
|
# Get providers using the same improved logic
|
|
providers = get_providers_for_tenant(tenants)
|
|
|
|
if not providers:
|
|
print("No providers found. Skipping appointment creation.")
|
|
return appointments
|
|
|
|
print(f"Found {len(providers)} providers for appointment creation")
|
|
|
|
start_date = django_timezone.now().date() - timedelta(days=days_back)
|
|
|
|
for day_offset in range(days_back + 30): # Past and future appointments
|
|
appointment_date = start_date + timedelta(days=day_offset)
|
|
|
|
# Create appointments for this day
|
|
daily_appointments = random.randint(int(appointments_per_day * 0.7), int(appointments_per_day * 1.3))
|
|
|
|
for _ in range(daily_appointments):
|
|
patient = random.choice(patients)
|
|
|
|
# Get providers for this patient's tenant, with fallback
|
|
tenant_providers = [p for p in providers if p.tenant == patient.tenant]
|
|
if not tenant_providers:
|
|
# If no providers for this tenant, skip this appointment
|
|
print(f"Warning: No providers found for tenant {patient.tenant.name}, skipping appointment for {patient.get_full_name()}")
|
|
continue
|
|
|
|
provider = random.choice(tenant_providers)
|
|
|
|
# Select appointment type and specialty
|
|
appointment_type = random.choices(
|
|
APPOINTMENT_TYPES,
|
|
weights=[20, 25, 10, 5, 15, 8, 5, 10, 3, 4] # Weighted by frequency
|
|
)[0]
|
|
|
|
specialty = random.choice(SAUDI_SPECIALTIES)
|
|
|
|
# Set priority based on appointment type
|
|
if appointment_type == 'EMERGENCY':
|
|
priority = 'EMERGENCY'
|
|
urgency_score = random.randint(8, 10)
|
|
elif appointment_type in ['SURGERY', 'PROCEDURE']:
|
|
priority = random.choices(['URGENT', 'STAT'], weights=[70, 30])[0]
|
|
urgency_score = random.randint(5, 8)
|
|
else:
|
|
priority = random.choices(['ROUTINE', 'URGENT'], weights=[85, 15])[0]
|
|
urgency_score = random.randint(1, 5)
|
|
|
|
# Determine status based on appointment date
|
|
if appointment_date < django_timezone.now().date():
|
|
# Past appointments
|
|
status = random.choices(
|
|
['COMPLETED', 'NO_SHOW', 'CANCELLED'],
|
|
weights=[75, 15, 10]
|
|
)[0]
|
|
elif appointment_date == django_timezone.now().date():
|
|
# Today's appointments
|
|
status = random.choices(
|
|
['CONFIRMED', 'CHECKED_IN', 'IN_PROGRESS', 'COMPLETED'],
|
|
weights=[40, 30, 20, 10]
|
|
)[0]
|
|
else:
|
|
# Future appointments
|
|
status = random.choices(
|
|
['SCHEDULED', 'CONFIRMED'],
|
|
weights=[40, 60]
|
|
)[0]
|
|
|
|
# Generate appointment time
|
|
preferred_time = time(
|
|
random.randint(8, 17),
|
|
random.choice([0, 15, 30, 45])
|
|
)
|
|
|
|
duration = 30
|
|
if appointment_type in ['SURGERY', 'PROCEDURE']:
|
|
duration = random.randint(60, 180)
|
|
elif appointment_type == 'CONSULTATION':
|
|
duration = random.randint(30, 60)
|
|
|
|
# Schedule datetime
|
|
scheduled_datetime = None
|
|
scheduled_end_datetime = None
|
|
if status != 'PENDING':
|
|
scheduled_datetime = django_timezone.make_aware(
|
|
datetime.combine(appointment_date, preferred_time)
|
|
)
|
|
scheduled_end_datetime = scheduled_datetime + timedelta(minutes=duration)
|
|
|
|
# Telemedicine configuration
|
|
is_telemedicine = appointment_type == 'TELEMEDICINE' or (
|
|
appointment_type in ['CONSULTATION', 'FOLLOW_UP'] and random.random() < 0.2
|
|
)
|
|
|
|
telemedicine_platform = None
|
|
meeting_url = None
|
|
meeting_id = None
|
|
if is_telemedicine:
|
|
telemedicine_platform = random.choice(TELEMEDICINE_PLATFORMS)
|
|
meeting_url = f"https://{telemedicine_platform.lower()}.com/j/{random.randint(100000000, 999999999)}"
|
|
meeting_id = str(random.randint(100000000, 999999999))
|
|
|
|
# Check-in and completion times for completed appointments
|
|
checked_in_at = None
|
|
completed_at = None
|
|
actual_duration = None
|
|
|
|
if status in ['CHECKED_IN', 'IN_PROGRESS', 'COMPLETED'] and scheduled_datetime:
|
|
# Random check-in time (usually 10-30 minutes before appointment)
|
|
checkin_offset = random.randint(-30, 5) # Can be late too
|
|
checked_in_at = scheduled_datetime + timedelta(minutes=checkin_offset)
|
|
|
|
if status == 'COMPLETED':
|
|
# Random completion time
|
|
actual_duration = random.randint(int(duration * 0.8), int(duration * 1.2))
|
|
completed_at = scheduled_datetime + timedelta(minutes=actual_duration)
|
|
|
|
# Cancellation information
|
|
cancelled_at = None
|
|
cancellation_reason = None
|
|
if status == 'CANCELLED':
|
|
cancelled_at = scheduled_datetime - timedelta(
|
|
hours=random.randint(1, 72)) if scheduled_datetime else django_timezone.now()
|
|
cancellation_reason = random.choice([
|
|
'Patient requested cancellation',
|
|
'Provider unavailable',
|
|
'Emergency situation',
|
|
'Insurance issues',
|
|
'Patient illness'
|
|
])
|
|
|
|
try:
|
|
appointment = AppointmentRequest.objects.create(
|
|
tenant=patient.tenant,
|
|
patient=patient,
|
|
provider=provider,
|
|
appointment_type=appointment_type,
|
|
specialty=specialty,
|
|
preferred_date=appointment_date,
|
|
preferred_time=preferred_time,
|
|
duration_minutes=duration,
|
|
flexible_scheduling=random.choice([True, False]),
|
|
earliest_acceptable_date=appointment_date - timedelta(days=random.randint(0, 3)),
|
|
latest_acceptable_date=appointment_date + timedelta(days=random.randint(0, 7)),
|
|
acceptable_times=['morning', 'afternoon'] if random.choice([True, False]) else [],
|
|
priority=priority,
|
|
urgency_score=urgency_score,
|
|
chief_complaint=random.choice(SAUDI_CHIEF_COMPLAINTS),
|
|
clinical_notes=f"Patient presenting with {random.choice(SAUDI_CHIEF_COMPLAINTS).lower()}",
|
|
referring_provider=f"Dr. {random.choice(['Ahmed', 'Fatima', 'Mohammed', 'Sara'])} Al-{random.choice(['Rashid', 'Ghamdi', 'Otaibi'])}" if random.choice(
|
|
[True, False]) else None,
|
|
insurance_verified=random.choice([True, False]),
|
|
authorization_required=appointment_type in ['SURGERY', 'PROCEDURE'],
|
|
authorization_number=f"AUTH-{random.randint(100000, 999999)}" if appointment_type in ['SURGERY',
|
|
'PROCEDURE'] else None,
|
|
status=status,
|
|
scheduled_datetime=scheduled_datetime,
|
|
scheduled_end_datetime=scheduled_end_datetime,
|
|
location=random.choice(SAUDI_LOCATIONS),
|
|
room_number=f"Room {random.randint(101, 350)}",
|
|
is_telemedicine=is_telemedicine,
|
|
telemedicine_platform=telemedicine_platform,
|
|
meeting_url=meeting_url,
|
|
meeting_id=meeting_id,
|
|
meeting_password=f"pwd{random.randint(1000, 9999)}" if is_telemedicine else None,
|
|
checked_in_at=checked_in_at,
|
|
completed_at=completed_at,
|
|
actual_duration_minutes=actual_duration,
|
|
cancelled_at=cancelled_at,
|
|
cancellation_reason=cancellation_reason,
|
|
reminder_preferences={
|
|
'sms': True,
|
|
'email': random.choice([True, False]),
|
|
'phone': False
|
|
},
|
|
special_requirements=random.choice([
|
|
None, None, None, # Most appointments don't have special requirements
|
|
'Wheelchair accessible',
|
|
'Interpreter needed',
|
|
'Elderly patient assistance',
|
|
'Child-friendly environment'
|
|
]),
|
|
interpreter_needed=random.choice([True, False]) if random.random() < 0.1 else False,
|
|
interpreter_language=random.choice(
|
|
['Urdu', 'Hindi', 'Bengali', 'English']) if random.random() < 0.1 else None,
|
|
created_at=django_timezone.now() - timedelta(days=random.randint(1, 30)),
|
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7))
|
|
)
|
|
appointments.append(appointment)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating appointment for {patient.get_full_name()}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(appointments)} appointment requests")
|
|
return appointments
|
|
|
|
|
|
def create_telemedicine_sessions(appointments):
|
|
"""Create telemedicine sessions for telemedicine appointments"""
|
|
sessions = []
|
|
|
|
telemedicine_appointments = [apt for apt in appointments if apt.is_telemedicine and apt.scheduled_datetime]
|
|
|
|
for appointment in telemedicine_appointments:
|
|
# Determine session status
|
|
if appointment.status == 'COMPLETED':
|
|
session_status = 'COMPLETED'
|
|
elif appointment.status in ['IN_PROGRESS']:
|
|
session_status = 'IN_PROGRESS'
|
|
elif appointment.status in ['CONFIRMED', 'CHECKED_IN']:
|
|
session_status = 'READY'
|
|
elif appointment.status == 'CANCELLED':
|
|
session_status = 'CANCELLED'
|
|
else:
|
|
session_status = 'SCHEDULED'
|
|
|
|
# Generate session timing
|
|
actual_start = None
|
|
actual_end = None
|
|
provider_joined_at = None
|
|
patient_joined_at = None
|
|
|
|
if session_status in ['COMPLETED', 'IN_PROGRESS']:
|
|
# Provider usually joins first
|
|
provider_joined_at = appointment.scheduled_datetime + timedelta(minutes=random.randint(-5, 2))
|
|
patient_joined_at = provider_joined_at + timedelta(minutes=random.randint(1, 8))
|
|
actual_start = max(provider_joined_at, patient_joined_at)
|
|
|
|
if session_status == 'COMPLETED':
|
|
duration = appointment.actual_duration_minutes or appointment.duration_minutes
|
|
actual_end = actual_start + timedelta(minutes=duration)
|
|
|
|
# Connection quality and technical issues
|
|
connection_quality = random.choices(
|
|
['EXCELLENT', 'GOOD', 'FAIR', 'POOR'],
|
|
weights=[40, 35, 20, 5]
|
|
)[0]
|
|
|
|
technical_issues = None
|
|
if connection_quality in ['FAIR', 'POOR']:
|
|
technical_issues = random.choice([
|
|
'Intermittent audio issues',
|
|
'Video quality degradation',
|
|
'Connection drops',
|
|
'Echo and feedback problems',
|
|
'Bandwidth limitations'
|
|
])
|
|
|
|
# Recording information
|
|
recording_enabled = random.choice([True, False]) if random.random() < 0.3 else False
|
|
recording_consent = recording_enabled
|
|
recording_url = f"https://recordings.{appointment.telemedicine_platform.lower()}.com/{uuid.uuid4()}" if recording_enabled and session_status == 'COMPLETED' else None
|
|
recording_duration = appointment.actual_duration_minutes if recording_enabled and session_status == 'COMPLETED' else None
|
|
|
|
try:
|
|
session = TelemedicineSession.objects.create(
|
|
appointment=appointment,
|
|
platform=appointment.telemedicine_platform,
|
|
meeting_url=appointment.meeting_url,
|
|
meeting_id=appointment.meeting_id,
|
|
meeting_password=appointment.meeting_password,
|
|
waiting_room_enabled=True,
|
|
recording_enabled=recording_enabled,
|
|
recording_consent=recording_consent,
|
|
encryption_enabled=True,
|
|
password_required=appointment.meeting_password is not None,
|
|
status=session_status,
|
|
scheduled_start=appointment.scheduled_datetime,
|
|
scheduled_end=appointment.scheduled_end_datetime,
|
|
actual_start=actual_start,
|
|
actual_end=actual_end,
|
|
provider_joined_at=provider_joined_at,
|
|
patient_joined_at=patient_joined_at,
|
|
connection_quality=connection_quality,
|
|
technical_issues=technical_issues,
|
|
recording_url=recording_url,
|
|
recording_duration_minutes=recording_duration,
|
|
session_notes=f"Telemedicine session for {appointment.chief_complaint}" if session_status == 'COMPLETED' else None,
|
|
created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)),
|
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3))
|
|
)
|
|
sessions.append(session)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating telemedicine session for appointment {appointment.request_id}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(sessions)} telemedicine sessions")
|
|
return sessions
|
|
|
|
|
|
def create_waiting_list_entries(tenants, num_entries_per_tenant=100):
|
|
"""Create waiting list entries with realistic clinical scenarios"""
|
|
waiting_list_entries = []
|
|
|
|
# Get patients and providers
|
|
patients = list(PatientProfile.objects.filter(tenant__in=tenants))
|
|
providers = get_providers_for_tenant(tenants)
|
|
departments = list(Department.objects.filter(tenant__in=tenants))
|
|
|
|
if not patients:
|
|
print("No patients found. Skipping waiting list creation.")
|
|
return waiting_list_entries
|
|
|
|
if not departments:
|
|
print("No departments found. Skipping waiting list creation.")
|
|
return waiting_list_entries
|
|
|
|
# Clinical scenarios for waiting list entries
|
|
clinical_scenarios = [
|
|
{
|
|
'specialty': 'CARDIOLOGY',
|
|
'appointment_type': 'CONSULTATION',
|
|
'clinical_indication': 'Chest pain evaluation and cardiac assessment',
|
|
'diagnosis_codes': ['R07.9', 'I25.10'],
|
|
'priority': 'URGENT',
|
|
'urgency_score': random.randint(6, 8),
|
|
'referral_urgency': 'URGENT'
|
|
},
|
|
{
|
|
'specialty': 'ORTHOPEDICS',
|
|
'appointment_type': 'FOLLOW_UP',
|
|
'clinical_indication': 'Post-operative knee replacement follow-up',
|
|
'diagnosis_codes': ['Z47.1', 'M17.9'],
|
|
'priority': 'ROUTINE',
|
|
'urgency_score': random.randint(2, 4),
|
|
'referral_urgency': 'ROUTINE'
|
|
},
|
|
{
|
|
'specialty': 'PEDIATRICS',
|
|
'appointment_type': 'SCREENING',
|
|
'clinical_indication': 'Well-child visit and developmental assessment',
|
|
'diagnosis_codes': ['Z00.129'],
|
|
'priority': 'ROUTINE',
|
|
'urgency_score': random.randint(1, 3),
|
|
'referral_urgency': 'ROUTINE'
|
|
},
|
|
{
|
|
'specialty': 'DERMATOLOGY',
|
|
'appointment_type': 'PROCEDURE',
|
|
'clinical_indication': 'Suspicious skin lesion requiring biopsy',
|
|
'diagnosis_codes': ['D48.5', 'L98.9'],
|
|
'priority': 'URGENT',
|
|
'urgency_score': random.randint(5, 7),
|
|
'referral_urgency': 'URGENT'
|
|
},
|
|
{
|
|
'specialty': 'GYNECOLOGY',
|
|
'appointment_type': 'CONSULTATION',
|
|
'clinical_indication': 'Abnormal uterine bleeding evaluation',
|
|
'diagnosis_codes': ['N93.9', 'N85.00'],
|
|
'priority': 'URGENT',
|
|
'urgency_score': random.randint(6, 8),
|
|
'referral_urgency': 'URGENT'
|
|
},
|
|
{
|
|
'specialty': 'NEUROLOGY',
|
|
'appointment_type': 'DIAGNOSTIC',
|
|
'clinical_indication': 'Headache evaluation and neurological assessment',
|
|
'diagnosis_codes': ['R51', 'G44.209'],
|
|
'priority': 'STAT',
|
|
'urgency_score': random.randint(7, 9),
|
|
'referral_urgency': 'STAT'
|
|
},
|
|
{
|
|
'specialty': 'ENDOCRINOLOGY',
|
|
'appointment_type': 'FOLLOW_UP',
|
|
'clinical_indication': 'Diabetes management and glycemic control',
|
|
'diagnosis_codes': ['E11.9', 'Z79.4'],
|
|
'priority': 'ROUTINE',
|
|
'urgency_score': random.randint(2, 5),
|
|
'referral_urgency': 'ROUTINE'
|
|
},
|
|
{
|
|
'specialty': 'SURGERY',
|
|
'appointment_type': 'CONSULTATION',
|
|
'clinical_indication': 'Gallbladder disease requiring surgical evaluation',
|
|
'diagnosis_codes': ['K80.20', 'R10.11'],
|
|
'priority': 'URGENT',
|
|
'urgency_score': random.randint(5, 8),
|
|
'referral_urgency': 'URGENT'
|
|
},
|
|
{
|
|
'specialty': 'PSYCHIATRY',
|
|
'appointment_type': 'THERAPY',
|
|
'clinical_indication': 'Depression and anxiety management',
|
|
'diagnosis_codes': ['F32.9', 'F41.9'],
|
|
'priority': 'ROUTINE',
|
|
'urgency_score': random.randint(3, 6),
|
|
'referral_urgency': 'ROUTINE'
|
|
},
|
|
{
|
|
'specialty': 'UROLOGY',
|
|
'appointment_type': 'DIAGNOSTIC',
|
|
'clinical_indication': 'Elevated PSA and prostate evaluation',
|
|
'diagnosis_codes': ['R97.20', 'N40.0'],
|
|
'priority': 'URGENT',
|
|
'urgency_score': random.randint(5, 7),
|
|
'referral_urgency': 'URGENT'
|
|
}
|
|
]
|
|
|
|
for tenant in tenants:
|
|
tenant_patients = [p for p in patients if p.tenant == tenant]
|
|
tenant_providers = [p for p in providers if p.tenant == tenant]
|
|
tenant_departments = [d for d in departments if d.tenant == tenant]
|
|
|
|
if not tenant_patients or not tenant_departments:
|
|
continue
|
|
|
|
for _ in range(num_entries_per_tenant):
|
|
patient = random.choice(tenant_patients)
|
|
scenario = random.choice(clinical_scenarios)
|
|
|
|
# Select appropriate department
|
|
suitable_departments = [
|
|
d for d in tenant_departments
|
|
if hasattr(d, 'department_type') and d.department_type == 'CLINICAL'
|
|
]
|
|
department = random.choice(suitable_departments) if suitable_departments else random.choice(tenant_departments)
|
|
|
|
# Provider assignment (optional, 70% have preferred provider)
|
|
provider = None
|
|
if random.random() < 0.7 and tenant_providers:
|
|
provider = random.choice(tenant_providers)
|
|
|
|
# Status distribution (weighted towards active entries)
|
|
status_weights = {
|
|
'ACTIVE': 60,
|
|
'CONTACTED': 15,
|
|
'OFFERED': 10,
|
|
'SCHEDULED': 8,
|
|
'CANCELLED': 4,
|
|
'EXPIRED': 2,
|
|
'TRANSFERRED': 1
|
|
}
|
|
status = random.choices(
|
|
list(status_weights.keys()),
|
|
weights=list(status_weights.values())
|
|
)[0]
|
|
|
|
# Contact information
|
|
contact_method = random.choice(['PHONE', 'EMAIL', 'SMS'])
|
|
contact_phone = f"+966{random.randint(500000000, 599999999)}" if contact_method == 'PHONE' else None
|
|
contact_email = f"{patient.email}" if contact_method == 'EMAIL' else None
|
|
|
|
# Timing and scheduling preferences
|
|
created_at = django_timezone.now() - timedelta(days=random.randint(1, 90))
|
|
preferred_date = created_at.date() + timedelta(days=random.randint(7, 60)) if random.random() < 0.8 else None
|
|
preferred_time = time(random.randint(8, 16), random.choice([0, 30])) if preferred_date else None
|
|
|
|
# Contact history
|
|
contact_attempts = 0
|
|
last_contacted = None
|
|
appointments_offered = 0
|
|
appointments_declined = 0
|
|
last_offer_date = None
|
|
|
|
if status in ['CONTACTED', 'OFFERED', 'SCHEDULED']:
|
|
contact_attempts = random.randint(1, 3)
|
|
last_contacted = created_at + timedelta(days=random.randint(1, 30))
|
|
|
|
if status in ['OFFERED', 'SCHEDULED']:
|
|
appointments_offered = random.randint(1, 2)
|
|
last_offer_date = last_contacted + timedelta(days=random.randint(1, 7))
|
|
|
|
if random.random() < 0.3: # 30% decline offers
|
|
appointments_declined = random.randint(1, appointments_offered)
|
|
|
|
# Authorization requirements (for procedures/surgery)
|
|
authorization_required = scenario['appointment_type'] in ['PROCEDURE', 'SURGERY']
|
|
authorization_status = 'NOT_REQUIRED'
|
|
authorization_number = None
|
|
|
|
if authorization_required:
|
|
authorization_status = random.choice(['PENDING', 'APPROVED', 'DENIED'])
|
|
if authorization_status == 'APPROVED':
|
|
authorization_number = f"AUTH-{random.randint(100000, 999999)}"
|
|
|
|
# Special requirements
|
|
requires_interpreter = random.random() < 0.05 # 5% need interpreter
|
|
interpreter_language = random.choice(['Arabic', 'English', 'Urdu', 'Hindi']) if requires_interpreter else None
|
|
|
|
accessibility_requirements = None
|
|
if random.random() < 0.1: # 10% have accessibility needs
|
|
accessibility_requirements = random.choice([
|
|
'Wheelchair accessible room required',
|
|
'Sign language interpreter needed',
|
|
'Hearing assistance devices needed',
|
|
'Visual assistance required'
|
|
])
|
|
|
|
transportation_needed = random.random() < 0.15 # 15% need transportation
|
|
|
|
# Referral information
|
|
referring_provider = f"Dr. {random.choice(['Ahmed', 'Fatima', 'Mohammed', 'Sara'])} Al-{random.choice(['Rashid', 'Ghamdi', 'Otaibi', 'Harbi'])}"
|
|
referral_date = created_at - timedelta(days=random.randint(1, 14))
|
|
|
|
# Notes
|
|
notes = random.choice([
|
|
None,
|
|
f"Patient prefers {scenario['specialty'].replace('_', ' ').title().lower()} specialist",
|
|
"Patient has multiple comorbidities",
|
|
"Requires extended appointment time",
|
|
"Patient has transportation limitations",
|
|
"Family member will accompany patient"
|
|
])
|
|
|
|
try:
|
|
waiting_entry = WaitingList.objects.create(
|
|
tenant=tenant,
|
|
patient=patient,
|
|
provider=provider,
|
|
department=department,
|
|
appointment_type=scenario['appointment_type'],
|
|
specialty=scenario['specialty'],
|
|
priority=scenario['priority'],
|
|
urgency_score=scenario['urgency_score'],
|
|
clinical_indication=scenario['clinical_indication'],
|
|
diagnosis_codes=scenario['diagnosis_codes'],
|
|
preferred_date=preferred_date,
|
|
preferred_time=preferred_time,
|
|
flexible_scheduling=random.choice([True, False]),
|
|
earliest_acceptable_date=preferred_date - timedelta(days=random.randint(0, 7)) if preferred_date else None,
|
|
latest_acceptable_date=preferred_date + timedelta(days=random.randint(7, 21)) if preferred_date else None,
|
|
acceptable_days=[0, 1, 2, 3, 4] if random.random() < 0.8 else [], # Monday-Friday
|
|
acceptable_times=['morning', 'afternoon'] if random.random() < 0.7 else [],
|
|
contact_method=contact_method,
|
|
contact_phone=contact_phone,
|
|
contact_email=contact_email,
|
|
status=status,
|
|
position=None, # Will be calculated later
|
|
estimated_wait_time=scenario['urgency_score'] * 7, # Days based on urgency
|
|
last_contacted=last_contacted,
|
|
contact_attempts=contact_attempts,
|
|
max_contact_attempts=3,
|
|
appointments_offered=appointments_offered,
|
|
appointments_declined=appointments_declined,
|
|
last_offer_date=last_offer_date,
|
|
requires_interpreter=requires_interpreter,
|
|
interpreter_language=interpreter_language,
|
|
accessibility_requirements=accessibility_requirements,
|
|
transportation_needed=transportation_needed,
|
|
insurance_verified=random.choice([True, False]),
|
|
authorization_required=authorization_required,
|
|
authorization_status=authorization_status,
|
|
authorization_number=authorization_number,
|
|
referring_provider=referring_provider,
|
|
referral_date=referral_date,
|
|
referral_urgency=scenario['referral_urgency'],
|
|
created_at=created_at,
|
|
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7)),
|
|
notes=notes
|
|
)
|
|
waiting_list_entries.append(waiting_entry)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating waiting list entry for {patient.get_full_name()}: {e}")
|
|
continue
|
|
|
|
# Update positions for active entries
|
|
for entry in waiting_list_entries:
|
|
if entry.status == 'ACTIVE':
|
|
try:
|
|
entry.position = entry.calculate_position()
|
|
entry.estimated_wait_time = entry.estimate_wait_time()
|
|
entry.save(update_fields=['position', 'estimated_wait_time'])
|
|
except Exception as e:
|
|
print(f"Error updating position for waiting list entry {entry.waiting_list_id}: {e}")
|
|
|
|
print(f"Created {len(waiting_list_entries)} waiting list entries")
|
|
return waiting_list_entries
|
|
|
|
|
|
def create_waiting_list_contact_logs(waiting_list_entries):
|
|
"""Create contact logs for waiting list entries"""
|
|
contact_logs = []
|
|
|
|
# Only create logs for entries that have been contacted
|
|
contacted_entries = [entry for entry in waiting_list_entries if entry.contact_attempts > 0]
|
|
|
|
for entry in contacted_entries:
|
|
# Create logs for each contact attempt
|
|
for attempt_num in range(1, entry.contact_attempts + 1):
|
|
contact_date = entry.last_contacted - timedelta(days=random.randint(0, 7) * (entry.contact_attempts - attempt_num))
|
|
|
|
# Contact method based on entry preferences
|
|
contact_method = entry.contact_method
|
|
if contact_method == 'PHONE':
|
|
contact_method = 'PHONE'
|
|
elif contact_method == 'EMAIL':
|
|
contact_method = 'EMAIL'
|
|
else:
|
|
contact_method = 'SMS'
|
|
|
|
# Contact outcomes
|
|
possible_outcomes = ['SUCCESSFUL', 'NO_ANSWER', 'BUSY', 'VOICEMAIL', 'EMAIL_SENT', 'SMS_SENT']
|
|
if attempt_num == entry.contact_attempts:
|
|
# Last attempt more likely to be successful
|
|
outcome_weights = [40, 20, 10, 10, 10, 10]
|
|
else:
|
|
outcome_weights = [20, 30, 15, 15, 10, 10]
|
|
|
|
contact_outcome = random.choices(possible_outcomes, weights=outcome_weights)[0]
|
|
|
|
# Patient responses (only for successful contacts)
|
|
patient_response = None
|
|
appointment_offered = False
|
|
offered_date = None
|
|
offered_time = None
|
|
|
|
if contact_outcome == 'SUCCESSFUL':
|
|
response_options = ['ACCEPTED', 'DECLINED', 'REQUESTED_DIFFERENT', 'WILL_CALL_BACK', 'NO_LONGER_NEEDED']
|
|
response_weights = [30, 20, 25, 15, 10]
|
|
patient_response = random.choices(response_options, weights=response_weights)[0]
|
|
|
|
# Offer appointment if appropriate
|
|
if patient_response in ['ACCEPTED', 'REQUESTED_DIFFERENT'] and random.random() < 0.7:
|
|
appointment_offered = True
|
|
offered_date = contact_date.date() + timedelta(days=random.randint(7, 21))
|
|
offered_time = time(random.randint(8, 16), random.choice([0, 30]))
|
|
|
|
# Notes based on outcome
|
|
notes_templates = {
|
|
'SUCCESSFUL': [
|
|
f"Spoke with patient regarding {entry.appointment_type.lower()} appointment",
|
|
"Patient confirmed interest in scheduling",
|
|
"Discussed clinical urgency and wait times",
|
|
"Patient requested specific time preferences"
|
|
],
|
|
'NO_ANSWER': [
|
|
"No answer after multiple rings",
|
|
"Call went to voicemail",
|
|
"Patient did not pick up"
|
|
],
|
|
'BUSY': [
|
|
"Line was busy",
|
|
"Patient indicated they were busy",
|
|
"Call disconnected due to network issues"
|
|
],
|
|
'VOICEMAIL': [
|
|
"Left detailed voicemail about appointment availability",
|
|
"Voicemail left with callback instructions",
|
|
"Message left explaining wait list status"
|
|
],
|
|
'EMAIL_SENT': [
|
|
"Email sent with appointment options",
|
|
"Detailed email with scheduling information",
|
|
"Follow-up email sent as requested"
|
|
],
|
|
'SMS_SENT': [
|
|
"SMS sent with appointment reminder",
|
|
"Text message with scheduling options",
|
|
"SMS follow-up sent"
|
|
]
|
|
}
|
|
|
|
notes = random.choice(notes_templates.get(contact_outcome, ["Contact attempt made"]))
|
|
|
|
# Next contact date for unsuccessful attempts
|
|
next_contact_date = None
|
|
if contact_outcome != 'SUCCESSFUL' and attempt_num < entry.max_contact_attempts:
|
|
next_contact_date = contact_date.date() + timedelta(days=random.randint(3, 7))
|
|
|
|
try:
|
|
contact_log = WaitingListContactLog.objects.create(
|
|
waiting_list_entry=entry,
|
|
contact_date=contact_date,
|
|
contact_method=contact_method,
|
|
contact_outcome=contact_outcome,
|
|
appointment_offered=appointment_offered,
|
|
offered_date=offered_date,
|
|
offered_time=offered_time,
|
|
patient_response=patient_response,
|
|
notes=notes,
|
|
next_contact_date=next_contact_date,
|
|
contacted_by=random.choice([entry.provider, None]) if entry.provider else None
|
|
)
|
|
contact_logs.append(contact_log)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating contact log for waiting list entry {entry.waiting_list_id}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(contact_logs)} waiting list contact logs")
|
|
return contact_logs
|
|
|
|
|
|
def create_queue_entries(queues, appointments):
|
|
"""Create queue entries for appointments"""
|
|
entries = []
|
|
|
|
# Get appointments that should have queue entries (checked in, in progress, or recently completed)
|
|
relevant_appointments = [
|
|
apt for apt in appointments
|
|
if apt.status in ['CHECKED_IN', 'IN_PROGRESS', 'COMPLETED']
|
|
and apt.scheduled_datetime
|
|
and apt.scheduled_datetime >= django_timezone.now() - timedelta(days=1)
|
|
]
|
|
|
|
for appointment in relevant_appointments:
|
|
# Find appropriate queue for this appointment
|
|
suitable_queues = [
|
|
q for q in queues
|
|
if q.tenant == appointment.tenant
|
|
and (
|
|
q.specialty == appointment.specialty or (
|
|
q.queue_type == 'EMERGENCY' and appointment.priority == 'EMERGENCY'))
|
|
]
|
|
|
|
if not suitable_queues:
|
|
continue
|
|
|
|
queue = random.choice(suitable_queues)
|
|
|
|
# Determine queue entry status
|
|
if appointment.status == 'CHECKED_IN':
|
|
entry_status = 'WAITING'
|
|
elif appointment.status == 'IN_PROGRESS':
|
|
entry_status = 'IN_SERVICE'
|
|
else: # COMPLETED
|
|
entry_status = 'COMPLETED'
|
|
|
|
# Calculate priority score
|
|
priority_weights = queue.priority_weights
|
|
base_score = priority_weights.get(appointment.priority, 1)
|
|
urgency_bonus = appointment.urgency_score * 0.1
|
|
priority_score = base_score + urgency_bonus
|
|
|
|
# Queue position (random for now)
|
|
queue_position = random.randint(1, min(10, queue.max_queue_size))
|
|
|
|
# Timing information
|
|
joined_at = appointment.checked_in_at or appointment.scheduled_datetime
|
|
estimated_service_time = joined_at + timedelta(minutes=queue.average_service_time_minutes)
|
|
|
|
called_at = None
|
|
served_at = None
|
|
|
|
if entry_status in ['IN_SERVICE', 'COMPLETED']:
|
|
# Patient was called before being served
|
|
call_delay = random.randint(5, 30) # 5-30 minutes wait
|
|
called_at = joined_at + timedelta(minutes=call_delay)
|
|
|
|
if entry_status == 'COMPLETED':
|
|
serve_delay = random.randint(1, 10) # 1-10 minutes after being called
|
|
served_at = called_at + timedelta(minutes=serve_delay)
|
|
|
|
# Notification information
|
|
notification_sent = entry_status != 'WAITING'
|
|
notification_method = random.choice(['SMS', 'APP', 'PAGER']) if notification_sent else None
|
|
|
|
try:
|
|
entry = QueueEntry.objects.create(
|
|
queue=queue,
|
|
patient=appointment.patient,
|
|
appointment=appointment,
|
|
queue_position=queue_position,
|
|
priority_score=priority_score,
|
|
joined_at=joined_at,
|
|
estimated_service_time=estimated_service_time,
|
|
called_at=called_at,
|
|
served_at=served_at,
|
|
status=entry_status,
|
|
assigned_provider=appointment.provider,
|
|
notification_sent=notification_sent,
|
|
notification_method=notification_method,
|
|
notes=f"Queue entry for {appointment.appointment_type.lower()} appointment",
|
|
updated_at=django_timezone.now() - timedelta(minutes=random.randint(0, 60))
|
|
)
|
|
entries.append(entry)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating queue entry for appointment {appointment.request_id}: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(entries)} queue entries")
|
|
return entries
|
|
|
|
|
|
def create_queue_configurations(queues):
|
|
"""Create queue configurations for Phase 11 advanced queue management"""
|
|
configurations = []
|
|
|
|
for queue in queues:
|
|
try:
|
|
config, created = QueueConfiguration.objects.get_or_create(
|
|
queue=queue,
|
|
defaults={
|
|
'use_dynamic_positioning': True,
|
|
'priority_weight': 0.5,
|
|
'wait_time_weight': 0.3,
|
|
'appointment_time_weight': 0.2,
|
|
'enable_overflow_queue': queue.queue_type == 'EMERGENCY',
|
|
'overflow_threshold': int(queue.max_queue_size * 0.9),
|
|
'use_historical_data': True,
|
|
'default_service_time_minutes': queue.average_service_time_minutes,
|
|
'historical_data_days': 30,
|
|
'enable_websocket_updates': True,
|
|
'update_interval_seconds': 30,
|
|
'load_factor_normal_threshold': 0.5,
|
|
'load_factor_moderate_threshold': 0.75,
|
|
'load_factor_high_threshold': 0.9,
|
|
'auto_reposition_enabled': True,
|
|
'reposition_interval_minutes': 15,
|
|
'notify_on_position_change': True,
|
|
'position_change_threshold': 3
|
|
}
|
|
)
|
|
configurations.append(config)
|
|
if created:
|
|
print(f"Created queue configuration for {queue.name}")
|
|
else:
|
|
print(f"Queue configuration already exists for {queue.name}")
|
|
except Exception as e:
|
|
print(f"Error creating queue configuration for {queue.name}: {e}")
|
|
continue
|
|
|
|
print(f"Total queue configurations: {len(configurations)}")
|
|
return configurations
|
|
|
|
|
|
def create_queue_metrics(queues, days_back=7):
|
|
"""Create queue metrics for Phase 11 analytics"""
|
|
metrics = []
|
|
|
|
start_date = django_timezone.now().date() - timedelta(days=days_back)
|
|
|
|
for queue in queues:
|
|
for day_offset in range(days_back):
|
|
metric_date = start_date + timedelta(days=day_offset)
|
|
|
|
# Create hourly metrics for operating hours (8 AM to 8 PM)
|
|
for hour in range(8, 21):
|
|
try:
|
|
# Simulate realistic metrics
|
|
total_entries = random.randint(5, 30)
|
|
completed = int(total_entries * random.uniform(0.7, 0.9))
|
|
no_shows = int(total_entries * random.uniform(0.05, 0.15))
|
|
left_queue = total_entries - completed - no_shows
|
|
|
|
avg_wait = random.randint(10, 45)
|
|
max_wait = int(avg_wait * random.uniform(1.5, 2.5))
|
|
min_wait = int(avg_wait * random.uniform(0.3, 0.6))
|
|
|
|
avg_service = queue.average_service_time_minutes + random.randint(-10, 10)
|
|
peak_size = random.randint(5, min(20, queue.max_queue_size))
|
|
avg_size = int(peak_size * random.uniform(0.5, 0.8))
|
|
|
|
utilization = (completed / total_entries * 100) if total_entries > 0 else 0
|
|
no_show_rate = (no_shows / total_entries * 100) if total_entries > 0 else 0
|
|
|
|
metric = QueueMetrics.objects.create(
|
|
queue=queue,
|
|
date=metric_date,
|
|
hour=hour,
|
|
total_entries=total_entries,
|
|
completed_entries=completed,
|
|
no_shows=no_shows,
|
|
left_queue=left_queue,
|
|
average_wait_time_minutes=avg_wait,
|
|
max_wait_time_minutes=max_wait,
|
|
min_wait_time_minutes=min_wait,
|
|
average_service_time_minutes=avg_service,
|
|
peak_queue_size=peak_size,
|
|
average_queue_size=avg_size,
|
|
utilization_rate=utilization,
|
|
no_show_rate=no_show_rate
|
|
)
|
|
metrics.append(metric)
|
|
|
|
except Exception as e:
|
|
print(f"Error creating queue metric for {queue.name} on {metric_date} at {hour}:00: {e}")
|
|
continue
|
|
|
|
print(f"Created {len(metrics)} queue metrics entries")
|
|
return metrics
|
|
|
|
|
|
def main():
|
|
"""Main function to generate all appointments data"""
|
|
print("Starting Saudi Healthcare Appointments Data Generation...")
|
|
|
|
# Get existing tenants
|
|
tenants = list(Tenant.objects.all())
|
|
if not tenants:
|
|
print("❌ No tenants found. Please run the core data generator first.")
|
|
return
|
|
|
|
# Create appointment templates
|
|
print("\n1. Creating Appointment Templates...")
|
|
templates = create_appointment_templates(tenants)
|
|
|
|
# Create waiting queues
|
|
print("\n2. Creating Waiting Queues...")
|
|
queues = create_waiting_queues(tenants)
|
|
|
|
# Create slot availability
|
|
print("\n3. Creating Provider Availability Slots...")
|
|
slots = create_slot_availability(tenants, 30)
|
|
|
|
# Create appointment requests
|
|
print("\n4. Creating Appointment Requests...")
|
|
appointments = create_appointment_requests(tenants, slots, 14, 50)
|
|
|
|
# Create telemedicine sessions
|
|
print("\n5. Creating Telemedicine Sessions...")
|
|
sessions = create_telemedicine_sessions(appointments)
|
|
|
|
# Create waiting list entries
|
|
print("\n6. Creating Waiting List Entries...")
|
|
waiting_list_entries = create_waiting_list_entries(tenants, 100)
|
|
|
|
# Create waiting list contact logs
|
|
print("\n7. Creating Waiting List Contact Logs...")
|
|
contact_logs = create_waiting_list_contact_logs(waiting_list_entries)
|
|
|
|
# Create queue entries
|
|
print("\n8. Creating Queue Entries...")
|
|
entries = create_queue_entries(queues, appointments)
|
|
|
|
# Create queue configurations (Phase 11)
|
|
print("\n9. Creating Queue Configurations (Phase 11)...")
|
|
queue_configs = create_queue_configurations(queues)
|
|
|
|
# Create queue metrics (Phase 11)
|
|
print("\n10. Creating Queue Metrics (Phase 11)...")
|
|
queue_metrics = create_queue_metrics(queues, days_back=7)
|
|
|
|
print(f"\n✅ Saudi Healthcare Appointments Data Generation Complete!")
|
|
print(f" Summary:")
|
|
print(f" - Appointment Templates: {len(templates)}")
|
|
print(f" - Waiting Queues: {len(queues)}")
|
|
print(f" - Availability Slots: {len(slots)}")
|
|
print(f" - Appointment Requests: {len(appointments)}")
|
|
print(f" - Telemedicine Sessions: {len(sessions)}")
|
|
print(f" - Waiting List Entries: {len(waiting_list_entries)}")
|
|
print(f" - Waiting List Contact Logs: {len(contact_logs)}")
|
|
print(f" - Queue Entries: {len(entries)}")
|
|
print(f" - Queue Configurations (Phase 11): {len(queue_configs)}")
|
|
print(f" - Queue Metrics (Phase 11): {len(queue_metrics)}")
|
|
|
|
# Only show distributions if appointments exist
|
|
if appointments:
|
|
# Appointment status distribution
|
|
status_counts = {}
|
|
for appointment in appointments:
|
|
status_counts[appointment.status] = status_counts.get(appointment.status, 0) + 1
|
|
|
|
print(f"\n📊 Appointment Status Distribution:")
|
|
for status, count in status_counts.items():
|
|
print(f" - {status.title()}: {count}")
|
|
|
|
# Appointment type distribution
|
|
type_counts = {}
|
|
for appointment in appointments:
|
|
type_counts[appointment.appointment_type] = type_counts.get(appointment.appointment_type, 0) + 1
|
|
|
|
print(f"\n📊 Appointment Type Distribution:")
|
|
for apt_type, count in sorted(type_counts.items(), key=lambda x: x[1], reverse=True)[:10]:
|
|
print(f" - {apt_type.replace('_', ' ').title()}: {count}")
|
|
|
|
# Telemedicine statistics
|
|
telemedicine_count = len([apt for apt in appointments if apt.is_telemedicine])
|
|
print(
|
|
f"\n💻 Telemedicine Appointments: {telemedicine_count} ({telemedicine_count / len(appointments) * 100:.1f}%)")
|
|
else:
|
|
print(f"\n⚠️ No appointments were created. Check provider and patient data.")
|
|
|
|
return {
|
|
'templates': templates,
|
|
'queues': queues,
|
|
'slots': slots,
|
|
'appointments': appointments,
|
|
'sessions': sessions,
|
|
'waiting_list_entries': waiting_list_entries,
|
|
'contact_logs': contact_logs,
|
|
'entries': entries,
|
|
'queue_configs': queue_configs,
|
|
'queue_metrics': queue_metrics
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|