HH/templates/dashboard/admin_evaluation.html

696 lines
31 KiB
HTML

{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{% trans "Admin Evaluation" %} - PX360{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h2 class="h4">{% trans "Admin Evaluation Dashboard" %}</h2>
<p class="text-muted">{% trans "Staff performance analysis for complaints and inquiries" %}</p>
</div>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<!-- Date Range -->
<div class="col-md-3">
<label class="form-label">{% trans "Date Range" %}</label>
<select class="form-select" 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 class="col-md-3">
<label class="form-label">{% trans "Hospital" %}</label>
<select class="form-select" 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 class="col-md-3">
<label class="form-label">{% trans "Department" %}</label>
<select class="form-select" 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 class="col-md-3">
<label class="form-label">{% trans "Compare Staff" %}</label>
<select class="form-select" 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>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="row mb-4">
<div class="col">
<div class="btn-group">
<a href="{% url 'dashboard:department_benchmarks' %}?date_range={{ date_range }}" class="btn btn-outline-primary">
<i class="bi bi-bar-chart-line me-2"></i>{% trans "Department Benchmarks" %}
</a>
<button class="btn btn-outline-success" onclick="exportReport('csv')">
<i class="bi bi-download me-2"></i>{% trans "Export CSV" %}
</button>
<button class="btn btn-outline-info" onclick="exportReport('json')">
<i class="bi bi-file-code me-2"></i>{% trans "Export JSON" %}
</button>
</div>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4" id="summaryCards">
{% if performance_data.staff_metrics %}
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body">
<h5 class="card-title">{% trans "Total Staff" %}</h5>
<h2 class="mb-0">{{ performance_data.staff_metrics|length }}</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title">{% trans "Total Complaints" %}</h5>
<h2 class="mb-0" id="totalComplaints">0</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-dark">
<div class="card-body">
<h5 class="card-title">{% trans "Total Inquiries" %}</h5>
<h2 class="mb-0" id="totalInquiries">0</h2>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">{% trans "Resolution Rate" %}</h5>
<h2 class="mb-0" id="resolutionRate">0%</h2>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info">
{% trans "No staff members with assigned complaints or inquiries found in the selected time period." %}
</div>
</div>
{% endif %}
</div>
<!-- Tabs -->
{% if performance_data.staff_metrics %}
<ul class="nav nav-tabs mb-4" id="evaluationTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="complaints-tab" data-bs-toggle="tab" data-bs-target="#complaints" type="button" role="tab">
{% trans "Complaints" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="inquiries-tab" data-bs-toggle="tab" data-bs-target="#inquiries" type="button" role="tab">
{% trans "Inquiries" %}
</button>
</li>
</ul>
<div class="tab-content" id="evaluationTabsContent">
<!-- Complaints Tab -->
<div class="tab-pane fade show active" id="complaints" role="tabpanel">
<!-- Charts Row 1 -->
<div class="row g-3 mb-4">
<div class="col-lg-6">
<div class="card table-card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-pie-chart me-2"></i>{% trans "Complaint Source Breakdown" %}</h5>
</div>
<div class="card-body">
<canvas id="complaintSourceChart" style="max-height: 320px;"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card table-card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-bar-chart me-2"></i>{% trans "Complaint Status Distribution" %}</h5>
</div>
<div class="card-body">
<canvas id="complaintStatusChart" style="max-height: 320px;"></canvas>
</div>
</div>
</div>
</div>
<!-- Charts Row 2 -->
<div class="row g-3 mb-4">
<div class="col-lg-6">
<div class="card table-card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-clock-history me-2"></i>{% trans "Complaint Activation Time" %}</h5>
<small class="text-muted">{% trans "Time from creation to assignment" %}</small>
</div>
<div class="card-body">
<canvas id="complaintActivationChart" style="max-height: 320px;"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card table-card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>{% trans "Complaint Response Time" %}</h5>
<small class="text-muted">{% trans "Time to first response/update" %}</small>
</div>
<div class="card-body">
<canvas id="complaintResponseChart" style="max-height: 320px;"></canvas>
</div>
</div>
</div>
</div>
<!-- Staff Comparison Table -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">{% trans "Staff Complaint Performance" %}</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Staff Name" %}</th>
<th>{% trans "Hospital" %}</th>
<th>{% trans "Department" %}</th>
<th class="text-center">{% trans "Total" %}</th>
<th class="text-center">{% trans "Internal" %}</th>
<th class="text-center">{% trans "External" %}</th>
<th class="text-center">{% trans "Open" %}</th>
<th class="text-center">{% trans "Resolved" %}</th>
<th class="text-center">{% trans "Activation ≤2h" %}</th>
<th class="text-center">{% trans "Response ≤24h" %}</th>
</tr>
</thead>
<tbody>
{% for staff in performance_data.staff_metrics %}
<tr>
<td>
<a href="{% url 'dashboard:staff_performance_detail' staff.id %}?date_range={{ date_range }}" class="fw-bold">
{{ staff.name }}
</a>
</td>
<td>{{ staff.hospital|default:"-" }}</td>
<td>{{ staff.department|default:"-" }}</td>
<td class="text-center fw-bold">{{ staff.complaints.total }}</td>
<td class="text-center">{{ staff.complaints.internal }}</td>
<td class="text-center">{{ staff.complaints.external }}</td>
<td class="text-center">
<span class="badge bg-warning">{{ staff.complaints.status.open }}</span>
</td>
<td class="text-center">
<span class="badge bg-success">{{ staff.complaints.status.resolved }}</span>
</td>
<td class="text-center">{{ staff.complaints.activation_time.within_2h }}</td>
<td class="text-center">{{ staff.complaints.response_time.within_24h }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Inquiries Tab -->
<div class="tab-pane fade" id="inquiries" role="tabpanel">
<!-- Charts Row -->
<div class="row g-3 mb-4">
<div class="col-lg-6">
<div class="card table-card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-bar-chart me-2"></i>{% trans "Inquiry Status Distribution" %}</h5>
</div>
<div class="card-body">
<canvas id="inquiryStatusChart" style="max-height: 320px;"></canvas>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card table-card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-speedometer2 me-2"></i>{% trans "Inquiry Response Time" %}</h5>
<small class="text-muted">{% trans "Time to first response/update" %}</small>
</div>
<div class="card-body">
<canvas id="inquiryResponseChart" style="max-height: 320px;"></canvas>
</div>
</div>
</div>
</div>
<!-- Staff Comparison Table -->
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">{% trans "Staff Inquiry Performance" %}</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>{% trans "Staff Name" %}</th>
<th>{% trans "Hospital" %}</th>
<th>{% trans "Department" %}</th>
<th class="text-center">{% trans "Total" %}</th>
<th class="text-center">{% trans "Open" %}</th>
<th class="text-center">{% trans "Resolved" %}</th>
<th class="text-center">{% trans "Response ≤24h" %}</th>
<th class="text-center">{% trans "Response ≤48h" %}</th>
<th class="text-center">{% trans "Response ≤72h" %}</th>
</tr>
</thead>
<tbody>
{% for staff in performance_data.staff_metrics %}
<tr>
<td>{{ staff.name }}</td>
<td>{{ staff.hospital|default:"-" }}</td>
<td>{{ staff.department|default:"-" }}</td>
<td class="text-center fw-bold">{{ staff.inquiries.total }}</td>
<td class="text-center">
<span class="badge bg-warning">{{ staff.inquiries.status.open }}</span>
</td>
<td class="text-center">
<span class="badge bg-success">{{ staff.inquiries.status.resolved }}</span>
</td>
<td class="text-center">{{ staff.inquiries.response_time.within_24h }}</td>
<td class="text-center">{{ staff.inquiries.response_time.within_48h }}</td>
<td class="text-center">{{ staff.inquiries.response_time.within_72h }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</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() {
// 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);
}
// Staff filter with a button to apply
if (staffFilter && staffFilter.parentNode) {
const applyStaffBtn = document.createElement('button');
applyStaffBtn.className = 'btn btn-primary btn-sm mt-2';
applyStaffBtn.textContent = '{% trans "Apply Staff Filter" %}';
applyStaffBtn.onclick = applyFilters;
staffFilter.parentNode.appendChild(applyStaffBtn);
}
// 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 when tab is shown
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 shown
const inquiryTab = document.getElementById('inquiries-tab');
if (inquiryTab) {
inquiryTab.addEventListener('shown.bs.tab', function () {
console.log('Inquiries tab shown, rendering charts...');
// Chart.js handles hidden elements better, no delay needed
renderInquiryCharts();
});
}
}
// Export function
window.exportReport = function(format) {
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 %}