663 lines
30 KiB
HTML
663 lines
30 KiB
HTML
{% extends "layouts/base.html" %}
|
||
{% load i18n %}
|
||
|
||
{% block title %}{% trans "Evaluation Charts" %} - PX360{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
:root {
|
||
--hh-navy: #005696;
|
||
--hh-blue: #007bbd;
|
||
--hh-light: #eef6fb;
|
||
--hh-slate: #64748b;
|
||
--hh-success: #10b981;
|
||
--hh-warning: #f59e0b;
|
||
--hh-danger: #ef4444;
|
||
}
|
||
|
||
.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%);
|
||
}
|
||
|
||
.form-select-px360 {
|
||
width: 100%;
|
||
padding: 0.625rem 1rem;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 0.75rem;
|
||
font-size: 0.875rem;
|
||
transition: all 0.2s ease;
|
||
background: white;
|
||
}
|
||
|
||
.form-select-px360:focus {
|
||
outline: none;
|
||
border-color: var(--hh-navy);
|
||
box-shadow: 0 0 0 3px rgba(0, 86, 150, 0.1);
|
||
}
|
||
|
||
.chart-card {
|
||
background: white;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 1rem;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.chart-card:hover {
|
||
border-color: var(--hh-navy);
|
||
box-shadow: 0 10px 25px -5px rgba(0, 86, 150, 0.15);
|
||
}
|
||
|
||
.chart-card-header {
|
||
padding: 1rem 1.5rem;
|
||
border-bottom: 2px solid #e2e8f0;
|
||
background: linear-gradient(to right, #f8fafc, #f1f5f9);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.chart-card-body {
|
||
padding: 1rem;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.comparison-data-table td.sticky-col {
|
||
min-width: 140px;
|
||
}
|
||
.comparison-data-table th,
|
||
.comparison-data-table td {
|
||
padding: 0.5rem;
|
||
font-size: 0.75rem;
|
||
}
|
||
}
|
||
|
||
@media print {
|
||
.page-header-gradient { box-shadow: none; }
|
||
.chart-card { break-inside: avoid; border: 1px solid #ccc; box-shadow: none; }
|
||
.no-print { display: none !important; }
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="px-6 py-4">
|
||
<!-- Page Header -->
|
||
<div class="page-header-gradient">
|
||
<div class="flex justify-between items-center">
|
||
<div>
|
||
<h1 class="text-2xl font-bold flex items-center gap-3">
|
||
<i data-lucide="bar-chart-2" class="w-8 h-8"></i>
|
||
{% trans "Employee Evaluation – Charts & Comparison" %}
|
||
</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 no-print">
|
||
<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">
|
||
<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="toggleCustomDates()">
|
||
<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>
|
||
<option value="this_month" {% if date_range == 'this_month' %}selected{% endif %}>{% trans "This Month" %}</option>
|
||
<option value="last_month" {% if date_range == 'last_month' %}selected{% endif %}>{% trans "Last Month" %}</option>
|
||
<option value="quarter" {% if date_range == 'quarter' %}selected{% endif %}>{% trans "This Quarter" %}</option>
|
||
<option value="year" {% if date_range == 'year' %}selected{% endif %}>{% trans "This Year" %}</option>
|
||
<option value="custom" {% if date_range == 'custom' %}selected{% endif %}>{% trans "Custom Range" %}</option>
|
||
</select>
|
||
<div id="customDateFields" class="mt-2 space-y-2 {% if date_range != 'custom' %}hidden{% endif %}">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate mb-1">{% trans "From" %}</label>
|
||
<input type="date" id="customStart" class="form-select-px360 w-full"
|
||
value="{% if custom_start %}{{ custom_start|date:'Y-m-d' }}{% endif %}">
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate mb-1">{% trans "To" %}</label>
|
||
<input type="date" id="customEnd" class="form-select-px360 w-full"
|
||
value="{% if custom_end %}{{ custom_end|date:'Y-m-d' }}{% endif %}">
|
||
</div>
|
||
<button onclick="applyCustomDates()" class="w-full px-4 py-2 text-white rounded-xl font-bold transition text-sm" style="background: linear-gradient(135deg, var(--hh-navy), var(--hh-blue));">
|
||
{% trans "Apply" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Status Filter -->
|
||
<div>
|
||
<label class="block text-xs font-bold text-slate uppercase tracking-wider mb-2">{% trans "Status" %}</label>
|
||
<select class="form-select-px360" id="statusFilter" onchange="updateFilters()">
|
||
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>{% trans "Active" %}</option>
|
||
<option value="inactive" {% if status_filter == 'inactive' %}selected{% endif %}>{% trans "Inactive" %}</option>
|
||
<option value="all" {% if status_filter == 'all' %}selected{% endif %}>{% trans "All" %}</option>
|
||
</select>
|
||
</div>
|
||
|
||
{% comment %} <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> {% endcomment %}
|
||
|
||
<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" data-tomselect 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>
|
||
|
||
<div class="mt-4 pt-4 border-t border-gray-200 flex gap-2 no-print">
|
||
{% comment %} <button onclick="window.print()" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-gray-200 rounded-xl text-sm font-semibold text-slate-600 hover:bg-gray-50 transition">
|
||
<i data-lucide="printer" class="w-4 h-4"></i> {% trans "Print" %}
|
||
</button> {% endcomment %}
|
||
<a href="{% url 'dashboard:employee_evaluation' %}" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-gray-200 rounded-xl text-sm font-semibold text-slate-600 hover:bg-gray-50 transition">
|
||
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Evaluation" %}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
{% if evaluation_data.staff_metrics %}
|
||
|
||
<!-- ============================================ -->
|
||
<!-- COMPARISON TABLE (same as employee_evaluation) -->
|
||
<!-- ============================================ -->
|
||
<div class="bg-white border-2 border-gray-200 rounded-xl mb-8 overflow-hidden">
|
||
<div class="p-4 border-b border-gray-200 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>
|
||
|
||
<div 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>
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<!-- ============================================ -->
|
||
<!-- CHARTS SECTION -->
|
||
<!-- ============================================ -->
|
||
<div class="mb-6">
|
||
<div class="flex items-center gap-2 mb-4">
|
||
<i data-lucide="bar-chart-2" class="w-5 h-5 text-navy"></i>
|
||
<h3 class="text-xl font-bold text-navy">{% trans "Visual Comparison" %}</h3>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||
<!-- Chart 1: Response Time Comparison -->
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<i data-lucide="clock" class="w-5 h-5 text-cyan-600"></i>
|
||
<h4 class="font-bold text-gray-800">{% trans "Response Time Comparison" %}</h4>
|
||
</div>
|
||
<div class="chart-card-body">
|
||
<div id="chartResponseTime" style="min-height: 320px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chart 2: Complaint Volume by Source -->
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<i data-lucide="layers" class="w-5 h-5 text-indigo-600"></i>
|
||
<h4 class="font-bold text-gray-800">{% trans "Complaint Volume by Source" %}</h4>
|
||
</div>
|
||
<div class="chart-card-body">
|
||
<div id="chartComplaintSources" style="min-height: 320px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chart 3: Delay vs Activation Rate -->
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<i data-lucide="activity" class="w-5 h-5 text-orange-600"></i>
|
||
<h4 class="font-bold text-gray-800">{% trans "Delay vs Activation Rate" %}</h4>
|
||
</div>
|
||
<div class="chart-card-body">
|
||
<div id="chartDelayActivation" style="min-height: 320px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chart 4: Performance Radar -->
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<i data-lucide="radar" class="w-5 h-5 text-purple-600"></i>
|
||
<h4 class="font-bold text-gray-800">{% trans "Performance Radar" %}</h4>
|
||
</div>
|
||
<div class="chart-card-body">
|
||
<div id="chartPerformanceRadar" style="min-height: 320px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chart 5: Escalations & Inquiries -->
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<i data-lucide="trending-up" class="w-5 h-5 text-red-600"></i>
|
||
<h4 class="font-bold text-gray-800">{% trans "Escalations & Inquiries" %}</h4>
|
||
</div>
|
||
<div class="chart-card-body">
|
||
<div id="chartEscalationsInquiries" style="min-height: 320px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chart 6: Report Completion -->
|
||
<div class="chart-card">
|
||
<div class="chart-card-header">
|
||
<i data-lucide="check-circle" class="w-5 h-5 text-green-600"></i>
|
||
<h4 class="font-bold text-gray-800">{% trans "Report Completion" %}</h4>
|
||
</div>
|
||
<div class="chart-card-body">
|
||
<div id="chartReportCompletion" style="min-height: 320px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% else %}
|
||
<!-- No Data State -->
|
||
<div class="bg-white border-2 border-gray-200 rounded-xl p-12 text-center">
|
||
<div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-6">
|
||
<i data-lucide="bar-chart-2" 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 "Select staff members and a date range to generate comparison charts." %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
{{ evaluation_data|json_script:"evaluationData" }}
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
lucide.createIcons();
|
||
|
||
var dataEl = document.getElementById('evaluationData');
|
||
if (!dataEl) return;
|
||
|
||
var evaluationData = JSON.parse(dataEl.textContent);
|
||
if (!evaluationData || !evaluationData.staff_metrics || evaluationData.staff_metrics.length === 0) return;
|
||
|
||
var staff = evaluationData.staff_metrics;
|
||
var colors = ['#005696', '#007bbd', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
|
||
var names = staff.map(function(s) { return s.name; });
|
||
|
||
// ---- Chart 1: Response Time Comparison (Grouped Bar) ----
|
||
new ApexCharts(document.getElementById('chartResponseTime'), {
|
||
chart: { type: 'bar', height: 320, toolbar: { show: true } },
|
||
series: [
|
||
{ name: '{% trans "24h" %}', data: staff.map(function(s) { return parseFloat(s.complaints_response_time.percentages['24h']) || 0; }) },
|
||
{ name: '{% trans "48h" %}', data: staff.map(function(s) { return parseFloat(s.complaints_response_time.percentages['48h']) || 0; }) },
|
||
{ name: '{% trans "72h" %}', data: staff.map(function(s) { return parseFloat(s.complaints_response_time.percentages['72h']) || 0; }) },
|
||
{ name: '{% trans ">72h" %}', data: staff.map(function(s) { return parseFloat(s.complaints_response_time.percentages.more_than_72h) || 0; }) }
|
||
],
|
||
colors: ['#10b981', '#007bbd', '#f59e0b', '#ef4444'],
|
||
xaxis: { categories: names },
|
||
yaxis: { labels: { formatter: function(v) { return v + '%'; } } },
|
||
plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
|
||
dataLabels: { enabled: false },
|
||
tooltip: { y: { formatter: function(v) { return v + '%'; } } },
|
||
legend: { position: 'top' }
|
||
}).render();
|
||
|
||
// ---- Chart 2: Complaint Volume by Source (Stacked Bar) ----
|
||
new ApexCharts(document.getElementById('chartComplaintSources'), {
|
||
chart: { type: 'bar', height: 320, toolbar: { show: true }, stacked: true },
|
||
series: [
|
||
{ name: 'MOH', data: staff.map(function(s) { return parseInt(s.complaint_sources.counts.MOH) || 0; }) },
|
||
{ name: 'CCHI', data: staff.map(function(s) { return parseInt(s.complaint_sources.counts.CCHI) || 0; }) },
|
||
{ name: '{% trans "Patients" %}', data: staff.map(function(s) { return parseInt(s.complaint_sources.counts.Patients) || 0; }) }
|
||
],
|
||
colors: ['#005696', '#007bbd', '#10b981'],
|
||
xaxis: { categories: names },
|
||
plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
|
||
dataLabels: { enabled: false },
|
||
legend: { position: 'top' }
|
||
}).render();
|
||
|
||
// ---- Chart 3: Delay vs Activation Rate (Grouped Bar) ----
|
||
new ApexCharts(document.getElementById('chartDelayActivation'), {
|
||
chart: { type: 'bar', height: 320, toolbar: { show: true } },
|
||
series: [
|
||
{ name: '{% trans "Delay Rate" %}', data: staff.map(function(s) { return parseFloat(s.delays_activation.percentages.delays) || 0; }) },
|
||
{ name: '{% trans "Activation Rate" %}', data: staff.map(function(s) { return parseFloat(s.delays_activation.percentages.activated) || 0; }) }
|
||
],
|
||
colors: ['#ef4444', '#10b981'],
|
||
xaxis: { categories: names },
|
||
yaxis: { labels: { formatter: function(v) { return v + '%'; } } },
|
||
plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
|
||
dataLabels: { enabled: false },
|
||
tooltip: { y: { formatter: function(v) { return v + '%'; } } },
|
||
legend: { position: 'top' }
|
||
}).render();
|
||
|
||
// ---- Chart 4: Performance Radar ----
|
||
var radarSeries = [];
|
||
for (var i = 0; i < staff.length; i++) {
|
||
var s = staff[i];
|
||
radarSeries.push({
|
||
name: s.name,
|
||
data: [
|
||
parseFloat(s.complaints_response_time.percentages['24h']) || 0,
|
||
parseFloat(s.complaints_response_time.percentages['48h']) || 0,
|
||
parseFloat(s.delays_activation.percentages.activated) || 0,
|
||
parseFloat(s.report_completion.completion_percentage) || 0,
|
||
Math.min(100, Math.max(0, 100 - (parseFloat(s.delays_activation.percentages.delays) || 0))),
|
||
Math.min(100, Math.max(0, 100 - (parseFloat(s.complaints_response_time.percentages.more_than_72h) || 0)))
|
||
]
|
||
});
|
||
}
|
||
new ApexCharts(document.getElementById('chartPerformanceRadar'), {
|
||
chart: { type: 'radar', height: 320, toolbar: { show: true } },
|
||
series: radarSeries,
|
||
colors: colors.slice(0, staff.length),
|
||
xaxis: {
|
||
categories: [
|
||
'{% trans "24h Response" %}',
|
||
'{% trans "48h Response" %}',
|
||
'{% trans "Activation" %}',
|
||
'{% trans "Completion" %}',
|
||
'{% trans "Low Delay" %}',
|
||
'{% trans "Low Overdue" %}'
|
||
]
|
||
},
|
||
yaxis: { max: 100, tickAmount: 5 },
|
||
stroke: { width: 2 },
|
||
fill: { opacity: 0.15 },
|
||
markers: { size: 3 },
|
||
legend: { position: 'top' }
|
||
}).render();
|
||
|
||
// ---- Chart 5: Escalations & Inquiries ----
|
||
new ApexCharts(document.getElementById('chartEscalationsInquiries'), {
|
||
chart: { type: 'bar', height: 320, toolbar: { show: true } },
|
||
series: [
|
||
{ name: '{% trans "Escalated" %}', data: staff.map(function(s) { return parseInt(s.escalated_complaints.total_escalated) || 0; }) },
|
||
{ name: '{% trans "Inquiries" %}', data: staff.map(function(s) { return parseInt(s.inquiries.total) || 0; }) },
|
||
{ name: '{% trans "Notes" %}', data: staff.map(function(s) { return parseInt(s.notes.total) || 0; }) }
|
||
],
|
||
colors: ['#ef4444', '#007bbd', '#8b5cf6'],
|
||
xaxis: { categories: names },
|
||
plotOptions: { bar: { borderRadius: 4, columnWidth: '60%' } },
|
||
dataLabels: { enabled: false },
|
||
legend: { position: 'top' }
|
||
}).render();
|
||
|
||
// ---- Chart 6: Report Completion (Radial Bar) ----
|
||
new ApexCharts(document.getElementById('chartReportCompletion'), {
|
||
chart: { type: 'radialBar', height: 320, toolbar: { show: true } },
|
||
series: staff.map(function(s) { return parseFloat(s.report_completion.completion_percentage) || 0; }),
|
||
labels: names,
|
||
colors: colors.slice(0, staff.length),
|
||
plotOptions: {
|
||
radialBar: {
|
||
hollow: { size: '35%' },
|
||
dataLabels: {
|
||
name: { fontSize: '12px' },
|
||
value: { fontSize: '14px', formatter: function(v) { return v + '%'; } },
|
||
total: {
|
||
show: true,
|
||
label: '{% trans "Average" %}',
|
||
formatter: function() {
|
||
var total = 0;
|
||
staff.forEach(function(s) { total += parseFloat(s.report_completion.completion_percentage) || 0; });
|
||
return (total / staff.length).toFixed(1) + '%';
|
||
}
|
||
}
|
||
}
|
||
}
|
||
},
|
||
legend: { position: 'bottom' }
|
||
}).render();
|
||
});
|
||
|
||
function toggleCustomDates() {
|
||
var dateRange = document.getElementById('dateRange').value;
|
||
var customFields = document.getElementById('customDateFields');
|
||
if (dateRange === 'custom') {
|
||
customFields.classList.remove('hidden');
|
||
} else {
|
||
customFields.classList.add('hidden');
|
||
updateFilters();
|
||
}
|
||
}
|
||
|
||
function applyCustomDates() {
|
||
var startDate = document.getElementById('customStart').value;
|
||
var endDate = document.getElementById('customEnd').value;
|
||
if (!startDate || !endDate) {
|
||
alert('{% trans "Please select both start and end dates." %}');
|
||
return;
|
||
}
|
||
if (startDate > endDate) {
|
||
alert('{% trans "Start date must be before end date." %}');
|
||
return;
|
||
}
|
||
updateFilters();
|
||
}
|
||
|
||
function updateFilters() {
|
||
var dateRange = document.getElementById('dateRange').value;
|
||
var department = document.getElementById('departmentFilter')?.value || '';
|
||
var status = document.getElementById('statusFilter')?.value || 'active';
|
||
var staffSelect = document.getElementById('staffFilter');
|
||
var selectedStaff = Array.from(staffSelect.selectedOptions).map(function(opt) { return opt.value; });
|
||
|
||
var params = new URLSearchParams();
|
||
params.append('date_range', dateRange);
|
||
if (dateRange === 'custom') {
|
||
var customStart = document.getElementById('customStart').value;
|
||
var customEnd = document.getElementById('customEnd').value;
|
||
if (customStart) params.append('custom_start', customStart);
|
||
if (customEnd) params.append('custom_end', customEnd);
|
||
}
|
||
if (department) params.append('department_id', department);
|
||
if (status && status !== 'all') params.append('status', status);
|
||
selectedStaff.forEach(function(id) { params.append('staff_ids', id); });
|
||
|
||
window.location.href = window.location.pathname + '?' + params.toString();
|
||
}
|
||
</script>
|
||
{% endblock %}
|