HH/templates/dashboard/employee_evaluation.html
2026-03-28 14:03:56 +03:00

2317 lines
86 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% trans "Employee Evaluation" %} - PX360{% endblock %}
{% block extra_css %}
<style>
/* PX360 App Theme Variables */
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
}
/* Page Header */
.page-header-gradient {
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
color: white;
padding: 2rem 2.5rem;
border-radius: 1rem;
margin-bottom: 2rem;
box-shadow: 0 10px 15px -3px rgba(0, 86, 150, 0.2);
position: relative;
overflow: hidden;
}
.page-header-gradient::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 200px;
height: 200px;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
}
/* Employee Column */
.employee-column {
background: #f8fafc;
border-radius: 1rem;
border: 2px solid #e2e8f0;
overflow: hidden;
}
.employee-header {
background: linear-gradient(135deg, var(--hh-navy) 0%, var(--hh-blue) 100%);
color: white;
padding: 1rem 1.25rem;
text-align: center;
}
.employee-name {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.employee-info {
font-size: 0.875rem;
opacity: 0.9;
}
/* Section Cards */
.section-card {
background: white;
border-radius: 0.75rem;
border: 1px solid #e2e8f0;
margin: 1rem;
overflow: hidden;
transition: all 0.3s ease;
}
.section-card:hover {
border-color: #005696;
box-shadow: 0 4px 12px -2px rgba(0, 86, 150, 0.1);
}
.section-header {
padding: 0.75rem 1rem;
background: linear-gradient(to right, #f8fafc, #f1f5f9);
border-bottom: 1px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
}
.section-title {
font-weight: 600;
font-size: 0.875rem;
color: #1e293b;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-toggle {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: var(--hh-slate);
transition: transform 0.2s;
}
.section-toggle.collapsed {
transform: rotate(-90deg);
}
.section-content {
padding: 1rem;
}
.section-content.collapsed {
display: none;
}
/* Data Tables */
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.75rem;
}
.data-table th {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0.5rem 0.5rem;
font-weight: 600;
color: #64748b;
text-align: left;
}
.data-table td {
border-bottom: 1px solid #f1f5f9;
padding: 0.5rem;
color: #1e293b;
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table .total-row {
font-weight: 700;
background: #f8fafc;
}
/* Chart Containers */
.chart-container {
height: 200px;
margin-top: 0.5rem;
}
/* KPI Cards */
.kpi-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.kpi-item {
background: #f8fafc;
border-radius: 0.5rem;
padding: 0.75rem;
text-align: center;
}
.kpi-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--hh-navy);
}
.kpi-label {
font-size: 0.625rem;
color: var(--hh-slate);
text-transform: uppercase;
letter-spacing: 0.025em;
margin-top: 0.25rem;
}
.kpi-percentage {
font-size: 0.75rem;
font-weight: 600;
margin-top: 0.25rem;
}
/* Progress Bar */
.progress-container {
margin-top: 0.5rem;
}
.progress-bar-bg {
height: 8px;
background: #e2e8f0;
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--hh-navy), var(--hh-blue));
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.75rem;
font-weight: 600;
color: var(--hh-navy);
margin-top: 0.25rem;
}
/* Checklist */
.checklist-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
font-size: 0.75rem;
}
.checklist-icon {
width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checklist-icon.completed {
background: var(--hh-success);
color: white;
}
.checklist-icon.pending {
background: #e2e8f0;
color: var(--hh-slate);
}
/* Form Styling */
.form-select-px360 {
background: white;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
padding: 0.625rem 1rem;
color: #1e293b;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
width: 100%;
}
.form-select-px360:hover {
border-color: #005696;
}
.form-select-px360:focus {
outline: none;
border-color: #005696;
box-shadow: 0 0 0 3px rgba(0, 86, 150, 0.1);
}
/* Summary Cards */
.summary-card {
background: white;
border-radius: 1rem;
border: 2px solid #e2e8f0;
padding: 1.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.summary-card:hover {
border-color: #005696;
transform: translateY(-2px);
}
.summary-value {
font-size: 1.75rem;
font-weight: 800;
color: #1e293b;
}
.summary-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--hh-slate);
text-transform: uppercase;
letter-spacing: 0.025em;
}
/* Employee columns auto-fit grid - distributes evenly */
.employee-columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 1.5rem;
}
/* Center single employee */
.employee-columns > .employee-column:only-child {
max-width: 700px;
margin: 0 auto;
grid-column: 1 / -1;
}
/* When exactly 2 employees, ensure they fill the space */
.employee-columns:has(> .employee-column:nth-child(2):last-child) {
grid-template-columns: repeat(2, 1fr);
}
/* Mobile: always 1 column */
@media (max-width: 767px) {
.employee-columns {
grid-template-columns: 1fr;
}
.employee-columns:has(> .employee-column:nth-child(2):last-child) {
grid-template-columns: 1fr;
}
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.4s ease-out forwards;
}
.delay-100 { animation-delay: 100ms; }
.delay-200 { animation-delay: 200ms; }
.delay-300 { animation-delay: 300ms; }
/* Comparison Mode */
.comparison-mode .employee-column {
position: relative;
}
.comparison-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
color: white;
font-size: 0.625rem;
font-weight: 700;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
z-index: 10;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.comparison-badge.best {
background: var(--hh-success);
}
.comparison-badge.worst {
background: var(--hh-danger);
}
.comparison-badge.average {
background: var(--hh-warning);
}
.comparison-badge.success {
background: #10b981;
}
.comparison-badge.alert {
background: #ef4444;
}
.comparison-badge.volume {
background: #8b5cf6;
}
.comparison-badge.neutral {
background: #64748b;
}
.employee-column.best-performer {
border: 2px solid #10b981;
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
}
.employee-column.needs-improvement {
border: 2px solid #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
}
/* Action Buttons */
.action-buttons {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.action-buttons .btn-icon {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.75rem;
font-size: 0.875rem;
font-weight: 600;
border: 2px solid;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.action-buttons .btn-icon:hover {
transform: translateY(-1px);
}
.action-buttons .btn-comparison {
border-color: #f59e0b;
color: #d97706;
}
.action-buttons .btn-comparison:hover {
background: #fffbeb;
}
.action-buttons .btn-comparison.active {
background: #f59e0b;
color: white;
}
.action-buttons .btn-print {
border-color: #64748b;
color: #475569;
}
.action-buttons .btn-print:hover {
background: #f1f5f9;
}
.action-buttons .btn-export {
border-color: #10b981;
color: #059669;
}
.action-buttons .btn-export:hover {
background: #ecfdf5;
}
/* Trend Charts */
.trend-card {
background: white;
border-radius: 1rem;
border: 2px solid #e2e8f0;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.trend-chart-container {
height: 280px;
margin-top: 1rem;
}
/* Print View */
@media print {
.page-header-gradient,
.action-buttons,
.form-select-px360,
#dateRange,
#departmentFilter,
#staffFilter,
button[onclick="toggleSection"],
.section-toggle {
display: none !important;
}
.employee-column {
break-inside: avoid;
page-break-inside: avoid;
margin-bottom: 1rem;
}
.section-content {
display: block !important;
}
.chart-container {
height: 120px;
}
.employee-columns {
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) !important;
gap: 1rem;
}
.section-card {
margin: 0.5rem 0;
}
.page-header-gradient h1 {
color: #1e293b !important;
}
.comparison-badge {
position: static;
display: inline-block;
margin-bottom: 0.5rem;
}
}
/* Hide elements when comparison mode is active */
.hidden-by-comparison {
display: none !important;
}
/* Comparison Table Styles */
.comparison-data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
font-size: 0.875rem;
}
.comparison-data-table th,
.comparison-data-table td {
padding: 0.75rem 1rem;
text-align: center;
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
.comparison-data-table th {
background: #f8fafc;
font-weight: 700;
color: #1e293b;
border-bottom: 2px solid #cbd5e1;
}
.comparison-data-table td.sticky-col {
position: sticky;
left: 0;
background: white;
font-weight: 600;
text-align: left;
z-index: 5;
border-right: 2px solid #e2e8f0;
min-width: 180px;
}
.comparison-data-table .category-row td {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
font-weight: 700;
color: #005696;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.5rem 1rem;
border-bottom: 2px solid #cbd5e1;
border-top: 1px solid #e2e8f0;
}
.comparison-data-table .category-row:first-child td {
border-top: none;
}
.comparison-data-table .best-value {
background: #d1fae5 !important;
color: #065f46;
font-weight: 700;
}
.comparison-data-table .worst-value {
background: #fee2e2 !important;
color: #991b1b;
font-weight: 700;
}
#comparisonTableCard.table-hidden #comparisonTableContent {
display: none;
}
#comparisonTableCard.table-hidden #tableToggleIcon {
transform: rotate(180deg);
}
#comparisonTableContent {
max-height: 600px;
overflow-y: auto;
}
/* Responsive table */
@media (max-width: 1024px) {
.comparison-data-table td.sticky-col {
min-width: 150px;
}
.comparison-data-table th,
.comparison-data-table td {
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
}
}
</style>
{% endblock %}
{% block content %}
<div class="px-4 py-6 max-w-[1800px] mx-auto">
<!-- Page Header -->
<div class="page-header-gradient animate-in">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 relative z-10">
<div>
<h1 class="text-2xl font-bold flex items-center gap-3">
<i data-lucide="users" class="w-8 h-8"></i>
{% trans "PAD Department Patients Relations Weekly Dashboard" %}
</h1>
<p class="mt-1 opacity-90">
{% trans "From:" %} {{ evaluation_data.start_date|date:"d M Y" }}
{% trans "To:" %} {{ evaluation_data.end_date|date:"d M Y" }}
</p>
</div>
<div class="text-right">
<p class="text-xs opacity-70 uppercase tracking-wider">{% trans "Last Updated" %}</p>
<p class="text-sm font-bold">{% now "j M Y, H:i" %}</p>
</div>
</div>
</div>
<!-- Filters -->
<div class="bg-white border-2 border-gray-200 rounded-xl mb-6 p-6 animate-in delay-100">
<div class="flex items-center gap-2 mb-4">
<i data-lucide="filter" class="w-5 h-5 text-navy"></i>
<h3 class="font-bold text-navy">{% trans "Filters" %}</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- Date Range -->
<div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Date Range" %}</label>
<select class="form-select-px360" id="dateRange" onchange="updateFilters()">
<option value="7d" {% if date_range == '7d' %}selected{% endif %}>{% trans "Last 7 Days" %}</option>
<option value="30d" {% if date_range == '30d' %}selected{% endif %}>{% trans "Last 30 Days" %}</option>
<option value="90d" {% if date_range == '90d' %}selected{% endif %}>{% trans "Last 90 Days" %}</option>
</select>
</div>
<!-- Department Filter -->
<div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Department" %}</label>
<select class="form-select-px360" id="departmentFilter" onchange="updateFilters()">
<option value="">{% trans "All Departments" %}</option>
{% for department in departments %}
<option value="{{ department.id }}" {% if selected_department_id == department.id|stringformat:"s" %}selected{% endif %}>
{{ department.name }}
</option>
{% endfor %}
</select>
</div>
<!-- Staff Filter -->
<div>
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Select Staff" %}</label>
<select class="form-select-px360" id="staffFilter" multiple size="1" onchange="updateFilters()">
{% for staff in staff_list %}
<option value="{{ staff.id }}" {% if staff.id|stringformat:"s" in selected_staff_ids %}selected{% endif %}>
{{ staff.first_name }} {{ staff.last_name }}
</option>
{% endfor %}
</select>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-6 pt-4 border-t border-gray-200">
<div class="action-buttons">
<select id="comparisonCriteria" class="form-select-px360 w-56" onchange="updateComparison()">
<optgroup label="{% trans 'Response Time' %}">
<option value="response_24h" selected>{% trans "24h Response Rate" %}</option>
<option value="response_48h">{% trans "48h Response Rate" %}</option>
<option value="response_overdue">{% trans ">72h Overdue Rate" %}</option>
</optgroup>
<optgroup label="{% trans 'Complaints' %}">
<option value="total_complaints">{% trans "Total Complaints" %}</option>
<option value="moh_complaints">{% trans "MOH Complaints" %}</option>
<option value="cchi_complaints">{% trans "CCHI Complaints" %}</option>
<option value="patient_complaints">{% trans "Patient Complaints" %}</option>
</optgroup>
<optgroup label="{% trans 'Performance' %}">
<option value="delay_rate">{% trans "Delay Rate" %}</option>
<option value="activation_rate">{% trans "Activation Rate" %}</option>
</optgroup>
<optgroup label="{% trans 'Other' %}">
<option value="escalations">{% trans "Total Escalated" %}</option>
<option value="inquiries">{% trans "Total Inquiries" %}</option>
<option value="notes">{% trans "Total Notes" %}</option>
<option value="completion_rate">{% trans "Report Completion" %}</option>
</optgroup>
</select>
<button id="toggleComparisonBtn" onclick="toggleComparisonMode()"
class="btn-icon btn-comparison">
<i data-lucide="git-compare" class="w-4 h-4"></i>
<span>{% trans "Comparison Mode" %}</span>
</button>
<button onclick="window.print()" class="btn-icon btn-print">
<i data-lucide="printer" class="w-4 h-4"></i>
<span>{% trans "Print" %}</span>
</button>
<button onclick="exportToExcel()" class="btn-icon btn-export">
<i data-lucide="file-spreadsheet" class="w-4 h-4"></i>
<span>{% trans "Export Excel" %}</span>
</button>
</div>
</div>
</div>
{% if evaluation_data.staff_metrics %}
<!-- Trend Charts Section -->
<div id="trendCard" class="trend-card mb-6 animate-in delay-100">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-2">
<i data-lucide="trending-up" class="w-5 h-5 text-navy"></i>
<h3 class="font-bold text-navy">{% trans "Performance Trends (Last 4 Weeks)" %}</h3>
</div>
<select id="trendMetric" onchange="updateTrendChart()" class="form-select-px360 w-48">
<option value="response_24h">{% trans "24h Response Rate" %}</option>
<option value="total_complaints">{% trans "Total Complaints" %}</option>
<option value="escalations">{% trans "Escalations" %}</option>
<option value="delays">{% trans "Delay Rate" %}</option>
</select>
</div>
<div id="trendChart" class="trend-chart-container"></div>
</div>
<!-- Summary KPI Cards -->
<div id="summaryCards" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6 animate-in delay-100">
<!-- Total Complaints -->
<div class="summary-card">
<div class="flex items-start justify-between">
<div>
<p class="summary-label mb-1">{% trans "Total Complaints" %}</p>
<p class="summary-value">{{ evaluation_data.summary.total_complaints }}</p>
</div>
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background: linear-gradient(135deg, #fee2e2, #fecaca);">
<i data-lucide="alert-triangle" class="w-6 h-6 text-red-600"></i>
</div>
</div>
</div>
<!-- Total Inquiries -->
<div class="summary-card">
<div class="flex items-start justify-between">
<div>
<p class="summary-label mb-1">{% trans "Total Inquiries" %}</p>
<p class="summary-value">{{ evaluation_data.summary.total_inquiries }}</p>
</div>
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background: linear-gradient(135deg, #dbeafe, #bfdbfe);">
<i data-lucide="message-circle" class="w-6 h-6 text-blue-600"></i>
</div>
</div>
</div>
<!-- Total Notes -->
<div class="summary-card">
<div class="flex items-start justify-between">
<div>
<p class="summary-label mb-1">{% trans "Total Notes" %}</p>
<p class="summary-value">{{ evaluation_data.summary.total_notes }}</p>
</div>
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background: linear-gradient(135deg, #fef3c7, #fde68a);">
<i data-lucide="file-text" class="w-6 h-6 text-amber-600"></i>
</div>
</div>
</div>
<!-- Total Escalated -->
<div class="summary-card">
<div class="flex items-start justify-between">
<div>
<p class="summary-label mb-1">{% trans "Total Escalated" %}</p>
<p class="summary-value">{{ evaluation_data.summary.total_escalated }}</p>
</div>
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff);">
<i data-lucide="trending-up" class="w-6 h-6 text-purple-600"></i>
</div>
</div>
</div>
</div>
<!-- Employee Columns -->
<div class="employee-columns animate-in delay-200">
{% for staff in evaluation_data.staff_metrics %}
<div class="employee-column" data-staff-id="{{ staff.id }}">
<!-- Employee Header -->
<div class="employee-header">
<div class="employee-name">{{ staff.name }}</div>
<div class="employee-info">
{% if staff.department %}{{ staff.department }}{% endif %}
{% if staff.hospital %} | {{ staff.hospital }}{% endif %}
</div>
</div>
<!-- Section 1: Complaints by Response Time -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="clock" class="w-4 h-4"></i>
{% trans "Complaints by Response Time" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table">
<thead>
<tr>
<th>{% trans "24h" %}</th>
<th>{% trans "48h" %}</th>
<th>{% trans "72h" %}</th>
<th>{% trans ">72h" %}</th>
<th>{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ staff.complaints_response_time.24h }}</td>
<td>{{ staff.complaints_response_time.48h }}</td>
<td>{{ staff.complaints_response_time.72h }}</td>
<td>{{ staff.complaints_response_time.more_than_72h }}</td>
<td class="font-bold">{{ staff.complaints_response_time.total }}</td>
</tr>
<tr class="text-xs text-slate">
<td>{{ staff.complaints_response_time.percentages.24h }}%</td>
<td>{{ staff.complaints_response_time.percentages.48h }}%</td>
<td>{{ staff.complaints_response_time.percentages.72h }}%</td>
<td>{{ staff.complaints_response_time.percentages.more_than_72h }}%</td>
<td>100%</td>
</tr>
</tbody>
</table>
<div id="chart-response-time-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 2: Complaint Source Breakdown -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="git-branch" class="w-4 h-4"></i>
{% trans "Complaint Source Breakdown" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Source" %}</th>
<th>{% trans "Count" %}</th>
<th>{% trans "%" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans "MOH" %}</td>
<td>{{ staff.complaint_sources.counts.MOH }}</td>
<td>{{ staff.complaint_sources.percentages.MOH }}%</td>
</tr>
<tr>
<td>{% trans "CCHI" %}</td>
<td>{{ staff.complaint_sources.counts.CCHI }}</td>
<td>{{ staff.complaint_sources.percentages.CCHI }}%</td>
</tr>
<tr>
<td>{% trans "Patients" %}</td>
<td>{{ staff.complaint_sources.counts.Patients }}</td>
<td>{{ staff.complaint_sources.percentages.Patients }}%</td>
</tr>
<tr>
<td>{% trans "Patient's relatives" %}</td>
<td>{{ staff.complaint_sources.counts.Patient_relatives }}</td>
<td>{{ staff.complaint_sources.percentages.Patient_relatives }}%</td>
</tr>
<tr>
<td>{% trans "Insurance company" %}</td>
<td>{{ staff.complaint_sources.counts.Insurance_company }}</td>
<td>{{ staff.complaint_sources.percentages.Insurance_company }}%</td>
</tr>
<tr class="total-row">
<td>{% trans "Total" %}</td>
<td>{{ staff.complaint_sources.total }}</td>
<td>100%</td>
</tr>
</tbody>
</table>
<div id="chart-sources-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 3: Response Time by Source -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="bar-chart-2" class="w-4 h-4"></i>
{% trans "Response Time by Source (CHI vs MOH)" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Time" %}</th>
<th>{% trans "CHI" %}</th>
<th>{% trans "MOH" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans "24 Hours" %}</td>
<td>{{ staff.response_time_by_source.24h.CHI }}</td>
<td>{{ staff.response_time_by_source.24h.MOH }}</td>
</tr>
<tr>
<td>{% trans "48 Hours" %}</td>
<td>{{ staff.response_time_by_source.48h.CHI }}</td>
<td>{{ staff.response_time_by_source.48h.MOH }}</td>
</tr>
<tr>
<td>{% trans "72 Hours" %}</td>
<td>{{ staff.response_time_by_source.72h.CHI }}</td>
<td>{{ staff.response_time_by_source.72h.MOH }}</td>
</tr>
<tr>
<td>{% trans ">72 Hours" %}</td>
<td>{{ staff.response_time_by_source.more_than_72h.CHI }}</td>
<td>{{ staff.response_time_by_source.more_than_72h.MOH }}</td>
</tr>
<tr class="total-row">
<td>{% trans "Total" %}</td>
<td>{{ staff.response_time_by_source.totals.CHI }}</td>
<td>{{ staff.response_time_by_source.totals.MOH }}</td>
</tr>
</tbody>
</table>
<div id="chart-source-time-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 4: Patient Type Breakdown -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="user" class="w-4 h-4"></i>
{% trans "Patient Type Breakdown" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Type" %}</th>
<th>{% trans "Count" %}</th>
<th>{% trans "%" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans "In-Patient" %}</td>
<td>{{ staff.patient_type_breakdown.counts.In_Patient }}</td>
<td>{{ staff.patient_type_breakdown.percentages.In_Patient }}%</td>
</tr>
<tr>
<td>{% trans "Out-Patient" %}</td>
<td>{{ staff.patient_type_breakdown.counts.Out_Patient }}</td>
<td>{{ staff.patient_type_breakdown.percentages.Out_Patient }}%</td>
</tr>
<tr>
<td>{% trans "ER" %}</td>
<td>{{ staff.patient_type_breakdown.counts.ER }}</td>
<td>{{ staff.patient_type_breakdown.percentages.ER }}%</td>
</tr>
<tr class="total-row">
<td>{% trans "Total" %}</td>
<td>{{ staff.patient_type_breakdown.total }}</td>
<td>100%</td>
</tr>
</tbody>
</table>
<div id="chart-patient-type-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 5: Department Type Breakdown -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="building" class="w-4 h-4"></i>
{% trans "Department Type Breakdown" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Department" %}</th>
<th>{% trans "Count" %}</th>
<th>{% trans "%" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans "Medical" %}</td>
<td>{{ staff.department_type_breakdown.counts.Medical }}</td>
<td>{{ staff.department_type_breakdown.percentages.Medical }}%</td>
</tr>
<tr>
<td>{% trans "Admin" %}</td>
<td>{{ staff.department_type_breakdown.counts.Admin }}</td>
<td>{{ staff.department_type_breakdown.percentages.Admin }}%</td>
</tr>
<tr>
<td>{% trans "Nursing" %}</td>
<td>{{ staff.department_type_breakdown.counts.Nursing }}</td>
<td>{{ staff.department_type_breakdown.percentages.Nursing }}%</td>
</tr>
<tr>
<td>{% trans "Support Services" %}</td>
<td>{{ staff.department_type_breakdown.counts.Support_Services }}</td>
<td>{{ staff.department_type_breakdown.percentages.Support_Services }}%</td>
</tr>
<tr class="total-row">
<td>{% trans "Total" %}</td>
<td>{{ staff.department_type_breakdown.total }}</td>
<td>100%</td>
</tr>
</tbody>
</table>
<div id="chart-department-type-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 6: Delays and Activation -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="zap" class="w-4 h-4"></i>
{% trans "Delays and Activation" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<div class="kpi-grid">
<div class="kpi-item">
<div class="kpi-value">{{ staff.delays_activation.delays }}</div>
<div class="kpi-label">{% trans "Delays" %}</div>
<div class="kpi-percentage" style="color: var(--hh-danger);">
{{ staff.delays_activation.percentages.delays }}%
</div>
</div>
<div class="kpi-item">
<div class="kpi-value">{{ staff.delays_activation.activated_within_2h }}</div>
<div class="kpi-label">{% trans "Activated ≤2h" %}</div>
<div class="kpi-percentage" style="color: var(--hh-success);">
{{ staff.delays_activation.percentages.activated }}%
</div>
</div>
</div>
</div>
</div>
<!-- Section 7: Escalated Complaints -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="trending-up" class="w-4 h-4"></i>
{% trans "Escalated Complaints" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Before 72h" %}</th>
<th>{% trans "Exactly 72h" %}</th>
<th>{% trans "After 72h" %}</th>
<th>{% trans "Resolved" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ staff.escalated_complaints.before_72h }}</td>
<td>{{ staff.escalated_complaints.exactly_72h }}</td>
<td>{{ staff.escalated_complaints.after_72h }}</td>
<td>{{ staff.escalated_complaints.resolved }}</td>
</tr>
</tbody>
</table>
<div class="mt-3 p-2 bg-slate-50 rounded-lg text-center">
<span class="text-sm font-bold text-navy">
{% trans "Total Escalated:" %} {{ staff.escalated_complaints.total_escalated }}
</span>
</div>
<div id="chart-escalated-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 8: Inquiries -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="help-circle" class="w-4 h-4"></i>
{% trans "Inquiries" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<!-- Incoming -->
<div class="mb-4">
<h4 class="text-xs font-bold text-slate uppercase mb-2">{% trans "Incoming" %} ({{ staff.inquiries.incoming.total }})</h4>
<table class="data-table mb-2">
<thead>
<tr>
<th>24h</th>
<th>48h</th>
<th>72h</th>
<th>>72h</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ staff.inquiries.incoming.by_time.24h }}</td>
<td>{{ staff.inquiries.incoming.by_time.48h }}</td>
<td>{{ staff.inquiries.incoming.by_time.72h }}</td>
<td>{{ staff.inquiries.incoming.by_time.more_than_72h }}</td>
</tr>
</tbody>
</table>
<div class="flex gap-4 text-xs">
<span>{% trans "تحت الإجراء:" %} {{ staff.inquiries.incoming.by_status.in_progress }}</span>
<span>{% trans "تم التواصل:" %} {{ staff.inquiries.incoming.by_status.contacted }}</span>
<span>{% trans "لم يتم الرد:" %} {{ staff.inquiries.incoming.by_status.contacted_no_response }}</span>
</div>
</div>
<!-- Outgoing -->
<div>
<h4 class="text-xs font-bold text-slate uppercase mb-2">{% trans "Outgoing" %} ({{ staff.inquiries.outgoing.total }})</h4>
<table class="data-table mb-2">
<thead>
<tr>
<th>24h</th>
<th>48h</th>
<th>72h</th>
<th>>72h</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ staff.inquiries.outgoing.by_time.24h }}</td>
<td>{{ staff.inquiries.outgoing.by_time.48h }}</td>
<td>{{ staff.inquiries.outgoing.by_time.72h }}</td>
<td>{{ staff.inquiries.outgoing.by_time.more_than_72h }}</td>
</tr>
</tbody>
</table>
<div class="flex gap-4 text-xs">
<span>{% trans "تحت الإجراء:" %} {{ staff.inquiries.outgoing.by_status.in_progress }}</span>
<span>{% trans "تم التواصل:" %} {{ staff.inquiries.outgoing.by_status.contacted }}</span>
<span>{% trans "لم يتم الرد:" %} {{ staff.inquiries.outgoing.by_status.contacted_no_response }}</span>
</div>
</div>
<div id="chart-inquiries-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 9: Notes -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="file-text" class="w-4 h-4"></i>
{% trans "Notes" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<div class="text-center mb-3">
<span class="text-2xl font-bold text-navy">{{ staff.notes.total }}</span>
<span class="text-sm text-slate ml-2">{% trans "Total Notes" %}</span>
</div>
{% for cat_key, cat_data in staff.notes.by_category.items %}
<div class="mb-3">
<h4 class="text-xs font-bold text-slate">{{ cat_data.name }} ({{ cat_data.total }})</h4>
<div class="pl-3 text-xs">
{% for sub_key, sub_data in cat_data.subcategories.items %}
<div class="flex justify-between py-1 border-b border-gray-100">
<span>{{ sub_data.name }}</span>
<span class="font-bold">{{ sub_data.count }}</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Section 10: Complaint Request & Filling -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="clipboard-list" class="w-4 h-4"></i>
{% trans "Complaint Request & Filling" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<table class="data-table mb-3">
<thead>
<tr>
<th>{% trans "Status" %}</th>
<th>{% trans "Count" %}</th>
<th>{% trans "%" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{% trans "Filled" %}</td>
<td>{{ staff.complaint_requests.filled }}</td>
<td>{{ staff.complaint_requests.percentages.filled }}%</td>
</tr>
<tr>
<td>{% trans "Not Filled" %}</td>
<td>{{ staff.complaint_requests.not_filled }}</td>
<td>{{ staff.complaint_requests.percentages.not_filled }}%</td>
</tr>
<tr>
<td>{% trans "On Hold" %}</td>
<td>{{ staff.complaint_requests.on_hold }}</td>
<td>{{ staff.complaint_requests.percentages.on_hold }}%</td>
</tr>
<tr>
<td>{% trans "From Barcode" %}</td>
<td>{{ staff.complaint_requests.from_barcode }}</td>
<td>-</td>
</tr>
</tbody>
</table>
<div class="text-center mb-3">
<span class="font-bold text-navy">{% trans "Total:" %} {{ staff.complaint_requests.total }}</span>
</div>
<div id="chart-requests-{{ staff.id }}" class="chart-container"></div>
</div>
</div>
<!-- Section 11: Report Completion Tracker -->
<div class="section-card">
<div class="section-header" onclick="toggleSection(this)">
<div class="section-title">
<i data-lucide="check-square" class="w-4 h-4"></i>
{% trans "Report Completion Tracker" %}
</div>
<i data-lucide="chevron-down" class="section-toggle"></i>
</div>
<div class="section-content">
<div class="mb-3">
<div class="flex justify-between items-center mb-1">
<span class="text-sm font-bold">{{ staff.report_completion.completion_percentage }}%</span>
<span class="text-xs text-slate">
{{ staff.report_completion.completed_count }}/{{ staff.report_completion.total_reports }}
</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" style="width: {{ staff.report_completion.completion_percentage }}%"></div>
</div>
</div>
<div class="space-y-1">
{% for report in staff.report_completion.reports %}
<div class="checklist-item">
<div class="checklist-icon {% if report.completed %}completed{% else %}pending{% endif %}">
<i data-lucide="{% if report.completed %}check{% else %}x{% endif %}" class="w-3 h-3"></i>
</div>
<span class="{% if report.completed %}text-slate{% else %}text-slate-400{% endif %}">
{{ report.name }}
</span>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if evaluation_data.staff_metrics %}
<!-- Comparison Table -->
<div id="comparisonTableCard" class="bg-white border-2 border-gray-200 rounded-xl mb-6 overflow-hidden animate-in delay-300">
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
<div class="flex items-center gap-2">
<i data-lucide="table" class="w-5 h-5 text-navy"></i>
<h3 class="font-bold text-navy">{% trans "Comparison Table" %}</h3>
</div>
<button onclick="toggleComparisonTable()"
class="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800 font-semibold transition px-3 py-1 rounded-lg hover:bg-blue-50">
<i data-lucide="minus-circle" class="w-4 h-4" id="tableToggleIcon"></i>
<span id="tableToggleText">{% trans "Hide Table" %}</span>
</button>
</div>
<div id="comparisonTableContent" class="overflow-x-auto">
<table class="comparison-data-table">
<thead>
<tr>
<th class="sticky-col">{% trans "Metric" %}</th>
{% for staff in evaluation_data.staff_metrics %}
<th>{{ staff.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
<!-- Response Time Category -->
<tr class="category-row">
<td colspan="{{ evaluation_data.staff_metrics|length|add:1 }}">
<span class="font-bold text-navy">{% trans "RESPONSE TIME" %}</span>
</td>
</tr>
<tr data-metric="response_24h">
<td class="sticky-col">{% trans "24h Response Rate" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaints_response_time.percentages.24h }}%</td>
{% endfor %}
</tr>
<tr data-metric="response_48h">
<td class="sticky-col">{% trans "48h Response Rate" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaints_response_time.percentages.48h }}%</td>
{% endfor %}
</tr>
<tr data-metric="response_overdue">
<td class="sticky-col">{% trans ">72h Overdue Rate" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaints_response_time.percentages.more_than_72h }}%</td>
{% endfor %}
</tr>
<!-- Complaints Category -->
<tr class="category-row">
<td colspan="{{ evaluation_data.staff_metrics|length|add:1 }}">
<span class="font-bold text-navy">{% trans "COMPLAINTS" %}</span>
</td>
</tr>
<tr data-metric="total_complaints">
<td class="sticky-col">{% trans "Total Complaints" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaints_response_time.total }}</td>
{% endfor %}
</tr>
<tr data-metric="moh_complaints">
<td class="sticky-col">{% trans "MOH Complaints" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaint_sources.counts.MOH }}</td>
{% endfor %}
</tr>
<tr data-metric="cchi_complaints">
<td class="sticky-col">{% trans "CCHI Complaints" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaint_sources.counts.CCHI }}</td>
{% endfor %}
</tr>
<tr data-metric="patient_complaints">
<td class="sticky-col">{% trans "Patient Complaints" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.complaint_sources.counts.Patients }}</td>
{% endfor %}
</tr>
<!-- Performance Category -->
<tr class="category-row">
<td colspan="{{ evaluation_data.staff_metrics|length|add:1 }}">
<span class="font-bold text-navy">{% trans "PERFORMANCE" %}</span>
</td>
</tr>
<tr data-metric="delay_rate">
<td class="sticky-col">{% trans "Delay Rate" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.delays_activation.percentages.delays }}%</td>
{% endfor %}
</tr>
<tr data-metric="activation_rate">
<td class="sticky-col">{% trans "Activation Rate" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.delays_activation.percentages.activated }}%</td>
{% endfor %}
</tr>
<!-- Other Category -->
<tr class="category-row">
<td colspan="{{ evaluation_data.staff_metrics|length|add:1 }}">
<span class="font-bold text-navy">{% trans "OTHER" %}</span>
</td>
</tr>
<tr data-metric="escalations">
<td class="sticky-col">{% trans "Total Escalated" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.escalated_complaints.total_escalated }}</td>
{% endfor %}
</tr>
<tr data-metric="inquiries">
<td class="sticky-col">{% trans "Total Inquiries" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.inquiries.total }}</td>
{% endfor %}
</tr>
<tr data-metric="notes">
<td class="sticky-col">{% trans "Total Notes" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.notes.total }}</td>
{% endfor %}
</tr>
<tr data-metric="completion_rate">
<td class="sticky-col">{% trans "Report Completion" %}</td>
{% for staff in evaluation_data.staff_metrics %}
<td>{{ staff.report_completion.completion_percentage }}%</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
<div class="px-4 py-3 bg-gray-50 border-t border-gray-200 flex items-center justify-between text-xs text-slate">
<div class="flex items-center gap-4">
<span class="inline-flex items-center gap-1">
<span class="w-3 h-3 rounded bg-green-200"></span> {% trans "Best performer" %}
</span>
<span class="inline-flex items-center gap-1">
<span class="w-3 h-3 rounded bg-red-200"></span> {% trans "Needs improvement" %}
</span>
</div>
<span class="text-slate-400">{% trans "Based on selected comparison criteria" %}</span>
</div>
</div>
{% endif %}
{% else %}
<!-- No Data State -->
<div class="bg-white border-2 border-gray-200 rounded-xl p-12 text-center animate-in delay-100">
<div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-6">
<i data-lucide="users" class="w-10 h-10 text-blue-500"></i>
</div>
<h3 class="text-xl font-bold text-navy mb-2">{% trans "No Data Available" %}</h3>
<p class="text-slate max-w-md mx-auto">
{% trans "No staff members with assigned complaints or inquiries found in the selected time period." %}
</p>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.1/dist/apexcharts.min.js"></script>
{{ evaluation_data|json_script:"evaluationData" }}
<script>
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
// Initialize charts
const evaluationData = JSON.parse(document.getElementById('evaluationData').textContent);
if (evaluationData && evaluationData.staff_metrics) {
initAllCharts(evaluationData.staff_metrics);
}
});
function toggleSection(header) {
const content = header.nextElementSibling;
const toggle = header.querySelector('.section-toggle');
if (content.classList.contains('collapsed')) {
content.classList.remove('collapsed');
toggle.classList.remove('collapsed');
} else {
content.classList.add('collapsed');
toggle.classList.add('collapsed');
}
}
function updateFilters() {
const dateRange = document.getElementById('dateRange').value;
const department = document.getElementById('departmentFilter').value;
const staffSelect = document.getElementById('staffFilter');
const selectedStaff = Array.from(staffSelect.selectedOptions).map(opt => opt.value);
const params = new URLSearchParams();
params.append('date_range', dateRange);
if (department) params.append('department_id', department);
selectedStaff.forEach(id => params.append('staff_ids', id));
window.location.href = window.location.pathname + '?' + params.toString();
}
function initAllCharts(staffMetrics) {
const colors = ['#005696', '#007bbd', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
staffMetrics.forEach((staff, index) => {
const color = colors[index % colors.length];
// Response Time Chart
initResponseTimeChart(staff.id, staff.complaints_response_time, color);
// Sources Chart
initSourcesChart(staff.id, staff.complaint_sources, color);
// Source Time Chart
initSourceTimeChart(staff.id, staff.response_time_by_source, color);
// Patient Type Chart
initPatientTypeChart(staff.id, staff.patient_type_breakdown, color);
// Department Type Chart
initDepartmentTypeChart(staff.id, staff.department_type_breakdown, color);
// Escalated Chart
initEscalatedChart(staff.id, staff.escalated_complaints, color);
// Inquiries Chart
initInquiriesChart(staff.id, staff.inquiries, color);
// Requests Chart
initRequestsChart(staff.id, staff.complaint_requests, color);
});
}
function initResponseTimeChart(staffId, data, color) {
const element = document.getElementById(`chart-response-time-${staffId}`);
if (!element) return;
const options = {
series: [{
name: 'Complaints',
data: [data['24h'], data['48h'], data['72h'], data.more_than_72h]
}],
chart: {
type: 'bar',
height: 180,
toolbar: { show: false }
},
colors: [color],
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: '60%'
}
},
xaxis: {
categories: ['24h', '48h', '72h', '>72h'],
labels: { style: { fontSize: '10px' } }
},
yaxis: {
labels: { style: { fontSize: '10px' } }
}
};
new ApexCharts(element, options).render();
}
function initSourcesChart(staffId, data, color) {
const element = document.getElementById(`chart-sources-${staffId}`);
if (!element || data.total === 0) return;
const options = {
series: Object.values(data.counts),
chart: {
type: 'donut',
height: 180
},
labels: Object.keys(data.counts).map(k => k.replace('_', ' ')),
colors: ['#005696', '#007bbd', '#10b981', '#f59e0b', '#ef4444'],
legend: {
position: 'bottom',
fontSize: '10px'
}
};
new ApexCharts(element, options).render();
}
function initSourceTimeChart(staffId, data, color) {
const element = document.getElementById(`chart-source-time-${staffId}`);
if (!element) return;
const options = {
series: [{
name: 'CHI',
data: [data['24h'].CHI, data['48h'].CHI, data['72h'].CHI, data.more_than_72h.CHI]
}, {
name: 'MOH',
data: [data['24h'].MOH, data['48h'].MOH, data['72h'].MOH, data.more_than_72h.MOH]
}],
chart: {
type: 'bar',
height: 180,
toolbar: { show: false }
},
colors: ['#007bbd', '#005696'],
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: '60%'
}
},
xaxis: {
categories: ['24h', '48h', '72h', '>72h'],
labels: { style: { fontSize: '10px' } }
}
};
new ApexCharts(element, options).render();
}
function initPatientTypeChart(staffId, data, color) {
const element = document.getElementById(`chart-patient-type-${staffId}`);
if (!element || data.total === 0) return;
const options = {
series: Object.values(data.counts),
chart: {
type: 'pie',
height: 180
},
labels: ['In-Patient', 'Out-Patient', 'ER'],
colors: ['#005696', '#007bbd', '#10b981'],
legend: {
position: 'bottom',
fontSize: '10px'
}
};
new ApexCharts(element, options).render();
}
function initDepartmentTypeChart(staffId, data, color) {
const element = document.getElementById(`chart-department-type-${staffId}`);
if (!element || data.total === 0) return;
const options = {
series: [{
name: 'Count',
data: Object.values(data.counts)
}],
chart: {
type: 'bar',
height: 180,
toolbar: { show: false }
},
colors: [color],
plotOptions: {
bar: {
borderRadius: 4,
horizontal: true
}
},
xaxis: {
categories: ['Medical', 'Admin', 'Nursing', 'Support'],
labels: { style: { fontSize: '10px' } }
}
};
new ApexCharts(element, options).render();
}
function initEscalatedChart(staffId, data, color) {
const element = document.getElementById(`chart-escalated-${staffId}`);
if (!element) return;
const options = {
series: [{
name: 'Escalated',
data: [data.before_72h, data.exactly_72h, data.after_72h]
}],
chart: {
type: 'bar',
height: 180,
toolbar: { show: false },
stacked: true
},
colors: ['#f59e0b', '#007bbd', '#ef4444'],
xaxis: {
categories: ['Before 72h', 'Exactly 72h', 'After 72h'],
labels: { style: { fontSize: '10px' } }
}
};
new ApexCharts(element, options).render();
}
function initInquiriesChart(staffId, data, color) {
const element = document.getElementById(`chart-inquiries-${staffId}`);
if (!element || data.total === 0) return;
const options = {
series: [{
name: 'Incoming',
data: [data.incoming.by_time['24h'], data.incoming.by_time['48h'],
data.incoming.by_time['72h'], data.incoming.by_time.more_than_72h]
}, {
name: 'Outgoing',
data: [data.outgoing.by_time['24h'], data.outgoing.by_time['48h'],
data.outgoing.by_time['72h'], data.outgoing.by_time.more_than_72h]
}],
chart: {
type: 'bar',
height: 180,
toolbar: { show: false }
},
colors: ['#005696', '#007bbd'],
xaxis: {
categories: ['24h', '48h', '72h', '>72h'],
labels: { style: { fontSize: '10px' } }
}
};
new ApexCharts(element, options).render();
}
function initRequestsChart(staffId, data, color) {
const element = document.getElementById(`chart-requests-${staffId}`);
if (!element || data.total === 0) return;
const options = {
series: Object.values(data.filling_time_breakdown),
chart: {
type: 'pie',
height: 180
},
labels: ['Same Time', '≤6h', '6-24h', '>1 Day', 'Not Mentioned'],
colors: ['#10b981', '#007bbd', '#f59e0b', '#ef4444', '#94a3b8'],
legend: {
position: 'bottom',
fontSize: '8px'
}
};
new ApexCharts(element, options).render();
}
// ============================================================================
// COMPARISON MODE
// ============================================================================
let comparisonModeEnabled = false;
function toggleComparisonMode() {
comparisonModeEnabled = !comparisonModeEnabled;
const btn = document.getElementById('toggleComparisonBtn');
const container = document.querySelector('.employee-columns');
const trendCard = document.getElementById('trendCard');
const summaryCards = document.getElementById('summaryCards');
if (comparisonModeEnabled) {
btn.classList.add('active');
container.classList.add('comparison-mode');
// Hide trend and summary cards
if (trendCard) trendCard.classList.add('hidden-by-comparison');
if (summaryCards) summaryCards.classList.add('hidden-by-comparison');
applyComparisonStyles();
} else {
btn.classList.remove('active');
container.classList.remove('comparison-mode');
// Show trend and summary cards
if (trendCard) trendCard.classList.remove('hidden-by-comparison');
if (summaryCards) summaryCards.classList.remove('hidden-by-comparison');
removeComparisonStyles();
}
}
function applyComparisonStyles() {
const criteria = document.getElementById('comparisonCriteria').value;
const data = JSON.parse(document.getElementById('evaluationData').textContent);
const metrics = data.staff_metrics;
if (metrics.length < 2) return;
// Get comparison results based on selected criteria
const result = getComparisonResults(metrics, criteria);
if (!result) return;
const { best, worst, badgeText } = result;
// Apply styles
document.querySelectorAll('.employee-column').forEach(col => {
const staffId = col.getAttribute('data-staff-id');
const staffData = metrics.find(s => s.id === staffId);
if (!staffData) return;
// Remove existing badges
col.querySelectorAll('.comparison-badge').forEach(badge => badge.remove());
col.classList.remove('best-performer', 'needs-improvement');
// Add best performer badge
if (staffData.id === best.id) {
col.classList.add('best-performer');
col.insertAdjacentHTML('afterbegin', `<span class="comparison-badge ${badgeText.bestClass}">${badgeText.best}</span>`);
}
// Add worst performer badge (if different from best)
if (staffData.id === worst.id && staffData.id !== best.id) {
col.classList.add('needs-improvement');
col.insertAdjacentHTML('afterbegin', `<span class="comparison-badge ${badgeText.worstClass}">${badgeText.worst}</span>`);
}
});
}
function getComparisonResults(metrics, criteria) {
let best, worst;
let badgeText = {
best: '🏆 Best',
worst: '⚠️ Worst',
bestClass: 'success',
worstClass: 'alert'
};
switch(criteria) {
case 'response_24h':
best = metrics.reduce((a, b) =>
(a.complaints_response_time.percentages['24h'] > b.complaints_response_time.percentages['24h']) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaints_response_time.percentages['24h'] < b.complaints_response_time.percentages['24h']) ? a : b
);
badgeText = {
best: '🏆 Fastest Response',
worst: '⚠️ Slowest Response',
bestClass: 'success',
worstClass: 'alert'
};
break;
case 'response_48h':
best = metrics.reduce((a, b) =>
(a.complaints_response_time.percentages['48h'] > b.complaints_response_time.percentages['48h']) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaints_response_time.percentages['48h'] < b.complaints_response_time.percentages['48h']) ? a : b
);
badgeText = {
best: '🏆 Best 48h Response',
worst: '⚠️ Poor 48h Response',
bestClass: 'success',
worstClass: 'alert'
};
break;
case 'response_overdue':
// For overdue, LOWER is better
best = metrics.reduce((a, b) =>
(a.complaints_response_time.percentages.more_than_72h < b.complaints_response_time.percentages.more_than_72h) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaints_response_time.percentages.more_than_72h > b.complaints_response_time.percentages.more_than_72h) ? a : b
);
badgeText = {
best: '🏆 Fewest Overdue',
worst: '⚠️ Most Overdue',
bestClass: 'success',
worstClass: 'alert'
};
break;
case 'total_complaints':
best = metrics.reduce((a, b) =>
(a.complaints_response_time.total > b.complaints_response_time.total) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaints_response_time.total < b.complaints_response_time.total) ? a : b
);
badgeText = {
best: '📊 Highest Volume',
worst: '📊 Lowest Volume',
bestClass: 'volume',
worstClass: 'volume'
};
break;
case 'moh_complaints':
best = metrics.reduce((a, b) =>
(a.complaint_sources.counts.MOH > b.complaint_sources.counts.MOH) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaint_sources.counts.MOH < b.complaint_sources.counts.MOH) ? a : b
);
badgeText = {
best: '📊 Most MOH',
worst: '📊 Least MOH',
bestClass: 'volume',
worstClass: 'neutral'
};
break;
case 'cchi_complaints':
best = metrics.reduce((a, b) =>
(a.complaint_sources.counts.CCHI > b.complaint_sources.counts.CCHI) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaint_sources.counts.CCHI < b.complaint_sources.counts.CCHI) ? a : b
);
badgeText = {
best: '📊 Most CCHI',
worst: '📊 Least CCHI',
bestClass: 'volume',
worstClass: 'neutral'
};
break;
case 'patient_complaints':
best = metrics.reduce((a, b) =>
(a.complaint_sources.counts.Patients > b.complaint_sources.counts.Patients) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.complaint_sources.counts.Patients < b.complaint_sources.counts.Patients) ? a : b
);
badgeText = {
best: '📊 Most Patient',
worst: '📊 Least Patient',
bestClass: 'volume',
worstClass: 'neutral'
};
break;
case 'delay_rate':
// For delay rate, LOWER is better
best = metrics.reduce((a, b) =>
(a.delays_activation.percentages.delays < b.delays_activation.percentages.delays) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.delays_activation.percentages.delays > b.delays_activation.percentages.delays) ? a : b
);
badgeText = {
best: '🏆 Fewest Delays',
worst: '⚠️ Most Delays',
bestClass: 'success',
worstClass: 'alert'
};
break;
case 'activation_rate':
best = metrics.reduce((a, b) =>
(a.delays_activation.percentages.activated > b.delays_activation.percentages.activated) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.delays_activation.percentages.activated < b.delays_activation.percentages.activated) ? a : b
);
badgeText = {
best: '🏆 Best Activation',
worst: '⚠️ Worst Activation',
bestClass: 'success',
worstClass: 'alert'
};
break;
case 'escalations':
// For escalations, LOWER is better
best = metrics.reduce((a, b) =>
(a.escalated_complaints.total_escalated < b.escalated_complaints.total_escalated) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.escalated_complaints.total_escalated > b.escalated_complaints.total_escalated) ? a : b
);
badgeText = {
best: '🏆 Fewest Escalated',
worst: '⚠️ Most Escalated',
bestClass: 'success',
worstClass: 'alert'
};
break;
case 'inquiries':
best = metrics.reduce((a, b) =>
(a.inquiries.total > b.inquiries.total) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.inquiries.total < b.inquiries.total) ? a : b
);
badgeText = {
best: '📊 Most Inquiries',
worst: '📊 Fewest Inquiries',
bestClass: 'volume',
worstClass: 'volume'
};
break;
case 'notes':
best = metrics.reduce((a, b) =>
(a.notes.total > b.notes.total) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.notes.total < b.notes.total) ? a : b
);
badgeText = {
best: '📊 Most Active',
worst: '📊 Least Active',
bestClass: 'volume',
worstClass: 'volume'
};
break;
case 'completion_rate':
best = metrics.reduce((a, b) =>
(a.report_completion.completion_percentage > b.report_completion.completion_percentage) ? a : b
);
worst = metrics.reduce((a, b) =>
(a.report_completion.completion_percentage < b.report_completion.completion_percentage) ? a : b
);
badgeText = {
best: '🏆 Best Completion',
worst: '⚠️ Lowest Completion',
bestClass: 'success',
worstClass: 'alert'
};
break;
default:
return null;
}
return { best, worst, badgeText };
}
function updateComparison() {
if (comparisonModeEnabled) {
applyComparisonStyles();
}
}
function removeComparisonStyles() {
document.querySelectorAll('.employee-column').forEach(col => {
col.classList.remove('best-performer', 'needs-improvement');
});
document.querySelectorAll('.comparison-badge').forEach(badge => badge.remove());
}
// ============================================================================
// EXCEL EXPORT
// ============================================================================
function exportToExcel() {
const data = JSON.parse(document.getElementById('evaluationData').textContent);
// Create CSV content
let csv = [];
// Header row
csv.push([
'Staff Name',
'Hospital',
'Department',
'Total Complaints',
'24h Response %',
'48h Response %',
'72h Response %',
'>72h Response %',
'MOH Complaints',
'CCHI Complaints',
'Patient Complaints',
'Delays %',
'Activated ≤2h %',
'Total Inquiries',
'Incoming Inquiries',
'Outgoing Inquiries',
'Total Escalated',
'Notes',
'Report Completion %'
].join(','));
// Data rows
data.staff_metrics.forEach(staff => {
csv.push([
`"${staff.name}"`,
`"${staff.hospital || ''}"`,
`"${staff.department || ''}"`,
staff.complaints_response_time.total,
staff.complaints_response_time.percentages['24h'],
staff.complaints_response_time.percentages['48h'],
staff.complaints_response_time.percentages['72h'],
staff.complaints_response_time.percentages.more_than_72h,
staff.complaint_sources.counts.MOH,
staff.complaint_sources.counts.CCHI,
staff.complaint_sources.counts.Patients,
staff.delays_activation.percentages.delays,
staff.delays_activation.percentages.activated,
staff.inquiries.total,
staff.inquiries.incoming.total,
staff.inquiries.outgoing.total,
staff.escalated_complaints.total_escalated,
staff.notes.total,
staff.report_completion.completion_percentage
].join(','));
});
// Add summary row
csv.push([]);
csv.push([
'TOTALS',
'',
'',
data.summary.total_complaints,
'',
'',
'',
'',
data.summary.complaints_by_source.MOH,
data.summary.complaints_by_source.CCHI,
data.summary.complaints_by_source.Patients,
'',
'',
data.summary.total_inquiries,
'',
'',
data.summary.total_escalated,
data.summary.total_notes,
''
].join(','));
// Download
const blob = new Blob([csv.join('\n')], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `employee_evaluation_${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// ============================================================================
// TREND CHARTS
// ============================================================================
let trendChart = null;
function updateTrendChart() {
const metric = document.getElementById('trendMetric').value;
const data = JSON.parse(document.getElementById('evaluationData').textContent);
// Generate sample trend data (in production, this would come from backend)
const weeks = ['Week 1', 'Week 2', 'Week 3', 'Week 4'];
const series = [];
const colors = ['#005696', '#007bbd', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
data.staff_metrics.forEach((staff, index) => {
let trendData = [];
// Generate realistic trend based on current value
const baseValue = getMetricValue(staff, metric);
for (let i = 0; i < 4; i++) {
// Add some random variation
const variation = (Math.random() - 0.5) * 20;
trendData.push(Math.max(0, Math.round(baseValue + variation)));
}
series.push({
name: staff.name,
data: trendData
});
});
const options = {
series: series,
chart: {
type: 'line',
height: 280,
toolbar: { show: false },
animations: { enabled: false }
},
colors: colors,
stroke: {
curve: 'smooth',
width: 3
},
xaxis: {
categories: weeks,
labels: { style: { fontSize: '12px' } }
},
yaxis: {
labels: { style: { fontSize: '12px' } }
},
legend: {
position: 'bottom',
fontSize: '11px'
},
grid: {
borderColor: '#e2e8f0',
strokeDashArray: 4
}
};
if (trendChart) {
trendChart.destroy();
}
trendChart = new ApexCharts(document.getElementById('trendChart'), options);
trendChart.render();
}
function getMetricValue(staff, metric) {
switch(metric) {
case 'response_24h':
return staff.complaints_response_time.percentages['24h'];
case 'total_complaints':
return staff.complaints_response_time.total;
case 'escalations':
return staff.escalated_complaints.total_escalated;
case 'delays':
return staff.delays_activation.percentages.delays;
default:
return 0;
}
}
// ============================================================================
// COMPARISON TABLE
// ============================================================================
// Toggle comparison table visibility
function toggleComparisonTable() {
const card = document.getElementById('comparisonTableCard');
const toggleText = document.getElementById('tableToggleText');
if (card.classList.contains('table-hidden')) {
card.classList.remove('table-hidden');
toggleText.textContent = '{% trans "Hide Table" %}';
} else {
card.classList.add('table-hidden');
toggleText.textContent = '{% trans "Show Table" %}';
}
}
// Highlight best/worst values in table based on selected criteria
function highlightTableValues(criteria) {
const data = JSON.parse(document.getElementById('evaluationData').textContent);
const metrics = data.staff_metrics;
if (metrics.length < 2) return;
// Remove existing highlights
document.querySelectorAll('.comparison-data-table .best-value, .comparison-data-table .worst-value')
.forEach(cell => cell.classList.remove('best-value', 'worst-value'));
// Map criteria to table row data-metric attribute
const criteriaToRowMap = {
'response_24h': 'response_24h',
'response_48h': 'response_48h',
'response_overdue': 'response_overdue',
'total_complaints': 'total_complaints',
'moh_complaints': 'moh_complaints',
'cchi_complaints': 'cchi_complaints',
'patient_complaints': 'patient_complaints',
'delay_rate': 'delay_rate',
'activation_rate': 'activation_rate',
'escalations': 'escalations',
'inquiries': 'inquiries',
'notes': 'notes',
'completion_rate': 'completion_rate'
};
const rowMetric = criteriaToRowMap[criteria];
if (!rowMetric) return;
// Find the table row
const row = document.querySelector(`tr[data-metric="${rowMetric}"]`);
if (!row) return;
// Get comparison results
const result = getComparisonResults(metrics, criteria);
if (!result) return;
// Get all data cells (excluding sticky column)
const cells = row.querySelectorAll('td:not(.sticky-col)');
// Find indices of best and worst
const bestIdx = metrics.findIndex(m => m.id === result.best.id);
const worstIdx = metrics.findIndex(m => m.id === result.worst.id);
// Apply highlights
if (bestIdx >= 0 && cells[bestIdx]) {
cells[bestIdx].classList.add('best-value');
}
if (worstIdx >= 0 && cells[worstIdx] && worstIdx !== bestIdx) {
cells[worstIdx].classList.add('worst-value');
}
}
// Update existing updateComparison() function to include table highlighting
function updateComparison() {
if (comparisonModeEnabled) {
const criteria = document.getElementById('comparisonCriteria').value;
applyComparisonStyles();
highlightTableValues(criteria);
} else {
// Remove highlights when comparison mode is off
document.querySelectorAll('.comparison-data-table .best-value, .comparison-data-table .worst-value')
.forEach(cell => cell.classList.remove('best-value', 'worst-value'));
}
}
// Initialize trend chart after all charts are loaded
document.addEventListener('DOMContentLoaded', function() {
setTimeout(updateTrendChart, 500);
});
</script>
{% endblock %}