411 lines
18 KiB
Python
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()
|