HH/templates/analytics/dashboard.html
2026-03-09 16:10:24 +03:00

707 lines
26 KiB
HTML

{% extends "layouts/base.html" %}
{% load i18n %}
{% load static %}
{% block title %}{% trans "Analytics Dashboard" %} - PX360{% endblock %}
{% block extra_css %}
<style>
/* PX360 App Theme Variables */
:root {
--hh-navy: #005696;
--hh-blue: #007bbd;
--hh-light: #eef6fb;
--hh-slate: #64748b;
--hh-success: #10b981;
--hh-warning: #f59e0b;
--hh-danger: #ef4444;
}
/* Page Header */
.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%);
}
/* Section Cards */
.section-card {
background: white;
border-radius: 1rem;
border: 2px solid #e2e8f0;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.section-card:hover {
border-color: #005696;
box-shadow: 0 10px 25px -5px rgba(0, 86, 150, 0.15);
}
.section-header {
padding: 1rem 1.5rem;
border-bottom: 2px solid #e2e8f0;
background: linear-gradient(to right, #f8fafc, #f1f5f9);
display: flex;
align-items: center;
gap: 1rem;
}
.section-icon {
width: 48px;
height: 48px;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
}
/* KPI Cards */
.kpi-card {
background: white;
border-radius: 1rem;
border: 2px solid #e2e8f0;
padding: 1.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
height: 100%;
}
.kpi-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
border-color: #005696;
}
.kpi-icon {
width: 48px;
height: 48px;
border-radius: 0.75rem;
display: flex;
align-items: center;
justify-content: center;
}
.kpi-value {
font-size: 1.75rem;
font-weight: 800;
color: #1e293b;
}
.kpi-label {
font-size: 0.8rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.025em;
}
/* Chart Cards */
.chart-card {
background: white;
border-radius: 1rem;
border: 2px solid #e2e8f0;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.chart-card:hover {
border-color: #005696;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.chart-header {
background: linear-gradient(135deg, #f8fafc, #eef6fb);
padding: 1rem 1.25rem;
border-bottom: 2px solid #e2e8f0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.chart-header h3 {
color: #005696;
font-weight: 700;
margin: 0;
}
/* Form Styling - PX360 Theme */
.form-select-px360 {
background: white;
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
padding: 0.625rem 1rem;
color: #1e293b;
font-size: 0.875rem;
min-width: 200px;
cursor: pointer;
transition: all 0.2s;
}
.form-select-px360:hover {
border-color: #005696;
}
.form-select-px360:focus {
outline: none;
border-color: #005696;
box-shadow: 0 0 0 3px rgba(0, 86, 150, 0.1);
}
/* Data Tables */
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.data-table th {
background: #f8fafc;
border-bottom: 2px solid #e2e8f0;
padding: 0.875rem 1rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
color: #64748b;
text-align: left;
}
.data-table td {
border-bottom: 1px solid #e2e8f0;
padding: 0.875rem 1rem;
color: #1e293b;
}
.data-table tr:hover td {
background: #f8fafc;
}
/* Status Badges */
.badge {
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-blue { background: #dbeafe; color: #1d4ed8; }
.badge-green { background: #d1fae5; color: #047857; }
.badge-orange { background: #fef3c7; color: #b45309; }
.badge-red { background: #fee2e2; color: #b91c1c; }
.badge-purple { background: #f3e8ff; color: #7c3aed; }
/* Progress Bars */
.progress-bar {
height: 8px;
border-radius: 4px;
background: #e2e8f0;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeIn 0.4s ease-out forwards;
}
.delay-100 { animation-delay: 100ms; }
.delay-200 { animation-delay: 200ms; }
.delay-300 { animation-delay: 300ms; }
.delay-400 { animation-delay: 400ms; }
</style>
{% endblock %}
{% block content %}
<div class="px-4 py-6 max-w-[1600px] mx-auto">
<!-- Page Header -->
<div class="page-header-gradient animate-in">
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 relative z-10">
<div>
<h1 class="text-2xl font-bold flex items-center gap-3">
<i data-lucide="bar-chart-3" class="w-8 h-8"></i>
{% trans "Analytics Dashboard" %}
</h1>
<p class="mt-1 opacity-90">{% trans "Comprehensive overview of patient experience metrics" %}</p>
</div>
<div class="flex items-center gap-3">
<select class="form-select-px360" onchange="window.location.href='?hospital='+this.value">
<option value="">{% trans "All Hospitals" %}</option>
{% for hospital in hospitals %}
<option value="{{ hospital.id }}" {% if selected_hospital and selected_hospital.id == hospital.id %}selected{% endif %}>
{{ hospital.name_en }}
</option>
{% endfor %}
</select>
<button onclick="refreshDashboard()" class="p-2.5 bg-white/20 hover:bg-white/30 rounded-xl transition" title="{% trans 'Refresh' %}">
<i data-lucide="refresh-cw" class="w-5 h-5 text-white"></i>
</button>
</div>
</div>
</div>
<!-- KPI Grid Row 1 -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
<!-- Total Complaints -->
<div class="kpi-card animate-in">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #fee2e2, #fecaca);">
<i data-lucide="message-square-warning" class="w-5 h-5 text-red-600"></i>
</div>
<span class="badge badge-red">{{ kpis.open_complaints }} {% trans "open" %}</span>
</div>
<p class="kpi-value">{{ kpis.total_complaints }}</p>
<p class="kpi-label mt-1">{% trans "Total Complaints" %}</p>
</div>
<!-- Resolution Rate -->
<div class="kpi-card animate-in delay-100">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #d1fae5, #a7f3d0);">
<i data-lucide="check-circle" class="w-5 h-5 text-emerald-600"></i>
</div>
</div>
<p class="kpi-value">{{ kpis.sla_compliance }}%</p>
<p class="kpi-label mt-1">{% trans "SLA Compliance" %}</p>
</div>
<!-- Avg Resolution -->
<div class="kpi-card animate-in delay-200">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff);">
<i data-lucide="clock" class="w-5 h-5 text-purple-600"></i>
</div>
</div>
<p class="kpi-value">{{ kpis.avg_resolution_hours }}h</p>
<p class="kpi-label mt-1">{% trans "Avg Resolution" %}</p>
</div>
<!-- Total Actions -->
<div class="kpi-card animate-in delay-300">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #fef3c7, #fde68a);">
<i data-lucide="zap" class="w-5 h-5 text-orange-600"></i>
</div>
<span class="badge badge-orange">{{ kpis.open_actions }} {% trans "open" %}</span>
</div>
<p class="kpi-value">{{ kpis.total_actions }}</p>
<p class="kpi-label mt-1">{% trans "Total Actions" %}</p>
</div>
<!-- Survey Score -->
<div class="kpi-card animate-in delay-400">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #dbeafe, #bfdbfe);">
<i data-lucide="star" class="w-5 h-5 text-blue-600"></i>
</div>
</div>
<p class="kpi-value">{{ kpis.avg_survey_score }}</p>
<p class="kpi-label mt-1">{% trans "Avg Survey Score" %}</p>
</div>
<!-- NPS Score -->
<div class="kpi-card animate-in">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #005696, #007bbd);">
<i data-lucide="thumbs-up" class="w-5 h-5 text-white"></i>
</div>
</div>
<p class="kpi-value">{{ kpis.nps_score }}</p>
<p class="kpi-label mt-1">{% trans "NPS Score" %}</p>
</div>
</div>
<!-- Charts Section: Complaints -->
<div class="section-card mb-6 animate-in delay-100">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #fee2e2, #fecaca);">
<i data-lucide="message-square-warning" class="w-6 h-6 text-red-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-navy">{% trans "Complaints Analytics" %}</h2>
<p class="text-sm text-slate">{% trans "Status, sources, and severity breakdown" %}</p>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Status Distribution -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="pie-chart" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Status Distribution" %}</h3>
</div>
<div class="p-4">
<canvas id="complaintStatusChart" class="h-64 w-full"></canvas>
</div>
</div>
<!-- Sources -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="git-branch" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Complaint Sources" %}</h3>
</div>
<div class="p-4">
<canvas id="complaintSourcesChart" class="h-64 w-full"></canvas>
</div>
</div>
<!-- Severity -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="alert-triangle" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Severity Levels" %}</h3>
</div>
<div class="p-4">
<canvas id="severityChart" class="h-64 w-full"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Section: Actions -->
<div class="section-card mb-6 animate-in delay-200">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #fef3c7, #fde68a);">
<i data-lucide="zap" class="w-6 h-6 text-orange-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-navy">{% trans "PX Actions Analytics" %}</h2>
<p class="text-sm text-slate">{% trans "Action status and categories" %}</p>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Action Status -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="layers" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Action Status" %}</h3>
</div>
<div class="p-4">
<canvas id="actionStatusChart" class="h-64 w-full"></canvas>
</div>
</div>
<!-- Action Categories -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="tag" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Action Categories" %}</h3>
</div>
<div class="p-4">
<canvas id="actionCategoriesChart" class="h-64 w-full"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Charts Section: Surveys -->
<div class="section-card mb-6 animate-in delay-300">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff);">
<i data-lucide="clipboard-check" class="w-6 h-6 text-purple-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-navy">{% trans "Survey Analytics" %}</h2>
<p class="text-sm text-slate">{% trans "Patient satisfaction and NPS trends" %}</p>
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- NPS Card -->
<div class="chart-card flex flex-col justify-center items-center">
<div class="chart-header w-full">
<i data-lucide="thumbs-up" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Net Promoter Score" %}</h3>
</div>
<div class="p-6 text-center">
<div class="text-6xl font-bold text-purple-600 mb-2">{{ kpis.nps_score }}</div>
<div class="text-sm text-slate mb-4">{% trans "Industry Avg: +32" %}</div>
<div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden" style="width: 200px;">
{% widthratio kpis.nps_score|add:100 2 1 as nps_width %}
<div class="bg-purple-500 h-full" style="width: {{ nps_width }}%"></div>
</div>
</div>
</div>
<!-- Score Trend -->
<div class="chart-card lg:col-span-2">
<div class="chart-header">
<i data-lucide="trending-up" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Survey Score Trend" %}</h3>
</div>
<div class="p-4">
<canvas id="surveyTrendChart" class="h-64 w-full"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Department Performance Table -->
<div class="section-card animate-in delay-400">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #dbeafe, #bfdbfe);">
<i data-lucide="building-2" class="w-6 h-6 text-blue-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-navy">{% trans "Department Performance" %}</h2>
<p class="text-sm text-slate">{% trans "Performance metrics by department" %}</p>
</div>
</div>
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Department" %}</th>
<th class="text-center">{% trans "Complaints" %}</th>
<th class="text-center">{% trans "Actions" %}</th>
<th class="text-center">{% trans "Survey Avg" %}</th>
<th class="text-center">{% trans "Resolution Rate" %}</th>
<th class="text-center">{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for dept in department_stats %}
<tr>
<td class="font-medium text-navy">{{ dept.name }}</td>
<td class="text-center">{{ dept.complaints }}</td>
<td class="text-center">{{ dept.actions }}</td>
<td class="text-center">{{ dept.survey_avg }}</td>
<td class="text-center">
<div class="flex items-center justify-center gap-2">
<div class="progress-bar" style="width: 60px;">
<div class="progress-fill {% if dept.resolution_rate >= 80 %}bg-emerald-500{% elif dept.resolution_rate >= 60 %}bg-blue-500{% else %}bg-orange-500{% endif %}" style="width: {{ dept.resolution_rate }}%"></div>
</div>
<span class="text-sm">{{ dept.resolution_rate }}%</span>
</div>
</td>
<td class="text-center">
{% if dept.resolution_rate >= 80 %}
<span class="badge badge-green">{% trans "Excellent" %}</span>
{% elif dept.resolution_rate >= 60 %}
<span class="badge badge-blue">{% trans "Good" %}</span>
{% else %}
<span class="badge badge-orange">{% trans "Needs Work" %}</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-8 text-slate">
{% trans "No department data available" %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script>
function refreshDashboard() {
const icon = document.querySelector('[data-lucide="refresh-cw"]');
icon.classList.add('animate-spin');
setTimeout(() => {
window.location.reload();
}, 500);
}
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
// Initialize charts
initCharts();
});
function initCharts() {
// Chart.js defaults
if (typeof Chart === 'undefined') return;
Chart.defaults.color = '#64748b';
Chart.defaults.borderColor = '#e2e8f0';
// 1. Complaint Status Chart
const statusCtx = document.getElementById('complaintStatusChart');
if (statusCtx) {
new Chart(statusCtx, {
type: 'doughnut',
data: {
labels: ['Open', 'In Progress', 'Resolved', 'Closed'],
datasets: [{
data: [{{ kpis.open_complaints|default:0 }}, {{ kpis.in_progress_complaints|default:0 }}, {{ kpis.resolved_complaints|default:0 }}, {{ kpis.closed_complaints|default:0 }}],
backgroundColor: ['#f59e0b', '#3b82f6', '#10b981', '#64748b'],
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 15, usePointStyle: true } }
}
}
});
}
// 2. Complaint Sources Chart
const sourcesCtx = document.getElementById('complaintSourcesChart');
if (sourcesCtx) {
new Chart(sourcesCtx, {
type: 'pie',
data: {
labels: ['Internal', 'External', 'Walk-in', 'Phone', 'Email', 'Web'],
datasets: [{
data: [{{ kpis.internal_complaints|default:0 }}, {{ kpis.external_complaints|default:0 }}, {{ kpis.walkin_complaints|default:0 }}, {{ kpis.phone_complaints|default:0 }}, {{ kpis.email_complaints|default:0 }}, {{ kpis.web_complaints|default:0 }}],
backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'],
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true, boxWidth: 10 } }
}
}
});
}
// 3. Severity Chart
const severityCtx = document.getElementById('severityChart');
if (severityCtx) {
new Chart(severityCtx, {
type: 'bar',
data: {
labels: ['Critical', 'High', 'Medium', 'Low'],
datasets: [{
label: 'Complaints',
data: [{{ kpis.critical_complaints|default:0 }}, {{ kpis.high_complaints|default:0 }}, {{ kpis.medium_complaints|default:0 }}, {{ kpis.low_complaints|default:0 }}],
backgroundColor: ['#ef4444', '#f97316', '#f59e0b', '#10b981'],
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#e2e8f0' } },
x: { grid: { display: false } }
}
}
});
}
// 4. Action Status Chart
const actionStatusCtx = document.getElementById('actionStatusChart');
if (actionStatusCtx) {
new Chart(actionStatusCtx, {
type: 'doughnut',
data: {
labels: ['Open', 'In Progress', 'Pending Approval', 'Closed'],
datasets: [{
data: [{{ kpis.open_actions|default:0 }}, {{ kpis.in_progress_actions|default:0 }}, {{ kpis.pending_actions|default:0 }}, {{ kpis.closed_actions|default:0 }}],
backgroundColor: ['#3b82f6', '#f59e0b', '#8b5cf6', '#10b981'],
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 15, usePointStyle: true } }
}
}
});
}
// 5. Action Categories Chart
const actionCatCtx = document.getElementById('actionCategoriesChart');
if (actionCatCtx) {
new Chart(actionCatCtx, {
type: 'bar',
data: {
labels: ['Training', 'Process', 'Policy', 'Facility', 'Other'],
datasets: [{
label: 'Actions',
data: [{{ kpis.training_actions|default:0 }}, {{ kpis.process_actions|default:0 }}, {{ kpis.policy_actions|default:0 }}, {{ kpis.facility_actions|default:0 }}, {{ kpis.other_actions|default:0 }}],
backgroundColor: '#3b82f6',
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#e2e8f0' } },
x: { grid: { display: false } }
}
}
});
}
// 6. Survey Trend Chart
const surveyCtx = document.getElementById('surveyTrendChart');
if (surveyCtx) {
new Chart(surveyCtx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Survey Score',
data: [{{ kpis.survey_trend_1|default:4.2 }}, {{ kpis.survey_trend_2|default:4.3 }}, {{ kpis.survey_trend_3|default:4.1 }}, {{ kpis.survey_trend_4|default:4.4 }}, {{ kpis.survey_trend_5|default:4.5 }}, {{ kpis.survey_trend_6|default:kpis.avg_survey_score }}],
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: '#8b5cf6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: false, min: 1, max: 5, grid: { color: '#e2e8f0' } },
x: { grid: { display: false } }
}
}
});
}
}
</script>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock %}