418 lines
15 KiB
Python
418 lines
15 KiB
Python
"""
|
|
Employee Evaluation Models
|
|
|
|
Models for tracking employee performance metrics and reports for the
|
|
PAD Department Weekly Dashboard evaluation system.
|
|
"""
|
|
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from apps.core.models import UUIDModel, TimeStampedModel
|
|
|
|
|
|
class EvaluationNote(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Notes for employee evaluation tracking.
|
|
|
|
Tracks notes categorized by type and sub-category for each staff member.
|
|
"""
|
|
|
|
CATEGORY_CHOICES = [
|
|
("non_medical", "Non-Medical"),
|
|
("medical", "Medical"),
|
|
("er", "ER"),
|
|
("hospital", "Hospital"),
|
|
]
|
|
|
|
SUBCATEGORY_CHOICES = [
|
|
("it_app", "IT - App"),
|
|
("lab", "LAB"),
|
|
("doctors_managers_reception", "Doctors/Managers/Reception"),
|
|
("hospital_general", "Hospital"),
|
|
("medical_reports", "Medical Reports"),
|
|
("doctors", "Doctors"),
|
|
("other", "Other"),
|
|
]
|
|
|
|
staff = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.CASCADE,
|
|
related_name="evaluation_notes",
|
|
help_text="Staff member this note is for",
|
|
)
|
|
|
|
category = models.CharField(max_length=50, choices=CATEGORY_CHOICES, help_text="Note category")
|
|
|
|
sub_category = models.CharField(max_length=50, choices=SUBCATEGORY_CHOICES, help_text="Note sub-category")
|
|
|
|
count = models.IntegerField(default=1, help_text="Number of notes in this category")
|
|
|
|
note_date = models.DateField(help_text="Date the note was recorded")
|
|
|
|
description = models.TextField(blank=True, help_text="Optional description")
|
|
|
|
created_by = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="notes_created",
|
|
help_text="User who created this note entry",
|
|
)
|
|
|
|
class Meta:
|
|
ordering = ["-note_date", "category", "sub_category"]
|
|
verbose_name = "Evaluation Note"
|
|
verbose_name_plural = "Evaluation Notes"
|
|
indexes = [
|
|
models.Index(fields=["staff", "note_date"]),
|
|
models.Index(fields=["category", "sub_category"]),
|
|
models.Index(fields=["note_date"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.staff} - {self.get_category_display()} - {self.note_date}"
|
|
|
|
|
|
class ComplaintRequest(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Tracks complaint request filling and status.
|
|
|
|
Monitors whether complaint requests were filled, on hold,
|
|
or came from barcode scanning.
|
|
"""
|
|
|
|
FILLING_TIME_CHOICES = [
|
|
("same_time", "Same Time"),
|
|
("within_6h", "Within 6 Hours"),
|
|
("6_to_24h", "6 to 24 Hours"),
|
|
("after_1_day", "After 1 Day"),
|
|
("not_mentioned", "Time Not Mentioned"),
|
|
]
|
|
|
|
NON_ACTIVATION_REASON_CHOICES = [
|
|
("converted_to_note", "تم تحويلها ملاحظة (Converted to observation)"),
|
|
("issue_resolved_immediately", "تم حل الاشكالية (Issue resolved immediately)"),
|
|
("not_meeting_conditions", "غير مستوفية للشروط (Does not meet conditions)"),
|
|
("raised_via_cchi", "تم رفع الشكوى عن طريق مجلس الضمان الصحي (Raised via CCHI)"),
|
|
("request_not_activated", "لم يتم تفعيل طلب الشكوى (Request not activated)"),
|
|
("complainant_withdrew", "بناء على طلب المشتكي (Per complainant request)"),
|
|
("complainant_retracted", "المشتكي تنازل عن الشكوى (Complainant retracted)"),
|
|
("duplicate", "مكررة (Duplicate)"),
|
|
("other", "Other"),
|
|
]
|
|
|
|
staff = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.CASCADE,
|
|
related_name="complaint_requests_sent",
|
|
help_text="Staff member who sent/filled the request",
|
|
)
|
|
|
|
complaint = models.ForeignKey(
|
|
"complaints.Complaint",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="complaint_request_records",
|
|
help_text="Related complaint (if any)",
|
|
)
|
|
|
|
hospital = models.ForeignKey(
|
|
"organizations.Hospital",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="complaint_requests",
|
|
help_text="Hospital where the request was made",
|
|
)
|
|
|
|
patient_name = models.CharField(max_length=200, blank=True, help_text="Patient name")
|
|
file_number = models.CharField(max_length=100, blank=True, help_text="Patient file number")
|
|
complained_department = models.ForeignKey(
|
|
"organizations.Department",
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name="complaint_requests",
|
|
help_text="Department being complained about",
|
|
)
|
|
incident_date = models.DateField(null=True, blank=True, help_text="Date of the incident")
|
|
phone_number = models.CharField(max_length=20, blank=True, help_text="Patient phone number")
|
|
|
|
filled = models.BooleanField(default=False, help_text="Whether the request was filled")
|
|
on_hold = models.BooleanField(default=False, help_text="Whether the request is on hold")
|
|
not_filled = models.BooleanField(default=False, help_text="Whether the request was not filled")
|
|
from_barcode = models.BooleanField(default=False, help_text="Whether the request came from barcode scanning")
|
|
|
|
filling_time_category = models.CharField(
|
|
max_length=20, choices=FILLING_TIME_CHOICES, default="not_mentioned", help_text="When the request was filled"
|
|
)
|
|
|
|
request_date = models.DateField(help_text="Date of the request")
|
|
request_time = models.TimeField(null=True, blank=True, help_text="Time of the request")
|
|
form_sent_at = models.DateTimeField(null=True, blank=True, help_text="When complaint form was sent to patient")
|
|
form_sent_time = models.TimeField(null=True, blank=True, help_text="Time when form was sent")
|
|
filled_at = models.DateTimeField(null=True, blank=True, help_text="When the request was filled")
|
|
filled_time = models.TimeField(null=True, blank=True, help_text="Time when request was filled")
|
|
|
|
reason_non_activation = models.CharField(
|
|
max_length=50, choices=NON_ACTIVATION_REASON_CHOICES, blank=True, help_text="Reason complaint was not activated"
|
|
)
|
|
reason_non_activation_other = models.CharField(max_length=200, blank=True, help_text="Other reason details")
|
|
pr_observations = models.TextField(blank=True, help_text="PR team observations about this request")
|
|
|
|
notes = models.TextField(blank=True, help_text="Additional notes about the request")
|
|
|
|
class Meta:
|
|
ordering = ["-request_date", "-created_at"]
|
|
verbose_name = "Complaint Request"
|
|
verbose_name_plural = "Complaint Requests"
|
|
indexes = [
|
|
models.Index(fields=["staff", "request_date"]),
|
|
models.Index(fields=["filled", "on_hold"]),
|
|
models.Index(fields=["request_date"]),
|
|
models.Index(fields=["hospital", "request_date"]),
|
|
models.Index(fields=["reason_non_activation"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
status = "Filled" if self.filled else "Not Filled"
|
|
if self.on_hold:
|
|
status = "On Hold"
|
|
return f"{self.staff} - {status} - {self.request_date}"
|
|
|
|
def mark_as_filled(self):
|
|
"""Mark the request as filled."""
|
|
from django.utils import timezone
|
|
|
|
self.filled = True
|
|
self.not_filled = False
|
|
self.filled_at = timezone.now()
|
|
self.save(update_fields=["filled", "not_filled", "filled_at"])
|
|
|
|
def mark_as_not_filled(self):
|
|
"""Mark the request as not filled."""
|
|
self.filled = False
|
|
self.not_filled = True
|
|
self.save(update_fields=["filled", "not_filled"])
|
|
|
|
|
|
class ReportCompletion(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Tracks weekly report completion for employees.
|
|
|
|
Monitors which reports were completed by staff members.
|
|
"""
|
|
|
|
REPORT_TYPE_CHOICES = [
|
|
("complaint_report", "Complaint Report"),
|
|
("complaint_request_report", "Complaint Request Report"),
|
|
("observation_report", "Observation Report"),
|
|
("incoming_inquiries_report", "Incoming Inquiries Report"),
|
|
("outgoing_inquiries_report", "Outgoing Inquiries Report"),
|
|
("extension_report", "Extension Report"),
|
|
("escalated_complaints_report", "Escalated Complaints Report"),
|
|
]
|
|
|
|
staff = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.CASCADE,
|
|
related_name="report_completions",
|
|
help_text="Staff member who should complete the report",
|
|
)
|
|
|
|
report_type = models.CharField(max_length=50, choices=REPORT_TYPE_CHOICES, help_text="Type of report")
|
|
|
|
is_completed = models.BooleanField(default=False, help_text="Whether the report is completed")
|
|
|
|
completed_at = models.DateTimeField(null=True, blank=True, help_text="When the report was completed")
|
|
|
|
week_start_date = models.DateField(help_text="Start date of the week this report is for")
|
|
|
|
notes = models.TextField(blank=True, help_text="Notes about the report completion")
|
|
|
|
class Meta:
|
|
ordering = ["-week_start_date", "report_type"]
|
|
verbose_name = "Report Completion"
|
|
verbose_name_plural = "Report Completions"
|
|
unique_together = [["staff", "report_type", "week_start_date"]]
|
|
indexes = [
|
|
models.Index(fields=["staff", "week_start_date"]),
|
|
models.Index(fields=["report_type", "is_completed"]),
|
|
models.Index(fields=["week_start_date"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
status = "✓" if self.is_completed else "✗"
|
|
return f"{self.staff} - {self.get_report_type_display()} - Week of {self.week_start_date} {status}"
|
|
|
|
def mark_completed(self):
|
|
"""Mark this report as completed."""
|
|
from django.utils import timezone
|
|
|
|
self.is_completed = True
|
|
self.completed_at = timezone.now()
|
|
self.save(update_fields=["is_completed", "completed_at"])
|
|
|
|
def mark_incomplete(self):
|
|
"""Mark this report as not completed."""
|
|
self.is_completed = False
|
|
self.completed_at = None
|
|
self.save(update_fields=["is_completed", "completed_at"])
|
|
|
|
@classmethod
|
|
def get_completion_percentage(cls, staff, week_start_date):
|
|
"""
|
|
Get completion percentage for a staff member for a given week.
|
|
|
|
Returns:
|
|
float: Percentage of completed reports (0-100)
|
|
"""
|
|
total_reports = len(cls.REPORT_TYPE_CHOICES)
|
|
completed_count = cls.objects.filter(staff=staff, week_start_date=week_start_date, is_completed=True).count()
|
|
return (completed_count / total_reports) * 100 if total_reports > 0 else 0
|
|
|
|
|
|
class EscalatedComplaintLog(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Logs escalated complaints with timing information.
|
|
|
|
Tracks when complaints were escalated relative to the 72-hour SLA.
|
|
"""
|
|
|
|
ESCALATION_TIMING_CHOICES = [
|
|
("before_72h", "Before 72 Hours"),
|
|
("exactly_72h", "72 Hours Exactly"),
|
|
("after_72h", "After 72 Hours"),
|
|
]
|
|
|
|
complaint = models.ForeignKey(
|
|
"complaints.Complaint",
|
|
on_delete=models.CASCADE,
|
|
related_name="escalation_logs",
|
|
help_text="The escalated complaint",
|
|
)
|
|
|
|
staff = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.CASCADE,
|
|
related_name="escalated_complaints",
|
|
help_text="Staff member who the complaint is assigned to",
|
|
)
|
|
|
|
escalation_timing = models.CharField(
|
|
max_length=20,
|
|
choices=ESCALATION_TIMING_CHOICES,
|
|
help_text="When the complaint was escalated relative to 72h SLA",
|
|
)
|
|
|
|
escalated_at = models.DateTimeField(help_text="When the complaint was escalated")
|
|
|
|
is_resolved = models.BooleanField(default=False, help_text="Whether the escalated complaint was resolved")
|
|
|
|
resolved_at = models.DateTimeField(null=True, blank=True, help_text="When the escalated complaint was resolved")
|
|
|
|
resolution_notes = models.TextField(blank=True, help_text="Notes about the resolution")
|
|
|
|
week_start_date = models.DateField(help_text="Start date of the week this escalation is recorded for")
|
|
|
|
class Meta:
|
|
ordering = ["-escalated_at", "-created_at"]
|
|
verbose_name = "Escalated Complaint Log"
|
|
verbose_name_plural = "Escalated Complaint Logs"
|
|
indexes = [
|
|
models.Index(fields=["staff", "week_start_date"]),
|
|
models.Index(fields=["escalation_timing", "is_resolved"]),
|
|
models.Index(fields=["week_start_date"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
status = "Resolved" if self.is_resolved else "Unresolved"
|
|
return f"{self.staff} - {self.get_escalation_timing_display()} - {status}"
|
|
|
|
def mark_resolved(self, notes=""):
|
|
"""Mark this escalated complaint as resolved."""
|
|
from django.utils import timezone
|
|
|
|
self.is_resolved = True
|
|
self.resolved_at = timezone.now()
|
|
if notes:
|
|
self.resolution_notes = notes
|
|
self.save(update_fields=["is_resolved", "resolved_at", "resolution_notes"])
|
|
|
|
|
|
class InquiryDetail(UUIDModel, TimeStampedModel):
|
|
"""
|
|
Detailed tracking of inquiries with types and statuses.
|
|
|
|
Extends the base Inquiry model with evaluation-specific tracking.
|
|
"""
|
|
|
|
INQUIRY_TYPE_CHOICES = [
|
|
("contact_doctor", "Contact the doctor"),
|
|
("sick_leave_reports", "Sick-Leave - Medical Reports"),
|
|
("blood_test", "Blood test result"),
|
|
("raise_complaint", "Raise a Complaint"),
|
|
("app_problem", "Problem with the app"),
|
|
("medication", "Ask about medication"),
|
|
("insurance_status", "Insurance request status"),
|
|
("general_question", "General question"),
|
|
("other", "Other"),
|
|
]
|
|
|
|
STATUS_CHOICES = [
|
|
("in_progress", "تحت الإجراء (In Progress)"),
|
|
("contacted", "تم التواصل (Contacted)"),
|
|
("contacted_no_response", "تم التواصل ولم يتم الرد (Contacted No Response)"),
|
|
]
|
|
|
|
RESPONSE_TIME_CHOICES = [
|
|
("24h", "24 Hours"),
|
|
("48h", "48 Hours"),
|
|
("72h", "72 Hours"),
|
|
("more_than_72h", "More than 72 Hours"),
|
|
]
|
|
|
|
inquiry = models.OneToOneField(
|
|
"complaints.Inquiry", on_delete=models.CASCADE, related_name="evaluation_detail", help_text="Related inquiry"
|
|
)
|
|
|
|
staff = models.ForeignKey(
|
|
"accounts.User",
|
|
on_delete=models.CASCADE,
|
|
related_name="inquiry_details",
|
|
help_text="Staff member handling the inquiry",
|
|
)
|
|
|
|
inquiry_type = models.CharField(max_length=50, choices=INQUIRY_TYPE_CHOICES, help_text="Type of inquiry")
|
|
|
|
is_outgoing = models.BooleanField(default=False, help_text="Whether this is an outgoing inquiry")
|
|
|
|
response_time_category = models.CharField(
|
|
max_length=20, choices=RESPONSE_TIME_CHOICES, blank=True, help_text="Response time category"
|
|
)
|
|
|
|
inquiry_status = models.CharField(
|
|
max_length=50, choices=STATUS_CHOICES, blank=True, help_text="Current status of the inquiry"
|
|
)
|
|
|
|
inquiry_date = models.DateField(help_text="Date of the inquiry")
|
|
|
|
notes = models.TextField(blank=True, help_text="Additional notes")
|
|
|
|
class Meta:
|
|
ordering = ["-inquiry_date", "-created_at"]
|
|
verbose_name = "Inquiry Detail"
|
|
verbose_name_plural = "Inquiry Details"
|
|
indexes = [
|
|
models.Index(fields=["staff", "inquiry_date"]),
|
|
models.Index(fields=["inquiry_type", "is_outgoing"]),
|
|
models.Index(fields=["inquiry_date"]),
|
|
]
|
|
|
|
def __str__(self):
|
|
direction = "Outgoing" if self.is_outgoing else "Incoming"
|
|
return f"{self.staff} - {direction} - {self.get_inquiry_type_display()} - {self.inquiry_date}"
|