HH/templates/analytics/dashboard.html
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

1880 lines
82 KiB
HTML
Raw Permalink 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-8 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>
<!-- SLA Compliance -->
<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>
<!-- Reopened -->
<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, #ffedd5, #fed7aa);">
<i data-lucide="rotate-ccw" class="w-5 h-5 text-orange-500"></i>
</div>
</div>
<p class="kpi-value">{{ kpis.reopened_complaints }}</p>
<p class="kpi-label mt-1">{% trans "Reopened" %}</p>
</div>
<!-- Escalated OVR -->
<div class="kpi-card animate-in delay-500">
<div class="flex items-start justify-between mb-3">
<div class="kpi-icon" style="background: linear-gradient(135deg, #f3e8ff, #e9d5ff);">
<i data-lucide="shield-alert" class="w-5 h-5 text-purple-500"></i>
</div>
</div>
<p class="kpi-value">{{ kpis.escalated_ovr_complaints }}</p>
<p class="kpi-label mt-1">{% trans "Escalated OVR" %}</p>
</div>
<!-- Survey Score -->
<div class="kpi-card animate-in delay-600">
<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 delay-700">
<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>
<!-- Cross-Module Yearly Trends - Area Charts -->
<div class="section-card mb-6 animate-in delay-150">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #dbeafe, #bfdbfe);">
<i data-lucide="area-chart" class="w-6 h-6 text-blue-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "Cross-Module Yearly Trends" %}</h2>
<p class="text-sm text-slate">{% trans "Quarterly volume trends across all modules" %}</p>
</div>
<div class="flex items-center gap-3">
<label for="fromYear" class="text-xs font-semibold text-slate">{% trans "From" %}</label>
<input type="number" id="fromYear" name="from_year" value="{{ chart_from_year|default:'' }}"
min="2020" max="2100" step="1"
class="px-3 py-1.5 border-2 border-gray-200 rounded-xl text-sm w-24 focus:border-navy focus:ring-0"
onchange="updateYearQuarterChart()">
<label for="toYear" class="text-xs font-semibold text-slate">{% trans "To" %}</label>
<input type="number" id="toYear" name="to_year" value="{{ chart_to_year|default:'' }}"
min="2020" max="2100" step="1"
class="px-3 py-1.5 border-2 border-gray-200 rounded-xl text-sm w-24 focus:border-navy focus:ring-0"
onchange="updateYearQuarterChart()">
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Complaints -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="message-square-warning" class="w-5 h-5 text-red-600"></i>
<h3>{% trans "Complaints" %}</h3>
</div>
<div class="p-4">
<canvas id="complaintsByQuarterChart" style="height: 280px;"></canvas>
</div>
</div>
<!-- Inquiries -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="help-circle" class="w-5 h-5 text-amber-600"></i>
<h3>{% trans "Inquiries" %}</h3>
</div>
<div class="p-4">
<canvas id="inquiriesByQuarterChart" style="height: 280px;"></canvas>
</div>
</div>
<!-- Suggestions -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="lightbulb" class="w-5 h-5 text-purple-600"></i>
<h3>{% trans "Suggestions" %}</h3>
</div>
<div class="p-4">
<canvas id="suggestionsByQuarterChart" style="height: 280px;"></canvas>
</div>
</div>
<!-- Observations -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="eye" class="w-5 h-5 text-green-600"></i>
<h3>{% trans "Observations" %}</h3>
</div>
<div class="p-4">
<canvas id="observationsByQuarterChart" style="height: 280px;"></canvas>
</div>
</div>
<!-- Appreciations -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="heart" class="w-5 h-5 text-pink-600"></i>
<h3>{% trans "Appreciations" %}</h3>
</div>
<div class="p-4">
<canvas id="appreciationsByQuarterChart" style="height: 280px;"></canvas>
</div>
</div>
<!-- Patient Visits -->
<div class="chart-card">
<div class="chart-header">
<i data-lucide="users" class="w-5 h-5 text-teal-600"></i>
<h3>{% trans "Patient Visits" %}</h3>
</div>
<div class="p-4">
<canvas id="visitsByQuarterChart" style="height: 280px;"></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>
<!-- Visit Analytics -->
<div class="section-card animate-in delay-350">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #d1fae5, #a7f3d0);">
<i data-lucide="route" class="w-6 h-6 text-emerald-600"></i>
</div>
<div>
<h2 class="text-xl font-bold text-navy">{% trans "Visit Analytics" %}</h2>
<p class="text-sm text-slate">{% trans "Patient visit metrics by type" %}</p>
</div>
</div>
<!-- KPI Cards -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 p-6">
<div class="bg-slate-50 rounded-xl p-4 border border-slate-200">
<p class="text-xs font-bold text-slate uppercase tracking-wider">{% trans "Total Visits" %}</p>
<p class="text-2xl font-bold text-navy mt-1">{{ total_visits }}</p>
<p class="text-[10px] text-slate mt-1">{% trans "All visit types" %}</p>
</div>
<div class="bg-amber-50 rounded-xl p-4 border border-amber-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-bold text-amber-600 uppercase tracking-wider">{% trans "Emergency (ED)" %}</p>
<p class="text-2xl font-bold text-amber-700 mt-1">{{ visit_type_data.ED.count }}</p>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full {% if visit_type_data.ED.completion_rate >= 90 %}bg-green-100 text-green-700{% else %}bg-amber-100 text-amber-700{% endif %}">{{ visit_type_data.ED.completion_rate }}%</span>
</div>
<p class="text-[10px] text-amber-500 mt-1">{% trans "Avg Duration" %}: {{ visit_type_data.ED.avg_duration }}</p>
</div>
<div class="bg-blue-50 rounded-xl p-4 border border-blue-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-bold text-blue-600 uppercase tracking-wider">{% trans "Inpatient (IP)" %}</p>
<p class="text-2xl font-bold text-blue-700 mt-1">{{ visit_type_data.IP.count }}</p>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full {% if visit_type_data.IP.completion_rate >= 90 %}bg-green-100 text-green-700{% else %}bg-amber-100 text-amber-700{% endif %}">{{ visit_type_data.IP.completion_rate }}%</span>
</div>
<p class="text-[10px] text-blue-500 mt-1">{% trans "Avg Duration" %}: {{ visit_type_data.IP.avg_duration }}</p>
</div>
<div class="bg-green-50 rounded-xl p-4 border border-green-200">
<div class="flex items-center justify-between">
<div>
<p class="text-xs font-bold text-green-600 uppercase tracking-wider">{% trans "Outpatient (OP)" %}</p>
<p class="text-2xl font-bold text-green-700 mt-1">{{ visit_type_data.OP.count }}</p>
</div>
<span class="text-xs font-bold px-2 py-1 rounded-full {% if visit_type_data.OP.completion_rate >= 90 %}bg-green-100 text-green-700{% else %}bg-amber-100 text-amber-700{% endif %}">{{ visit_type_data.OP.completion_rate }}%</span>
</div>
<p class="text-[10px] text-green-500 mt-1">{% trans "Avg Duration" %}: {{ visit_type_data.OP.avg_duration }}</p>
</div>
</div>
<!-- Charts Row -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 px-6 pb-6">
<div class="chart-card">
<div class="chart-header">
<i data-lucide="bar-chart-3" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Visit Volume by Type" %}</h3>
</div>
<div class="p-4">
<div class="relative" style="height: 280px;">
<canvas id="visitVolumeChart"></canvas>
</div>
</div>
</div>
<div class="chart-card">
<div class="chart-header">
<i data-lucide="trending-up" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Monthly Visit Trend" %}</h3>
</div>
<div class="p-4">
<div class="relative" style="height: 280px;">
<canvas id="visitTrendChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Stage Duration Breakdown -->
<div class="px-6 pb-6">
<div class="chart-card">
<div class="chart-header">
<i data-lucide="layers" class="w-5 h-5 text-navy"></i>
<h3>{% trans "Avg Time per Journey Stage" %}</h3>
</div>
<div class="p-4">
<div class="relative" style="height: 320px;">
<canvas id="visitStageChart"></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 %}
<!-- Visit Efficiency Insights -->
{% if visit_efficiency %}
<div class="section-card mb-6 animate-in">
<div class="section-header">
<div class="section-icon" style="background: linear-gradient(135deg, #d1fae5, #a7f3d0);">
<i data-lucide="route" class="w-6 h-6 text-emerald-600"></i>
</div>
<div class="flex-1">
<h2 class="text-xl font-bold text-navy">{% trans "Visit Efficiency Insights" %}</h2>
<p class="text-sm text-slate">{% trans "AI-powered patient flow analysis and bottleneck detection" %}</p>
</div>
{% if visit_efficiency.efficiency_score %}
<span class="badge {% if visit_efficiency.efficiency_score >= 80 %}badge-green{% elif visit_efficiency.efficiency_score >= 60 %}badge-orange{% else %}badge-red{% endif %}">
{% trans "Efficiency" %}: {{ visit_efficiency.efficiency_score }}/100
</span>
{% endif %}
{% if visit_efficiency.priority_type %}
<span class="badge badge-purple ml-2">
{% trans "Priority" %}: {{ visit_efficiency.priority_type }}
</span>
{% endif %}
</div>
<div class="p-6">
{% if visit_efficiency.summary_en %}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-4">
<div>
<h3 class="font-semibold text-navy mb-2 flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4"></i> {% trans "Operational Summary" %}
</h3>
<p class="text-slate leading-relaxed">{{ visit_efficiency.summary_en }}</p>
</div>
<div dir="rtl">
<h3 class="font-semibold text-navy mb-2 flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4"></i> {% trans "الملخص التشغيلي" %}
</h3>
<p class="text-slate leading-relaxed font-arabic">{{ visit_efficiency.summary_ar }}</p>
</div>
</div>
{% endif %}
{% if visit_efficiency.bottlenecks_en %}
<div class="mb-4">
<h4 class="font-semibold text-navy mb-3 flex items-center gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4 text-amber-500"></i>
{% trans "Top Bottlenecks Identified" %}
</h4>
<div class="space-y-2">
{% for bn in visit_efficiency.bottlenecks_en %}
<div class="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<i data-lucide="chevron-right" class="w-4 h-4 text-amber-600 mt-0.5 flex-shrink-0"></i>
<span class="text-sm text-amber-800">{{ bn }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if visit_efficiency.recommendations_en %}
<div class="pt-4 border-t border-slate-200">
<h4 class="font-semibold text-navy mb-3 flex items-center gap-2">
<i data-lucide="zap" class="w-4 h-4 text-emerald-500"></i>
{% trans "Efficiency Recommendations" %}
</h4>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3">
{% for rec in visit_efficiency.recommendations_en %}
<div class="flex items-start gap-2 p-3 bg-emerald-50 border border-emerald-200 rounded-lg">
<i data-lucide="arrow-right" class="w-4 h-4 text-emerald-600 mt-0.5 flex-shrink-0"></i>
<span class="text-sm text-emerald-800">{{ rec }}</span>
</div>
{% endfor %}
</div>
{% if visit_efficiency.recommendations_ar %}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-3 mt-3" dir="rtl">
{% for rec in visit_efficiency.recommendations_ar %}
<div class="flex items-start gap-2 p-3 bg-slate-50 border border-slate-200 rounded-lg">
<i data-lucide="arrow-left" class="w-4 h-4 text-slate-500 mt-0.5 flex-shrink-0"></i>
<span class="text-sm text-slate font-arabic">{{ rec }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</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. Visit Analytics Charts
const visitVolumeCtx = document.getElementById('visitVolumeChart');
if (visitVolumeCtx) {
const vd = {{ visit_type_data_json|default:'{}'|safe }};
new Chart(visitVolumeCtx, {
type: 'bar',
data: {
labels: [
'{% trans "Emergency (ED)" %}',
'{% trans "Inpatient (IP)" %}',
'{% trans "Outpatient (OP)" %}'
],
datasets: [{
label: '{% trans "Visits" %}',
data: [vd.ED.count || 0, vd.IP.count || 0, vd.OP.count || 0],
backgroundColor: ['#f59e0b', '#3b82f6', '#10b981'],
borderRadius: 8,
barThickness: 50
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(ctx) {
const types = ['ED', 'IP', 'OP'];
const d = vd[types[ctx.dataIndex]];
return [
ctx.formattedValue + ' {% trans "visits" %}',
'{% trans "Avg Duration" %}: ' + (d.avg_duration || '-'),
'{% trans "Completion" %}: ' + (d.completion_rate || 0) + '%'
];
}
}
}
},
scales: {
x: { grid: { color: '#f1f5f9' }, ticks: { font: { size: 11 } } },
y: { grid: { display: false }, ticks: { font: { size: 12, weight: 600 } } }
}
}
});
}
const visitTrendCtx = document.getElementById('visitTrendChart');
if (visitTrendCtx) {
const trendMonths = JSON.parse('{{ visit_trend_months|default:"[]"|escapejs }}');
const trendED = JSON.parse('{{ visit_trend_ed|default:"[]"|escapejs }}');
const trendIP = JSON.parse('{{ visit_trend_ip|default:"[]"|escapejs }}');
const trendOP = JSON.parse('{{ visit_trend_op|default:"[]"|escapejs }}');
const shortMonths = trendMonths.map(m => {
const parts = m.split('-');
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
return monthNames[parseInt(parts[1]) - 1] + ' ' + parts[0].slice(2);
});
new Chart(visitTrendCtx, {
type: 'line',
data: {
labels: shortMonths.length > 0 ? shortMonths : ['No data'],
datasets: [
{
label: '{% trans "Emergency (ED)" %}',
data: trendED,
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: '#f59e0b'
},
{
label: '{% trans "Inpatient (IP)" %}',
data: trendIP,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: '#3b82f6'
},
{
label: '{% trans "Outpatient (OP)" %}',
data: trendOP,
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 4,
pointBackgroundColor: '#10b981'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'top', labels: { usePointStyle: true, pointStyle: 'circle', padding: 16, font: { size: 11 } } }
},
scales: {
y: { beginAtZero: true, grid: { color: '#e2e8f0' }, ticks: { font: { size: 11 } } },
x: { grid: { display: false }, ticks: { font: { size: 11 } } }
}
}
});
}
// 7b. Visit Stage Duration Breakdown (stacked bar)
const stageCtx = document.getElementById('visitStageChart');
if (stageCtx) {
const sd = {{ visit_stage_data_json|default:'{}'|safe }};
if (sd.stages && sd.stages.length > 0) {
const stageColors = {
'Registration': '#3b82f6',
'Lab': '#8b5cf6',
'Radiology': '#f59e0b',
'Pharmacy': '#10b981',
'Doctor': '#06b6d4',
'Other': '#94a3b8'
};
function _fmtMin(m) {
if (m <= 0) return '-';
if (m < 60) return Math.round(m) + 'm';
const h = Math.floor(m / 60);
const mins = Math.round(m % 60);
return h + 'h' + (mins ? ' ' + mins + 'm' : '');
}
const datasets = sd.stages.map((stage, idx) => ({
label: stage,
data: [
sd.ED ? sd.ED[idx] : 0,
sd.IP ? sd.IP[idx] : 0,
sd.OP ? sd.OP[idx] : 0
],
backgroundColor: stageColors[stage] || '#94a3b8',
borderRadius: 4,
borderSkipped: false
}));
new Chart(stageCtx, {
type: 'bar',
data: {
labels: [
'{% trans "Emergency (ED)" %}',
'{% trans "Inpatient (IP)" %}',
'{% trans "Outpatient (OP)" %}'
],
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
plugins: {
legend: {
position: 'top',
labels: { usePointStyle: true, pointStyle: 'rectRounded', padding: 14, font: { size: 11 } }
},
tooltip: {
callbacks: {
label: function(ctx) {
return ctx.dataset.label + ': ' + _fmtMin(ctx.raw);
},
footer: function(items) {
const total = items.reduce((s, i) => s + i.raw, 0);
return '{% trans "Total" %}: ' + _fmtMin(total);
}
}
}
},
scales: {
x: {
stacked: true,
grid: { color: '#f1f5f9' },
ticks: {
font: { size: 11 },
callback: function(val) { return _fmtMin(val); }
},
title: { display: true, text: '{% trans "Average Minutes" %}', font: { size: 12, weight: 600 } }
},
y: { stacked: true, grid: { display: false }, ticks: { font: { size: 12, weight: 600 } } }
}
}
});
}
}
// 8. 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 } }
}
}
});
}
}
// 9. Cross-Module Yearly Trends — Area Charts
function renderYearQuarterChart(canvasId, dataJson, label, color) {
const data = buildChartData(dataJson);
if (!data || data.length === 0) return;
const ctx = document.getElementById(canvasId);
if (!ctx) return;
const labels = data.map(d => d.year + ' Q' + d.quarter);
const values = data.map(d => d.count);
const showDatalabels = typeof ChartDataLabels !== 'undefined';
new Chart(ctx, {
type: 'line',
plugins: showDatalabels ? [ChartDataLabels] : [],
data: {
labels: labels,
datasets: [{
label: label,
data: values,
backgroundColor: color.replace(')', ', 0.12)').replace('rgb', 'rgba'),
borderColor: color,
borderWidth: 2,
fill: true,
tension: 0.3,
pointRadius: 4,
pointBackgroundColor: color,
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: function(items) {
return items[0].label;
},
label: function(item) {
return label + ': ' + item.raw;
}
}
},
datalabels: {
anchor: 'end',
align: 'top',
offset: 4,
color: color,
font: {
weight: 'bold',
size: 10
},
formatter: function(value) {
return value;
}
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: label
},
grid: { color: '#e2e8f0' }
},
x: {
grid: { display: false }
}
}
}
});
}
renderYearQuarterChart('complaintsByQuarterChart', '{{ complaints_by_quarter|escapejs }}', '{% trans "Complaints" %}', 'rgb(239, 68, 68)');
renderYearQuarterChart('inquiriesByQuarterChart', '{{ inquiries_by_quarter|escapejs }}', '{% trans "Inquiries" %}', 'rgb(245, 158, 11)');
renderYearQuarterChart('suggestionsByQuarterChart', '{{ suggestions_by_quarter|escapejs }}', '{% trans "Suggestions" %}', 'rgb(168, 85, 247)');
renderYearQuarterChart('observationsByQuarterChart', '{{ observations_by_quarter|escapejs }}', '{% trans "Observations" %}', 'rgb(34, 197, 94)');
renderYearQuarterChart('appreciationsByQuarterChart', '{{ appreciations_by_quarter|escapejs }}', '{% trans "Appreciations" %}', 'rgb(236, 72, 153)');
renderYearQuarterChart('visitsByQuarterChart', '{{ visits_by_quarter|escapejs }}', '{% trans "Patient Visits" %}', 'rgb(20, 184, 166)');
}
function updateYearQuarterChart() {
const params = new URLSearchParams(window.location.search);
const fromYear = document.getElementById('fromYear').value;
const toYear = document.getElementById('toYear').value;
if (fromYear) params.set('from_year', fromYear);
if (toYear) params.set('to_year', toYear);
window.location.search = params.toString();
}
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>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2" onerror="console.warn('Datalabels plugin failed to load')"></script>
{% endblock %}