774 lines
37 KiB
HTML
774 lines
37 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Admin Evaluation" %} - PX360{% endblock %}
|
|
|
|
{% block page_title %}{% trans "Admin Evaluation Dashboard" %}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
<!-- Page Header -->
|
|
<header class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-navy flex items-center gap-3">
|
|
<i data-lucide="shield-check" class="w-7 h-7"></i>
|
|
{% trans "Admin Evaluation Dashboard" %}
|
|
</h1>
|
|
<p class="text-sm text-slate mt-1">{% trans "Staff performance analysis for complaints and inquiries" %}</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<p class="text-xs text-slate uppercase tracking-wider">{% trans "Last Updated" %}</p>
|
|
<p class="text-sm font-bold text-navy">{% now "j M Y, H:i" %}</p>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Filters -->
|
|
<div class="card">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<!-- Date Range -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{% trans "Date Range" %}</label>
|
|
<select class="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-navy focus:ring-2 focus:ring-navy/20 outline-none transition" id="dateRange">
|
|
<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>
|
|
|
|
<!-- Hospital Filter (PX Admins only) -->
|
|
{% if hospitals.exists %}
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{% trans "Hospital" %}</label>
|
|
<select class="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-navy focus:ring-2 focus:ring-navy/20 outline-none transition" id="hospitalFilter">
|
|
<option value="">{% trans "All Hospitals" %}</option>
|
|
{% for hospital in hospitals %}
|
|
<option value="{{ hospital.id }}" {% if selected_hospital_id == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ hospital.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Department Filter -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{% trans "Department" %}</label>
|
|
<select class="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-navy focus:ring-2 focus:ring-navy/20 outline-none transition disabled:bg-gray-50 disabled:cursor-not-allowed" id="departmentFilter" {% if not selected_hospital_id and not request.user.hospital %}disabled{% endif %}>
|
|
<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 Multi-Select -->
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-2">{% trans "Compare Staff" %}</label>
|
|
<select class="w-full px-4 py-2.5 rounded-xl border border-gray-200 focus:border-navy focus:ring-2 focus:ring-navy/20 outline-none transition h-[42px]" id="staffFilter" multiple>
|
|
{% 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>
|
|
<button id="applyStaffFilter" class="mt-2 w-full px-4 py-2 bg-light0 text-white rounded-xl font-semibold hover:bg-navy transition">
|
|
{% trans "Apply Staff Filter" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex flex-wrap gap-3">
|
|
<a href="{% url 'dashboard:department_benchmarks' %}?date_range={{ date_range }}" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-navy text-navy rounded-lg font-semibold hover:bg-light transition">
|
|
<i data-lucide="bar-chart-2" class="w-4 h-4"></i>
|
|
{% trans "Department Benchmarks" %}
|
|
</a>
|
|
<button onclick="exportReport('csv')" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-green-500 text-green-500 rounded-lg font-semibold hover:bg-green-50 transition">
|
|
<i data-lucide="download" class="w-4 h-4"></i>
|
|
{% trans "Export CSV" %}
|
|
</button>
|
|
<button onclick="exportReport('json')" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-blue-500 text-blue-500 rounded-lg font-semibold hover:bg-blue-50 transition">
|
|
<i data-lucide="file-code" class="w-4 h-4"></i>
|
|
{% trans "Export JSON" %}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div id="summaryCards" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{% if performance_data.staff_metrics %}
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Staff" %}</p>
|
|
<p class="text-3xl font-bold text-navy">{{ performance_data.staff_metrics|length }}</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="building" class="w-4 h-4 text-blue"></i>
|
|
<span class="text-sm text-slate">{% trans "Active Staff" %}</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-blue-50 rounded-xl">
|
|
<i data-lucide="users" class="w-6 h-6 text-blue"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Complaints" %}</p>
|
|
<p class="text-3xl font-bold text-navy" id="totalComplaints">0</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="trending-up" class="w-4 h-4 text-red-500"></i>
|
|
<span class="text-xs text-slate">{% trans "Requires Attention" %}</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-red-50 rounded-xl">
|
|
<i data-lucide="alert-triangle" class="w-6 h-6 text-red-500"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Inquiries" %}</p>
|
|
<p class="text-3xl font-bold text-navy" id="totalInquiries">0</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="message-circle" class="w-4 h-4 text-blue"></i>
|
|
<span class="text-xs text-slate">{% trans "Open Requests" %}</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-blue-50 rounded-xl">
|
|
<i data-lucide="message-circle" class="w-6 h-6 text-blue"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Resolution Rate" %}</p>
|
|
<p class="text-3xl font-bold text-navy" id="resolutionRate">0%</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="trending-up" class="w-4 h-4 text-green-500"></i>
|
|
<span class="text-sm font-bold text-green-500">{% trans "Performance" %}</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-green-50 rounded-xl">
|
|
<i data-lucide="check-circle" class="w-6 h-6 text-green-500"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="col-span-full card">
|
|
<div class="flex flex-col items-center justify-center py-8 text-center">
|
|
<div class="bg-blue-50 w-16 h-16 rounded-full flex items-center justify-center mb-4">
|
|
<i data-lucide="info" class="w-8 h-8 text-blue"></i>
|
|
</div>
|
|
<p class="text-sm text-slate">{% trans "No staff members with assigned complaints or inquiries found in the selected time period." %}</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
{% if performance_data.staff_metrics %}
|
|
<div class="card">
|
|
<div class="border-b border-slate-100">
|
|
<nav class="flex gap-1" id="evaluationTabs">
|
|
<button class="px-6 py-3 rounded-lg font-semibold bg-navy text-white" data-tab="complaints" id="complaints-tab">
|
|
{% trans "Complaints" %}
|
|
</button>
|
|
<button class="px-6 py-3 rounded-lg font-semibold text-slate hover:text-navy hover:bg-light transition" data-tab="inquiries" id="inquiries-tab">
|
|
{% trans "Inquiries" %}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Complaints Tab Content -->
|
|
<div id="complaints-content" class="tab-content">
|
|
<!-- Charts Row 1 -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="pie-chart" class="w-4 h-4"></i>
|
|
{% trans "Complaint Source Breakdown" %}
|
|
</h3>
|
|
</div>
|
|
<div class="h-[320px]">
|
|
<canvas id="complaintSourceChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="bar-chart-2" class="w-4 h-4"></i>
|
|
{% trans "Complaint Status Distribution" %}
|
|
</h3>
|
|
</div>
|
|
<div class="h-[320px]">
|
|
<canvas id="complaintStatusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row 2 -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="clock" class="w-4 h-4"></i>
|
|
{% trans "Complaint Activation Time" %}
|
|
</h3>
|
|
<p class="text-sm text-slate">{% trans "Time from creation to assignment" %}</p>
|
|
</div>
|
|
<div class="h-[320px]">
|
|
<canvas id="complaintActivationChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="gauge" class="w-4 h-4"></i>
|
|
{% trans "Complaint Response Time" %}
|
|
</h3>
|
|
<p class="text-sm text-slate">{% trans "Time to first response/update" %}</p>
|
|
</div>
|
|
<div class="h-[320px]">
|
|
<canvas id="complaintResponseChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Staff Comparison Table -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="users" class="w-4 h-4"></i>
|
|
{% trans "Staff Complaint Performance" %}
|
|
</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Staff Name" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Hospital" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Department" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Total" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Internal" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "External" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Open" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Resolved" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Activation ≤2h" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤24h" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for staff in performance_data.staff_metrics %}
|
|
<tr class="hover:bg-light transition">
|
|
<td class="px-6 py-4">
|
|
<a href="{% url 'dashboard:staff_performance_detail' staff.id %}?date_range={{ date_range }}" class="font-bold text-navy hover:text-blue transition group">
|
|
{{ staff.name }}
|
|
</a>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-slate">{{ staff.hospital|default:"-" }}</td>
|
|
<td class="px-6 py-4 text-sm text-slate">{{ staff.department|default:"-" }}</td>
|
|
<td class="px-6 py-4 text-center font-bold text-navy">{{ staff.complaints.total }}</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.complaints.internal }}</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.complaints.external }}</td>
|
|
<td class="px-6 py-4 text-center">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-600">{{ staff.complaints.status.open }}</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-center">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-600">{{ staff.complaints.status.resolved }}</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.complaints.activation_time.within_2h }}</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.complaints.response_time.within_24h }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inquiries Tab Content -->
|
|
<div id="inquiries-content" class="tab-content hidden">
|
|
<!-- Charts Row -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="bar-chart-2" class="w-4 h-4"></i>
|
|
{% trans "Inquiry Status Distribution" %}
|
|
</h3>
|
|
</div>
|
|
<div class="h-[320px]">
|
|
<canvas id="inquiryStatusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="gauge" class="w-4 h-4"></i>
|
|
{% trans "Inquiry Response Time" %}
|
|
</h3>
|
|
<p class="text-sm text-slate">{% trans "Time to first response/update" %}</p>
|
|
</div>
|
|
<div class="h-[320px]">
|
|
<canvas id="inquiryResponseChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Staff Comparison Table -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="users" class="w-4 h-4"></i>
|
|
{% trans "Staff Inquiry Performance" %}
|
|
</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Staff Name" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Hospital" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Department" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Total" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Open" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Resolved" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤24h" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤48h" %}</th>
|
|
<th class="px-6 py-4 text-center text-xs font-bold text-slate uppercase tracking-wider">{% trans "Response ≤72h" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for staff in performance_data.staff_metrics %}
|
|
<tr class="hover:bg-light transition">
|
|
<td class="px-6 py-4 font-bold text-navy">{{ staff.name }}</td>
|
|
<td class="px-6 py-4 text-sm text-slate">{{ staff.hospital|default:"-" }}</td>
|
|
<td class="px-6 py-4 text-sm text-slate">{{ staff.department|default:"-" }}</td>
|
|
<td class="px-6 py-4 text-center font-bold text-navy">{{ staff.inquiries.total }}</td>
|
|
<td class="px-6 py-4 text-center">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-600">{{ staff.inquiries.status.open }}</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-center">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-600">{{ staff.inquiries.status.resolved }}</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_24h }}</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_48h }}</td>
|
|
<td class="px-6 py-4 text-center">{{ staff.inquiries.response_time.within_72h }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
{{ performance_data|json_script:"performanceData" }}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize Lucide icons
|
|
lucide.createIcons();
|
|
|
|
// Tab switching
|
|
const tabs = document.querySelectorAll('#evaluationTabs button[data-tab]');
|
|
const contents = document.querySelectorAll('.tab-content');
|
|
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', function() {
|
|
const tabName = this.dataset.tab;
|
|
|
|
// Update tab buttons
|
|
tabs.forEach(t => {
|
|
t.classList.remove('bg-light0', 'text-white');
|
|
t.classList.add('text-gray-500', 'hover:bg-gray-100');
|
|
});
|
|
this.classList.remove('text-gray-500', 'hover:bg-gray-100');
|
|
this.classList.add('bg-light0', 'text-white');
|
|
|
|
// Show/hide content
|
|
contents.forEach(content => {
|
|
if (content.id === `${tabName}-content`) {
|
|
content.classList.remove('hidden');
|
|
} else {
|
|
content.classList.add('hidden');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Helper function to get safe numeric value
|
|
function getSafeNumber(obj, path, defaultValue = 0) {
|
|
if (!obj) return defaultValue;
|
|
const parts = path.split('.');
|
|
let value = obj;
|
|
for (const part of parts) {
|
|
if (value === null || value === undefined) return defaultValue;
|
|
value = value[part];
|
|
}
|
|
if (typeof value !== 'number' || isNaN(value) || !isFinite(value)) {
|
|
return defaultValue;
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// Calculate and display summary card statistics
|
|
const performanceDataEl = document.getElementById('performanceData');
|
|
if (performanceDataEl) {
|
|
const performanceData = JSON.parse(performanceDataEl.textContent);
|
|
const staffMetrics = performanceData.staff_metrics || [];
|
|
|
|
let totalComplaints = 0;
|
|
let resolvedComplaints = 0;
|
|
let totalInquiries = 0;
|
|
|
|
staffMetrics.forEach(staff => {
|
|
const complaints = staff.complaints || {};
|
|
const inquiries = staff.inquiries || {};
|
|
|
|
totalComplaints += getSafeNumber(complaints, 'total', 0);
|
|
resolvedComplaints += getSafeNumber(complaints.status, 'resolved', 0);
|
|
totalInquiries += getSafeNumber(inquiries, 'total', 0);
|
|
});
|
|
|
|
const resolutionRate = totalComplaints > 0 ? Math.round((resolvedComplaints / totalComplaints) * 100) : 0;
|
|
|
|
const totalComplaintsEl = document.getElementById('totalComplaints');
|
|
const totalInquiriesEl = document.getElementById('totalInquiries');
|
|
const resolutionRateEl = document.getElementById('resolutionRate');
|
|
|
|
if (totalComplaintsEl) totalComplaintsEl.textContent = totalComplaints;
|
|
if (totalInquiriesEl) totalInquiriesEl.textContent = totalInquiries;
|
|
if (resolutionRateEl) resolutionRateEl.textContent = resolutionRate + '%';
|
|
}
|
|
|
|
// Filter change handlers
|
|
const dateRange = document.getElementById('dateRange');
|
|
const hospitalFilter = document.getElementById('hospitalFilter');
|
|
const departmentFilter = document.getElementById('departmentFilter');
|
|
const staffFilter = document.getElementById('staffFilter');
|
|
|
|
function applyFilters() {
|
|
const params = new URLSearchParams();
|
|
params.set('date_range', dateRange.value);
|
|
if (hospitalFilter && hospitalFilter.value) {
|
|
params.set('hospital_id', hospitalFilter.value);
|
|
}
|
|
if (departmentFilter && departmentFilter.value) {
|
|
params.set('department_id', departmentFilter.value);
|
|
}
|
|
|
|
if (staffFilter) {
|
|
const selectedStaff = Array.from(staffFilter.selectedOptions).map(opt => opt.value);
|
|
selectedStaff.forEach(id => params.append('staff_ids', id));
|
|
}
|
|
|
|
window.location.href = '?' + params.toString();
|
|
}
|
|
|
|
if (dateRange) {
|
|
dateRange.addEventListener('change', applyFilters);
|
|
}
|
|
if (hospitalFilter) {
|
|
hospitalFilter.addEventListener('change', applyFilters);
|
|
}
|
|
if (departmentFilter) {
|
|
departmentFilter.addEventListener('change', applyFilters);
|
|
}
|
|
|
|
const applyStaffBtn = document.getElementById('applyStaffFilter');
|
|
if (applyStaffBtn) {
|
|
applyStaffBtn.addEventListener('click', applyFilters);
|
|
}
|
|
|
|
// Initialize Charts with Chart.js
|
|
const charts = {};
|
|
|
|
// Helper function to create or update chart
|
|
function createOrUpdateChart(canvasId, config) {
|
|
const canvas = document.getElementById(canvasId);
|
|
if (!canvas) {
|
|
console.warn('Canvas element not found:', canvasId);
|
|
return;
|
|
}
|
|
|
|
// Check if chart already exists
|
|
if (charts[canvasId]) {
|
|
charts[canvasId].destroy();
|
|
delete charts[canvasId];
|
|
}
|
|
|
|
try {
|
|
const ctx = canvas.getContext('2d');
|
|
const chart = new Chart(ctx, config);
|
|
charts[canvasId] = chart;
|
|
console.log('Chart created successfully:', canvasId);
|
|
} catch (error) {
|
|
console.error('Error creating chart:', canvasId, error);
|
|
}
|
|
}
|
|
|
|
if (performanceDataEl) {
|
|
const performanceData = JSON.parse(performanceDataEl.textContent);
|
|
const staffMetrics = performanceData.staff_metrics || [];
|
|
|
|
// Aggregate complaint data for charts
|
|
let internalTotal = 0, externalTotal = 0;
|
|
let statusOpen = 0, statusInProgress = 0, statusResolved = 0, statusClosed = 0;
|
|
let activationWithin2h = 0, activationMoreThan2h = 0;
|
|
let responseWithin24h = 0, responseWithin48h = 0, responseWithin72h = 0, responseMoreThan72h = 0;
|
|
|
|
staffMetrics.forEach(staff => {
|
|
const c = staff.complaints || {};
|
|
internalTotal += getSafeNumber(c, 'internal', 0);
|
|
externalTotal += getSafeNumber(c, 'external', 0);
|
|
|
|
statusOpen += getSafeNumber(c.status, 'open', 0);
|
|
statusInProgress += getSafeNumber(c.status, 'in_progress', 0);
|
|
statusResolved += getSafeNumber(c.status, 'resolved', 0);
|
|
statusClosed += getSafeNumber(c.status, 'closed', 0);
|
|
|
|
activationWithin2h += getSafeNumber(c.activation_time, 'within_2h', 0);
|
|
activationMoreThan2h += getSafeNumber(c.activation_time, 'more_than_2h', 0);
|
|
|
|
responseWithin24h += getSafeNumber(c.response_time, 'within_24h', 0);
|
|
responseWithin48h += getSafeNumber(c.response_time, 'within_48h', 0);
|
|
responseWithin72h += getSafeNumber(c.response_time, 'within_72h', 0);
|
|
responseMoreThan72h += getSafeNumber(c.response_time, 'more_than_72h', 0);
|
|
});
|
|
|
|
// Complaint Source Chart (Pie)
|
|
if (internalTotal > 0 || externalTotal > 0) {
|
|
createOrUpdateChart('complaintSourceChart', {
|
|
type: 'pie',
|
|
data: {
|
|
labels: ['{% trans "Internal" %}', '{% trans "External" %}'],
|
|
datasets: [{
|
|
data: [internalTotal, externalTotal],
|
|
backgroundColor: ['#6366f1', '#f59e0b']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Complaint Status Chart (Bar)
|
|
if (statusOpen + statusInProgress + statusResolved + statusClosed > 0) {
|
|
createOrUpdateChart('complaintStatusChart', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['{% trans "Open" %}', '{% trans "In Progress" %}', '{% trans "Resolved" %}', '{% trans "Closed" %}'],
|
|
datasets: [{
|
|
data: [statusOpen, statusInProgress, statusResolved, statusClosed],
|
|
backgroundColor: ['#f59e0b', '#6366f1', '#10b981', '#6b7280']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Complaint Activation Chart (Bar)
|
|
if (activationWithin2h + activationMoreThan2h > 0) {
|
|
createOrUpdateChart('complaintActivationChart', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['≤ 2 hours', '> 2 hours'],
|
|
datasets: [{
|
|
data: [activationWithin2h, activationMoreThan2h],
|
|
backgroundColor: ['#10b981', '#ef4444']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Complaint Response Chart (Bar)
|
|
if (responseWithin24h + responseWithin48h + responseWithin72h + responseMoreThan72h > 0) {
|
|
createOrUpdateChart('complaintResponseChart', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['≤ 24h', '24-48h', '48-72h', '> 72h'],
|
|
datasets: [{
|
|
data: [responseWithin24h, responseWithin48h, responseWithin72h, responseMoreThan72h],
|
|
backgroundColor: ['#10b981', '#6366f1', '#f59e0b', '#ef4444']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Aggregate inquiry data
|
|
let inquiryStatusOpen = 0, inquiryStatusInProgress = 0, inquiryStatusResolved = 0, inquiryStatusClosed = 0;
|
|
let inquiryResponseWithin24h = 0, inquiryResponseWithin48h = 0, inquiryResponseWithin72h = 0, inquiryResponseMoreThan72h = 0;
|
|
|
|
staffMetrics.forEach(staff => {
|
|
const i = staff.inquiries || {};
|
|
inquiryStatusOpen += getSafeNumber(i.status, 'open', 0);
|
|
inquiryStatusInProgress += getSafeNumber(i.status, 'in_progress', 0);
|
|
inquiryStatusResolved += getSafeNumber(i.status, 'resolved', 0);
|
|
inquiryStatusClosed += getSafeNumber(i.status, 'closed', 0);
|
|
|
|
inquiryResponseWithin24h += getSafeNumber(i.response_time, 'within_24h', 0);
|
|
inquiryResponseWithin48h += getSafeNumber(i.response_time, 'within_48h', 0);
|
|
inquiryResponseWithin72h += getSafeNumber(i.response_time, 'within_72h', 0);
|
|
inquiryResponseMoreThan72h += getSafeNumber(i.response_time, 'more_than_72h', 0);
|
|
});
|
|
|
|
// Function to render inquiry charts
|
|
function renderInquiryCharts() {
|
|
console.log('Rendering inquiry charts...');
|
|
|
|
// Inquiry Status Chart (Bar)
|
|
if (inquiryStatusOpen + inquiryStatusInProgress + inquiryStatusResolved + inquiryStatusClosed > 0) {
|
|
createOrUpdateChart('inquiryStatusChart', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['{% trans "Open" %}', '{% trans "In Progress" %}', '{% trans "Resolved" %}', '{% trans "Closed" %}'],
|
|
datasets: [{
|
|
data: [inquiryStatusOpen, inquiryStatusInProgress, inquiryStatusResolved, inquiryStatusClosed],
|
|
backgroundColor: ['#f59e0b', '#6366f1', '#10b981', '#6b7280']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Inquiry Response Chart (Bar)
|
|
if (inquiryResponseWithin24h + inquiryResponseWithin48h + inquiryResponseWithin72h + inquiryResponseMoreThan72h > 0) {
|
|
createOrUpdateChart('inquiryResponseChart', {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['≤ 24h', '24-48h', '48-72h', '> 72h'],
|
|
datasets: [{
|
|
data: [inquiryResponseWithin24h, inquiryResponseWithin48h, inquiryResponseWithin72h, inquiryResponseMoreThan72h],
|
|
backgroundColor: ['#10b981', '#6366f1', '#f59e0b', '#ef4444']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Render inquiry charts when tab is clicked
|
|
const inquiryTab = document.getElementById('inquiries-tab');
|
|
if (inquiryTab) {
|
|
inquiryTab.addEventListener('click', function () {
|
|
setTimeout(renderInquiryCharts, 100);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Export function
|
|
window.exportReport = function(format) {
|
|
const performanceDataEl = document.getElementById('performanceData');
|
|
if (!performanceDataEl) return;
|
|
|
|
const performanceData = JSON.parse(performanceDataEl.textContent);
|
|
const staffIds = performanceData.staff_metrics.map(s => s.id);
|
|
|
|
fetch('{% url "dashboard:export_staff_performance" %}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
|
|
},
|
|
body: JSON.stringify({
|
|
staff_ids: staffIds,
|
|
date_range: dateRange.value,
|
|
format: format
|
|
})
|
|
})
|
|
.then(response => {
|
|
if (format === 'json') {
|
|
return response.json();
|
|
} else {
|
|
return response.blob();
|
|
}
|
|
})
|
|
.then(data => {
|
|
if (format === 'json') {
|
|
console.log('Export data:', data);
|
|
alert('Export ready! Check console for data.');
|
|
} else {
|
|
// Download file
|
|
const url = window.URL.createObjectURL(data);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `staff_performance_${new Date().toISOString().slice(0,10)}.${format}`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
document.body.removeChild(a);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Export error:', error);
|
|
alert('Export failed. Please try again.');
|
|
});
|
|
};
|
|
});
|
|
</script>
|
|
{% endblock %} |