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

1139 lines
59 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

{% extends 'layouts/base.html' %}
{% load i18n %}
{% 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 %}