712 lines
35 KiB
HTML
712 lines
35 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>
|
|
|
|
<!-- Hospital -->
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Hospital" %}</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="hospital" id="hospitalFilter" onchange="loadDepartments()">
|
|
<option value="">{% trans "All Hospitals" %}</option>
|
|
{% for hospital in hospitals %}
|
|
<option value="{{ hospital.id }}" {% if filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>
|
|
{{ hospital.name_en|default:hospital.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</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"
|
|
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.name_en|default:department.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>
|
|
</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>
|
|
</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 = document.getElementById('hospitalFilter').value;
|
|
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;
|
|
|
|
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('hospitalFilter').value = '';
|
|
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();
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %} |