193 lines
14 KiB
HTML
193 lines
14 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Predictive Insights" %} — PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="p-4 lg:p-6 max-w-[1600px] mx-auto">
|
|
|
|
{# ── Header ── #}
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<a href="{% url 'executive:executive_dashboard' %}#insights" class="text-gray-400 hover:text-gray-600 transition">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
|
</a>
|
|
<h1 class="text-2xl font-bold text-gray-900">{% trans "Predictive Insights" %}</h1>
|
|
</div>
|
|
<p class="text-sm text-gray-500">{% trans "AI-detected patterns, anomalies, and early warnings" %}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-gray-100 text-gray-700">{% trans "Total" %}: {{ total_insights }}</span>
|
|
{% if critical_insights %}
|
|
<span class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-red-100 text-red-800">{% trans "Critical" %}: {{ critical_insights }}</span>
|
|
{% endif %}
|
|
{% if high_insights %}
|
|
<span class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-amber-100 text-amber-800">{% trans "High" %}: {{ high_insights }}</span>
|
|
{% endif %}
|
|
<span class="px-3 py-1.5 text-xs font-semibold rounded-lg bg-blue-100 text-blue-800">{% trans "New" %}: {{ new_insights }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Collapsible Filters ── #}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
|
<button id="filterToggle" onclick="document.getElementById('filterBody').classList.toggle('hidden'); this.querySelector('.chevron').classList.toggle('rotate-180');"
|
|
class="w-full flex items-center justify-between px-5 py-3 text-sm font-medium text-gray-700 hover:bg-gray-50 rounded-xl transition">
|
|
<span class="flex items-center gap-2">
|
|
<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="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/></svg>
|
|
{% trans "Filters" %}
|
|
{% if current_filters.severity or current_filters.status or current_filters.insight_type or current_filters.hospital %}
|
|
<span class="px-1.5 py-0.5 text-[10px] font-bold bg-indigo-100 text-indigo-700 rounded-full">
|
|
{% widthratio 1 1 0 %}
|
|
{% if current_filters.severity %}1{% endif %}
|
|
{% if current_filters.status %}1{% endif %}
|
|
{% if current_filters.insight_type %}1{% endif %}
|
|
{% if current_filters.hospital %}1{% endif %}
|
|
active
|
|
</span>
|
|
{% endif %}
|
|
</span>
|
|
<svg class="w-4 h-4 chevron transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
|
</button>
|
|
<div id="filterBody" class="{% if not current_filters.severity and not current_filters.status and not current_filters.insight_type and not current_filters.hospital %}hidden{% endif %} border-t border-gray-100 px-5 py-4">
|
|
<form method="get" id="filterForm" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "Severity" %}</label>
|
|
<select name="severity" onchange="this.form.submit()" class="w-full text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
<option value="">{% trans "All Severities" %}</option>
|
|
{% for value, label in severity_choices %}
|
|
<option value="{{ value }}" {% if current_filters.severity == value %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "Status" %}</label>
|
|
<select name="status" onchange="this.form.submit()" class="w-full text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
<option value="">{% trans "All Statuses" %}</option>
|
|
{% for value, label in status_choices %}
|
|
<option value="{{ value }}" {% if current_filters.status == value %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "Type" %}</label>
|
|
<select name="insight_type" onchange="this.form.submit()" class="w-full text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
<option value="">{% trans "All Types" %}</option>
|
|
{% for value, label in insight_type_choices %}
|
|
<option value="{{ value }}" {% if current_filters.insight_type == value %}selected{% endif %}>{{ label }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% if available_hospitals %}
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">{% trans "Hospital" %}</label>
|
|
<select name="hospital" onchange="this.form.submit()" class="w-full text-sm border-gray-300 rounded-lg py-2 px-3">
|
|
<option value="">{% trans "All Hospitals" %}</option>
|
|
{% for hospital in available_hospitals %}
|
|
<option value="{{ hospital.id }}" {% if current_filters.hospital == hospital.id|stringformat:"s" %}selected{% endif %}>{{ hospital.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% endif %}
|
|
</form>
|
|
{% if current_filters.severity or current_filters.status or current_filters.insight_type or current_filters.hospital %}
|
|
<div class="mt-3 pt-3 border-t border-gray-100">
|
|
<a href="{% url 'executive:predictive_insights' %}" class="text-xs text-indigo-600 hover:text-indigo-700 font-medium">{% trans "Clear all filters" %}</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Insights List ── #}
|
|
<div class="space-y-3">
|
|
{% for insight in insights %}
|
|
<div id="insight-{{ insight.id }}" class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden border-l-4
|
|
{% if insight.severity == 'critical' %}border-l-red-500
|
|
{% elif insight.severity == 'high' %}border-l-amber-500
|
|
{% elif insight.severity == 'medium' %}border-l-yellow-500
|
|
{% else %}border-l-blue-500{% endif %}">
|
|
<div class="p-5">
|
|
<div class="flex items-start justify-between gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2 mb-2 flex-wrap">
|
|
<span class="px-2 py-0.5 text-xs font-semibold rounded-full
|
|
{% if insight.severity == 'critical' %}bg-red-100 text-red-800
|
|
{% elif insight.severity == 'high' %}bg-amber-100 text-amber-800
|
|
{% elif insight.severity == 'medium' %}bg-yellow-100 text-yellow-800
|
|
{% else %}bg-blue-100 text-blue-800{% endif %}">
|
|
{{ insight.get_severity_display }}
|
|
</span>
|
|
<span class="text-xs text-gray-400">{{ insight.get_insight_type_display }}</span>
|
|
<span class="text-xs text-gray-400">·</span>
|
|
<span class="text-xs text-gray-400">{{ insight.created_at|timesince }} {% trans "ago" %}</span>
|
|
{% if insight.status == 'acknowledged' %}
|
|
<span class="px-2 py-0.5 text-xs font-medium bg-green-100 text-green-800 rounded-full">{% trans "Acknowledged" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
<h3 class="text-base font-semibold text-gray-900 mb-1">{{ insight.title_en }}</h3>
|
|
<p class="text-sm text-gray-600 mb-2">{{ insight.description_en|truncatewords:40 }}</p>
|
|
|
|
{% if insight.recommendation_en %}
|
|
<div class="bg-indigo-50 rounded-lg p-3 mb-2">
|
|
<p class="text-xs font-semibold text-indigo-900 mb-0.5">{% trans "AI Recommendation" %}</p>
|
|
<p class="text-xs text-indigo-800">{{ insight.recommendation_en }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="flex items-center gap-4 text-xs text-gray-500">
|
|
{% if insight.hospital %}<span>{{ insight.hospital.name }}</span>{% endif %}
|
|
{% if insight.department %}<span>{{ insight.department.name }}</span>{% endif %}
|
|
{% if insight.confidence_score %}<span>{% trans "Confidence" %}: {{ insight.confidence_score|floatformat:0 }}%</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if insight.status == 'new' %}
|
|
<div class="flex-shrink-0">
|
|
<button hx-post="{% url 'executive:acknowledge_insight' insight.id %}"
|
|
hx-target="#insight-{{ insight.id }} .ack-btn-area"
|
|
hx-swap="innerHTML"
|
|
class="px-3 py-1.5 bg-indigo-600 text-white text-xs font-medium rounded-lg hover:bg-indigo-700 transition">
|
|
{% trans "Acknowledge" %}
|
|
</button>
|
|
<span class="ack-btn-area"></span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="bg-white rounded-xl p-12 text-center shadow-sm border border-gray-200">
|
|
<svg class="w-12 h-12 mx-auto text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
<h3 class="text-lg font-semibold text-gray-900 mb-1">{% trans "No Insights Match" %}</h3>
|
|
<p class="text-sm text-gray-500">{% trans "Try adjusting your filters" %}</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{# ── Pagination ── #}
|
|
{% if page_obj.has_other_pages %}
|
|
<div class="mt-6 flex items-center justify-center gap-2">
|
|
{% if page_obj.has_previous %}
|
|
<a href="?page=1{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
|
|
class="px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition">{% trans "First" %}</a>
|
|
<a href="?page={{ page_obj.previous_page_number }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
|
|
class="px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition">{% trans "Previous" %}</a>
|
|
{% endif %}
|
|
<span class="px-4 py-2 text-sm text-gray-600">{% trans "Page" %} {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span>
|
|
{% if page_obj.has_next %}
|
|
<a href="?page={{ page_obj.next_page_number }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
|
|
class="px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition">{% trans "Next" %}</a>
|
|
<a href="?page={{ page_obj.paginator.num_pages }}{% if current_filters.severity %}&severity={{ current_filters.severity }}{% endif %}{% if current_filters.status %}&status={{ current_filters.status }}{% endif %}{% if current_filters.insight_type %}&insight_type={{ current_filters.insight_type }}{% endif %}{% if current_filters.hospital %}&hospital={{ current_filters.hospital }}{% endif %}"
|
|
class="px-3 py-2 text-sm bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition">{% trans "Last" %}</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
if (typeof lucide !== 'undefined') lucide.createIcons();
|
|
</script>
|
|
{% endblock %}
|