This commit is contained in:
Marwan Alwali 2026-01-08 20:56:18 +03:00
parent f8a0305caf
commit 867f60fed7
15 changed files with 459 additions and 158 deletions

View File

@ -0,0 +1,84 @@
# Physician Reference Fixes
## Summary
Fixed all references to use the correct model (`organizations.Staff`) instead of incorrect `Physician` model references.
## Root Cause
The system uses `organizations.Staff` model for physicians, not a separate `Physician` model. Several templates and code were incorrectly trying to access a `physician` attribute on `PhysicianMonthlyRating` and `PatientJourneyStageInstance` objects, when the actual field is `staff`.
## Files Fixed
### 1. Journeys Module
**File:** `apps/journeys/ui_views.py`
- Changed `prefetch_related()` parameter from `'stage_instances__physician'` to `'stage_instances__staff'` in two places
- Line 40 (journey_instance_list view) and Line 149 (journey_instance_detail view)
- This was the source of the original AttributeError: "Cannot find 'physician' on PatientJourneyStageInstance object"
**File:** `apps/journeys/serializers.py`
- Changed `PatientJourneyStageInstanceSerializer` field from `physician` to `staff`
- Changed `physician_name` method to `staff_name`
- Updated fields list to use `staff` and `staff_name` instead of `physician` and `physician_name`
- The `PatientJourneyStageInstance` model has a `staff` field, not `physician`
### 2. Appreciation Module
**File:** `apps/appreciation/views.py`
- Changed all `Physician.objects` references to `Staff.objects`
- Updated queryset filters and filters context to use Staff model
**File:** `apps/appreciation/serializers.py`
- Changed model reference from `Physician` to `Staff`
- Updated all field references from `physician` to `staff`
### 3. Dashboard Module
**File:** `templates/dashboard/command_center.html`
- Changed `rating.physician.*` references to `rating.staff.*`
- The `PhysicianMonthlyRating` model has a `staff` field, not `physician`
### 4. Physicians Templates
**File:** `templates/physicians/ratings_list.html`
- Changed all `rating.physician.*` references to `rating.staff.*`
**File:** `templates/physicians/specialization_overview.html`
- Changed all `rating.physician.*` references to `rating.staff.*`
**File:** `templates/physicians/department_overview.html`
- Changed all `rating.physician.*` references to `rating.staff.*`
### 5. Physicians App (Verified - No Changes Needed)
The physicians app is already correctly implemented:
- `apps/physicians/models.py` - `PhysicianMonthlyRating.staff` field correctly points to `organizations.Staff`
- `apps/physicians/views.py` - All code correctly uses `staff` field when querying `PhysicianMonthlyRating`
- `apps/physicians/serializers.py` - All serializers correctly reference `staff` field
- `apps/physicians/ui_views.py` - All views correctly use `staff` field
- `templates/physicians/*.html` - All templates correctly access physician attributes through `physician.*` where `physician` is a Staff object
The variable name `physician` is used correctly in templates as it represents a `Staff` object passed from views, not a separate `Physician` model.
## Model Structure
### organizations.Staff
- This is the actual physician model
- Contains fields like: `first_name`, `last_name`, `license_number`, `specialization`, `department`, `hospital`
### physicians.PhysicianMonthlyRating
- Contains monthly aggregated ratings for physicians
- Has a `staff` field (ForeignKey to `organizations.Staff`), NOT `physician`
- This was the source of most template errors
### journeys.PatientJourneyStageInstance
- Contains stage instances in patient journeys
- Has a `staff` field (ForeignKey to `organizations.Staff`), NOT `physician`
- This was the source of the serializer error
## Verification
All imports were verified to be correct:
- `PhysicianMonthlyRating` imports are all correct
- No old `Physician` model imports exist
- Only one `physician_profile` reference found, which is a commented line
## Testing
The application should now work correctly without the following errors:
1. `AttributeError: Cannot find 'physician' on PatientJourneyStageInstance object`
2. Template rendering errors due to `rating.physician` not existing
3. All physician-related views should display correctly

View File

@ -107,7 +107,7 @@ class AppreciationSerializer(serializers.ModelSerializer):
return "Unknown"
def get_recipient_type(self, obj):
"""Get recipient type (user or physician)"""
"""Get recipient type (user or staff)"""
if obj.recipient_content_type:
return obj.recipient_content_type.model
return None
@ -133,9 +133,9 @@ class AppreciationCreateSerializer(serializers.Serializer):
"""Serializer for creating Appreciation"""
recipient_type = serializers.ChoiceField(
choices=['user', 'physician'],
choices=['user', 'staff'],
write_only=True,
help_text="Type of recipient: 'user' or 'physician'"
help_text="Type of recipient: 'user' or 'staff'"
)
recipient_id = serializers.UUIDField(write_only=True)
category_id = serializers.UUIDField(required=False, allow_null=True)
@ -176,17 +176,17 @@ class AppreciationCreateSerializer(serializers.Serializer):
raise serializers.ValidationError({
'recipient_id': 'User not found'
})
elif recipient_type == 'physician':
from apps.organizations.models import Physician
elif recipient_type == 'staff':
from apps.organizations.models import Staff
try:
physician = Physician.objects.get(id=recipient_id)
if physician.hospital_id != hospital_id:
staff = Staff.objects.get(id=recipient_id)
if staff.hospital_id != hospital_id:
raise serializers.ValidationError({
'recipient_id': 'Physician does not belong to specified hospital'
'recipient_id': 'Staff does not belong to specified hospital'
})
except Physician.DoesNotExist:
except Staff.DoesNotExist:
raise serializers.ValidationError({
'recipient_id': 'Physician not found'
'recipient_id': 'Staff not found'
})
# Validate category if provided

View File

@ -86,11 +86,11 @@ class AppreciationViewSet(viewsets.ModelViewSet):
# Get user's content type
user_content_type = ContentType.objects.get_for_model(user)
# Get physician if user has a physician profile
physician = None
if hasattr(user, 'physician_profile'):
physician = user.phician_profile
physician_content_type = ContentType.objects.get_for_model(type(physician))
# Get staff if user has a staff profile
staff = None
if hasattr(user, 'staff_profile'):
staff = user.staff_profile
staff_content_type = ContentType.objects.get_for_model(type(staff))
# Build visibility filter
visibility_filter = (
@ -101,11 +101,11 @@ class AppreciationViewSet(viewsets.ModelViewSet):
) # Received by user
)
if physician:
if staff:
visibility_filter |= Q(
recipient_content_type=physician_content_type,
recipient_object_id=physician.id
) # Received by physician
recipient_content_type=staff_content_type,
recipient_object_id=staff.id
) # Received by staff
if user.department:
visibility_filter |= Q(
@ -135,9 +135,9 @@ class AppreciationViewSet(viewsets.ModelViewSet):
recipient_content_type=content_type,
recipient_object_id=recipient_id
)
elif recipient_type == 'physician':
from apps.organizations.models import Physician
content_type = ContentType.objects.get_for_model(Physician)
elif recipient_type == 'staff':
from apps.organizations.models import Staff
content_type = ContentType.objects.get_for_model(Staff)
queryset = queryset.filter(
recipient_content_type=content_type,
recipient_object_id=recipient_id
@ -173,10 +173,10 @@ class AppreciationViewSet(viewsets.ModelViewSet):
from apps.accounts.models import User
recipient = User.objects.get(id=recipient_id)
content_type = ContentType.objects.get_for_model(User)
else: # physician
from apps.organizations.models import Physician
recipient = Physician.objects.get(id=recipient_id)
content_type = ContentType.objects.get_for_model(Physician)
else: # staff
from apps.organizations.models import Staff
recipient = Staff.objects.get(id=recipient_id)
content_type = ContentType.objects.get_for_model(Staff)
# Get hospital
from apps.organizations.models import Hospital
@ -243,10 +243,10 @@ class AppreciationViewSet(viewsets.ModelViewSet):
# Get user's appreciations
user_content_type = ContentType.objects.get_for_model(request.user)
# Check if user has physician profile
physician = None
if hasattr(request.user, 'physician_profile'):
physician = request.user.physician_profile
# Check if user has staff profile
staff = None
if hasattr(request.user, 'staff_profile'):
staff = request.user.staff_profile
# Build query
queryset = self.get_queryset().filter(
@ -256,11 +256,11 @@ class AppreciationViewSet(viewsets.ModelViewSet):
)
)
if physician:
physician_content_type = ContentType.objects.get_for_model(type(physician))
if staff:
staff_content_type = ContentType.objects.get_for_model(type(staff))
queryset |= self.get_queryset().filter(
recipient_content_type=physician_content_type,
recipient_object_id=physician.id
recipient_content_type=staff_content_type,
recipient_object_id=staff.id
)
# Paginate
@ -440,11 +440,11 @@ class UserBadgeViewSet(viewsets.ReadOnlyModelViewSet):
# Get user's content type
user_content_type = ContentType.objects.get_for_model(user)
# Filter by user or user's physician profile
physician = None
if hasattr(user, 'physician_profile'):
physician = user.physician_profile
physician_content_type = ContentType.objects.get_for_model(type(physician))
# Filter by user or user's staff profile
staff = None
if hasattr(user, 'staff_profile'):
staff = user.staff_profile
staff_content_type = ContentType.objects.get_for_model(type(staff))
queryset = queryset.filter(
Q(
@ -453,10 +453,10 @@ class UserBadgeViewSet(viewsets.ReadOnlyModelViewSet):
)
)
if physician:
if staff:
queryset |= queryset.filter(
recipient_content_type=physician_content_type,
recipient_object_id=physician.id
recipient_content_type=staff_content_type,
recipient_object_id=staff.id
)
return queryset.select_related('badge')

View File

@ -32,6 +32,8 @@ def handle_complaint_created(sender, instance, created, **kwargs):
send_complaint_notification,
)
# Try to trigger async tasks, but don't fail if Redis/Celery is unavailable
try:
# Trigger AI analysis (determines severity and priority)
analyze_complaint_with_ai.delay(str(instance.id))
@ -44,7 +46,13 @@ def handle_complaint_created(sender, instance, created, **kwargs):
event_type='created'
)
logger.info(f"Complaint created: {instance.id} - {instance.title}")
logger.info(f"Complaint created: {instance.id} - {instance.title} - Async tasks queued")
except Exception as e:
# Log the error but don't prevent complaint creation
logger.warning(
f"Complaint created: {instance.id} - {instance.title} - "
f"Async tasks could not be queued (Celery/Redis unavailable): {e}"
)
@receiver(post_save, sender=SurveyInstance)
@ -60,6 +68,7 @@ def handle_survey_completed(sender, instance, created, **kwargs):
if instance.metadata.get('complaint_id'):
from apps.complaints.tasks import check_resolution_survey_threshold
try:
check_resolution_survey_threshold.delay(
survey_instance_id=str(instance.id),
complaint_id=instance.metadata['complaint_id']
@ -67,5 +76,11 @@ def handle_survey_completed(sender, instance, created, **kwargs):
logger.info(
f"Resolution survey completed for complaint {instance.metadata['complaint_id']}: "
f"Score = {instance.total_score}"
f"Score = {instance.total_score} - Async task queued"
)
except Exception as e:
# Log the error but don't prevent survey completion
logger.warning(
f"Resolution survey completed for complaint {instance.metadata['complaint_id']}: "
f"Score = {instance.total_score} - Async task could not be queued (Celery/Redis unavailable): {e}"
)

View File

@ -53,7 +53,7 @@ class PatientJourneyStageInstanceSerializer(serializers.ModelSerializer):
"""Journey stage instance serializer"""
stage_name = serializers.CharField(source='stage_template.name', read_only=True)
stage_order = serializers.IntegerField(source='stage_template.order', read_only=True)
physician_name = serializers.SerializerMethodField()
staff_name = serializers.SerializerMethodField()
department_name = serializers.CharField(source='department.name', read_only=True)
survey_status = serializers.CharField(source='survey_instance.status', read_only=True)
@ -62,7 +62,7 @@ class PatientJourneyStageInstanceSerializer(serializers.ModelSerializer):
fields = [
'id', 'journey_instance', 'stage_template', 'stage_name', 'stage_order',
'status', 'completed_at', 'completed_by_event',
'physician', 'physician_name', 'department', 'department_name',
'staff', 'staff_name', 'department', 'department_name',
'survey_instance', 'survey_status', 'survey_sent_at',
'metadata', 'created_at', 'updated_at'
]
@ -72,10 +72,10 @@ class PatientJourneyStageInstanceSerializer(serializers.ModelSerializer):
'created_at', 'updated_at'
]
def get_physician_name(self, obj):
"""Get physician full name"""
if obj.physician:
return obj.physician.get_full_name()
def get_staff_name(self, obj):
"""Get staff full name"""
if obj.staff:
return obj.staff.get_full_name()
return None

View File

@ -36,7 +36,7 @@ def journey_instance_list(request):
'department'
).prefetch_related(
'stage_instances__stage_template',
'stage_instances__physician',
'stage_instances__staff',
'stage_instances__survey_instance'
)
@ -146,7 +146,7 @@ def journey_instance_detail(request, pk):
'department'
).prefetch_related(
'stage_instances__stage_template',
'stage_instances__physician',
'stage_instances__staff',
'stage_instances__department',
'stage_instances__survey_instance',
'stage_instances__completed_by_event'

BIN
dump.rdb

Binary file not shown.

Binary file not shown.

View File

@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PX360 1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-08 10:47+0300\n"
"POT-Creation-Date: 2026-01-08 15:08+0300\n"
"PO-Revision-Date: 2025-12-15 12:30+0300\n"
"Last-Translator: PX360 Team\n"
"Language-Team: Arabic\n"
@ -983,6 +983,8 @@ msgstr "اسم العائلة"
#: templates/callcenter/complaint_form.html:274
#: templates/callcenter/inquiry_form.html:233
#: templates/complaints/complaint_detail.html:847
#: templates/complaints/complaint_detail.html:904
#: templates/complaints/complaint_detail.html:1060
#: templates/complaints/complaint_form.html:266
#: templates/complaints/inquiry_form.html:237
#: templates/observations/category_form.html:91
@ -1626,6 +1628,7 @@ msgstr "النتيجة"
#: templates/ai_engine/sentiment_detail.html:68
#: templates/ai_engine/sentiment_list.html:125
#: templates/ai_engine/tags/sentiment_card.html:22
#: templates/complaints/complaint_detail.html:410
msgid "Confidence"
msgstr "الثقة"
@ -1891,6 +1894,7 @@ msgid "PX Command Center"
msgstr "مركز قيادة تجربة المرضى"
#: templates/analytics/command_center.html:99
#: templates/complaints/complaint_detail.html:942
msgid "Loading..."
msgstr "جارٍ التحميل..."
@ -1970,6 +1974,7 @@ msgid "All Hospitals"
msgstr "جميع المستشفيات"
#: templates/analytics/command_center.html:188
#: templates/complaints/complaint_detail.html:929
#: templates/complaints/complaint_list.html:258
#: templates/observations/observation_list.html:234
#: templates/physicians/leaderboard.html:94
@ -2348,6 +2353,7 @@ msgstr "نوع المستلم"
#: templates/appreciation/appreciation_send_form.html:57
#: templates/appreciation/leaderboard.html:95
#: templates/complaints/complaint_detail.html:986
msgid "Recipient"
msgstr "المستلم"
@ -3695,10 +3701,88 @@ msgstr "المرفقات"
msgid "PX Actions"
msgstr "إجراءات تجربة المريض"
#: templates/complaints/complaint_detail.html:264
msgid "AI Mapped"
msgstr "تم التعيين بواسطة الذكاء الاصطناعي"
#: templates/complaints/complaint_detail.html:276
msgid "Staff Member"
msgstr "موظف"
#: templates/complaints/complaint_detail.html:290
msgid "AI Matched"
msgstr "مطابق بالذكاء الاصطناعي"
#: templates/complaints/complaint_detail.html:296
msgid "Extracted from complaint"
msgstr "مستخرج من الشكوى"
#: templates/complaints/complaint_detail.html:298
#: templates/complaints/complaint_detail.html:350
msgid "confidence"
msgstr "مستوى الثقة"
#: templates/complaints/complaint_detail.html:313
msgid "Staff Suggestions"
msgstr "اقتراحات الموظفين"
#: templates/complaints/complaint_detail.html:315
msgid "Needs Review"
msgstr "بحاجة إلى مراجعة"
#: templates/complaints/complaint_detail.html:321
msgid "AI extracted name"
msgstr "اسم مستخرج بالذكاء الاصطناعي"
#: templates/complaints/complaint_detail.html:322
msgid "potential match"
msgstr "تطابق محتمل"
#: templates/complaints/complaint_detail.html:322
msgid "No matches found"
msgstr "لم يتم العثور على تطابقات"
#: templates/complaints/complaint_detail.html:354
msgid "Currently assigned"
msgstr "مُعيّن حاليًا"
#: templates/complaints/complaint_detail.html:358
msgid "Select"
msgstr "اختيار"
#: templates/complaints/complaint_detail.html:369
msgid "Search All Staff"
msgstr "البحث في جميع الموظفين"
#: templates/complaints/complaint_detail.html:392
msgid "AI Analysis"
msgstr "تحليل الذكاء الاصطناعي"
#: templates/complaints/complaint_detail.html:394
msgid "AI Generated"
msgstr "تم إنشاؤه بالذكاء الاصطناعي"
#: templates/complaints/complaint_detail.html:400
msgid "Emotion Analysis"
msgstr "تحليل المشاعر"
#: templates/complaints/complaint_detail.html:415
msgid "Intensity"
msgstr "الشدة"
#: templates/complaints/complaint_detail.html:439
msgid "Summary"
msgstr "ملخص"
#: templates/complaints/complaint_detail.html:449
msgid "Suggested Action"
msgstr "الإجراء المقترح"
#: templates/complaints/complaint_detail.html:455
#: templates/observations/convert_to_action.html:94
msgid "Create PX Action"
msgstr "إنشاء إجراء PX"
#: templates/complaints/complaint_detail.html:465
msgid "Resolution"
msgstr "الحل"
@ -3810,10 +3894,132 @@ msgstr "تصعيد الشكوى"
msgid "This will escalate"
msgstr "سيتم التصعيد"
#: templates/complaints/complaint_detail.html:838
#, fuzzy
#| msgid "Checklist Items Management"
msgid "complaint to higher management"
msgstr "إدارة عناصر قائمة التحقق"
#: templates/complaints/complaint_detail.html:843
msgid "Explain why this complaint needs escalation..."
msgstr "اشرح سبب حاجة هذه الشكوى إلى التصعيد..."
#: templates/complaints/complaint_detail.html:863
#, fuzzy
#| msgid "Create PX Action"
msgid "Create PX Action from AI Suggestion"
msgstr "إنشاء إجراء PX"
#: templates/complaints/complaint_detail.html:870
msgid "This will create a PX Action based on the AI-suggested action for this complaint"
msgstr "سيؤدي هذا إلى إنشاء إجراء PX بناءً على الإجراء المقترح بالذكاء الاصطناعي لهذه الشكوى"
msgid "Auto-mapped Category"
msgstr "فئة معينة تلقائيًا"
msgid "Category automatically mapped from complaint category"
msgstr "تم تعيين الفئة تلقائيًا من فئة الشكوى"
#: templates/complaints/complaint_detail.html:886
#, fuzzy
#| msgid "Rating (Optional)"
msgid "Assign To (Optional)"
msgstr "التقييم (اختياري)"
#: templates/complaints/complaint_detail.html:888
#, fuzzy
#| msgid "Unassigned"
msgid "Leave unassigned"
msgstr "غير معين"
#: templates/complaints/complaint_detail.html:893
msgid "If left unassigned, you can assign the action later"
msgstr "إذا تُرك بدون تعيين، يمكنك تعيين الإجراء لاحقًا"
#: templates/complaints/complaint_detail.html:897
#, fuzzy
#| msgid "Description"
msgid "Action Description"
msgstr "الوصف"
#: templates/complaints/complaint_detail.html:906
#, fuzzy
#| msgid "Create PX Action"
msgid "Create Action"
msgstr "إنشاء إجراء PX"
#: templates/complaints/complaint_detail.html:919
#, fuzzy
#| msgid "Staff Member"
msgid "Select Staff Member"
msgstr "موظف"
#: templates/complaints/complaint_detail.html:927
#, fuzzy
#| msgid "Compliance by Department"
msgid "Filter by Department"
msgstr "الامتثال حسب القسم"
#: templates/complaints/complaint_detail.html:933
#, fuzzy
#| msgid "Search All Staff"
msgid "Search Staff"
msgstr "البحث في جميع الموظفين"
#: templates/complaints/complaint_detail.html:963
#, fuzzy
#| msgid "Send Notification"
msgid "Send Complaint Notification"
msgstr "إرسال إشعار"
#: templates/complaints/complaint_detail.html:972
#, fuzzy
#| msgid "Summary"
msgid "AI Summary"
msgstr "ملخص"
#: templates/complaints/complaint_detail.html:973
msgid "you can edit this before sending"
msgstr "يمكنك تعديل هذا قبل الإرسال"
msgid "This is AI-generated summary. You can edit it before sending"
msgstr "هذا ملخص تم إنشاؤه بالذكاء الاصطناعي. يمكنك تعديله قبل الإرسال"
msgid "Staff Member Assigned"
msgstr "تم تعيين الموظف"
msgid "This staff member has no user account in the system"
msgstr "هذا الموظف لا يملك حساب مستخدم في النظام"
#: templates/complaints/complaint_detail.html:1019
#, fuzzy
#| msgid "Department View"
msgid "Department Head of"
msgstr "عرض الأقسام"
#: templates/complaints/complaint_detail.html:1025
#: templates/complaints/complaint_detail.html:1042
#, fuzzy
#| msgid "No rating history available"
msgid "No recipient available"
msgstr "لا يوجد سجل تقييمات"
#: templates/complaints/complaint_detail.html:1026
msgid "The assigned staff has no user account and no department manager is set"
msgstr "الموظف المعيّن لا يملك حساب مستخدم ولا يوجد مدير قسم محدد"
msgid "Manager of"
msgstr "مدير"
msgid "No staff or department manager assigned to this complaint"
msgstr "لم يتم تعيين موظف أو مدير قسم لهذه الشكوى"
msgid "Additional Message (Optional)"
msgstr "رسالة إضافية (اختياري)"
msgid "Send Email"
msgstr "إرسال البريد الإلكتروني"
#: templates/complaints/complaint_form.html:5
#: templates/complaints/complaint_list.html:94
msgid "New Complaint"
@ -5005,10 +5211,6 @@ msgstr "تعيين إلى القسم"
msgid "Assign to User"
msgstr "تعيين إلى المستخدم"
#: templates/observations/convert_to_action.html:94
msgid "Create PX Action"
msgstr "إنشاء إجراء PX"
#: templates/observations/observation_detail.html:5
msgid "Observation Detail"
msgstr "تفاصيل الملاحظة"

View File

@ -261,7 +261,7 @@
</div>
{% if complaint.metadata.ai_analysis.old_department %}
<span class="badge bg-info ms-auto">
<i class="bi bi-robot me-1"></i>AI Mapped
<i class="bi bi-robot me-1"></i>{{ _("AI Mapped")}}
</span>
{% endif %}
</div>
@ -287,15 +287,15 @@
{% endif %}
</div>
<span class="badge bg-success ms-auto">
<i class="bi bi-robot me-1"></i>AI Matched
<i class="bi bi-robot me-1"></i>{{ _("AI Matched")}}
</span>
</div>
{% if complaint.metadata.ai_analysis.extracted_staff_name %}
<small class="text-muted mt-1 d-block">
<i class="bi bi-lightning me-1"></i>
Extracted from complaint: "{{ complaint.metadata.ai_analysis.extracted_staff_name }}"
{{ _("Extracted from complaint")}}: "{{ complaint.metadata.ai_analysis.extracted_staff_name }}"
{% if complaint.metadata.ai_analysis.staff_confidence %}
(confidence: {{ complaint.metadata.ai_analysis.staff_confidence|floatformat:0 }}%)
({{ _("confidence") }}: {{ complaint.metadata.ai_analysis.staff_confidence|floatformat:0 }}%)
{% endif %}
</small>
{% endif %}
@ -310,16 +310,16 @@
<div class="col-md-12">
<div class="info-label">
<i class="bi bi-people me-1"></i>
Staff Suggestions
{{ _("Staff Suggestions")}}
{% if complaint.metadata.ai_analysis.needs_staff_review %}
<span class="badge bg-warning ms-2">Needs Review</span>
<span class="badge bg-warning ms-2">{{ _("Needs Review")}}</span>
{% endif %}
</div>
<div class="info-value">
{% if complaint.metadata.ai_analysis.extracted_staff_name %}
<p class="text-muted mb-2">
AI extracted name: <strong>"{{ complaint.metadata.ai_analysis.extracted_staff_name }}"</strong>
({% if complaint.metadata.ai_analysis.staff_match_count %}{{ complaint.metadata.ai_analysis.staff_match_count }} potential match{{ complaint.metadata.ai_analysis.staff_match_count|pluralize }}{% else %}No matches found{% endif %})
{{ _("AI extracted name")}}: <strong>"{{ complaint.metadata.ai_analysis.extracted_staff_name }}"</strong>
({% if complaint.metadata.ai_analysis.staff_match_count %}{{ complaint.metadata.ai_analysis.staff_match_count }}{{ _("potential match")}}{{ complaint.metadata.ai_analysis.staff_match_count|pluralize }}{% else %}{{ _("No matches found")}}{% endif %})
</p>
{% endif %}
@ -347,15 +347,15 @@
</div>
<div class="text-end">
<span class="badge {% if staff_match.confidence >= 0.7 %}bg-success{% elif staff_match.confidence >= 0.5 %}bg-warning{% else %}bg-danger{% endif %}">
{{ staff_match.confidence|mul:100|floatformat:0 }}% confidence
{{ staff_match.confidence|mul:100|floatformat:0 }}% {{ _("confidence") }}
</span>
{% if complaint.staff and staff_match.id == complaint.staff.id|stringformat:"s" %}
<div class="small text-success mt-1">
<i class="bi bi-check-circle-fill"></i> Currently assigned
<i class="bi bi-check-circle-fill"></i> {{ _("Currently assigned")}}
</div>
{% elif not complaint.staff %}
<button class="btn btn-sm btn-outline-primary mt-1" onclick="assignStaff('{{ staff_match.id }}', '{{ staff_match.name_en }}')">
<i class="bi bi-person-plus"></i> Select
<i class="bi bi-person-plus"></i> {{ _("Select") }}
</button>
{% endif %}
</div>
@ -366,7 +366,7 @@
<div class="mt-3">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#staffSelectionModal">
<i class="bi bi-search me-1"></i> Search All Staff
<i class="bi bi-search me-1"></i> {{ _("Search All Staff")}}
</button>
</div>
</div>
@ -384,20 +384,20 @@
</div>
<!-- AI Analysis Section -->
{% if complaint.short_description or complaint.suggested_action %}
{% if complaint.short_description_ar or complaint.suggested_action_ar %}
<hr>
<div class="mb-3">
<div class="d-flex align-items-center mb-3">
<div class="info-label mb-0 me-2">
<i class="bi bi-robot me-1"></i>AI Analysis
<i class="bi bi-robot me-1"></i>{{ _("AI Analysis")}}
</div>
<span class="badge bg-info">AI Generated</span>
<span class="badge bg-info">{{ _("AI Generated")}}</span>
</div>
<!-- Emotion Analysis -->
{% if complaint.emotion %}
<div class="mb-3">
<div class="info-label" style="font-size: 0.8rem;">Emotion Analysis</div>
<div class="info-label" style="font-size: 0.8rem;">{{ _("Emotion Analysis")}}</div>
<div class="card mb-0" style="background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-left: 4px solid #6c757d;">
<div class="card-body py-2">
<div class="row align-items-center mb-2">
@ -407,12 +407,12 @@
</span>
</div>
<div class="col-md-6 text-md-end">
<small class="text-muted">Confidence: {{ complaint.emotion_confidence|mul:100|floatformat:0 }}%</small>
<small class="text-muted">{{ _("Confidence") }}: {{ complaint.emotion_confidence|mul:100|floatformat:0 }}%</small>
</div>
</div>
<div class="mb-1">
<div class="d-flex justify-content-between mb-1">
<small class="text-muted">Intensity</small>
<small class="text-muted">{{ _("Intensity") }}</small>
<small>{{ complaint.emotion_intensity|floatformat:2 }} / 1.0</small>
</div>
<div class="progress" style="height: 6px;">
@ -434,25 +434,25 @@
</div>
{% endif %}
{% if complaint.short_description %}
{% if complaint.short_description_ar %}
<div class="mb-3">
<div class="info-label" style="font-size: 0.8rem;">Summary</div>
<div class="info-label" style="font-size: 0.8rem;">{{ _("Summary") }}</div>
<div class="alert alert-light border-info mb-0" style="background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);">
<i class="bi bi-info-circle me-1 text-info"></i>
<small>{{ complaint.short_description }}</small>
<small>{{ complaint.short_description_ar }}</small>
</div>
</div>
{% endif %}
{% if complaint.suggested_action %}
{% if complaint.suggested_action_ar %}
<div class="mb-0">
<div class="info-label" style="font-size: 0.8rem;">Suggested Action</div>
<div class="info-label" style="font-size: 0.8rem;">{{ _("Suggested Action")}}</div>
<div class="alert alert-success mb-0" style="background: linear-gradient(135deg, #e8f5e9 0%, #e1f5fe 100%); border-color: #4caf50;">
<i class="bi bi-lightning me-1 text-success"></i>
<small>{{ complaint.suggested_action }}</small>
<small>{{ complaint.suggested_action_ar }}</small>
</div>
<button type="button" class="btn btn-sm btn-primary mt-2" data-bs-toggle="modal" data-bs-target="#createActionModal">
<i class="bi bi-plus-circle me-1"></i> Create PX Action
<i class="bi bi-plus-circle me-1"></i> {{ _("Create PX Action")}}
</button>
</div>
{% endif %}
@ -835,7 +835,7 @@
<div class="modal-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle me-2"></i>
{{ _("This will escalate")}} complaint to higher management}}.
{{ _("This will escalate")}} {{ _("complaint to higher management")}}}}.
</div>
<div class="mb-3">
<label class="form-label">{% trans "Reason for Escalation" %}</label>
@ -860,18 +860,18 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-lightning-fill me-2"></i>Create PX Action from AI Suggestion
<i class="bi bi-lightning-fill me-2"></i>{{ _("Create PX Action from AI Suggestion")}}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
This will create a PX Action based on the AI-suggested action for this complaint.
{{ _("This will create a PX Action based on the AI-suggested action for this complaint")}}.
</div>
<div class="mb-3">
<label class="form-label">Auto-mapped Category</label>
<label class="form-label">{{ _("Auto-mapped Category")}}</label>
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-tag"></i>
@ -879,31 +879,31 @@
<input type="text" id="autoMappedCategory" class="form-control" readonly
value="{% if complaint.category %}{{ complaint.category.name_en }}{% else %}Other{% endif %}">
</div>
<small class="text-muted">Category automatically mapped from complaint category</small>
<small class="text-muted">{{ _("Category automatically mapped from complaint category")}}</small>
</div>
<div class="mb-3">
<label class="form-label">Assign To (Optional)</label>
<label class="form-label">{{ _("Assign To (Optional)")}}</label>
<select id="actionAssignTo" class="form-select">
<option value="">Leave unassigned</option>
<option value="">{{ _("Leave unassigned")}}</option>
{% for user_obj in assignable_users %}
<option value="{{ user_obj.id }}">{{ user_obj.get_full_name }}</option>
{% endfor %}
</select>
<small class="text-muted">If left unassigned, you can assign the action later.</small>
<small class="text-muted">{{ _("If left unassigned, you can assign the action later")}}.</small>
</div>
<div class="mb-0">
<label class="form-label">Action Description</label>
<label class="form-label">{{ _("Action Description")}}</label>
<div class="alert alert-success mb-0">
<small>{{ complaint.suggested_action }}</small>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Cancel") }}</button>
<button type="button" id="createActionBtn" class="btn btn-primary" onclick="createAction()">
<i class="bi bi-plus-circle me-1"></i>Create Action
<i class="bi bi-plus-circle me-1"></i>{{ _("Create Action")}}
</button>
</div>
</div>
@ -916,7 +916,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-person-badge me-2"></i>Select Staff Member
<i class="bi bi-person-badge me-2"></i>{{ _("Select Staff Member")}}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
@ -924,13 +924,13 @@
<!-- Department Filter -->
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Filter by Department</label>
<label class="form-label">{{ _("Filter by Department")}}</label>
<select id="staffDepartmentFilter" class="form-select">
<option value="">All Departments</option>
<option value="">{{ _("All Departments")}}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Search Staff</label>
<label class="form-label">{{ _("Search Staff")}}</label>
<input type="text" id="staffSearchInput" class="form-control" placeholder="Search by name or job title...">
</div>
</div>
@ -939,7 +939,7 @@
<div id="staffListContainer" class="border rounded" style="max-height: 400px; overflow-y: auto;">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden">{{ _("Loading...")}}</span>
</div>
</div>
</div>
@ -960,7 +960,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="bi bi-envelope me-2"></i>Send Complaint Notification
<i class="bi bi-envelope me-2"></i>{{ _("Send Complaint Notification")}}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
@ -969,13 +969,13 @@
<div class="mb-3">
<label class="form-label">
<i class="bi bi-robot me-1"></i>
AI Summary
<small class="text-muted">(you can edit this before sending)</small>
{{ _("AI Summary")}}
<small class="text-muted">({{ _("you can edit this before sending")}})</small>
</label>
<textarea id="emailMessage" class="form-control" rows="5"
placeholder="Enter message to send...">{{ complaint.short_description }}</textarea>
<small class="text-muted">
This is AI-generated summary. You can edit it before sending.
{{ _("This is AI-generated summary. You can edit it before sending")}}.
</small>
</div>
@ -983,7 +983,7 @@
<div class="card mb-3">
<div class="card-body">
<h6 class="card-title">
<i class="bi bi-person-check me-1"></i>Recipient
<i class="bi bi-person-check me-1"></i>{{ _("Recipient") }}
</h6>
{% if complaint.staff and complaint.staff.user %}
@ -1000,14 +1000,14 @@
<!-- Staff exists but has no user account -->
<div class="alert alert-warning mb-2">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
<strong>Staff Member Assigned:</strong> {{ complaint.staff.get_full_name }}
<strong>{{ _("Staff Member Assigned")}}:</strong> {{ complaint.staff.get_full_name }}
{% if complaint.staff.job_title %}
<br><small class="text-muted">{{ complaint.staff.job_title }}</small>
{% endif %}
<hr class="my-2">
<small class="text-muted">
<i class="bi bi-info-circle me-1"></i>
This staff member has no user account in the system.
{{ _("This staff member has no user account in the system")}}.
</small>
</div>
@ -1016,14 +1016,14 @@
<div class="alert alert-info mb-0">
<i class="bi bi-person-badge me-1"></i>
<strong>Actual Recipient:</strong> {{ complaint.department.manager.get_full_name }}
<br><small class="text-muted">Department Head of {{ complaint.department.name_en }}</small>
<br><small class="text-muted">{{ _("Department Head of")}} {{ complaint.department.name_en }}</small>
</div>
{% else %}
<!-- No fallback recipient -->
<div class="alert alert-danger mb-0">
<i class="bi bi-x-circle-fill me-1"></i>
<strong>No recipient available</strong>
<br><small>The assigned staff has no user account and no department manager is set.</small>
<strong>{{ _("No recipient available")}}</strong>
<br><small>{{ _("The assigned staff has no user account and no department manager is set")}}.</small>
</div>
{% endif %}
@ -1032,15 +1032,15 @@
<div class="alert alert-info mb-0">
<i class="bi bi-person-badge me-1"></i>
<strong>Department Head:</strong> {{ complaint.department.manager.get_full_name }}
<br><small class="text-muted">Manager of {{ complaint.department.name_en }}</small>
<br><small class="text-muted">{{ _("Manager of")}} {{ complaint.department.name_en }}</small>
</div>
{% else %}
<!-- No recipient at all -->
<div class="alert alert-danger mb-0">
<i class="bi bi-exclamation-triangle-fill me-1"></i>
<strong>No recipient available</strong>
<br><small>No staff or department manager assigned to this complaint.</small>
<strong>{{ _("No recipient available")}}</strong>
<br><small>{{ _("No staff or department manager assigned to this complaint")}}.</small>
</div>
{% endif %}
</div>
@ -1049,7 +1049,7 @@
<!-- Optional Additional Message -->
<div class="mb-3">
<label class="form-label">
Additional Message (Optional)
{{ _("Additional Message (Optional)")}}
</label>
<textarea id="additionalMessage" class="form-control" rows="3"
placeholder="Add any additional notes to send..."></textarea>
@ -1057,11 +1057,11 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Cancel
{{ _("Cancel") }}
</button>
<button type="button" id="sendNotificationBtn" class="btn btn-primary"
onclick="sendNotification()">
<i class="bi bi-send me-1"></i>Send Email
<i class="bi bi-send me-1"></i>{{ _("Send Email")}}
</button>
</div>
</div>

View File

@ -150,7 +150,7 @@
</tr>
</thead>
<tbody>
{% for rating in top_physicians %}
{% for rating in top_staff %}
<tr onclick="window.location=''" style="cursor: pointer;">
<td>
{% if forloop.counter == 1 %}
@ -164,13 +164,13 @@
{% endif %}
</td>
<td>
<strong>{{ rating.physician.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.physician.license_number }}</small>
<strong>{{ rating.staff }}</strong><br>
<small class="text-muted">{{ rating.staff.license_number }}</small>
</td>
<td>{{ rating.physician.specialization }}</td>
<td>{{ rating.staff.specialization }}</td>
<td>
{% if rating.physician.department %}
{{ rating.physician.department.name }}
{% if rating.staff.department %}
{{ rating.staff.department.name }}
{% else %}
<span class="text-muted">-</span>
{% endif %}

View File

@ -227,10 +227,10 @@
</div>
{% endif %}
{% if stage.physician %}
{% if stage.staff %}
<div class="stage-meta-item">
<i class="bi bi-person-badge"></i>
<small>Dr. {{ stage.physician.first_name }} {{ stage.physician.last_name }}</small>
<small>{{ stage.staff.first_name }} {{ stage.staff.last_name }}</small>
</div>
{% endif %}

View File

@ -125,7 +125,7 @@
</thead>
<tbody>
{% for rating in dept_data.physicians %}
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.physician.id %}'" style="cursor: pointer;">
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.staff.id %}'" style="cursor: pointer;">
<td>
{% if forloop.counter <= 3 %}
<strong class="text-primary">#{{ forloop.counter }}</strong>
@ -134,10 +134,10 @@
{% endif %}
</td>
<td>
<strong>{{ rating.physician.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.physician.license_number }}</small>
<strong>{{ rating.staff.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.staff.license_number }}</small>
</td>
<td>{{ rating.physician.specialization }}</td>
<td>{{ rating.staff.specialization }}</td>
<td>
<strong class="text-success">{{ rating.average_rating|floatformat:2 }}</strong>
</td>
@ -152,7 +152,7 @@
{% endif %}
</td>
<td onclick="event.stopPropagation();">
<a href="{% url 'physicians:physician_detail' rating.physician.id %}"
<a href="{% url 'physicians:physician_detail' rating.staff.id %}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>

View File

@ -109,23 +109,23 @@
</thead>
<tbody>
{% for rating in ratings %}
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.physician.id %}'" style="cursor: pointer;">
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.staff.id %}'" style="cursor: pointer;">
<td>
<strong>{{ rating.year }}-{{ rating.month|stringformat:"02d" }}</strong>
</td>
<td>
<strong>{{ rating.physician.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.physician.license_number }}</small>
<strong>{{ rating.staff.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.staff.license_number }}</small>
</td>
<td>{{ rating.physician.specialization }}</td>
<td>{{ rating.staff.specialization }}</td>
<td>
{% if rating.physician.department %}
{{ rating.physician.department.name }}
{% if rating.staff.department %}
{{ rating.staff.department.name }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>{{ rating.physician.hospital.name }}</td>
<td>{{ rating.staff.hospital.name }}</td>
<td>
<h5 class="mb-0">{{ rating.average_rating|floatformat:2 }}</h5>
</td>
@ -159,7 +159,7 @@
</div>
</td>
<td onclick="event.stopPropagation();">
<a href="{% url 'physicians:physician_detail' rating.physician.id %}"
<a href="{% url 'physicians:physician_detail' rating.staff.id %}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>

View File

@ -124,7 +124,7 @@
</thead>
<tbody>
{% for rating in spec_data.physicians %}
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.physician.id %}'" style="cursor: pointer;">
<tr onclick="window.location='{% url 'physicians:physician_detail' rating.staff.id %}'" style="cursor: pointer;">
<td>
{% if forloop.counter <= 3 %}
<strong class="text-primary">#{{ forloop.counter }}</strong>
@ -133,17 +133,17 @@
{% endif %}
</td>
<td>
<strong>{{ rating.physician.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.physician.license_number }}</small>
<strong>{{ rating.staff.get_full_name }}</strong><br>
<small class="text-muted">{{ rating.staff.license_number }}</small>
</td>
<td>
{% if rating.physician.department %}
{{ rating.physician.department.name }}
{% if rating.staff.department %}
{{ rating.staff.department.name }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>{{ rating.physician.hospital.name }}</td>
<td>{{ rating.staff.hospital.name }}</td>
<td>
<strong class="text-success">{{ rating.average_rating|floatformat:2 }}</strong>
</td>
@ -151,7 +151,7 @@
<span class="badge bg-light text-dark">{{ rating.total_surveys }}</span>
</td>
<td onclick="event.stopPropagation();">
<a href="{% url 'physicians:physician_detail' rating.physician.id %}"
<a href="{% url 'physicians:physician_detail' rating.staff.id %}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>