696 lines
31 KiB
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 %} |