All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001): - new ReferenceSequence model + generate_reference() helper (apps/core) - Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save() - prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only) - removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources - migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012 - unit tests (format, sanitization, monthly reset, 40-thread concurrency) QA audit: - isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env) - feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion) - reports/feedback-modules-qa-report.md Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules.
2236 lines
105 KiB
HTML
2236 lines
105 KiB
HTML
{% extends "layouts/base.html" %}
|
||
{% load i18n %}
|
||
{% load static %}
|
||
|
||
{% block title %}{% trans "Analytics Dashboard" %} - PX360{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
/* PX360 App Theme Variables */
|
||
:root {
|
||
--hh-navy: #005696;
|
||
--hh-blue: #007bbd;
|
||
--hh-light: #eef6fb;
|
||
--hh-slate: #64748b;
|
||
--hh-success: #10b981;
|
||
--hh-warning: #f59e0b;
|
||
--hh-danger: #ef4444;
|
||
}
|
||
|
||
/* Page Header */
|
||
.page-header-gradient {
|
||
background: linear-gradient(135deg, var(--hh-navy) 0%, #0069a8 50%, var(--hh-blue) 100%);
|
||
color: white;
|
||
padding: 2rem 2.5rem;
|
||
border-radius: 1rem;
|
||
margin-bottom: 2rem;
|
||
box-shadow: 0 10px 15px -3px rgba(0, 86, 150, 0.2);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.page-header-gradient::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 200px;
|
||
height: 200px;
|
||
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
||
}
|
||
|
||
/* Section Cards */
|
||
.section-card {
|
||
background: white;
|
||
border-radius: 1rem;
|
||
border: 2px solid #e2e8f0;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
transition: all 0.3s ease;
|
||
}
|
||
.section-card:hover {
|
||
border-color: #005696;
|
||
box-shadow: 0 10px 25px -5px rgba(0, 86, 150, 0.15);
|
||
}
|
||
|
||
.section-header {
|
||
padding: 1rem 1.5rem;
|
||
border-bottom: 2px solid #e2e8f0;
|
||
background: linear-gradient(to right, #f8fafc, #f1f5f9);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.section-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 0.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
/* KPI Cards */
|
||
.kpi-card {
|
||
background: white;
|
||
border-radius: 1rem;
|
||
border: 2px solid #e2e8f0;
|
||
padding: 1.25rem;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
transition: all 0.3s ease;
|
||
height: 100%;
|
||
}
|
||
.kpi-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||
border-color: #005696;
|
||
}
|
||
|
||
.kpi-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 0.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-size: 1.75rem;
|
||
font-weight: 800;
|
||
color: #1e293b;
|
||
}
|
||
|
||
.kpi-label {
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.025em;
|
||
}
|
||
|
||
/* Chart Cards */
|
||
.chart-card {
|
||
background: white;
|
||
border-radius: 1rem;
|
||
border: 2px solid #e2e8f0;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||
transition: all 0.3s ease;
|
||
}
|
||
.chart-card:hover {
|
||
border-color: #005696;
|
||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.chart-header {
|
||
background: linear-gradient(135deg, #f8fafc, #eef6fb);
|
||
padding: 1rem 1.25rem;
|
||
border-bottom: 2px solid #e2e8f0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.chart-header h3 {
|
||
color: #005696;
|
||
font-weight: 700;
|
||
margin: 0;
|
||
}
|
||
|
||
/* Form Styling - PX360 Theme */
|
||
.form-select-px360 {
|
||
background: white;
|
||
border: 2px solid #e2e8f0;
|
||
border-radius: 0.75rem;
|
||
padding: 0.625rem 1rem;
|
||
color: #1e293b;
|
||
font-size: 0.875rem;
|
||
min-width: 200px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.form-select-px360:hover {
|
||
border-color: #005696;
|
||
}
|
||
.form-select-px360:focus {
|
||
outline: none;
|
||
border-color: #005696;
|
||
box-shadow: 0 0 0 3px rgba(0, 86, 150, 0.1);
|
||
}
|
||
|
||
/* Data Tables */
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
}
|
||
.data-table th {
|
||
background: #f8fafc;
|
||
border-bottom: 2px solid #e2e8f0;
|
||
padding: 0.875rem 1rem;
|
||
font-size: 0.75rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
color: #64748b;
|
||
text-align: left;
|
||
}
|
||
.data-table td {
|
||
border-bottom: 1px solid #e2e8f0;
|
||
padding: 0.875rem 1rem;
|
||
color: #1e293b;
|
||
}
|
||
.data-table tr:hover td {
|
||
background: #f8fafc;
|
||
}
|
||
|
||
/* Status Badges */
|
||
.badge {
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 9999px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
}
|
||
.badge-blue { background: #dbeafe; color: #1d4ed8; }
|
||
.badge-green { background: #d1fae5; color: #047857; }
|
||
.badge-orange { background: #fef3c7; color: #b45309; }
|
||
.badge-red { background: #fee2e2; color: #b91c1c; }
|
||
.badge-purple { background: #f3e8ff; color: #7c3aed; }
|
||
|
||
/* Progress Bars */
|
||
.progress-bar {
|
||
height: 8px;
|
||
border-radius: 4px;
|
||
background: #e2e8f0;
|
||
overflow: hidden;
|
||
}
|
||
.progress-fill {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
/* Animations */
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.animate-in {
|
||
animation: fadeIn 0.4s ease-out forwards;
|
||
}
|
||
.delay-100 { animation-delay: 100ms; }
|
||
.delay-200 { animation-delay: 200ms; }
|
||
.delay-300 { animation-delay: 300ms; }
|
||
.delay-400 { animation-delay: 400ms; }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="px-4 py-6 max-w-[1600px] mx-auto">
|
||
|
||
<!-- Page Header -->
|
||
<div class="page-header-gradient animate-in">
|
||
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 relative z-10">
|
||
<div>
|
||
<h1 class="text-2xl font-bold flex items-center gap-3">
|
||
<i data-lucide="bar-chart-3" class="w-8 h-8"></i>
|
||
{% trans "Analytics Dashboard" %}
|
||
</h1>
|
||
<p class="mt-1 opacity-90">{% trans "Comprehensive overview of patient experience metrics" %}</p>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<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>
|
||
|
||
<!-- Module Analytics: Tabbed Section -->
|
||
<div class="section-card mb-6 animate-in delay-100">
|
||
<div class="section-header">
|
||
<div class="section-icon" style="background: linear-gradient(135deg, #ede9fe, #ddd6fe);">
|
||
<i data-lucide="bar-chart-3" class="w-6 h-6 text-purple-600"></i>
|
||
</div>
|
||
<div>
|
||
<h2 class="text-xl font-bold text-navy">{% trans "Module Analytics" %}</h2>
|
||
<p class="text-sm text-slate">{% trans "Breakdown by status, severity, department, and category" %}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Bar -->
|
||
<div class="border-b border-gray-200 px-6">
|
||
<nav class="flex gap-1 -mb-px" id="moduleTabNav">
|
||
<button onclick="switchModuleTab('complaints')" data-tab="complaints"
|
||
class="module-tab-btn px-5 py-3 text-sm font-semibold border-b-2 border-navy text-navy transition-colors">
|
||
<span class="flex items-center gap-2"><i data-lucide="message-square-warning" class="w-4 h-4"></i> {% trans "Complaints" %}</span>
|
||
</button>
|
||
<button onclick="switchModuleTab('inquiries')" data-tab="inquiries"
|
||
class="module-tab-btn px-5 py-3 text-sm font-semibold border-b-2 border-transparent text-slate hover:text-navy transition-colors">
|
||
<span class="flex items-center gap-2"><i data-lucide="help-circle" class="w-4 h-4"></i> {% trans "Inquiries" %}</span>
|
||
</button>
|
||
<button onclick="switchModuleTab('observations')" data-tab="observations"
|
||
class="module-tab-btn px-5 py-3 text-sm font-semibold border-b-2 border-transparent text-slate hover:text-navy transition-colors">
|
||
<span class="flex items-center gap-2"><i data-lucide="eye" class="w-4 h-4"></i> {% trans "Observations" %}</span>
|
||
</button>
|
||
<button onclick="switchModuleTab('suggestions')" data-tab="suggestions"
|
||
class="module-tab-btn px-5 py-3 text-sm font-semibold border-b-2 border-transparent text-slate hover:text-navy transition-colors">
|
||
<span class="flex items-center gap-2"><i data-lucide="lightbulb" class="w-4 h-4"></i> {% trans "Suggestions" %}</span>
|
||
</button>
|
||
<button onclick="switchModuleTab('appreciation')" data-tab="appreciation"
|
||
class="module-tab-btn px-5 py-3 text-sm font-semibold border-b-2 border-transparent text-slate hover:text-navy transition-colors">
|
||
<span class="flex items-center gap-2"><i data-lucide="heart" class="w-4 h-4"></i> {% trans "Appreciation" %}</span>
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- ===== COMPLAINTS TAB ===== -->
|
||
<div class="module-tab-panel p-6" id="tab-complaints">
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
<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>
|
||
<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 class="chart-card">
|
||
<div class="chart-header"><i data-lucide="building-2" class="w-5 h-5 text-navy"></i><h3>{% trans "By Department" %}</h3></div>
|
||
<div class="p-4"><canvas id="complaintsByDeptChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== INQUIRIES TAB ===== -->
|
||
<div class="module-tab-panel p-6 hidden" id="tab-inquiries">
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="pie-chart" class="w-5 h-5 text-blue-600"></i><h3>{% trans "Status Distribution" %}</h3></div>
|
||
<div class="p-4"><canvas id="inquiryStatusChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="signal" class="w-5 h-5 text-blue-600"></i><h3>{% trans "By Priority" %}</h3></div>
|
||
<div class="p-4"><canvas id="inquiryPriorityChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="building-2" class="w-5 h-5 text-blue-600"></i><h3>{% trans "By Department" %}</h3></div>
|
||
<div class="p-4"><canvas id="inquiriesByDeptChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="tag" class="w-5 h-5 text-blue-600"></i><h3>{% trans "By Category" %}</h3></div>
|
||
<div class="p-4"><canvas id="inquiryCategoryChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== OBSERVATIONS TAB ===== -->
|
||
<div class="module-tab-panel p-6 hidden" id="tab-observations">
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="pie-chart" class="w-5 h-5 text-amber-600"></i><h3>{% trans "Status Distribution" %}</h3></div>
|
||
<div class="p-4"><canvas id="observationStatusChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="alert-triangle" class="w-5 h-5 text-amber-600"></i><h3>{% trans "Severity Levels" %}</h3></div>
|
||
<div class="p-4"><canvas id="observationSeverityChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="building-2" class="w-5 h-5 text-amber-600"></i><h3>{% trans "By Department" %}</h3></div>
|
||
<div class="p-4"><canvas id="observationsByDeptChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="tag" class="w-5 h-5 text-amber-600"></i><h3>{% trans "By Category" %}</h3></div>
|
||
<div class="p-4"><canvas id="observationCategoryChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== SUGGESTIONS TAB ===== -->
|
||
<div class="module-tab-panel p-6 hidden" id="tab-suggestions">
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="pie-chart" class="w-5 h-5 text-teal-600"></i><h3>{% trans "Status Distribution" %}</h3></div>
|
||
<div class="p-4"><canvas id="suggestionStatusChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="smile" class="w-5 h-5 text-teal-600"></i><h3>{% trans "Sentiment" %}</h3></div>
|
||
<div class="p-4"><canvas id="suggestionSentimentChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="building-2" class="w-5 h-5 text-teal-600"></i><h3>{% trans "By Department" %}</h3></div>
|
||
<div class="p-4"><canvas id="suggestionsByDeptChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="tag" class="w-5 h-5 text-teal-600"></i><h3>{% trans "By Category" %}</h3></div>
|
||
<div class="p-4"><canvas id="suggestionCategoryChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== APPRECIATION TAB ===== -->
|
||
<div class="module-tab-panel p-6 hidden" id="tab-appreciation">
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6">
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="pie-chart" class="w-5 h-5 text-pink-600"></i><h3>{% trans "Status Distribution" %}</h3></div>
|
||
<div class="p-4"><canvas id="appreciationStatusChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="award" class="w-5 h-5 text-pink-600"></i><h3>{% trans "By Category" %}</h3></div>
|
||
<div class="p-4"><canvas id="appreciationCategoryChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="building-2" class="w-5 h-5 text-pink-600"></i><h3>{% trans "By Department" %}</h3></div>
|
||
<div class="p-4"><canvas id="appreciationsByDeptChart" class="h-64 w-full"></canvas></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-header"><i data-lucide="globe" class="w-5 h-5 text-pink-600"></i><h3>{% trans "Visibility" %}</h3></div>
|
||
<div class="p-4"><canvas id="appreciationVisibilityChart" 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);
|
||
}
|
||
|
||
function switchModuleTab(tabName) {
|
||
document.querySelectorAll('.module-tab-btn').forEach(btn => {
|
||
btn.classList.remove('border-navy', 'text-navy', 'font-semibold');
|
||
btn.classList.add('border-transparent', 'text-slate');
|
||
});
|
||
document.querySelectorAll('.module-tab-panel').forEach(p => p.classList.add('hidden'));
|
||
const activeBtn = document.querySelector(`[data-tab="${tabName}"]`);
|
||
if (activeBtn) {
|
||
activeBtn.classList.add('border-navy', 'text-navy', 'font-semibold');
|
||
activeBtn.classList.remove('border-transparent', 'text-slate');
|
||
}
|
||
const activePanel = document.getElementById('tab-' + tabName);
|
||
if (activePanel) activePanel.classList.remove('hidden');
|
||
}
|
||
|
||
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 } }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ---- Complaints by Department (horizontal bar) ----
|
||
const complaintsDeptCtx = document.getElementById('complaintsByDeptChart');
|
||
if (complaintsDeptCtx) {
|
||
const deptData = buildChartData('{{ complaints_by_dept|escapejs }}');
|
||
if (deptData.length > 0) {
|
||
new Chart(complaintsDeptCtx, {
|
||
type: 'bar',
|
||
data: {
|
||
labels: deptData.map(d => d.department__name),
|
||
datasets: [{ label: 'Complaints', data: deptData.map(d => d.count),
|
||
backgroundColor: '#3b82f6', borderRadius: 4 }]
|
||
},
|
||
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, grid: { color: '#e2e8f0' } }, y: { grid: { display: false } } }
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============ INQUIRY CHARTS ============
|
||
const inqStatusCtx = document.getElementById('inquiryStatusChart');
|
||
if (inqStatusCtx) {
|
||
const d = buildChartData('{{ inquiry_status_counts|escapejs }}');
|
||
const colors = ['#3b82f6','#f59e0b','#10b981','#64748b','#8b5cf6','#ef4444'];
|
||
const statusLabels = {open:'Open',in_progress:'In Progress',resolved:'Resolved',closed:'Closed',pending_external:'Pending External',ovr_pending:'OVR Pending'};
|
||
new Chart(inqStatusCtx, {
|
||
type: 'doughnut',
|
||
data: { labels: d.map(i => statusLabels[i.status]||i.status), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: colors.slice(0,d.length), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
|
||
const inqPriCtx = document.getElementById('inquiryPriorityChart');
|
||
if (inqPriCtx) {
|
||
const d = buildChartData('{{ inquiry_priority_counts|escapejs }}');
|
||
const priColors = {low:'#10b981',medium:'#f59e0b',high:'#ef4444'};
|
||
const priLabels = {low:'Low',medium:'Medium',high:'High'};
|
||
new Chart(inqPriCtx, {
|
||
type: 'bar',
|
||
data: { labels: d.map(i=>priLabels[i.priority]||i.priority), datasets: [{ label:'Inquiries', data: d.map(i=>i.count),
|
||
backgroundColor: d.map(i=>priColors[i.priority]||'#3b82f6'), borderRadius: 6 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, grid: { color: '#e2e8f0' } }, x: { grid: { display: false } } } }
|
||
});
|
||
}
|
||
|
||
const inqDeptCtx = document.getElementById('inquiriesByDeptChart');
|
||
if (inqDeptCtx) {
|
||
const d = buildChartData('{{ inquiries_by_dept|escapejs }}');
|
||
if (d.length > 0) {
|
||
new Chart(inqDeptCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>i.department__name), datasets: [{ label:'Inquiries', data: d.map(i=>i.count),
|
||
backgroundColor: '#6366f1', borderRadius: 4 }] },
|
||
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, grid: { color: '#e2e8f0' } }, y: { grid: { display: false } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
const inqCatCtx = document.getElementById('inquiryCategoryChart');
|
||
if (inqCatCtx) {
|
||
const d = buildChartData('{{ inquiries_by_category|escapejs }}');
|
||
const catLabels = {appointment:'Appointment',billing:'Billing',medical_records:'Medical Records',general:'General',other:'Other'};
|
||
const catColors = ['#3b82f6','#10b981','#f59e0b','#8b5cf6','#ef4444'];
|
||
if (d.length > 0) {
|
||
new Chart(inqCatCtx, {
|
||
type: 'pie', data: { labels: d.map(i=>catLabels[i.category]||i.category), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: catColors.slice(0,d.length), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============ OBSERVATION CHARTS ============
|
||
const obsStatusCtx = document.getElementById('observationStatusChart');
|
||
if (obsStatusCtx) {
|
||
const d = buildChartData('{{ observation_status_counts|escapejs }}');
|
||
const colors = ['#f59e0b','#3b82f6','#8b5cf6','#6366f1','#10b981','#64748b','#ef4444','#ec4899','#14b8a6','#a3a3a3'];
|
||
const sLabels = {new:'New',triaged:'Triaged',assigned:'Assigned',in_progress:'In Progress',resolved:'Resolved',closed:'Closed',rejected:'Rejected',duplicate:'Duplicate',contacted:'Contacted',contacted_no_response:'No Response'};
|
||
new Chart(obsStatusCtx, {
|
||
type: 'doughnut',
|
||
data: { labels: d.map(i=>sLabels[i.status]||i.status), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: colors.slice(0,d.length), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
|
||
const obsSevCtx = document.getElementById('observationSeverityChart');
|
||
if (obsSevCtx) {
|
||
const d = buildChartData('{{ observation_severity_counts|escapejs }}');
|
||
const sevColors = {low:'#10b981',medium:'#f59e0b',high:'#f97316',critical:'#ef4444'};
|
||
const sevLabels = {low:'Low',medium:'Medium',high:'High',critical:'Critical'};
|
||
new Chart(obsSevCtx, {
|
||
type: 'bar',
|
||
data: { labels: d.map(i=>sevLabels[i.severity]||i.severity), datasets: [{ label:'Observations', data: d.map(i=>i.count),
|
||
backgroundColor: d.map(i=>sevColors[i.severity]||'#3b82f6'), borderRadius: 6 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, grid: { color: '#e2e8f0' } }, x: { grid: { display: false } } } }
|
||
});
|
||
}
|
||
|
||
const obsDeptCtx = document.getElementById('observationsByDeptChart');
|
||
if (obsDeptCtx) {
|
||
const d = buildChartData('{{ observations_by_dept|escapejs }}');
|
||
if (d.length > 0) {
|
||
new Chart(obsDeptCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>i.assigned_department__name), datasets: [{ label:'Observations', data: d.map(i=>i.count),
|
||
backgroundColor: '#f59e0b', borderRadius: 4 }] },
|
||
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, grid: { color: '#e2e8f0' } }, y: { grid: { display: false } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
const obsCatCtx = document.getElementById('observationCategoryChart');
|
||
if (obsCatCtx) {
|
||
const d = buildChartData('{{ observations_by_category|escapejs }}');
|
||
if (d.length > 0) {
|
||
new Chart(obsCatCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>i.category__name_en), datasets: [{ label:'Observations', data: d.map(i=>i.count),
|
||
backgroundColor: '#14b8a6', borderRadius: 6 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, grid: { color: '#e2e8f0' } }, x: { grid: { display: false }, ticks: { maxRotation: 45 } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============ SUGGESTION CHARTS ============
|
||
const sugStatusCtx = document.getElementById('suggestionStatusChart');
|
||
if (sugStatusCtx) {
|
||
const d = buildChartData('{{ suggestion_status_counts|escapejs }}');
|
||
const colors = ['#14b8a6','#3b82f6','#8b5cf6','#64748b'];
|
||
const sLabels = {submitted:'Submitted',reviewed:'Reviewed',acknowledged:'Acknowledged',closed:'Closed'};
|
||
new Chart(sugStatusCtx, {
|
||
type: 'doughnut',
|
||
data: { labels: d.map(i=>sLabels[i.status]||i.status), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: colors.slice(0,d.length), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
|
||
const sugSentCtx = document.getElementById('suggestionSentimentChart');
|
||
if (sugSentCtx) {
|
||
const d = buildChartData('{{ suggestion_sentiment_counts|escapejs }}');
|
||
const sentColors = {positive:'#10b981',neutral:'#f59e0b',negative:'#ef4444'};
|
||
const sentLabels = {positive:'Positive',neutral:'Neutral',negative:'Negative'};
|
||
new Chart(sugSentCtx, {
|
||
type: 'pie',
|
||
data: { labels: d.map(i=>sentLabels[i.sentiment]||i.sentiment), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: d.map(i=>sentColors[i.sentiment]||'#3b82f6'), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
|
||
const sugDeptCtx = document.getElementById('suggestionsByDeptChart');
|
||
if (sugDeptCtx) {
|
||
const d = buildChartData('{{ suggestions_by_dept|escapejs }}');
|
||
if (d.length > 0) {
|
||
new Chart(sugDeptCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>i.department__name), datasets: [{ label:'Suggestions', data: d.map(i=>i.count),
|
||
backgroundColor: '#14b8a6', borderRadius: 4 }] },
|
||
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, grid: { color: '#e2e8f0' } }, y: { grid: { display: false } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
const sugCatCtx = document.getElementById('suggestionCategoryChart');
|
||
if (sugCatCtx) {
|
||
const d = buildChartData('{{ suggestions_by_category|escapejs }}');
|
||
const catLabels = {clinical_care:'Clinical Care',staff_service:'Staff Service',facility:'Facility',communication:'Communication',appointment:'Appointment',billing:'Billing',food_service:'Food Service',cleanliness:'Cleanliness',technology:'Technology',other:'Other'};
|
||
if (d.length > 0) {
|
||
new Chart(sugCatCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>catLabels[i.category]||i.category), datasets: [{ label:'Suggestions', data: d.map(i=>i.count),
|
||
backgroundColor: '#0d9488', borderRadius: 6 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, grid: { color: '#e2e8f0' } }, x: { grid: { display: false }, ticks: { maxRotation: 45 } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============ APPRECIATION CHARTS ============
|
||
const appStatusCtx = document.getElementById('appreciationStatusChart');
|
||
if (appStatusCtx) {
|
||
const d = buildChartData('{{ appreciation_status_counts|escapejs }}');
|
||
const colors = ['#64748b','#3b82f6','#8b5cf6','#10b981','#f59e0b'];
|
||
const sLabels = {draft:'Draft',activated:'Activated',ai_analyzed:'AI Analyzed',sent:'Sent',acknowledged:'Acknowledged'};
|
||
new Chart(appStatusCtx, {
|
||
type: 'doughnut',
|
||
data: { labels: d.map(i=>sLabels[i.status]||i.status), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: colors.slice(0,d.length), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 12, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
|
||
const appCatCtx = document.getElementById('appreciationCategoryChart');
|
||
if (appCatCtx) {
|
||
const d = buildChartData('{{ appreciations_by_category|escapejs }}');
|
||
if (d.length > 0) {
|
||
new Chart(appCatCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>i.category__name_en), datasets: [{ label:'Appreciations', data: d.map(i=>i.count),
|
||
backgroundColor: '#ec4899', borderRadius: 6 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, grid: { color: '#e2e8f0' } }, x: { grid: { display: false }, ticks: { maxRotation: 45 } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
const appDeptCtx = document.getElementById('appreciationsByDeptChart');
|
||
if (appDeptCtx) {
|
||
const d = buildChartData('{{ appreciations_by_dept|escapejs }}');
|
||
if (d.length > 0) {
|
||
new Chart(appDeptCtx, {
|
||
type: 'bar', data: { labels: d.map(i=>i.department__name), datasets: [{ label:'Appreciations', data: d.map(i=>i.count),
|
||
backgroundColor: '#f472b6', borderRadius: 4 }] },
|
||
options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, grid: { color: '#e2e8f0' } }, y: { grid: { display: false } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
const appVisCtx = document.getElementById('appreciationVisibilityChart');
|
||
if (appVisCtx) {
|
||
const d = buildChartData('{{ appreciations_by_visibility|escapejs }}');
|
||
const visColors = {private:'#64748b',department:'#3b82f6',hospital:'#8b5cf6',public:'#10b981'};
|
||
const visLabels = {private:'Private',department:'Department',hospital:'Hospital',public:'Public'};
|
||
if (d.length > 0) {
|
||
new Chart(appVisCtx, {
|
||
type: 'pie',
|
||
data: { labels: d.map(i=>visLabels[i.visibility]||i.visibility), datasets: [{ data: d.map(i=>i.count),
|
||
backgroundColor: d.map(i=>visColors[i.visibility]||'#3b82f6'), borderColor: '#fff', borderWidth: 2 }] },
|
||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 10, usePointStyle: true } } } }
|
||
});
|
||
}
|
||
}
|
||
|
||
// 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 %}
|