2317 lines
86 KiB
HTML
2317 lines
86 KiB
HTML
{% 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 %} |