HH/templates/analytics/dashboard.html
2026-04-09 13:46:34 +03:00

1297 lines
55 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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">
<button id="refreshAiBtn" onclick="refreshAiAnalytics()" class="px-4 py-2.5 bg-white/20 hover:bg-white/30 rounded-xl transition text-sm font-medium flex items-center gap-2" title="{% trans 'Refresh AI Analytics' %}">
<i data-lucide="sparkles" class="w-4 h-4 text-white"></i>
<span>{% trans "Refresh AI" %}</span>
</button>
<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_ar }}</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>
<!-- ====== AI-POWERED ANALYTICS SECTION ====== -->
<div class="flex items-center gap-3 mb-6 mt-8 animate-in">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center">
<i data-lucide="brain" class="w-5 h-5 text-white"></i>
</div>
<div>
<h2 class="text-xl font-bold text-navy">{% trans "AI-Powered Insights" %}</h2>
<p class="text-sm text-slate">{% trans "Predictive analytics and intelligent recommendations" %}</p>
</div>
</div>
<!-- 1. Executive Summary Card -->
{% if exec_summary %}
<div class="section-card mb-6 animate-in">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff);">
<i data-lucide="file-text" class="w-6 h-6 text-purple-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "AI Executive Summary" %}</h2>
<p class="text-sm text-slate">{% trans "Auto-generated analysis of the past 30 days" %}</p>
</div>
<span class="badge {% if exec_summary.risk_level == 'high' %}badge-red{% elif exec_summary.risk_level == 'medium' %}badge-orange{% else %}badge-green{% endif %}">
{% if exec_summary.risk_level == 'high' %}{% trans "High Risk" %}{% elif exec_summary.risk_level == 'medium' %}{% trans "Medium Risk" %}{% else %}{% trans "Low Risk" %}{% endif %}
</span>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- English Summary -->
<div>
<h3 class="font-semibold text-navy mb-2 flex items-center gap-2">
<i data-lucide="globe" class="w-4 h-4"></i> {% trans "English Summary" %}
</h3>
<p class="text-slate leading-relaxed">{{ exec_summary.summary_en }}</p>
{% if exec_summary.key_findings_en %}
<ul class="mt-3 space-y-1">
{% for finding in exec_summary.key_findings_en %}
<li class="flex items-start gap-2 text-sm text-slate">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0"></i>
<span>{{ finding }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
<!-- Arabic Summary -->
<div dir="rtl">
<h3 class="font-semibold text-navy mb-2 flex items-center gap-2">
<i data-lucide="globe" class="w-4 h-4"></i> {% trans "الملخص العربي" %}
</h3>
<p class="text-slate leading-relaxed font-arabic">{{ exec_summary.summary_ar }}</p>
{% if exec_summary.key_findings_ar %}
<ul class="mt-3 space-y-1">
{% for finding in exec_summary.key_findings_ar %}
<li class="flex items-start gap-2 text-sm text-slate">
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0"></i>
<span>{{ finding }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</div>
{% if exec_summary.recommendations_en %}
<div class="mt-4 pt-4 border-t border-slate-200">
<h4 class="font-semibold text-navy mb-2">{% trans "Recommended Actions" %}</h4>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
{% for rec in exec_summary.recommendations_en %}
<div class="flex items-start gap-2 p-3 bg-slate-50 rounded-lg">
<i data-lucide="arrow-right" class="w-4 h-4 text-purple-500 mt-0.5 flex-shrink-0"></i>
<span class="text-sm text-slate">{{ rec }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- 2. Early Warning System -->
{% if early_warnings %}
<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="alert-triangle" class="w-6 h-6 text-red-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "Early Warning System" %}</h2>
<p class="text-sm text-slate">{% trans "Departments showing risk signals across multiple channels" %}</p>
</div>
<span class="badge badge-red">{{ early_warnings|length }} {% trans "at risk" %}</span>
</div>
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Department" %}</th>
<th class="text-center">{% trans "Risk Score" %}</th>
<th class="text-center">{% trans "Risk Level" %}</th>
<th class="text-center">{% trans "Active Signals" %}</th>
<th class="text-center">{% trans "Complaint Δ" %}</th>
<th class="text-center">{% trans "Survey Δ" %}</th>
<th class="text-center">{% trans "SLA Δ" %}</th>
</tr>
</thead>
<tbody>
{% for dept in early_warnings %}
<tr>
<td class="font-medium text-navy">{{ dept.department_name }}</td>
<td class="text-center">
<div class="flex items-center justify-center gap-2">
<div class="progress-bar" style="width: 80px;">
<div class="progress-fill {% if dept.risk_score >= 70 %}bg-red-500{% elif dept.risk_score >= 50 %}bg-orange-500{% elif dept.risk_score >= 30 %}bg-blue-500{% else %}bg-slate-400{% endif %}" style="width: {{ dept.risk_score }}%"></div>
</div>
<span class="text-sm font-semibold">{{ dept.risk_score }}%</span>
</div>
</td>
<td class="text-center">
<span class="badge {% if dept.risk_level == 'critical' %}badge-red{% elif dept.risk_level == 'high' %}badge-orange{% elif dept.risk_level == 'medium' %}badge-blue{% else %}badge-green{% endif %}">
{{ dept.risk_level|title }}
</span>
</td>
<td class="text-center font-semibold">{{ dept.active_signals }}/5</td>
<td class="text-center {% if dept.complaint_volume_spike.change_pct > 20 %}text-red-600 font-semibold{% elif dept.complaint_volume_spike.change_pct < -10 %}text-green-600{% endif %}">
{% if dept.complaint_volume_spike.change_pct > 0 %}+{% endif %}{{ dept.complaint_volume_spike.change_pct }}%
</td>
<td class="text-center {% if dept.survey_score_decline.change_pct < -10 %}text-red-600 font-semibold{% elif dept.survey_score_decline.change_pct > 5 %}text-green-600{% endif %}">
{{ dept.survey_score_decline.change_pct }}%
</td>
<td class="text-center {% if dept.sla_breach_increase.change_pp > 10 %}text-red-600 font-semibold{% endif %}">
+{{ dept.sla_breach_increase.change_pp }}pp
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- 3. Predictive Complaint Volume -->
{% if complaint_forecast.labels %}
<div class="section-card mb-6 animate-in delay-200">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #dbeafe, #bfdbfe);">
<i data-lucide="trending-up" class="w-6 h-6 text-blue-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "Predicted Complaint Volume" %}</h2>
<p class="text-sm text-slate">{% trans "30-day forecast with confidence bands" %}</p>
</div>
<div class="flex items-center gap-4">
<div class="text-center">
<p class="text-xs text-slate">{% trans "Predicted" %}</p>
<p class="text-lg font-bold text-blue-600">{{ complaint_forecast.total_predicted_30d }}</p>
</div>
<div class="text-center">
<p class="text-xs text-slate">{% trans "vs Recent" %}</p>
<p class="text-lg font-bold {% if complaint_forecast.change_pct > 10 %}text-red-600{% elif complaint_forecast.change_pct < -10 %}text-green-600{% else %}text-slate{% endif %}">
{% if complaint_forecast.change_pct > 0 %}+{% endif %}{{ complaint_forecast.change_pct }}%
</p>
</div>
<span class="badge {% if complaint_forecast.confidence_level == 'high' %}badge-green{% elif complaint_forecast.confidence_level == 'medium' %}badge-orange{% else %}badge-red{% endif %}">
{% if complaint_forecast.confidence_level == 'high' %}{% trans "High Confidence" %}{% elif complaint_forecast.confidence_level == 'medium' %}{% trans "Medium Confidence" %}{% else %}{% trans "Low Confidence" %}{% endif %}
</span>
</div>
</div>
<div class="p-6">
<div class="chart-card">
<div class="p-4">
<canvas id="forecastChart" class="h-72 w-full"></canvas>
</div>
</div>
{% if complaint_forecast.day_of_week_pattern %}
<div class="mt-4 p-4 bg-slate-50 rounded-lg">
<h4 class="font-semibold text-navy mb-2 text-sm">{% trans "Day-of-Week Pattern Detected" %}</h4>
<div class="flex flex-wrap gap-2">
{% for day, pattern in complaint_forecast.day_of_week_pattern.items %}
<span class="px-3 py-1 rounded-full text-xs font-semibold {% if pattern == 'above_average' %}bg-red-100 text-red-700{% elif pattern == 'below_average' %}bg-green-100 text-green-700{% else %}bg-slate-200 text-slate-600{% endif %}">
{{ day }}: {{ pattern|title }}
</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- 4. SLA Breach Predictions -->
{% if sla_breach_predictions %}
<div class="section-card mb-6 animate-in delay-300">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #fef3c7, #fde68a);">
<i data-lucide="clock-alert" class="w-6 h-6 text-orange-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "SLA Breach Risk" %}</h2>
<p class="text-sm text-slate">{% trans "Complaints at risk of breaching SLA deadline" %}</p>
</div>
<span class="badge badge-orange">{{ sla_breach_predictions|length }} {% trans "at risk" %}</span>
</div>
<div class="overflow-x-auto">
<table class="data-table">
<thead>
<tr>
<th>{% trans "Complaint" %}</th>
<th class="text-center">{% trans "Breach Risk" %}</th>
<th class="text-center">{% trans "Severity" %}</th>
<th class="text-center">{% trans "Hours Left" %}</th>
<th class="text-center">{% trans "Department" %}</th>
<th>{% trans "Risk Factors" %}</th>
<th>{% trans "Recommendation" %}</th>
</tr>
</thead>
<tbody>
{% for pred in sla_breach_predictions %}
<tr>
<td class="font-medium text-navy text-sm max-w-xs truncate" title="{{ pred.title }}">{{ pred.title }}</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 pred.breach_probability >= 80 %}bg-red-500{% elif pred.breach_probability >= 60 %}bg-orange-500{% elif pred.breach_probability >= 40 %}bg-blue-500{% else %}bg-slate-400{% endif %}" style="width: {{ pred.breach_probability }}%"></div>
</div>
<span class="text-sm font-semibold">{{ pred.breach_probability }}%</span>
</div>
</td>
<td class="text-center">
<span class="badge {% if pred.severity == 'critical' %}badge-red{% elif pred.severity == 'high' %}badge-orange{% elif pred.severity == 'medium' %}badge-blue{% else %}badge-green{% endif %}">
{{ pred.severity|title }}
</span>
</td>
<td class="text-center {% if pred.hours_remaining < 4 %}text-red-600 font-bold{% elif pred.hours_remaining < 12 %}text-orange-600{% endif %}">
{% if pred.hours_remaining < 0 %}{% trans "EXPIRED" %}{% else %}{{ pred.hours_remaining }}h{% endif %}
</td>
<td class="text-sm">{{ pred.department|default:"—" }}</td>
<td class="text-sm max-w-xs">
<ul class="list-disc list-inside space-y-0.5 text-xs text-slate">
{% for factor in pred.risk_factors %}
<li>{{ factor }}</li>
{% endfor %}
</ul>
</td>
<td class="text-sm font-medium text-navy">{{ pred.recommendation }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- 5. AI Action Recommendations -->
{% if action_recommendations %}
<div class="section-card mb-6 animate-in delay-400">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #d1fae5, #a7f3d0);">
<i data-lucide="lightbulb" class="w-6 h-6 text-emerald-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "AI-Recommended Actions" %}</h2>
<p class="text-sm text-slate">{% trans "Systemic issues identified from complaint pattern analysis" %}</p>
</div>
{% if action_recommendations.0.source == "ai_generated" %}
<span class="badge badge-purple">{% trans "AI Generated" %}</span>
{% else %}
<span class="badge badge-blue">{% trans "Rule-Based" %}</span>
{% endif %}
</div>
<div class="p-6 space-y-4">
{% for rec in action_recommendations %}
<div class="border border-slate-200 rounded-xl p-4 hover:border-blue-300 transition">
<div class="flex items-start justify-between mb-2">
<div class="flex items-center gap-3">
<span class="badge {% if rec.priority == 'critical' %}badge-red{% elif rec.priority == 'high' %}badge-orange{% elif rec.priority == 'medium' %}badge-blue{% else %}badge-green{% endif %}">
{{ rec.priority|title }}
</span>
<span class="text-sm text-slate">{{ rec.category }}</span>
</div>
<span class="text-sm font-semibold text-slate">{{ rec.complaint_count }} {% trans "complaints" %}</span>
</div>
<h4 class="font-semibold text-navy mb-1">{{ rec.problem_summary_en }}</h4>
{% if rec.recommended_actions_en %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 mt-3">
{% for action in rec.recommended_actions_en %}
<div class="flex items-start gap-2 p-2.5 bg-emerald-50 rounded-lg text-sm">
<i data-lucide="zap" class="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0"></i>
<span>{{ action }}</span>
</div>
{% endfor %}
</div>
{% endif %}
{% if rec.expected_impact_en %}
<p class="text-xs text-slate mt-2 flex items-center gap-1">
<i data-lucide="target" class="w-3 h-3"></i>
{{ rec.expected_impact_en }}
</p>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<script>
function refreshDashboard() {
const icon = document.querySelector('[data-lucide="refresh-cw"]');
icon.classList.add('animate-spin');
// Call API to trigger cache refresh
fetch('{% url "analytics:refresh_dashboard_cache" %}', {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json',
},
})
.then(r => r.json())
.then(data => {
showToast(data.message || 'Dashboard cache refresh triggered');
// Reload after a short delay to get fresh data
setTimeout(() => {
window.location.reload();
}, 2000);
})
.catch(err => {
console.error('Failed to trigger dashboard refresh:', err);
showToast('Failed to trigger dashboard refresh');
icon.classList.remove('animate-spin');
// Fallback: just reload the page
setTimeout(() => {
window.location.reload();
}, 500);
});
}
function refreshAiAnalytics() {
const btn = document.getElementById('refreshAiBtn');
const icon = btn.querySelector('[data-lucide="sparkles"]');
icon.classList.add('animate-spin');
btn.disabled = true;
btn.classList.add('opacity-60');
// POST to trigger refresh
fetch('{% url "analytics:refresh_ai_analytics" %}', {
method: 'POST',
headers: {
'X-CSRFToken': '{{ csrf_token }}',
'Content-Type': 'application/json',
},
})
.then(r => r.json())
.then(data => {
// Show toast notification
showToast(data.message || 'AI analytics refresh triggered');
// Poll for completion
setTimeout(() => pollAiReady(), 10000);
})
.catch(err => {
console.error('Failed to trigger AI refresh:', err);
showToast('Failed to trigger AI refresh');
})
.finally(() => {
icon.classList.remove('animate-spin');
btn.disabled = false;
btn.classList.remove('opacity-60');
});
}
function pollAiReady(retries = 6) {
if (retries <= 0) return;
fetch('{% url "analytics:refresh_ai_analytics" %}')
.then(r => r.json())
.then(data => {
if (data.cached && data.cached.executive_summary) {
showToast('AI analytics updated — refresh the page to see new data', 'success');
} else {
setTimeout(() => pollAiReady(retries - 1), 10000);
}
})
.catch(() => {});
}
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-xl shadow-lg text-white text-sm z-50 transition-all ${type === 'success' ? 'bg-emerald-600' : 'bg-blue-600'}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
document.addEventListener('DOMContentLoaded', function() {
lucide.createIcons();
// Initialize charts
initCharts();
});
function initCharts() {
// Check if Chart.js loaded from CDN
if (typeof Chart === 'undefined') {
console.error('Chart.js failed to load from CDN. Charts will not render.');
showChartError();
return;
}
Chart.defaults.color = '#64748b';
Chart.defaults.borderColor = '#e2e8f0';
// Helper: build labels and data from JSON passed by view
function buildChartData(jsonStr) {
try {
return JSON.parse(jsonStr || '[]');
} catch (e) {
console.error('Failed to parse chart data:', e);
return [];
}
}
// 1. Complaint Status Chart (doughnut)
const statusCtx = document.getElementById('complaintStatusChart');
if (statusCtx) {
const statusData = [
{ label: 'Open', value: {{ kpis.open_complaints|default:0 }} },
{ label: 'In Progress', value: {{ kpis.in_progress_complaints|default:0 }} },
{ label: 'Resolved', value: {{ kpis.resolved_complaints|default:0 }} },
{ label: 'Closed', value: {{ kpis.closed_complaints|default:0 }} },
];
// Filter out zero values for cleaner chart
const filtered = statusData.filter(d => d.value > 0);
new Chart(statusCtx, {
type: 'doughnut',
data: {
labels: filtered.map(d => d.label),
datasets: [{
data: filtered.map(d => d.value),
backgroundColor: ['#f59e0b', '#3b82f6', '#10b981', '#64748b'].slice(0, filtered.length),
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 15, usePointStyle: true } }
}
}
});
}
// 2. Complaint Sources Chart (pie) - dynamic from PXSource data
const sourcesCtx = document.getElementById('complaintSourcesChart');
if (sourcesCtx) {
const sourcesData = buildChartData('{{ complaint_sources|escapejs }}');
if (sourcesData.length > 0) {
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'];
new Chart(sourcesCtx, {
type: 'pie',
data: {
labels: sourcesData.map((s, i) => s.source__name_en || 'Unknown'),
datasets: [{
data: sourcesData.map(s => s.count),
backgroundColor: colors.slice(0, sourcesData.length),
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true, boxWidth: 10 } }
}
}
});
} else {
// Fallback to internal/external breakdown
new Chart(sourcesCtx, {
type: 'pie',
data: {
labels: ['Internal', 'External'],
datasets: [{
data: [{{ kpis.internal_complaints|default:0 }}, {{ kpis.external_complaints|default:0 }}],
backgroundColor: ['#3b82f6', '#10b981'],
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true, boxWidth: 10 } }
}
}
});
}
}
// 3. Severity Chart (bar)
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 (doughnut)
const actionStatusCtx = document.getElementById('actionStatusChart');
if (actionStatusCtx) {
const actionStatusData = [
{ label: 'Open', value: {{ kpis.open_actions|default:0 }} },
{ label: 'In Progress', value: {{ kpis.in_progress_actions|default:0 }} },
{ label: 'Pending Approval', value: {{ kpis.pending_actions|default:0 }} },
{ label: 'Closed', value: {{ kpis.closed_actions|default:0 }} },
];
const filteredActions = actionStatusData.filter(d => d.value > 0);
new Chart(actionStatusCtx, {
type: 'doughnut',
data: {
labels: filteredActions.map(d => d.label),
datasets: [{
data: filteredActions.map(d => d.value),
backgroundColor: ['#3b82f6', '#f59e0b', '#8b5cf6', '#10b981'].slice(0, filteredActions.length),
borderColor: '#ffffff',
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom', labels: { padding: 15, usePointStyle: true } }
}
}
});
}
// 5. Action Categories Chart (bar) - dynamic labels
const actionCatCtx = document.getElementById('actionCategoriesChart');
if (actionCatCtx) {
const actionCatsData = buildChartData('{{ action_categories|escapejs }}');
const categoryLabels = {
'clinical_quality': 'Clinical Quality',
'patient_safety': 'Patient Safety',
'service_quality': 'Service Quality',
'staff_behavior': 'Staff Behavior',
'facility': 'Facility',
'process_improvement': 'Process',
'other': 'Other',
'training': 'Training',
'policy': 'Policy',
};
if (actionCatsData.length > 0) {
const catColors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
new Chart(actionCatCtx, {
type: 'bar',
data: {
labels: actionCatsData.map(c => categoryLabels[c.category] || c.category),
datasets: [{
label: 'Actions',
data: actionCatsData.map(c => c.count),
backgroundColor: catColors.slice(0, actionCatsData.length),
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: '#e2e8f0' } },
x: { grid: { display: false } }
}
}
});
} else {
// Fallback to explicit category KPIs
new Chart(actionCatCtx, {
type: 'bar',
data: {
labels: ['Clinical', 'Safety', 'Service', '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 (line) - using actual 6-month data from view
const surveyCtx = document.getElementById('surveyTrendChart');
if (surveyCtx) {
const trendLabels = JSON.parse('{{ survey_trend_labels|safe }}');
const trendValues = [
{{ kpis.survey_trend_1|default:0 }},
{{ kpis.survey_trend_2|default:0 }},
{{ kpis.survey_trend_3|default:0 }},
{{ kpis.survey_trend_4|default:0 }},
{{ kpis.survey_trend_5|default:0 }},
{{ kpis.survey_trend_6|default:0 }}
];
const maxVal = Math.max(...trendValues.filter(v => v > 0));
const minVal = Math.min(...trendValues.filter(v => v > 0));
const padding = ((maxVal - minVal) * 0.2) || 0.5;
new Chart(surveyCtx, {
type: 'line',
data: {
labels: trendLabels.length > 0 ? trendLabels : ['M1', 'M2', 'M3', 'M4', 'M5', 'M6'],
datasets: [{
label: 'Survey Score',
data: trendValues,
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: Math.max(0, minVal - padding),
max: Math.min(5, maxVal + padding),
grid: { color: '#e2e8f0' }
},
x: { grid: { display: false } }
}
}
});
}
// 7. Complaint Volume Forecast Chart (line with confidence bands)
const forecastData = {{ complaint_forecast|default:'{}'|safe }};
if (forecastData.labels && forecastData.labels.length > 0) {
const forecastCtx = document.getElementById('forecastChart');
if (forecastCtx) {
// Format dates for display
const shortLabels = forecastData.labels.map(d => {
const parts = d.split('-');
return parts[1] + '/' + parts[2];
});
new Chart(forecastCtx, {
type: 'line',
data: {
labels: shortLabels,
datasets: [
{
label: 'Upper Bound',
data: forecastData.upper_band,
borderColor: 'transparent',
backgroundColor: 'rgba(59, 130, 246, 0.08)',
fill: false,
pointRadius: 0,
tension: 0.4
},
{
label: 'Lower Bound',
data: forecastData.lower_band,
borderColor: 'transparent',
backgroundColor: 'rgba(59, 130, 246, 0.08)',
fill: '-1',
pointRadius: 0,
tension: 0.4
},
{
label: 'Predicted',
data: forecastData.predicted,
borderColor: '#3b82f6',
backgroundColor: 'transparent',
fill: false,
pointRadius: 3,
pointBackgroundColor: '#3b82f6',
tension: 0.4,
borderWidth: 2
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 15,
usePointStyle: true,
filter: function(item) { return item.text !== 'Upper Bound' && item.text !== 'Lower Bound'; }
}
},
tooltip: {
callbacks: {
afterBody: function(items) {
const idx = items[0].dataIndex;
return 'Range: ' + forecastData.lower_band[idx] + ' ' + forecastData.upper_band[idx];
}
}
}
},
scales: {
y: { beginAtZero: true, grid: { color: '#e2e8f0' } },
x: { grid: { display: false } }
}
}
});
}
}
}
function showChartError() {
const chartContainers = document.querySelectorAll('.chart-card canvas');
chartContainers.forEach(container => {
const parent = container.parentElement;
if (parent) {
container.style.display = 'none';
const errDiv = document.createElement('div');
errDiv.className = 'text-center py-8 text-slate';
errDiv.innerHTML = '<i data-lucide="alert-triangle" class="w-8 h-8 mx-auto mb-2 text-orange-400"></i><p class="text-sm">Chart library unavailable</p>';
parent.appendChild(errDiv);
}
});
lucide.createIcons();
}
</script>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js" onerror="showChartError()"></script>
{% endblock %}