316 lines
15 KiB
HTML
316 lines
15 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Complaints Analytics" %} - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="mb-8">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-800 mb-2">{% trans "Complaints Analytics" %}</h1>
|
|
<p class="text-gray-400">{% trans "Comprehensive complaints metrics and insights" %}</p>
|
|
</div>
|
|
<div>
|
|
<form method="get" class="inline-flex">
|
|
<select name="date_range" class="px-4 py-3 border border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition" onchange="this.form.submit()">
|
|
<option value="7" {% if date_range == 7 %}selected{% endif %}>{% trans "Last 7 Days" %}</option>
|
|
<option value="30" {% if date_range == 30 %}selected{% endif %}>{% trans "Last 30 Days" %}</option>
|
|
<option value="90" {% if date_range == 90 %}selected{% endif %}>{% trans "Last 90 Days" %}</option>
|
|
</select>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-blue-500 border border-gray-50 hover:shadow-md transition">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<div class="text-xs font-bold text-blue-600 uppercase mb-1">{% trans "Total Complaints" %}</div>
|
|
<div class="text-3xl font-bold text-gray-800 mb-2">{{ dashboard_summary.status_counts.total }}</div>
|
|
<small class="text-gray-500 flex items-center gap-1">
|
|
{% if dashboard_summary.trend.percentage_change > 0 %}
|
|
<i data-lucide="trending-up" class="w-4 h-4 text-red-500"></i> +{{ dashboard_summary.trend.percentage_change }}%
|
|
{% elif dashboard_summary.trend.percentage_change < 0 %}
|
|
<i data-lucide="trending-down" class="w-4 h-4 text-green-500"></i> {{ dashboard_summary.trend.percentage_change }}%
|
|
{% else %}
|
|
<i data-lucide="minus" class="w-4 h-4 text-gray-400"></i> 0%
|
|
{% endif %}
|
|
{% trans "vs last period" %}
|
|
</small>
|
|
</div>
|
|
<div class="bg-blue-100 p-3 rounded-xl">
|
|
<i data-lucide="activity" class="text-blue-500 w-6 h-6"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-orange-500 border border-gray-50 hover:shadow-md transition">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<div class="text-xs font-bold text-orange-600 uppercase mb-1">{% trans "Open" %}</div>
|
|
<div class="text-3xl font-bold text-gray-800">{{ dashboard_summary.status_counts.open }}</div>
|
|
</div>
|
|
<div class="bg-orange-100 p-3 rounded-xl">
|
|
<i data-lucide="folder-open" class="text-orange-500 w-6 h-6"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-red-500 border border-gray-50 hover:shadow-md transition">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<div class="text-xs font-bold text-red-600 uppercase mb-1">{% trans "Overdue" %}</div>
|
|
<div class="text-3xl font-bold text-red-500">{{ dashboard_summary.status_counts.overdue }}</div>
|
|
</div>
|
|
<div class="bg-red-100 p-3 rounded-xl">
|
|
<i data-lucide="alert-triangle" class="text-red-500 w-6 h-6"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border-l-4 border-green-500 border border-gray-50 hover:shadow-md transition">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<div class="text-xs font-bold text-green-600 uppercase mb-1">{% trans "Resolved" %}</div>
|
|
<div class="text-3xl font-bold text-gray-800">{{ dashboard_summary.status_counts.resolved }}</div>
|
|
</div>
|
|
<div class="bg-green-100 p-3 rounded-xl">
|
|
<i data-lucide="check-circle" class="text-green-500 w-6 h-6"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
<!-- Complaints Trend -->
|
|
<div class="lg:col-span-2 bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="trending-up" class="w-5 h-5"></i> {% trans "Complaints Trend" %}
|
|
</h3>
|
|
<div id="trendChart"></div>
|
|
</div>
|
|
|
|
<!-- Top Categories -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="pie-chart" class="w-5 h-5"></i> {% trans "Top Categories" %}
|
|
</h3>
|
|
<div id="categoryChart"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
<!-- SLA Compliance -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="clock" class="w-5 h-5"></i> {% trans "SLA Compliance" %}
|
|
</h3>
|
|
<div class="text-center mb-6">
|
|
<h2 class="{% if sla_compliance.overall_compliance_rate >= 80 %}text-green-500{% elif sla_compliance.overall_compliance_rate >= 60 %}text-orange-500{% else %}text-red-500{% endif %} text-4xl font-bold mb-2">
|
|
{{ sla_compliance.overall_compliance_rate }}%
|
|
</h2>
|
|
<p class="text-gray-400">{% trans "Overall Compliance Rate" %}</p>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4 text-center">
|
|
<div class="bg-green-50 rounded-xl p-4">
|
|
<h4 class="text-2xl font-bold text-green-600">{{ sla_compliance.on_time }}</h4>
|
|
<small class="text-gray-500">{% trans "On Time" %}</small>
|
|
</div>
|
|
<div class="bg-red-50 rounded-xl p-4">
|
|
<h4 class="text-2xl font-bold text-red-500">{{ sla_compliance.overdue }}</h4>
|
|
<small class="text-gray-500">{% trans "Overdue" %}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Resolution Rate -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="check-square" class="w-5 h-5"></i> {% trans "Resolution Metrics" %}
|
|
</h3>
|
|
<div class="mb-6">
|
|
<div class="flex justify-between mb-2">
|
|
<span class="text-gray-600">{% trans "Resolution Rate" %}</span>
|
|
<strong class="text-gray-800">{{ resolution_rate.resolution_rate }}%</strong>
|
|
</div>
|
|
<div class="h-3 bg-gray-100 rounded-full overflow-hidden">
|
|
<div class="h-full bg-green-500 rounded-full" style="width: {{ resolution_rate.resolution_rate }}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4 text-center">
|
|
<div>
|
|
<h4 class="text-2xl font-bold text-gray-800">{{ resolution_rate.resolved }}</h4>
|
|
<small class="text-gray-500">{% trans "Resolved" %}</small>
|
|
</div>
|
|
<div>
|
|
<h4 class="text-2xl font-bold text-gray-800">{{ resolution_rate.pending }}</h4>
|
|
<small class="text-gray-500">{% trans "Pending" %}</small>
|
|
</div>
|
|
</div>
|
|
{% if resolution_rate.avg_resolution_time_hours %}
|
|
<div class="mt-6 text-center bg-gray-50 rounded-xl p-4">
|
|
<p class="text-gray-600 mb-1">{% trans "Avg Resolution Time" %}</p>
|
|
<h5 class="text-xl font-bold text-gray-800">{{ resolution_rate.avg_resolution_time_hours }} {% trans "hours" %}</h5>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Overdue Complaints -->
|
|
{% if overdue_complaints %}
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-50">
|
|
<div class="p-6 border-b border-gray-100">
|
|
<h3 class="text-lg font-bold text-red-500 flex items-center gap-2">
|
|
<i data-lucide="alert-triangle" class="w-5 h-5"></i> {% trans "Overdue Complaints" %}
|
|
</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "ID" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Source" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Title" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Patient" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Severity" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Due Date" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Assigned To" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-50">
|
|
{% for complaint in overdue_complaints %}
|
|
<tr class="hover:bg-gray-50 transition">
|
|
<td class="px-6 py-4">
|
|
<a href="{% url 'complaints:complaint_detail' complaint.id %}" class="text-navy hover:underline">
|
|
<code class="bg-gray-100 px-2 py-1 rounded text-sm font-semibold text-gray-700">{{ complaint.id|slice:8 }}</code>
|
|
</a>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
{% if complaint.source_name %}
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-blue-100 text-blue-600" title="{% trans 'PX Source' %}: {{ complaint.source_name }}">
|
|
<i data-lucide="cloud-arrow-down" class="w-3 h-3 inline mr-1"></i> {{ complaint.source_name|truncatechars:12 }}
|
|
</span>
|
|
{% elif complaint.complaint_source_type == 'internal' %}
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-indigo-100 text-indigo-600" title="{% trans 'Internal' %}">
|
|
<i data-lucide="building" class="w-3 h-3 inline mr-1"></i> {% trans "Internal" %}
|
|
</span>
|
|
{% else %}
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold bg-gray-100 text-gray-600" title="{% trans 'External' %}">
|
|
<i data-lucide="user" class="w-3 h-3 inline mr-1"></i> {% trans "Patient" %}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="text-gray-700">{{ complaint.title|truncatechars:50 }}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="text-gray-700">{{ complaint.patient_full_name }}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-bold {% if complaint.severity == 'critical' %}bg-red-500 text-white{% elif complaint.severity == 'high' %}bg-orange-100 text-orange-600{% else %}bg-gray-100 text-gray-600{% endif %}">
|
|
{{ complaint.severity }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="text-red-500 font-semibold">{{ complaint.due_at|date:"Y-m-d H:i" }}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="text-gray-700">{{ complaint.assigned_to_full_name|default:"Unassigned" }}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<a href="{% url 'complaints:complaint_detail' complaint.id %}" class="p-2 bg-light text-navy rounded-lg hover:bg-blue-100 transition">
|
|
<i data-lucide="eye" class="w-4 h-4"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<script>
|
|
// Trend Chart - ApexCharts
|
|
var trendOptions = {
|
|
series: [{
|
|
name: '{% trans "Complaints" %}',
|
|
data: {{ trends.data|safe }}
|
|
}],
|
|
chart: {
|
|
type: 'line',
|
|
height: 320,
|
|
toolbar: {
|
|
show: false
|
|
}
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 3
|
|
},
|
|
colors: ['#4bc0c0'],
|
|
xaxis: {
|
|
categories: {{ trends.labels|safe }},
|
|
labels: {
|
|
style: {
|
|
fontSize: '12px'
|
|
}
|
|
}
|
|
},
|
|
yaxis: {
|
|
min: 0,
|
|
forceNiceScale: true,
|
|
labels: {
|
|
style: {
|
|
fontSize: '12px'
|
|
}
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#e7e7e7',
|
|
strokeDashArray: 5
|
|
},
|
|
tooltip: {
|
|
theme: 'light'
|
|
}
|
|
};
|
|
var trendChart = new ApexCharts(document.querySelector("#trendChart"), trendOptions);
|
|
trendChart.render();
|
|
|
|
// Category Chart - ApexCharts
|
|
var categoryOptions = {
|
|
series: [{% for cat in top_categories.categories %}{{ cat.count }}{% if not forloop.last %},{% endif %}{% endfor %}],
|
|
chart: {
|
|
type: 'donut',
|
|
height: 360
|
|
},
|
|
labels: [{% for cat in top_categories.categories %}'{{ cat.category }}'{% if not forloop.last %},{% endif %}{% endfor %}],
|
|
colors: ['#ff6384', '#36a2eb', '#ffce56', '#4bc0c0', '#9966ff', '#ff9f40'],
|
|
legend: {
|
|
position: 'bottom',
|
|
fontSize: '12px'
|
|
},
|
|
dataLabels: {
|
|
enabled: true,
|
|
formatter: function (val) {
|
|
return val.toFixed(1) + "%"
|
|
}
|
|
},
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%'
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
theme: 'light'
|
|
}
|
|
};
|
|
var categoryChart = new ApexCharts(document.querySelector("#categoryChart"), categoryOptions);
|
|
categoryChart.render();
|
|
</script>
|
|
{% endblock %} |