HH/apps/complaints/management/commands/test_staff_matching_in_complaint.py
2026-01-24 15:27:27 +03:00

411 lines
18 KiB
Python

"""
Management command to test staff matching functionality in complaints.
This command creates a test complaint with 2-3 staff members mentioned
and verifies if the AI-based staff matching is working correctly.
"""
import random
import uuid
from datetime import datetime
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
from apps.core.ai_service import AIService
# English complaint templates with placeholders for staff names
ENGLISH_COMPLAINT_TEMPLATES = [
{
'title': 'Issues with multiple staff members',
'description': 'I had a very unpleasant experience during my stay. Nurse {staff1_name} was rude and dismissive when I asked for pain medication. Later, Dr. {staff2_name} did not explain my treatment plan properly and seemed rushed. The third staff member, {staff3_name}, was actually helpful but the overall experience was poor.',
'category': 'staff_behavior',
'severity': 'high',
'priority': 'high'
},
{
'title': 'Excellent care from nursing team',
'description': 'I want to commend the excellent care I received. Nurse {staff1_name} was particularly attentive and caring throughout my stay. {staff2_name} also went above and beyond to ensure my comfort. Dr. {staff3_name} was thorough and took time to answer all my questions.',
'category': 'clinical_care',
'severity': 'low',
'priority': 'low'
},
{
'title': 'Mixed experience with hospital staff',
'description': 'My experience was mixed. Nurse {staff1_name} was professional and efficient, but {staff2_name} made a medication error that was concerning. Dr. {staff3_name} was helpful in resolving the situation, but the initial error was unacceptable.',
'category': 'clinical_care',
'severity': 'high',
'priority': 'high'
}
]
# Arabic complaint templates with placeholders for staff names
ARABIC_COMPLAINT_TEMPLATES = [
{
'title': 'مشاكل مع عدة موظفين',
'description': 'كانت لدي تجربة غير سارة جداً خلال إقامتي. الممرضة {staff1_name} كانت غير مهذبة ومتجاهلة عندما طلبت دواء للم. لاحقاً، د. {staff2_name} لم يوضح خطة علاجي بشكل صحيح وبدو متسرع. كان الموظف الثالث {staff3_name} مفيداً فعلاً ولكن التجربة العامة كانت سيئة.',
'category': 'staff_behavior',
'severity': 'high',
'priority': 'high'
},
{
'title': 'رعاية ممتازة من فريق التمريض',
'description': 'أريد أن أشكر الرعاية الممتازة التي تلقيتها. الممرضة {staff1_name} كانت مهتمة وراعية بشكل خاص طوال إقامتي. {staff2_name} أيضاً بذل ما هو أبعد من المتوقع لضمان راحتي. د. {staff3_name} كان دقيقاً وأخذ وقتاً للإجابة على جميع أسئلتي.',
'category': 'clinical_care',
'severity': 'low',
'priority': 'low'
},
{
'title': 'تجربة مختلطة مع موظفي المستشفى',
'description': 'كانت تجربتي مختلطة. الممرضة {staff1_name} كانت مهنية وفعالة، لكن {staff2_name} ارتكب خطأ في الدواء كان مقلقاً. د. {staff3_name} كان مفيداً في حل الموقف، لكن الخطأ الأولي كان غير مقبول.',
'category': 'clinical_care',
'severity': 'high',
'priority': 'high'
}
]
class Command(BaseCommand):
help = 'Test staff matching functionality by creating a complaint with mentioned staff'
def add_arguments(self, parser):
parser.add_argument(
'--hospital-code',
type=str,
help='Target hospital code (default: first active hospital)'
)
parser.add_argument(
'--staff-count',
type=int,
default=3,
help='Number of staff to test (2 or 3, default: 3)'
)
parser.add_argument(
'--language',
type=str,
default='en',
choices=['en', 'ar'],
help='Complaint language (en/ar, default: en)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Preview without creating complaint'
)
parser.add_argument(
'--template-index',
type=int,
help='Template index to use (0-2, default: random)'
)
def handle(self, *args, **options):
hospital_code = options['hospital_code']
staff_count = options['staff_count']
language = options['language']
dry_run = options['dry_run']
template_index = options['template_index']
# Validate staff count
if staff_count not in [2, 3]:
self.stdout.write(self.style.ERROR("staff-count must be 2 or 3"))
return
self.stdout.write(f"\n{'='*80}")
self.stdout.write("🧪 STAFF MATCHING TEST COMMAND")
self.stdout.write(f"{'='*80}\n")
# Get hospital
if hospital_code:
hospital = Hospital.objects.filter(code=hospital_code).first()
if not hospital:
self.stdout.write(
self.style.ERROR(f"Hospital with code '{hospital_code}' not found")
)
return
else:
hospital = Hospital.objects.filter(status='active').first()
if not hospital:
self.stdout.write(
self.style.ERROR("No active hospitals found")
)
return
self.stdout.write(f"🏥 Hospital: {hospital.name} (Code: {hospital.code})")
# Get active staff from hospital
all_staff = Staff.objects.filter(hospital=hospital, status='active')
if all_staff.count() < staff_count:
self.stdout.write(
self.style.ERROR(
f"Not enough staff found. Found {all_staff.count()}, need {staff_count}"
)
)
return
# Select random staff
selected_staff = random.sample(list(all_staff), staff_count)
self.stdout.write(f"\n👥 Selected Staff ({staff_count} members):")
for i, staff in enumerate(selected_staff, 1):
if language == 'ar' and staff.first_name_ar:
name = f"{staff.first_name_ar} {staff.last_name_ar}"
name_en = f"{staff.first_name} {staff.last_name}"
else:
name = f"{staff.first_name} {staff.last_name}"
name_en = name
self.stdout.write(
f" {i}. {name} (EN: {name_en})"
)
self.stdout.write(f" ID: {staff.id}")
self.stdout.write(f" Job Title: {staff.job_title}")
self.stdout.write(f" Department: {staff.department.name if staff.department else 'N/A'}")
# Select template
templates = ARABIC_COMPLAINT_TEMPLATES if language == 'ar' else ENGLISH_COMPLAINT_TEMPLATES
if template_index is not None:
if 0 <= template_index < len(templates):
template = templates[template_index]
else:
self.stdout.write(
self.style.WARNING(f"Template index {template_index} out of range, using random")
)
template = random.choice(templates)
else:
template = random.choice(templates)
# Prepare complaint data
complaint_data = self.prepare_complaint(
template=template,
staff=selected_staff,
hospital=hospital,
language=language
)
self.stdout.write(f"\n📋 Complaint Details:")
self.stdout.write(f" Title: {complaint_data['title']}")
self.stdout.write(f" Category: {complaint_data['category']}")
self.stdout.write(f" Severity: {complaint_data['severity']}")
self.stdout.write(f" Priority: {complaint_data['priority']}")
self.stdout.write(f"\n Description:")
self.stdout.write(f" {complaint_data['description']}")
# Test staff matching
self.stdout.write(f"\n{'='*80}")
self.stdout.write("🔍 STAFF MATCHING TEST")
self.stdout.write(f"{'='*80}\n")
from apps.complaints.tasks import match_staff_from_name
matched_staff = []
unmatched_staff = []
for staff in selected_staff:
if language == 'ar' and staff.first_name_ar:
name_to_match = f"{staff.first_name_ar} {staff.last_name_ar}"
else:
name_to_match = f"{staff.first_name} {staff.last_name}"
self.stdout.write(f"\n🔎 Testing: '{name_to_match}'")
self.stdout.write(f" Staff ID: {staff.id}")
# Test matching
matches, confidence, method = match_staff_from_name(
staff_name=name_to_match,
hospital_id=str(hospital.id),
department_name=None,
return_all=True
)
if matches:
found = any(m['id'] == str(staff.id) for m in matches)
if found:
self.stdout.write(
self.style.SUCCESS(f" ✅ MATCHED! (confidence: {confidence:.2f}, method: {method})")
)
matched_staff.append({
'staff': staff,
'confidence': confidence,
'method': method
})
else:
self.stdout.write(
self.style.WARNING(f" ⚠️ Found {len(matches)} matches but not the correct one")
)
for i, match in enumerate(matches[:3], 1):
self.stdout.write(f" {i}. {match['name_en']} (confidence: {match['confidence']:.2f})")
unmatched_staff.append(staff)
else:
self.stdout.write(
self.style.ERROR(f" ❌ NO MATCHES (confidence: {confidence:.2f}, method: {method})")
)
unmatched_staff.append(staff)
# Summary
self.stdout.write(f"\n{'='*80}")
self.stdout.write("📊 TEST SUMMARY")
self.stdout.write(f"{'='*80}\n")
self.stdout.write(f"Total staff tested: {len(selected_staff)}")
self.stdout.write(f"Matched: {len(matched_staff)}")
self.stdout.write(f"Unmatched: {len(unmatched_staff)}")
if matched_staff:
self.stdout.write(f"\n✅ Matched Staff:")
for item in matched_staff:
staff = item['staff']
name = f"{staff.first_name} {staff.last_name}"
self.stdout.write(f" - {name} (confidence: {item['confidence']:.2f}, method: {item['method']})")
if unmatched_staff:
self.stdout.write(f"\n❌ Unmatched Staff:")
for staff in unmatched_staff:
name = f"{staff.first_name} {staff.last_name}"
self.stdout.write(f" - {name} (ID: {staff.id})")
# Create complaint if not dry run
if not dry_run:
self.stdout.write(f"\n{'='*80}")
self.stdout.write("💾 CREATING COMPLAINT")
self.stdout.write(f"{'='*80}\n")
try:
with transaction.atomic():
# Create complaint
complaint = Complaint.objects.create(
reference_number=self.generate_reference_number(hospital.code),
hospital=hospital,
department=selected_staff[0].department if selected_staff[0].department else None,
category=complaint_data['category'],
title=complaint_data['title'],
description=complaint_data['description'],
severity=complaint_data['severity'],
priority=complaint_data['priority'],
source=self.get_source_instance(),
status='open',
contact_name='Test Patient',
contact_phone='+966500000000',
contact_email='test@example.com',
)
# Create timeline entry
ComplaintUpdate.objects.create(
complaint=complaint,
update_type='status_change',
old_status='',
new_status='open',
message='Complaint created for staff matching test',
created_by=None
)
self.stdout.write(
self.style.SUCCESS(f"✓ Complaint created successfully!")
)
self.stdout.write(f" Reference: {complaint.reference_number}")
self.stdout.write(f" ID: {complaint.id}")
# Trigger AI analysis
self.stdout.write(f"\n{'='*80}")
self.stdout.write("🤖 AI ANALYSIS")
self.stdout.write(f"{'='*80}\n")
ai_service = AIService()
analysis = ai_service.analyze_complaint(
title=complaint.title,
description=complaint.description,
category=complaint.category.name_en if complaint.category else None,
hospital_id=hospital.id
)
self.stdout.write(f"AI Analysis Results:")
# Display extracted staff names
staff_names = analysis.get('staff_names', [])
if staff_names:
self.stdout.write(f"\n Extracted Staff Names ({len(staff_names)}):")
for i, staff_name in enumerate(staff_names, 1):
self.stdout.write(f" {i}. {staff_name}")
else:
self.stdout.write(f" No staff names extracted")
# Display primary staff
primary_staff = analysis.get('primary_staff_name', '')
if primary_staff:
self.stdout.write(f"\n Primary Staff: {primary_staff}")
# Display classification results
self.stdout.write(f"\n Classification:")
self.stdout.write(f" - Complaint Type: {analysis.get('complaint_type', 'N/A')}")
self.stdout.write(f" - Severity: {analysis.get('severity', 'N/A')}")
self.stdout.write(f" - Priority: {analysis.get('priority', 'N/A')}")
self.stdout.write(f" - Category: {analysis.get('category', 'N/A')}")
self.stdout.write(f" - Subcategory: {analysis.get('subcategory', 'N/A')}")
self.stdout.write(f" - Department: {analysis.get('department', 'N/A')}")
self.stdout.write(f"\n{'='*80}")
self.stdout.write(f"✅ TEST COMPLETED")
self.stdout.write(f"{'='*80}\n")
except Exception as e:
self.stdout.write(
self.style.ERROR(f"Error creating complaint: {str(e)}")
)
import traceback
self.stdout.write(traceback.format_exc())
else:
self.stdout.write(f"\n{'='*80}")
self.stdout.write(self.style.WARNING("🔍 DRY RUN - No changes made"))
self.stdout.write(f"{'='*80}\n")
def prepare_complaint(self, template, staff, hospital, language):
"""Prepare complaint data from template with staff names"""
# Get category
category = ComplaintCategory.objects.filter(
code=template['category'],
is_active=True
).first()
# Format description with staff names
description = template['description']
if len(staff) == 2:
description = description.format(
staff1_name=self.get_staff_name(staff[0], language),
staff2_name=self.get_staff_name(staff[1], language),
staff3_name=''
)
elif len(staff) == 3:
description = description.format(
staff1_name=self.get_staff_name(staff[0], language),
staff2_name=self.get_staff_name(staff[1], language),
staff3_name=self.get_staff_name(staff[2], language)
)
return {
'title': template['title'],
'description': description,
'category': category,
'severity': template['severity'],
'priority': template['priority']
}
def get_staff_name(self, staff, language):
"""Get staff name in appropriate language"""
if language == 'ar' and staff.first_name_ar:
return f"{staff.first_name_ar} {staff.last_name_ar}"
else:
return f"{staff.first_name} {staff.last_name}"
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 get_source_instance(self):
"""Get PXSource instance"""
try:
return PXSource.objects.get(name_en='Online Form', is_active=True)
except PXSource.DoesNotExist:
return PXSource.objects.filter(is_active=True).first()