555 lines
35 KiB
HTML
555 lines
35 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Executive Summary" %} — PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="p-4 lg:p-6 max-w-[1600px] mx-auto">
|
|
|
|
{# Hidden fields for HTMX analysis requests #}
|
|
<input type="hidden" name="date_range" value="{{ date_range }}" id="htmx-date-range">
|
|
{% if selected_hospital %}<input type="hidden" name="hospital" value="{{ selected_hospital.id }}" id="htmx-hospital">{% endif %}
|
|
|
|
{# ── Header & Filters ── #}
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-900">{% trans "Executive Summary" %}</h1>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
{% if selected_hospital %}{{ selected_hospital.name }}{% else %}{% trans "All Hospitals" %}{% endif %}
|
|
<span class="text-gray-300 mx-1">·</span>
|
|
<span class="text-gray-400">{% trans "Updated" %} {{ last_updated }}</span>
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<form method="get" class="flex items-center gap-2">
|
|
{% if is_px_admin %}
|
|
<select name="hospital" onchange="this.form.submit()" class="text-sm border-gray-300 rounded-lg py-2 px-3 focus:ring-indigo-500 focus:border-indigo-500">
|
|
<option value="">{% trans "All Hospitals" %}</option>
|
|
{% for h in available_hospitals %}
|
|
<option value="{{ h.id }}" {% if selected_hospital and selected_hospital.id == h.id %}selected{% endif %}>{{ h.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
{% endif %}
|
|
<select name="date_range" onchange="this.form.submit()" class="text-sm border-gray-300 rounded-lg py-2 px-3 focus:ring-indigo-500 focus:border-indigo-500">
|
|
{% for val, label in date_range_options %}
|
|
<option value="{{ val }}" {% if date_range == val %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</form>
|
|
<a href="{% url 'analytics:dashboard' %}" class="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-indigo-700 bg-indigo-50 rounded-lg hover:bg-indigo-100 transition">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
|
{% trans "Analytics" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Tabs ── #}
|
|
<div class="border-b border-gray-200 mb-6">
|
|
<nav class="flex gap-1 -mb-px" id="tabNav">
|
|
<button data-tab="overview" class="tab-btn active px-4 py-3 text-sm font-semibold border-b-2 border-indigo-600 text-indigo-600">{% trans "Overview" %}</button>
|
|
<button data-tab="trends" class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 transition">{% trans "Trends" %}</button>
|
|
<button data-tab="insights" class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 transition">
|
|
{% trans "Insights" %}
|
|
{% if insights_critical_count %}
|
|
<span class="ml-1 px-1.5 py-0.5 text-[10px] font-bold bg-red-100 text-red-700 rounded-full">{{ insights_critical_count }}</span>
|
|
{% endif %}
|
|
</button>
|
|
<button data-tab="reports" class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 transition">{% trans "Reports" %}</button>
|
|
</nav>
|
|
</div>
|
|
|
|
{# ═══════════════ OVERVIEW ═══════════════ #}
|
|
<section id="tab-overview" class="tab-content">
|
|
|
|
{# KPI Cards #}
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
{% for key, card in kpi_cards.items %}
|
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-gray-200 hover:shadow-md transition">
|
|
<div class="flex items-start justify-between mb-2">
|
|
<div>
|
|
<p class="text-xs font-medium text-gray-500 uppercase tracking-wide">
|
|
{% if key == "complaints" %}{% trans "Complaints" %}{% elif key == "surveys" %}{% trans "Satisfaction" %}{% elif key == "actions" %}{% trans "PX Actions" %}{% endif %}
|
|
</p>
|
|
<p class="text-2xl font-bold text-gray-900 mt-0.5">
|
|
{% if key == "surveys" %}{{ card.satisfaction|floatformat:1 }}<span class="text-base font-normal text-gray-400">/5</span>
|
|
{% else %}{{ card.total }}{% endif %}
|
|
</p>
|
|
</div>
|
|
{% if card.variance.percentage %}
|
|
<span class="inline-flex items-center gap-0.5 px-2 py-0.5 rounded-full text-xs font-semibold
|
|
{% if card.variance.direction == 'up' %}
|
|
{% if card.variance.is_positive %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}
|
|
{% elif card.variance.direction == 'down' %}
|
|
{% if card.variance.is_positive %}bg-red-100 text-red-800{% else %}bg-green-100 text-green-800{% endif %}
|
|
{% else %}bg-gray-100 text-gray-600{% endif %}">
|
|
{% if card.variance.direction == 'up' %}↑{% elif card.variance.direction == 'down' %}↓{% else %}→{% endif %}
|
|
{{ card.variance.percentage|floatformat:1 }}%
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex gap-3 text-xs text-gray-500 mb-3">
|
|
{% if key == "complaints" %}
|
|
<span>{% trans "Critical" %}: <b class="text-gray-700">{{ card.critical }}</b></span>
|
|
<span>{% trans "Overdue" %}: <b class="text-gray-700">{{ card.overdue }}</b></span>
|
|
<span>{% trans "Avg" %}: <b class="text-gray-700">{{ card.resolution_time|floatformat:0 }}h</b></span>
|
|
{% elif key == "surveys" %}
|
|
<span>NPS: <b class="text-gray-700">{{ card.nps|floatformat:0 }}</b></span>
|
|
<span>{% trans "Total" %}: <b class="text-gray-700">{{ card.total }}</b></span>
|
|
{% elif key == "actions" %}
|
|
<span>{% trans "Open" %}: <b class="text-gray-700">{{ card.open }}</b></span>
|
|
<span>{% trans "Overdue" %}: <b class="text-gray-700">{{ card.overdue }}</b></span>
|
|
<span>{% trans "Closed" %}: <b class="text-gray-700">{{ card.closed }}</b></span>
|
|
{% endif %}
|
|
</div>
|
|
<div id="sparkline-{{ key }}" class="h-8"></div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{# Risk Alert Summary #}
|
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-gray-200">
|
|
<h3 class="text-sm font-bold text-gray-900 mb-3 flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
|
{% trans "Risk Alerts" %}
|
|
{% if risk_alerts_count %}<span class="px-1.5 py-0.5 text-[10px] font-bold bg-red-100 text-red-700 rounded-full">{{ risk_alerts_count }}</span>{% endif %}
|
|
</h3>
|
|
{% if has_risk_alerts %}
|
|
<div class="space-y-2">
|
|
{% for alert in risk_alerts|slice:":3" %}
|
|
<div class="flex items-start gap-2 p-2 rounded-lg {% if alert.severity == 'critical' %}bg-red-50{% else %}bg-amber-50{% endif %}">
|
|
<span class="mt-1 w-2 h-2 rounded-full flex-shrink-0 {% if alert.severity == 'critical' %}bg-red-500{% else %}bg-amber-500{% endif %}"></span>
|
|
<div class="min-w-0">
|
|
<p class="text-xs font-medium text-gray-900 truncate">{{ alert.title_en }}</p>
|
|
<p class="text-[11px] text-gray-500">{% if alert.hospital %}{{ alert.hospital.name }} · {% endif %}{{ alert.created_at|timesince }} {% trans "ago" %}</p>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
<button onclick="switchTab('insights')" class="text-xs text-indigo-600 hover:text-indigo-700 font-medium">{% trans "View all insights" %} →</button>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-sm text-gray-400 py-4 text-center">{% trans "No active risk alerts" %}</p>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# Latest AI Report #}
|
|
<div class="lg:col-span-2 bg-white rounded-xl p-5 shadow-sm border border-gray-200">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-bold text-gray-900">{% trans "Latest AI Report" %}</h3>
|
|
{% if report_type %}<span class="text-xs text-gray-400">{{ report_type }} · {{ report_period }}</span>{% endif %}
|
|
</div>
|
|
{% if ai_narrative %}
|
|
<p class="text-sm text-gray-700 leading-relaxed mb-3">{{ ai_narrative|truncatewords:80 }}</p>
|
|
<div class="flex gap-6 flex-wrap">
|
|
{% if ai_highlights %}
|
|
<div class="flex-1 min-w-[180px]">
|
|
<p class="text-xs font-semibold text-green-700 mb-1">{% trans "Highlights" %}</p>
|
|
<ul class="space-y-0.5">
|
|
{% for h in ai_highlights|slice:":2" %}
|
|
<li class="text-xs text-gray-600"><span class="text-green-500">✓</span> {{ h|truncatewords:10 }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
{% if ai_concerns %}
|
|
<div class="flex-1 min-w-[180px]">
|
|
<p class="text-xs font-semibold text-red-700 mb-1">{% trans "Concerns" %}</p>
|
|
<ul class="space-y-0.5">
|
|
{% for c in ai_concerns|slice:":2" %}
|
|
<li class="text-xs text-gray-600"><span class="text-red-500">!</span> {{ c|truncatewords:10 }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if latest_report and latest_report.pdf_file %}
|
|
<a href="{% url 'executive:report_pdf' latest_report.id %}" class="inline-flex items-center gap-1 mt-3 text-xs font-medium text-indigo-600 hover:text-indigo-700">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
{% trans "Download PDF" %}
|
|
</a>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-center py-6">
|
|
<svg class="w-10 h-10 text-gray-300 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
<p class="text-sm text-gray-400">{% trans "No AI report generated yet" %}</p>
|
|
<button onclick="switchTab('reports')" class="text-sm text-indigo-600 hover:text-indigo-700 font-medium mt-1">{% trans "Generate your first report" %} →</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# AI Overview Analysis #}
|
|
<div class="mt-6">
|
|
<div id="ai-overview-result">
|
|
<div class="text-center py-4">
|
|
<button hx-post="{% url 'executive:analyze_overview' %}"
|
|
hx-include="[name='date_range'],[name='hospital']"
|
|
hx-target="#ai-overview-result"
|
|
hx-swap="innerHTML"
|
|
class="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
|
{% trans "Analyze Overview with AI" %}
|
|
</button>
|
|
<p class="text-xs text-gray-400 mt-1">{% trans "Get AI-powered analysis of your current KPIs" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section id="tab-trends" class="tab-content hidden">
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
|
<div class="lg:col-span-2 bg-white rounded-xl p-6 shadow-sm border border-gray-200">
|
|
<h3 class="text-sm font-bold text-gray-900 mb-4">{% trans "Complaints Trend" %}</h3>
|
|
<div id="complaintsChart"></div>
|
|
<div id="complaintsChartEmpty" class="hidden text-center py-10">
|
|
<p class="text-sm text-gray-400">{% trans "No data for selected period" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
|
|
<h3 class="text-sm font-bold text-gray-900 mb-4 flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-yellow-500" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/></svg>
|
|
{% trans "Hospital Leaderboard" %}
|
|
</h3>
|
|
<div class="space-y-2">
|
|
{% for h in hospital_leaderboard %}
|
|
<div class="flex items-center justify-between p-2.5 rounded-lg
|
|
{% if forloop.counter == 1 %}bg-yellow-50 border border-yellow-200
|
|
{% elif forloop.counter == 2 %}bg-gray-50 border border-gray-200
|
|
{% elif forloop.counter == 3 %}bg-orange-50 border border-orange-200
|
|
{% else %}bg-gray-50{% endif %}">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-6 h-6 flex items-center justify-center rounded-full text-xs font-bold
|
|
{% if forloop.counter == 1 %}bg-yellow-500 text-white
|
|
{% elif forloop.counter == 2 %}bg-gray-400 text-white
|
|
{% elif forloop.counter == 3 %}bg-orange-400 text-white
|
|
{% else %}bg-gray-200 text-gray-600{% endif %}">{{ forloop.counter }}</span>
|
|
<span class="text-sm font-medium text-gray-900">{{ h.hospital_name }}</span>
|
|
</div>
|
|
<span class="text-sm font-bold
|
|
{% if h.satisfaction_rate >= 80 %}text-green-600
|
|
{% elif h.satisfaction_rate >= 60 %}text-amber-600
|
|
{% else %}text-red-600{% endif %}">
|
|
{{ h.satisfaction_rate|floatformat:0 }}%
|
|
</span>
|
|
</div>
|
|
{% empty %}
|
|
<p class="text-sm text-gray-400 text-center py-4">{% trans "No hospital data" %}</p>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl p-6 shadow-sm border border-gray-200">
|
|
<h3 class="text-sm font-bold text-gray-900 mb-4">{% trans "Satisfaction Trend" %}</h3>
|
|
<div id="satisfactionChart"></div>
|
|
<div id="satisfactionChartEmpty" class="hidden text-center py-10">
|
|
<p class="text-sm text-gray-400">{% trans "No data for selected period" %}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{# AI Trend Analysis #}
|
|
<div class="mt-6">
|
|
<div id="ai-trends-result">
|
|
<div class="text-center py-4">
|
|
<button hx-post="{% url 'executive:analyze_trends' %}"
|
|
hx-include="[name='date_range'],[name='hospital']"
|
|
hx-target="#ai-trends-result"
|
|
hx-swap="innerHTML"
|
|
class="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
|
{% trans "Analyze Trends with AI" %}
|
|
</button>
|
|
<p class="text-xs text-gray-400 mt-1">{% trans "Get AI-powered trend analysis and predictions" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section id="tab-insights" class="tab-content hidden">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
{% if insights_critical_count %}
|
|
<span class="px-3 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800">{% trans "Critical" %}: {{ insights_critical_count }}</span>
|
|
{% endif %}
|
|
{% if insights_high_count %}
|
|
<span class="px-3 py-1 text-xs font-semibold rounded-full bg-amber-100 text-amber-800">{% trans "High" %}: {{ insights_high_count }}</span>
|
|
{% endif %}
|
|
<a href="{% url 'executive:predictive_insights' %}" class="px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 text-gray-700 hover:bg-gray-200 transition">{% trans "View All Insights" %} →</a>
|
|
</div>
|
|
|
|
{# Risk Alerts #}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
|
<div class="px-5 py-4 border-b border-gray-100">
|
|
<h3 class="text-sm font-bold text-gray-900">{% trans "Active Risk Alerts" %}</h3>
|
|
</div>
|
|
{% if has_risk_alerts %}
|
|
<div class="divide-y divide-gray-50">
|
|
{% for alert in risk_alerts %}
|
|
<div id="alert-{{ alert.id }}" class="flex items-center justify-between px-5 py-3 hover:bg-gray-50 transition">
|
|
<div class="flex items-center gap-3 min-w-0">
|
|
<span class="w-2.5 h-2.5 rounded-full flex-shrink-0 {% if alert.severity == 'critical' %}bg-red-500{% else %}bg-amber-500{% endif %}"></span>
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium text-gray-900 truncate">{{ alert.title_en }}</p>
|
|
<p class="text-xs text-gray-500">
|
|
{{ alert.get_insight_type_display }}
|
|
{% if alert.hospital %} · {{ alert.hospital.name }}{% endif %}
|
|
· {{ alert.created_at|timesince }} {% trans "ago" %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-shrink-0 ml-4">
|
|
{% if alert.status == "new" %}
|
|
<button hx-post="{% url 'executive:acknowledge_insight' alert.id %}"
|
|
hx-target="#alert-{{ alert.id }} .ack-area"
|
|
hx-swap="innerHTML"
|
|
class="text-xs px-3 py-1.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition font-medium">
|
|
{% trans "Acknowledge" %}
|
|
</button>
|
|
{% endif %}
|
|
<span class="ack-area">
|
|
{% if alert.status == "acknowledged" %}
|
|
<span class="text-xs text-green-600 font-medium">{% trans "Acknowledged" %}</span>
|
|
{% endif %}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="py-8 text-center"><p class="text-sm text-gray-400">{% trans "No active risk alerts" %}</p></div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# AI Recommendations #}
|
|
{% if ai_recommendations %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div class="px-5 py-4 border-b border-gray-100">
|
|
<h3 class="text-sm font-bold text-gray-900 flex items-center gap-2">
|
|
<svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
|
{% trans "AI Recommended Actions" %}
|
|
</h3>
|
|
</div>
|
|
<div class="divide-y divide-gray-50">
|
|
{% for rec in ai_recommendations %}
|
|
<div class="px-5 py-4">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="min-w-0">
|
|
<p class="text-sm font-medium text-gray-900">{{ rec.title_en }}</p>
|
|
<p class="text-xs text-gray-500 mt-1">{{ rec.description_en|truncatewords:25 }}</p>
|
|
{% if rec.hospital %}<span class="inline-block mt-1.5 px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">{{ rec.hospital.name }}</span>{% endif %}
|
|
</div>
|
|
<span class="flex-shrink-0 px-2 py-0.5 text-xs font-semibold rounded-full
|
|
{% if rec.priority == 'urgent' %}bg-red-100 text-red-800
|
|
{% elif rec.priority == 'high' %}bg-amber-100 text-amber-800
|
|
{% else %}bg-blue-100 text-blue-800{% endif %}">
|
|
{{ rec.get_priority_display }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# AI Insights Analysis #}
|
|
<div class="mt-6">
|
|
<div id="ai-insights-result">
|
|
<div class="text-center py-4">
|
|
<button hx-post="{% url 'executive:analyze_insights' %}"
|
|
hx-include="[name='hospital']"
|
|
hx-target="#ai-insights-result"
|
|
hx-swap="innerHTML"
|
|
class="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
|
{% trans "Analyze Risks with AI" %}
|
|
</button>
|
|
<p class="text-xs text-gray-400 mt-1">{% trans "Get AI-powered risk assessment and recommendations" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section id="tab-reports" class="tab-content hidden">
|
|
<div class="bg-white rounded-xl p-5 shadow-sm border border-gray-200 mb-6">
|
|
<h3 class="text-sm font-bold text-gray-900 mb-3">{% trans "Generate New Report" %}</h3>
|
|
<form method="post" action="{% url 'executive:generate_report' %}" class="flex flex-wrap items-end gap-3">
|
|
{% csrf_token %}
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "Type" %}</label>
|
|
<select name="report_type" class="text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
<option value="weekly">{% trans "Weekly" %}</option>
|
|
<option value="monthly">{% trans "Monthly" %}</option>
|
|
<option value="quarterly">{% trans "Quarterly" %}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "Start" %}</label>
|
|
<input type="date" name="start_date" class="text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "End" %}</label>
|
|
<input type="date" name="end_date" class="text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
</div>
|
|
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition">
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
{% trans "Generate" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div class="px-5 py-4 border-b border-gray-100">
|
|
<h3 class="text-sm font-bold text-gray-900">{% trans "Recent Reports" %}</h3>
|
|
</div>
|
|
{% if recent_reports %}
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50 text-xs text-gray-500 uppercase">
|
|
<tr>
|
|
<th class="px-5 py-3 text-left">{% trans "Type" %}</th>
|
|
<th class="px-5 py-3 text-left">{% trans "Period" %}</th>
|
|
<th class="px-5 py-3 text-left">{% trans "Generated" %}</th>
|
|
<th class="px-5 py-3 text-left">{% trans "Status" %}</th>
|
|
<th class="px-5 py-3 text-right">{% trans "Actions" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-50">
|
|
{% for report in recent_reports %}
|
|
<tr class="hover:bg-gray-50 transition">
|
|
<td class="px-5 py-3 font-medium text-gray-900">{{ report.get_report_type_display }}</td>
|
|
<td class="px-5 py-3 text-gray-600">{{ report.start_date }} — {{ report.end_date }}</td>
|
|
<td class="px-5 py-3 text-gray-500">{{ report.created_at|timesince }} {% trans "ago" %}</td>
|
|
<td class="px-5 py-3">
|
|
<span class="px-2 py-0.5 text-xs font-semibold rounded-full
|
|
{% if report.status == 'completed' %}bg-green-100 text-green-800
|
|
{% elif report.status == 'generating' %}bg-blue-100 text-blue-800
|
|
{% elif report.status == 'failed' %}bg-red-100 text-red-800
|
|
{% else %}bg-gray-100 text-gray-800{% endif %}">
|
|
{{ report.get_status_display }}
|
|
</span>
|
|
</td>
|
|
<td class="px-5 py-3 text-right">
|
|
{% if report.status == 'completed' %}
|
|
<a href="{% url 'executive:report_pdf' report.id %}" class="inline-flex items-center gap-1 text-indigo-600 hover:text-indigo-700 font-medium">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
{% trans "PDF" %}
|
|
</a>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="py-10 text-center">
|
|
<svg class="w-12 h-12 text-gray-300 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
|
<p class="text-sm text-gray-400">{% trans "No reports yet" %}</p>
|
|
<p class="text-xs text-gray-400 mt-1">{% trans "Use the form above to generate your first report" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
(function() {
|
|
var tabs = document.querySelectorAll('.tab-btn');
|
|
var sections = document.querySelectorAll('.tab-content');
|
|
|
|
function switchTab(id) {
|
|
tabs.forEach(function(b) {
|
|
var on = b.dataset.tab === id;
|
|
b.classList.toggle('border-indigo-600', on);
|
|
b.classList.toggle('text-indigo-600', on);
|
|
b.classList.toggle('font-semibold', on);
|
|
b.classList.toggle('border-transparent', !on);
|
|
b.classList.toggle('text-gray-500', !on);
|
|
b.classList.toggle('font-medium', !on);
|
|
});
|
|
sections.forEach(function(s) { s.classList.toggle('hidden', s.id !== 'tab-' + id); });
|
|
history.replaceState(null, '', '#' + id);
|
|
if (id === 'trends') initCharts();
|
|
if (id === 'overview') initSparklines();
|
|
}
|
|
|
|
tabs.forEach(function(b) { b.addEventListener('click', function() { switchTab(this.dataset.tab); }); });
|
|
var h = location.hash.replace('#','');
|
|
if (h && document.getElementById('tab-' + h)) switchTab(h);
|
|
window.switchTab = switchTab;
|
|
|
|
// ── Sparklines ──
|
|
var sparksDone = false;
|
|
function initSparklines() {
|
|
if (sparksDone || typeof ApexCharts === 'undefined') return;
|
|
sparksDone = true;
|
|
var cfg = {
|
|
complaints: { d: {{ kpi_cards.complaints.sparkline|safe }}, c: '#ef4444' },
|
|
surveys: { d: {{ kpi_cards.surveys.sparkline|safe }}, c: '#10b981' },
|
|
actions: { d: {{ kpi_cards.actions.sparkline|safe }}, c: '#3b82f6' },
|
|
};
|
|
Object.keys(cfg).forEach(function(k) {
|
|
var el = document.getElementById('sparkline-' + k);
|
|
if (!el || !cfg[k].d || !cfg[k].d.length) return;
|
|
new ApexCharts(el, {
|
|
series: [{ data: cfg[k].d }],
|
|
chart: { type: 'area', height: 32, sparkline: { enabled: true }, animations: { enabled: false } },
|
|
stroke: { curve: 'smooth', width: 1.5 },
|
|
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.3, opacityTo: 0.05, stops: [0, 100] } },
|
|
colors: [cfg[k].c],
|
|
tooltip: { enabled: false },
|
|
grid: { show: false },
|
|
xaxis: { labels: { show: false }, axisBorder: { show: false }, axisTicks: { show: false } },
|
|
yaxis: { show: false },
|
|
}).render();
|
|
});
|
|
}
|
|
|
|
// ── Charts ──
|
|
var chartsDone = false;
|
|
function initCharts() {
|
|
if (chartsDone || typeof ApexCharts === 'undefined') return;
|
|
chartsDone = true;
|
|
|
|
var cd = {{ chart_data.complaints_trend|safe }};
|
|
var ce = document.getElementById('complaintsChart');
|
|
var cempty = document.getElementById('complaintsChartEmpty');
|
|
if (ce && cd && cd.length > 0) {
|
|
ce.classList.remove('hidden');
|
|
if (cempty) cempty.classList.add('hidden');
|
|
new ApexCharts(ce, {
|
|
series: [{ name: '{% trans "Complaints" %}', data: cd.map(function(d){return d.value;}) }],
|
|
chart: { type: 'area', height: 320, toolbar: { show: false }, fontFamily: 'Inter, sans-serif' },
|
|
colors: ['#ef4444'], stroke: { curve: 'smooth', width: 2 },
|
|
fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.1 } },
|
|
dataLabels: { enabled: false },
|
|
xaxis: { categories: cd.map(function(d){return d.date;}), labels: { rotate: -45, style: { fontSize: '10px' } } },
|
|
yaxis: { labels: { formatter: function(v){return Math.round(v);} } },
|
|
grid: { borderColor: '#f1f5f9', strokeDashArray: 5 },
|
|
tooltip: { theme: 'light' },
|
|
}).render();
|
|
} else if (ce) { ce.classList.add('hidden'); if(cempty) cempty.classList.remove('hidden'); }
|
|
|
|
var sd = {{ chart_data.surveys_satisfaction_trend|safe }};
|
|
var se = document.getElementById('satisfactionChart');
|
|
var sempty = document.getElementById('satisfactionChartEmpty');
|
|
if (se && sd && sd.length > 0) {
|
|
se.classList.remove('hidden');
|
|
if (sempty) sempty.classList.add('hidden');
|
|
new ApexCharts(se, {
|
|
series: [{ name: '{% trans "Satisfaction" %}', data: sd.map(function(d){return d.value;}) }],
|
|
chart: { type: 'line', height: 300, toolbar: { show: false }, fontFamily: 'Inter, sans-serif' },
|
|
colors: ['#10b981'], stroke: { curve: 'smooth', width: 2 },
|
|
dataLabels: { enabled: false },
|
|
xaxis: { categories: sd.map(function(d){return d.date;}), labels: { rotate: -45, style: { fontSize: '10px' } } },
|
|
yaxis: { min: 0, max: 5, labels: { formatter: function(v){return v.toFixed(1);} } },
|
|
grid: { borderColor: '#f1f5f9', strokeDashArray: 5 },
|
|
tooltip: { theme: 'light' },
|
|
}).render();
|
|
} else if (se) { se.classList.add('hidden'); if(sempty) sempty.classList.remove('hidden'); }
|
|
}
|
|
|
|
initSparklines();
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|