1139 lines
59 KiB
HTML
1139 lines
59 KiB
HTML
{% extends 'layouts/base.html' %}
|
||
{% load i18n %}
|
||
|
||
{% block title %}{% trans "PX Command Center" %}{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.kpi-card {
|
||
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||
}
|
||
.kpi-card:hover {
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
||
}
|
||
.chart-container {
|
||
min-height: 350px;
|
||
}
|
||
.loading-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(255,255,255,0.9);
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 9999;
|
||
}
|
||
.loading-overlay.active {
|
||
display: flex;
|
||
}
|
||
.physician-row:hover {
|
||
background-color: #FFF1F2;
|
||
cursor: pointer;
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||
<!-- Loading Overlay -->
|
||
<div class="loading-overlay" id="loadingOverlay">
|
||
<div class="text-center">
|
||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-navy"></div>
|
||
<p class="mt-2 text-gray-600">{% trans "Loading dashboard data..." %}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Page Header -->
|
||
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-8 gap-4">
|
||
<div>
|
||
<h2 class="text-3xl font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||
<i data-lucide="bar-chart-2" class="w-8 h-8 text-navy"></i>
|
||
{% trans "PX Command Center" %}
|
||
</h2>
|
||
<p class="text-gray-500">{% trans "Comprehensive Patient Experience Analytics Dashboard" %}</p>
|
||
</div>
|
||
<div class="flex flex-wrap gap-3">
|
||
<button class="bg-emerald-500 text-white px-6 py-3 rounded-xl font-bold hover:bg-emerald-600 transition flex items-center gap-2 shadow-lg shadow-emerald-200" onclick="exportDashboard('excel')">
|
||
<i data-lucide="file-spreadsheet" class="w-5 h-5"></i> {% trans "Export Excel" %}
|
||
</button>
|
||
<button class="bg-blue-500 text-white px-6 py-3 rounded-xl font-bold hover:bg-blue-600 transition flex items-center gap-2 shadow-lg shadow-blue-200" onclick="exportDashboard('pdf')">
|
||
<i data-lucide="file-text" class="w-5 h-5"></i> {% trans "Export PDF" %}
|
||
</button>
|
||
<button class="bg-light0 text-white px-6 py-3 rounded-xl font-bold hover:bg-navy transition flex items-center gap-2 shadow-lg shadow-blue-200" onclick="refreshDashboard()">
|
||
<i data-lucide="refresh-cw" class="w-5 h-5"></i> {% trans "Refresh" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter Panel -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 mb-8">
|
||
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="filter" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Filters" %}
|
||
</h3>
|
||
<button type="button" id="toggleFilters" class="text-gray-400 hover:text-gray-600 transition">
|
||
<i data-lucide="chevron-down" class="w-5 h-5"></i>
|
||
</button>
|
||
</div>
|
||
<div id="filterContent" class="px-6 py-4">
|
||
<form id="filterForm">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
<!-- Date Range -->
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Date Range" %}</label>
|
||
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"
|
||
name="date_range" id="dateRange" onchange="handleDateRangeChange()">
|
||
<option value="7d" {% if filters.date_range == '7d' %}selected{% endif %}>{% trans "Last 7 Days" %}</option>
|
||
<option value="30d" {% if filters.date_range == '30d' %}selected{% endif %}>{% trans "Last 30 Days" %}</option>
|
||
<option value="90d" {% if filters.date_range == '90d' %}selected{% endif %}>{% trans "Last 90 Days" %}</option>
|
||
<option value="this_month" {% if filters.date_range == 'this_month' %}selected{% endif %}>{% trans "This Month" %}</option>
|
||
<option value="last_month" {% if filters.date_range == 'last_month' %}selected{% endif %}>{% trans "Last Month" %}</option>
|
||
<option value="quarter" {% if filters.date_range == 'quarter' %}selected{% endif %}>{% trans "This Quarter" %}</option>
|
||
<option value="year" {% if filters.date_range == 'year' %}selected{% endif %}>{% trans "This Year" %}</option>
|
||
<option value="custom" {% if filters.date_range == 'custom' %}selected{% endif %}>{% trans "Custom Range" %}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Custom Date Range -->
|
||
<div class="hidden" id="customDateRange">
|
||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Custom Range" %}</label>
|
||
<div class="flex gap-2">
|
||
<input type="date" class="flex-1 px-3 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition text-sm"
|
||
name="custom_start" id="customStart" value="{{ filters.custom_start|default:'' }}">
|
||
<span class="text-gray-400 py-2.5">to</span>
|
||
<input type="date" class="flex-1 px-3 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition text-sm"
|
||
name="custom_end" id="customEnd" value="{{ filters.custom_end|default:'' }}">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Department -->
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Department" %}</label>
|
||
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition" data-tomselect
|
||
name="department" id="departmentFilter">
|
||
<option value="">{% trans "All Departments" %}</option>
|
||
{% for department in departments %}
|
||
<option value="{{ department.id }}" {% if filters.department == department.id|stringformat:"s" %}selected{% endif %}>
|
||
{{ department.get_localized_name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- KPI Category -->
|
||
<div>
|
||
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "KPI Category" %}</label>
|
||
<select class="w-full px-4 py-2.5 border-2 border-gray-200 rounded-xl text-gray-800 focus:ring-2 focus:ring-navy focus:border-transparent transition"
|
||
name="kpi_category" id="kpiCategoryFilter">
|
||
<option value="">{% trans "All Categories" %}</option>
|
||
<option value="complaints">{% trans "Complaints" %}</option>
|
||
<option value="surveys">{% trans "Surveys" %}</option>
|
||
<option value="actions">{% trans "Actions" %}</option>
|
||
<option value="physicians">{% trans "Physicians" %}</option>
|
||
</select>
|
||
</div>
|
||
|
||
<!-- Apply/Reset Buttons -->
|
||
<div class="flex gap-2 items-end">
|
||
<button type="submit" class="flex-1 bg-light0 text-white px-6 py-2.5 rounded-xl font-bold hover:bg-navy transition flex items-center justify-center gap-2">
|
||
<i data-lucide="check-circle" class="w-4 h-4"></i> {% trans "Apply Filters" %}
|
||
</button>
|
||
<button type="button" class="flex-1 border-2 border-gray-300 text-gray-700 px-6 py-2.5 rounded-xl font-bold hover:bg-gray-50 transition flex items-center justify-center gap-2" onclick="resetFilters()">
|
||
<i data-lucide="x-circle" class="w-4 h-4"></i> {% trans "Reset" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KPI Cards Section -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8" id="kpiSection">
|
||
<!-- Total Complaints -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-navy">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Total Complaints" %}</div>
|
||
<div class="text-4xl font-bold text-navy" id="totalComplaints">0</div>
|
||
</div>
|
||
<div class="text-right">
|
||
<small class="text-gray-400" id="complaintsTrend">
|
||
{% if kpis.complaints_trend.percentage_change > 0 %}
|
||
<span class="text-navy"><i data-lucide="trending-up" class="w-4 h-4 inline"></i> {{ kpis.complaints_trend.percentage_change|floatformat:1 }}%</span>
|
||
{% elif kpis.complaints_trend.percentage_change < 0 %}
|
||
<span class="text-emerald-500"><i data-lucide="trending-down" class="w-4 h-4 inline"></i> {{ kpis.complaints_trend.percentage_change|floatformat:1 }}%</span>
|
||
{% else %}
|
||
<span class="text-gray-400"><i data-lucide="minus" class="w-4 h-4 inline"></i> 0%</span>
|
||
{% endif %}
|
||
</small>
|
||
<div class="text-gray-400 text-xs">{% trans "vs last period" %}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Open Complaints -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-amber-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Open Complaints" %}</div>
|
||
<div class="text-4xl font-bold text-amber-500" id="openComplaints">0</div>
|
||
</div>
|
||
<div class="bg-amber-100 p-3 rounded-full">
|
||
<i data-lucide="alert-triangle" class="w-6 h-6 text-amber-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Overdue Complaints -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-red-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Overdue Complaints" %}</div>
|
||
<div class="text-4xl font-bold text-red-500" id="overdueComplaints">0</div>
|
||
</div>
|
||
<div class="bg-red-100 p-3 rounded-full">
|
||
<i data-lucide="clock" class="w-6 h-6 text-red-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Resolved Complaints -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-emerald-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Resolved Complaints" %}</div>
|
||
<div class="text-4xl font-bold text-emerald-500" id="resolvedComplaints">0</div>
|
||
</div>
|
||
<div class="bg-emerald-100 p-3 rounded-full">
|
||
<i data-lucide="check-circle" class="w-6 h-6 text-emerald-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Total Actions -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-cyan-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Total Actions" %}</div>
|
||
<div class="text-4xl font-bold text-cyan-500" id="totalActions">0</div>
|
||
</div>
|
||
<div class="bg-cyan-100 p-3 rounded-full">
|
||
<i data-lucide="list-todo" class="w-6 h-6 text-cyan-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Overdue Actions -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-gray-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Overdue Actions" %}</div>
|
||
<div class="text-4xl font-bold text-gray-500" id="overdueActions">0</div>
|
||
</div>
|
||
<div class="bg-gray-100 p-3 rounded-full">
|
||
<i data-lucide="history" class="w-6 h-6 text-gray-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Avg Survey Score -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-emerald-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Avg Survey Score" %}</div>
|
||
<div class="text-4xl font-bold text-emerald-500" id="avgSurveyScore">0.0</div>
|
||
</div>
|
||
<div class="bg-emerald-100 p-3 rounded-full">
|
||
<i data-lucide="star" class="w-6 h-6 text-emerald-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Negative Surveys -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-red-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Negative Surveys" %}</div>
|
||
<div class="text-4xl font-bold text-red-500" id="negativeSurveys">0</div>
|
||
</div>
|
||
<div class="bg-red-100 p-3 rounded-full">
|
||
<i data-lucide="frown" class="w-6 h-6 text-red-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reopened Complaints -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-orange-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Reopened" %}</div>
|
||
<div class="text-4xl font-bold text-orange-500" id="reopenedComplaints">0</div>
|
||
</div>
|
||
<div class="bg-orange-100 p-3 rounded-full">
|
||
<i data-lucide="rotate-ccw" class="w-6 h-6 text-orange-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Escalated OVR -->
|
||
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 kpi-card border-l-4 border-l-purple-500">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<div class="text-sm font-bold text-gray-500 uppercase mb-1">{% trans "Escalated OVR" %}</div>
|
||
<div class="text-4xl font-bold text-purple-500" id="escalatedOvr">0</div>
|
||
</div>
|
||
<div class="bg-purple-100 p-3 rounded-full">
|
||
<i data-lucide="shield-alert" class="w-6 h-6 text-purple-500"></i>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Charts Row 1 -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||
<!-- Complaints Trend Chart -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="trending-up" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Complaints Trend" %}
|
||
</h3>
|
||
</div>
|
||
<div class="chart-container" id="complaintsTrendChart"></div>
|
||
</div>
|
||
|
||
<!-- Complaints by Category Chart -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="pie-chart" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Complaints by Category" %}
|
||
</h3>
|
||
</div>
|
||
<div class="chart-container" id="complaintsByCategoryChart"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Charts Row 2 -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||
<!-- Survey Satisfaction Trend -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="bar-chart" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Survey Satisfaction Trend" %}
|
||
</h3>
|
||
</div>
|
||
<div class="chart-container" id="surveySatisfactionChart"></div>
|
||
</div>
|
||
|
||
<!-- Survey Distribution -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="donut" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Survey Distribution" %}
|
||
</h3>
|
||
</div>
|
||
<div class="chart-container" id="surveyDistributionChart"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Charts Row 3 -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||
<!-- Department Performance -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="bar-chart-3" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Department Performance" %}
|
||
</h3>
|
||
</div>
|
||
<div class="chart-container" id="departmentPerformanceChart"></div>
|
||
</div>
|
||
|
||
<!-- Physician Leaderboard -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 p-6">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="award" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Physician Leaderboard" %}
|
||
</h3>
|
||
</div>
|
||
<div class="chart-container" id="physicianLeaderboardChart"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tables Section -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 overflow-hidden mb-8">
|
||
<div class="px-6 py-4 border-b border-gray-100">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="alert-circle" class="w-5 h-5 text-red-500"></i>
|
||
{% trans "Overdue Complaints" %}
|
||
</h3>
|
||
</div>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full" id="overdueComplaintsTable">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "ID" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Title" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Patient" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Severity" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Hospital" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Department" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Due Date" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Actions" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
<!-- Data will be loaded via JavaScript -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Physician Details Table -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 overflow-hidden">
|
||
<div class="px-6 py-4 border-b border-gray-100">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="trophy" class="w-5 h-5 text-navy"></i>
|
||
{% trans "Top Performing Physicians" %}
|
||
</h3>
|
||
</div>
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full" id="physiciansTable">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Rank" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Physician" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Specialization" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Department" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Rating" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Surveys" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Positive" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Neutral" %}</th>
|
||
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Negative" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
<!-- Data will be loaded via JavaScript -->
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ====== AI-POWERED INSIGHTS SECTION ====== -->
|
||
<div class="bg-gradient-to-r from-purple-50 to-indigo-50 rounded-2xl shadow-sm border border-purple-100 overflow-hidden mb-8 mt-8">
|
||
<div class="px-6 py-4 border-b border-purple-100 flex justify-between items-center">
|
||
<h3 class="font-bold text-gray-800 flex items-center gap-2">
|
||
<i data-lucide="brain" class="w-5 h-5 text-purple-600"></i>
|
||
{% trans "AI-Powered Insights" %}
|
||
</h3>
|
||
<button onclick="refreshAiCommandCenter()" id="refreshAiCcBtn" class="text-sm text-purple-600 hover:text-purple-800 font-medium flex items-center gap-1 transition">
|
||
<i data-lucide="sparkles" class="w-4 h-4"></i> {% trans "Refresh AI" %}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- AI Sub-Tabs -->
|
||
<div class="border-b border-purple-100 bg-white/50">
|
||
<nav class="flex gap-1 px-6" id="aiTabs">
|
||
<button class="ai-tab-btn px-4 py-3 text-sm font-semibold border-b-2 border-purple-600 text-purple-700" data-tab="summary" onclick="switchAiTab('summary')">
|
||
<i data-lucide="file-text" class="w-4 h-4 inline mr-1"></i> {% trans "Executive Summary" %}
|
||
</button>
|
||
<button class="ai-tab-btn px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-500 hover:text-purple-600" data-tab="warnings" onclick="switchAiTab('warnings')">
|
||
<i data-lucide="alert-triangle" class="w-4 h-4 inline mr-1"></i> {% trans "Early Warnings" %}
|
||
</button>
|
||
<button class="ai-tab-btn px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-500 hover:text-purple-600" data-tab="forecast" onclick="switchAiTab('forecast')">
|
||
<i data-lucide="trending-up" class="w-4 h-4 inline mr-1"></i> {% trans "Forecast" %}
|
||
</button>
|
||
<button class="ai-tab-btn px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-500 hover:text-purple-600" data-tab="sla-risk" onclick="switchAiTab('sla-risk')">
|
||
<i data-lucide="clock-alert" class="w-4 h-4 inline mr-1"></i> {% trans "SLA Risk" %}
|
||
</button>
|
||
<button class="ai-tab-btn px-4 py-3 text-sm font-semibold border-b-2 border-transparent text-gray-500 hover:text-purple-600" data-tab="recommendations" onclick="switchAiTab('recommendations')">
|
||
<i data-lucide="lightbulb" class="w-4 h-4 inline mr-1"></i> {% trans "Recommendations" %}
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
<!-- AI Tab Content -->
|
||
<div class="p-6">
|
||
<!-- Executive Summary Tab -->
|
||
<div class="ai-tab-content" id="tab-summary">
|
||
{% if exec_summary %}
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<div>
|
||
<div class="flex items-center gap-2 mb-3">
|
||
<span class="badge {% if exec_summary.risk_level == 'high' %}bg-red-100 text-red-700{% elif exec_summary.risk_level == 'medium' %}bg-amber-100 text-amber-700{% else %}bg-green-100 text-green-700{% endif %} px-3 py-1 rounded-full text-xs font-semibold">
|
||
{{ exec_summary.risk_level|title }} {% trans "Risk" %}
|
||
</span>
|
||
</div>
|
||
<h4 class="font-bold text-gray-800 mb-2">{% trans "English Summary" %}</h4>
|
||
<p class="text-gray-600 leading-relaxed text-sm">{{ exec_summary.summary_en }}</p>
|
||
{% if exec_summary.key_findings_en %}
|
||
<ul class="mt-3 space-y-1">
|
||
{% for f in exec_summary.key_findings_en %}
|
||
<li class="flex items-start gap-2 text-sm text-gray-600">
|
||
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0"></i>
|
||
<span>{{ f }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
</div>
|
||
<div dir="rtl">
|
||
<h4 class="font-bold text-gray-800 mb-2">{% trans "الملخص العربي" %}</h4>
|
||
<p class="text-gray-600 leading-relaxed text-sm font-arabic">{{ exec_summary.summary_ar }}</p>
|
||
{% if exec_summary.key_findings_ar %}
|
||
<ul class="mt-3 space-y-1">
|
||
{% for f in exec_summary.key_findings_ar %}
|
||
<li class="flex items-start gap-2 text-sm text-gray-600">
|
||
<i data-lucide="check-circle" class="w-4 h-4 text-emerald-500 mt-0.5 flex-shrink-0"></i>
|
||
<span>{{ f }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% if exec_summary.recommendations_en %}
|
||
<div class="mt-4 pt-4 border-t border-purple-100">
|
||
<h4 class="font-bold text-gray-800 mb-2 text-sm">{% trans "Recommended Actions" %}</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
|
||
{% for r in exec_summary.recommendations_en %}
|
||
<div class="flex items-start gap-2 p-3 bg-purple-50 rounded-lg text-sm text-gray-700">
|
||
<i data-lucide="arrow-right" class="w-4 h-4 text-purple-500 mt-0.5 flex-shrink-0"></i>
|
||
<span>{{ r }}</span>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<div class="text-center py-8 text-gray-400">
|
||
<i data-lucide="brain" class="w-8 h-8 mx-auto mb-2 text-purple-300"></i>
|
||
<p class="text-sm">{% trans "AI summary loading — click Refresh AI or wait for daily generation at 6 AM" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Early Warnings Tab -->
|
||
<div class="ai-tab-content hidden" id="tab-warnings">
|
||
{% if early_warnings %}
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full">
|
||
<thead class="bg-purple-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">{% trans "Department" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Risk" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Level" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Signals" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Complaint Δ" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Survey Δ" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "SLA Δ" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
{% for d in early_warnings %}
|
||
<tr class="hover:bg-purple-50/50 transition">
|
||
<td class="px-4 py-3 font-medium text-sm">{{ d.department_name }}</td>
|
||
<td class="px-4 py-3 text-center">
|
||
<div class="flex items-center justify-center gap-2">
|
||
<div class="w-16 bg-gray-200 rounded-full h-2 overflow-hidden">
|
||
<div class="h-2 rounded-full {% if d.risk_score >= 70 %}bg-red-500{% elif d.risk_score >= 50 %}bg-amber-500{% elif d.risk_score >= 30 %}bg-blue-500{% else %}bg-gray-400{% endif %}" style="width:{{ d.risk_score }}%"></div>
|
||
</div>
|
||
<span class="text-xs font-bold">{{ d.risk_score }}%</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-4 py-3 text-center">
|
||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold {% if d.risk_level == 'critical' %}bg-red-100 text-red-700{% elif d.risk_level == 'high' %}bg-amber-100 text-amber-700{% elif d.risk_level == 'medium' %}bg-blue-100 text-blue-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||
{{ d.risk_level|title }}
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-3 text-center text-sm font-semibold">{{ d.active_signals }}/5</td>
|
||
<td class="px-4 py-3 text-center text-sm {% if d.complaint_volume_spike.change_pct > 20 %}text-red-600 font-bold{% endif %}">
|
||
{% if d.complaint_volume_spike.change_pct > 0 %}+{% endif %}{{ d.complaint_volume_spike.change_pct }}%
|
||
</td>
|
||
<td class="px-4 py-3 text-center text-sm {% if d.survey_score_decline.change_pct < -10 %}text-red-600 font-bold{% endif %}">
|
||
{{ d.survey_score_decline.change_pct }}%
|
||
</td>
|
||
<td class="px-4 py-3 text-center text-sm {% if d.sla_breach_increase.change_pp > 10 %}text-red-600 font-bold{% endif %}">
|
||
+{{ d.sla_breach_increase.change_pp }}pp
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-8 text-gray-400">
|
||
<i data-lucide="shield-check" class="w-8 h-8 mx-auto mb-2 text-green-300"></i>
|
||
<p class="text-sm">{% trans "No departments currently showing risk signals" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Forecast Tab -->
|
||
<div class="ai-tab-content hidden" id="tab-forecast">
|
||
{% if complaint_forecast.labels %}
|
||
<div class="flex items-center gap-6 mb-4">
|
||
<div class="text-center">
|
||
<p class="text-xs text-gray-500">{% trans "Predicted 30d" %}</p>
|
||
<p class="text-2xl font-bold text-blue-600">{{ complaint_forecast.total_predicted_30d }}</p>
|
||
</div>
|
||
<div class="text-center">
|
||
<p class="text-xs text-gray-500">{% trans "vs Recent" %}</p>
|
||
<p class="text-2xl font-bold {% if complaint_forecast.change_pct > 10 %}text-red-600{% elif complaint_forecast.change_pct < -10 %}text-green-600{% endif %}">
|
||
{% if complaint_forecast.change_pct > 0 %}+{% endif %}{{ complaint_forecast.change_pct }}%
|
||
</p>
|
||
</div>
|
||
<span class="px-3 py-1 rounded-full text-xs font-semibold {% if complaint_forecast.confidence_level == 'high' %}bg-green-100 text-green-700{% elif complaint_forecast.confidence_level == 'medium' %}bg-amber-100 text-amber-700{% else %}bg-red-100 text-red-700{% endif %}">
|
||
{{ complaint_forecast.confidence_level|title }} {% trans "Confidence" %}
|
||
</span>
|
||
</div>
|
||
<canvas id="ccForecastChart" class="h-64 w-full"></canvas>
|
||
{% else %}
|
||
<div class="text-center py-8 text-gray-400">
|
||
<i data-lucide="trending-up" class="w-8 h-8 mx-auto mb-2 text-blue-300"></i>
|
||
<p class="text-sm">{% trans "Insufficient historical data for forecasting (need 14+ days)" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- SLA Risk Tab -->
|
||
<div class="ai-tab-content hidden" id="tab-sla-risk">
|
||
{% if sla_breach_predictions %}
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full">
|
||
<thead class="bg-amber-50">
|
||
<tr>
|
||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">{% trans "Complaint" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Breach Risk" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Severity" %}</th>
|
||
<th class="px-4 py-3 text-center text-xs font-bold text-gray-500 uppercase">{% trans "Hours Left" %}</th>
|
||
<th class="px-4 py-3 text-left text-xs font-bold text-gray-500 uppercase">{% trans "Recommendation" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-gray-100">
|
||
{% for p in sla_breach_predictions %}
|
||
<tr class="hover:bg-amber-50/30 transition">
|
||
<td class="px-4 py-3 text-sm font-medium max-w-xs truncate" title="{{ p.title }}">{{ p.title }}</td>
|
||
<td class="px-4 py-3 text-center">
|
||
<div class="flex items-center justify-center gap-2">
|
||
<div class="w-12 bg-gray-200 rounded-full h-2 overflow-hidden">
|
||
<div class="h-2 rounded-full {% if p.breach_probability >= 80 %}bg-red-500{% elif p.breach_probability >= 60 %}bg-amber-500{% elif p.breach_probability >= 40 %}bg-blue-500{% else %}bg-gray-400{% endif %}" style="width:{{ p.breach_probability }}%"></div>
|
||
</div>
|
||
<span class="text-xs font-bold">{{ p.breach_probability }}%</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-4 py-3 text-center">
|
||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold {% if p.severity == 'critical' %}bg-red-100 text-red-700{% elif p.severity == 'high' %}bg-amber-100 text-amber-700{% elif p.severity == 'medium' %}bg-blue-100 text-blue-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||
{{ p.severity|title }}
|
||
</span>
|
||
</td>
|
||
<td class="px-4 py-3 text-center text-sm {% if p.hours_remaining < 4 %}text-red-600 font-bold{% elif p.hours_remaining < 12 %}text-amber-600{% endif %}">
|
||
{% if p.hours_remaining < 0 %}{% trans "EXPIRED" %}{% else %}{{ p.hours_remaining }}h{% endif %}
|
||
</td>
|
||
<td class="px-4 py-3 text-sm text-gray-700">{{ p.recommendation }}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-8 text-gray-400">
|
||
<i data-lucide="shield-check" class="w-8 h-8 mx-auto mb-2 text-green-300"></i>
|
||
<p class="text-sm">{% trans "No complaints currently at risk of SLA breach" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Recommendations Tab -->
|
||
<div class="ai-tab-content hidden" id="tab-recommendations">
|
||
{% if action_recommendations %}
|
||
<div class="space-y-3">
|
||
{% for rec in action_recommendations %}
|
||
<div class="border border-purple-100 rounded-xl p-4 hover:border-purple-300 transition bg-white">
|
||
<div class="flex items-start justify-between mb-1">
|
||
<div class="flex items-center gap-2">
|
||
<span class="px-2 py-0.5 rounded-full text-xs font-semibold {% if rec.priority == 'critical' %}bg-red-100 text-red-700{% elif rec.priority == 'high' %}bg-amber-100 text-amber-700{% elif rec.priority == 'medium' %}bg-blue-100 text-blue-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||
{{ rec.priority|title }}
|
||
</span>
|
||
<span class="text-xs text-gray-500">{{ rec.category }}</span>
|
||
</div>
|
||
<span class="text-xs font-semibold text-gray-500">{{ rec.complaint_count }} {% trans "complaints" %}</span>
|
||
</div>
|
||
<h4 class="font-semibold text-gray-800 text-sm mb-2">{{ rec.problem_summary_en }}</h4>
|
||
{% if rec.recommended_actions_en %}
|
||
<div class="flex flex-wrap gap-2">
|
||
{% for a in rec.recommended_actions_en %}
|
||
<span class="flex items-center gap-1 px-3 py-1.5 bg-emerald-50 text-emerald-700 rounded-lg text-xs">
|
||
<i data-lucide="zap" class="w-3 h-3"></i> {{ a }}
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="text-center py-8 text-gray-400">
|
||
<i data-lucide="lightbulb" class="w-8 h-8 mx-auto mb-2 text-yellow-300"></i>
|
||
<p class="text-sm">{% trans "No actionable patterns detected yet — need more complaint data" %}</p>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JavaScript -->
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
lucide.createIcons();
|
||
|
||
// Toggle filter panel
|
||
document.getElementById('toggleFilters').addEventListener('click', function() {
|
||
const filterContent = document.getElementById('filterContent');
|
||
filterContent.classList.toggle('hidden');
|
||
this.querySelector('i').classList.toggle('rotate-180');
|
||
});
|
||
|
||
// Load initial data
|
||
loadDashboardData();
|
||
|
||
// Handle filter form submission
|
||
document.getElementById('filterForm').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
updateFilters();
|
||
loadDashboardData();
|
||
});
|
||
});
|
||
|
||
let charts = {};
|
||
let currentFilters = {
|
||
date_range: '30d',
|
||
hospital: '',
|
||
department: '',
|
||
kpi_category: '',
|
||
custom_start: '',
|
||
custom_end: ''
|
||
};
|
||
|
||
function handleDateRangeChange() {
|
||
const dateRange = document.getElementById('dateRange').value;
|
||
const customDateRange = document.getElementById('customDateRange');
|
||
|
||
if (dateRange === 'custom') {
|
||
customDateRange.classList.remove('hidden');
|
||
} else {
|
||
customDateRange.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
function updateFilters() {
|
||
currentFilters.date_range = document.getElementById('dateRange').value;
|
||
currentFilters.hospital = '{{ current_hospital.id|default:"" }}';
|
||
currentFilters.department = document.getElementById('departmentFilter').value;
|
||
currentFilters.kpi_category = document.getElementById('kpiCategoryFilter').value;
|
||
currentFilters.custom_start = document.getElementById('customStart').value;
|
||
currentFilters.custom_end = document.getElementById('customEnd').value;
|
||
}
|
||
|
||
function loadDashboardData() {
|
||
showLoading();
|
||
|
||
fetch(`/analytics/api/command-center/?${new URLSearchParams(currentFilters)}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
updateKPIs(data.kpis);
|
||
updateCharts(data.charts);
|
||
updateTables(data.tables);
|
||
})
|
||
.catch(error => {
|
||
console.error('Error loading dashboard data:', error);
|
||
showError();
|
||
})
|
||
.finally(() => {
|
||
hideLoading();
|
||
});
|
||
}
|
||
|
||
function updateKPIs(kpis) {
|
||
document.getElementById('totalComplaints').textContent = kpis.total_complaints || 0;
|
||
document.getElementById('openComplaints').textContent = kpis.open_complaints || 0;
|
||
document.getElementById('overdueComplaints').textContent = kpis.overdue_complaints || 0;
|
||
document.getElementById('resolvedComplaints').textContent = kpis.resolved_complaints || 0;
|
||
document.getElementById('totalActions').textContent = kpis.total_actions || 0;
|
||
document.getElementById('overdueActions').textContent = kpis.overdue_actions || 0;
|
||
document.getElementById('avgSurveyScore').textContent = (kpis.avg_survey_score || 0).toFixed(2);
|
||
document.getElementById('negativeSurveys').textContent = kpis.negative_surveys || 0;
|
||
document.getElementById('reopenedComplaints').textContent = kpis.reopened_complaints || 0;
|
||
document.getElementById('escalatedOvr').textContent = kpis.escalated_ovr_complaints || 0;
|
||
|
||
if (kpis.complaints_trend) {
|
||
const trendElement = document.getElementById('complaintsTrend');
|
||
const change = kpis.complaints_trend.percentage_change;
|
||
if (change > 0) {
|
||
trendElement.innerHTML = `<span class="text-navy"><i data-lucide="trending-up" class="w-4 h-4 inline"></i> ${change.toFixed(1)}%</span>`;
|
||
} else if (change < 0) {
|
||
trendElement.innerHTML = `<span class="text-emerald-500"><i data-lucide="trending-down" class="w-4 h-4 inline"></i> ${change.toFixed(1)}%</span>`;
|
||
} else {
|
||
trendElement.innerHTML = `<span class="text-gray-400"><i data-lucide="minus" class="w-4 h-4 inline"></i> 0%</span>`;
|
||
}
|
||
lucide.createIcons();
|
||
}
|
||
}
|
||
|
||
function updateCharts(chartData) {
|
||
if (chartData.complaints_trend) {
|
||
renderChart('complaintsTrendChart', chartData.complaints_trend, 'line');
|
||
}
|
||
if (chartData.complaints_by_category) {
|
||
renderChart('complaintsByCategoryChart', chartData.complaints_by_category, 'donut');
|
||
}
|
||
if (chartData.survey_satisfaction_trend) {
|
||
renderChart('surveySatisfactionChart', chartData.survey_satisfaction_trend, 'line');
|
||
}
|
||
if (chartData.survey_distribution) {
|
||
renderChart('surveyDistributionChart', chartData.survey_distribution, 'donut');
|
||
}
|
||
if (chartData.department_performance) {
|
||
renderChart('departmentPerformanceChart', chartData.department_performance, 'bar');
|
||
}
|
||
if (chartData.physician_leaderboard) {
|
||
renderChart('physicianLeaderboardChart', chartData.physician_leaderboard, 'bar');
|
||
}
|
||
}
|
||
|
||
function renderChart(elementId, chartData, chartType) {
|
||
const element = document.getElementById(elementId);
|
||
if (!element) return;
|
||
|
||
if (charts[elementId]) {
|
||
charts[elementId].destroy();
|
||
}
|
||
|
||
const hhChartColors = ['#e11d48', '#f97316', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6'];
|
||
|
||
const options = {
|
||
series: chartData.series || [],
|
||
chart: {
|
||
type: chartType,
|
||
height: 350,
|
||
toolbar: { show: true },
|
||
fontFamily: 'Inter, sans-serif'
|
||
},
|
||
labels: chartData.labels || [],
|
||
colors: hhChartColors,
|
||
dataLabels: { enabled: chartType === 'donut' },
|
||
legend: {
|
||
position: chartType === 'donut' ? 'bottom' : 'top'
|
||
},
|
||
xaxis: { categories: chartData.labels },
|
||
yaxis: { min: 0, forceNiceScale: true },
|
||
grid: { borderColor: '#e7e7e7', strokeDashArray: 5 },
|
||
tooltip: { theme: 'light' }
|
||
};
|
||
|
||
if (chartType === 'line') {
|
||
options.stroke = { curve: 'smooth', width: 3 };
|
||
options.fill = {
|
||
type: 'solid',
|
||
gradient: {
|
||
shadeIntensity: 1,
|
||
opacityFrom: 0.4,
|
||
opacityTo: 0.1,
|
||
}
|
||
};
|
||
}
|
||
|
||
charts[elementId] = new ApexCharts(element, options);
|
||
charts[elementId].render();
|
||
}
|
||
|
||
function updateTables(tableData) {
|
||
if (tableData.overdue_complaints) {
|
||
const tbody = document.querySelector('#overdueComplaintsTable tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (tableData.overdue_complaints.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" class="text-center text-gray-500 py-8">No overdue complaints</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tableData.overdue_complaints.forEach(complaint => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'hover:bg-gray-50 transition';
|
||
row.innerHTML = `
|
||
<td class="px-6 py-4"><small class="text-gray-400">${complaint.id.substring(0, 8)}</small></td>
|
||
<td class="px-6 py-4 font-medium text-gray-800">${complaint.title.substring(0, 50)}${complaint.title.length > 50 ? '...' : ''}</td>
|
||
<td class="px-6 py-4 text-gray-600">${complaint.patient_full_name || 'N/A'}</td>
|
||
<td class="px-6 py-4"><span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-${getSeverityBadgeClass(complaint.severity)}">${complaint.severity}</span></td>
|
||
<td class="px-6 py-4 text-gray-600">${complaint.hospital || 'N/A'}</td>
|
||
<td class="px-6 py-4 text-gray-600">${complaint.department || 'N/A'}</td>
|
||
<td class="px-6 py-4 text-red-600">${complaint.due_at}</td>
|
||
<td class="px-6 py-4">
|
||
<a href="/complaints/${complaint.id}/" class="inline-flex items-center gap-1 px-3 py-2 text-navy bg-light rounded-lg hover:bg-light transition font-medium text-sm">
|
||
<i data-lucide="eye" class="w-4 h-4"></i>
|
||
</a>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
lucide.createIcons();
|
||
}
|
||
|
||
if (tableData.physician_leaderboard) {
|
||
const tbody = document.querySelector('#physiciansTable tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
if (tableData.physician_leaderboard.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="9" class="text-center text-gray-500 py-8">No physician data available</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tableData.physician_leaderboard.forEach((physician, index) => {
|
||
const row = document.createElement('tr');
|
||
row.className = 'physician-row transition';
|
||
row.onclick = () => window.location.href = `/physicians/${physician.physician_id}/`;
|
||
row.innerHTML = `
|
||
<td class="px-6 py-4"><span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-light text-navy">${index + 1}</span></td>
|
||
<td class="px-6 py-4"><strong class="text-gray-800">${physician.name}</strong></td>
|
||
<td class="px-6 py-4 text-gray-600">${physician.specialization || 'N/A'}</td>
|
||
<td class="px-6 py-4 text-gray-600">${physician.department || 'N/A'}</td>
|
||
<td class="px-6 py-4"><span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-emerald-100 text-emerald-700">${physician.rating.toFixed(2)}</span></td>
|
||
<td class="px-6 py-4 text-gray-800">${physician.surveys}</td>
|
||
<td class="px-6 py-4 text-emerald-600">${physician.positive}</td>
|
||
<td class="px-6 py-4 text-gray-500">${physician.neutral}</td>
|
||
<td class="px-6 py-4 text-red-600">${physician.negative}</td>
|
||
`;
|
||
tbody.appendChild(row);
|
||
});
|
||
}
|
||
}
|
||
|
||
function getSeverityBadgeClass(severity) {
|
||
const severityMap = {
|
||
'low': 'gray-100 text-gray-700',
|
||
'medium': 'amber-100 text-amber-700',
|
||
'high': 'orange-100 text-orange-700',
|
||
'critical': 'red-100 text-red-700'
|
||
};
|
||
return severityMap[severity] || 'gray-100 text-gray-700';
|
||
}
|
||
|
||
function showLoading() {
|
||
document.getElementById('loadingOverlay').classList.add('active');
|
||
}
|
||
|
||
function hideLoading() {
|
||
document.getElementById('loadingOverlay').classList.remove('active');
|
||
}
|
||
|
||
function showError() {
|
||
alert('Error loading dashboard data. Please try again.');
|
||
}
|
||
|
||
function refreshDashboard() {
|
||
loadDashboardData();
|
||
}
|
||
|
||
function resetFilters() {
|
||
document.getElementById('dateRange').value = '30d';
|
||
document.getElementById('departmentFilter').value = '';
|
||
document.getElementById('kpiCategoryFilter').value = '';
|
||
document.getElementById('customStart').value = '';
|
||
document.getElementById('customEnd').value = '';
|
||
document.getElementById('customDateRange').classList.add('hidden');
|
||
|
||
updateFilters();
|
||
loadDashboardData();
|
||
}
|
||
|
||
function exportDashboard(format) {
|
||
showLoading();
|
||
|
||
fetch(`/analytics/api/command-center/export/${format}/?${new URLSearchParams(currentFilters)}`)
|
||
.then(response => {
|
||
if (response.ok) {
|
||
return response.blob();
|
||
}
|
||
throw new Error('Export failed');
|
||
})
|
||
.then(blob => {
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = `px360_dashboard_${new Date().toISOString().slice(0,10)}.${format === 'excel' ? 'xlsx' : 'pdf'}`;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
a.remove();
|
||
})
|
||
.catch(error => {
|
||
console.error('Export error:', error);
|
||
alert('Error exporting dashboard. Please try again.');
|
||
})
|
||
.finally(() => {
|
||
hideLoading();
|
||
});
|
||
}
|
||
|
||
// ====== AI-Powered Insights JavaScript ======
|
||
|
||
function switchAiTab(tabName) {
|
||
// Update tab buttons
|
||
document.querySelectorAll('.ai-tab-btn').forEach(btn => {
|
||
if (btn.dataset.tab === tabName) {
|
||
btn.classList.remove('border-transparent', 'text-gray-500');
|
||
btn.classList.add('border-purple-600', 'text-purple-700');
|
||
} else {
|
||
btn.classList.add('border-transparent', 'text-gray-500');
|
||
btn.classList.remove('border-purple-600', 'text-purple-700');
|
||
}
|
||
});
|
||
// Update tab content
|
||
document.querySelectorAll('.ai-tab-content').forEach(content => content.classList.add('hidden'));
|
||
const target = document.getElementById('tab-' + tabName);
|
||
if (target) target.classList.remove('hidden');
|
||
|
||
// Render forecast chart if switching to that tab
|
||
if (tabName === 'forecast' && typeof Chart !== 'undefined') {
|
||
renderForecastChart();
|
||
}
|
||
// Re-init lucide icons for newly visible tab
|
||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||
}
|
||
|
||
function renderForecastChart() {
|
||
const canvas = document.getElementById('ccForecastChart');
|
||
if (!canvas) return;
|
||
|
||
// Check if chart already rendered
|
||
if (canvas._chartInstance) return;
|
||
|
||
const forecastData = {{ complaint_forecast|default:'{}'|safe }};
|
||
if (!forecastData.labels || forecastData.labels.length === 0) return;
|
||
|
||
const shortLabels = forecastData.labels.map(d => {
|
||
const parts = d.split('-');
|
||
return parts[1] + '/' + parts[2];
|
||
});
|
||
|
||
canvas._chartInstance = new Chart(canvas, {
|
||
type: 'line',
|
||
data: {
|
||
labels: shortLabels,
|
||
datasets: [
|
||
{
|
||
label: 'Upper Bound',
|
||
data: forecastData.upper_band,
|
||
borderColor: 'transparent',
|
||
backgroundColor: 'rgba(139, 92, 246, 0.08)',
|
||
fill: false,
|
||
pointRadius: 0,
|
||
tension: 0.4,
|
||
},
|
||
{
|
||
label: 'Lower Bound',
|
||
data: forecastData.lower_band,
|
||
borderColor: 'transparent',
|
||
backgroundColor: 'rgba(139, 92, 246, 0.08)',
|
||
fill: '-1',
|
||
pointRadius: 0,
|
||
tension: 0.4,
|
||
},
|
||
{
|
||
label: 'Predicted',
|
||
data: forecastData.predicted,
|
||
borderColor: '#8b5cf6',
|
||
backgroundColor: 'transparent',
|
||
fill: false,
|
||
pointRadius: 3,
|
||
pointBackgroundColor: '#8b5cf6',
|
||
tension: 0.4,
|
||
borderWidth: 2,
|
||
}
|
||
]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: {
|
||
legend: {
|
||
position: 'bottom',
|
||
labels: {
|
||
filter: function(item) {
|
||
return item.text !== 'Upper Bound' && item.text !== 'Lower Bound';
|
||
},
|
||
padding: 15,
|
||
usePointStyle: true,
|
||
}
|
||
},
|
||
tooltip: {
|
||
callbacks: {
|
||
afterBody: function(items) {
|
||
const idx = items[0].dataIndex;
|
||
return 'Range: ' + forecastData.lower_band[idx] + ' – ' + forecastData.upper_band[idx];
|
||
}
|
||
}
|
||
}
|
||
},
|
||
scales: {
|
||
y: { beginAtZero: true, grid: { color: '#e2e8f0' } },
|
||
x: { grid: { display: false } }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
function refreshAiCommandCenter() {
|
||
const btn = document.getElementById('refreshAiCcBtn');
|
||
const icon = btn.querySelector('[data-lucide="sparkles"]');
|
||
icon.classList.add('animate-spin');
|
||
btn.disabled = true;
|
||
btn.style.opacity = '0.5';
|
||
|
||
fetch('{% url "analytics:refresh_ai_analytics" %}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': '{{ csrf_token }}',
|
||
'Content-Type': 'application/json',
|
||
},
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
showToast(data.message || 'AI analytics refresh triggered');
|
||
// Reload dashboard data after delay
|
||
setTimeout(() => {
|
||
loadDashboardData();
|
||
showToast('Dashboard refreshed with latest AI data', 'success');
|
||
}, 5000);
|
||
})
|
||
.catch(() => showToast('Failed to trigger AI refresh'))
|
||
.finally(() => {
|
||
icon.classList.remove('animate-spin');
|
||
btn.disabled = false;
|
||
btn.style.opacity = '1';
|
||
});
|
||
}
|
||
|
||
function showToast(message, type) {
|
||
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-purple-600');
|
||
toast.textContent = message;
|
||
document.body.appendChild(toast);
|
||
setTimeout(() => {
|
||
toast.style.opacity = '0';
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, 5000);
|
||
}
|
||
</script>
|
||
{% endblock %} |