338 lines
19 KiB
HTML
338 lines
19 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n static %}
|
|
|
|
{% block title %}{% trans "HIS Logs" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.stats-card {
|
|
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
|
}
|
|
.stats-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
|
|
}
|
|
.channel-badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
.channel-email { background: #eff6ff; color: #3b82f6; }
|
|
.channel-sms { background: #fff7ed; color: #f97316; }
|
|
.channel-his_event { background: #f0fdf4; color: #22c55e; }
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
.status-success, .status-sent { background: #dcfce7; color: #16a34a; }
|
|
.status-failed { background: #fee2e2; color: #dc2626; }
|
|
.status-partial { background: #fef3c7; color: #d97706; }
|
|
.json-preview {
|
|
background: #f5f5f5;
|
|
padding: 8px;
|
|
border-radius: 8px;
|
|
font-family: monospace;
|
|
font-size: 0.85rem;
|
|
max-height: 100px;
|
|
overflow: auto;
|
|
}
|
|
.log-row:hover {
|
|
background-color: #fff1f2;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<!-- 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="bot" class="w-8 h-8 text-navy"></i>
|
|
{% trans "HIS Logs" %}
|
|
</h2>
|
|
<p class="text-gray-500">{% trans "View all HIS requests and responses" %}</p>
|
|
</div>
|
|
{% if user.is_superuser or user.is_px_admin %}
|
|
<form method="post" action="{% url 'simulator:clear_logs' %}" onsubmit="return confirm('{% trans "Are you sure you want to clear all HIS logs? This cannot be undone." %}');" class="inline-flex">
|
|
{% csrf_token %}
|
|
<button type="submit" class="bg-red-500 text-white px-6 py-3 rounded-xl font-bold hover:bg-red-600 transition flex items-center gap-2 shadow-lg shadow-red-200">
|
|
<i data-lucide="trash-2" class="w-5 h-5"></i> {% trans "Clear All Logs" %}
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Statistics Dashboard -->
|
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8">
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 stats-card">
|
|
<div class="text-4xl font-bold text-gray-800 mb-1">{{ stats.total }}</div>
|
|
<div class="text-gray-400 text-sm font-medium">{% trans "Total Requests" %}</div>
|
|
</div>
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 stats-card">
|
|
<div class="text-4xl font-bold text-emerald-500 mb-1">{{ stats.success }}</div>
|
|
<div class="text-gray-400 text-sm font-medium">{% trans "Success" %}</div>
|
|
</div>
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 stats-card">
|
|
<div class="text-4xl font-bold text-red-500 mb-1">{{ stats.failed }}</div>
|
|
<div class="text-gray-400 text-sm font-medium">{% trans "Failed" %}</div>
|
|
</div>
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 stats-card">
|
|
<div class="text-4xl font-bold text-amber-500 mb-1">{{ stats.partial }}</div>
|
|
<div class="text-gray-400 text-sm font-medium">{% trans "Partial" %}</div>
|
|
</div>
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 stats-card">
|
|
<div class="text-4xl font-bold text-navy mb-1">{{ stats.success_rate }}%</div>
|
|
<div class="text-gray-400 text-sm font-medium">{% trans "Success Rate" %}</div>
|
|
</div>
|
|
<div class="bg-white p-6 rounded-[2rem] shadow-sm border border-gray-50 stats-card">
|
|
<div class="text-4xl font-bold text-cyan-500 mb-1">{{ stats.avg_processing_time }}ms</div>
|
|
<div class="text-gray-400 text-sm font-medium">{% trans "Avg. Process Time" %}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Channel Breakdown -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-50">
|
|
<h3 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="hash" class="w-5 h-5 text-navy"></i>
|
|
{% trans "By Channel" %}
|
|
</h3>
|
|
<div class="flex flex-wrap gap-2">
|
|
<span class="channel-badge channel-email">
|
|
📧 {% trans "Email" %}: {{ stats.channels.email }}
|
|
</span>
|
|
<span class="channel-badge channel-sms">
|
|
📱 {% trans "SMS" %}: {{ stats.channels.sms }}
|
|
</span>
|
|
<span class="channel-badge channel-his_event">
|
|
🏥 {% trans "HIS Events" %}: {{ stats.channels.his_event }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-50">
|
|
<h3 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="check-circle-2" class="w-5 h-5 text-navy"></i>
|
|
{% trans "By Status" %}
|
|
</h3>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for stat in status_stats %}
|
|
<span class="status-badge status-{{ stat.status }}">
|
|
{{ stat.status|title }}: {{ stat.count }}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white p-6 rounded-2xl shadow-sm border border-gray-50">
|
|
<h3 class="font-bold text-gray-800 mb-4 flex items-center gap-2">
|
|
<i data-lucide="building" class="w-5 h-5 text-navy"></i>
|
|
{% trans "By Hospital" %}
|
|
</h3>
|
|
<div class="flex flex-wrap gap-2">
|
|
{% for stat in hospital_stats|slice:":3" %}
|
|
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-gray-100 text-gray-700">
|
|
{{ stat.hospital_code }}: {{ stat.count }}
|
|
</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<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">
|
|
<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>
|
|
</div>
|
|
<div class="p-6">
|
|
<form method="get" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Channel" %}</label>
|
|
<select name="channel" 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">
|
|
<option value="">{% trans "All" %}</option>
|
|
<option value="email" {% if filters.channel == 'email' %}selected{% endif %}>{% trans "Email" %}</option>
|
|
<option value="sms" {% if filters.channel == 'sms' %}selected{% endif %}>{% trans "SMS" %}</option>
|
|
<option value="his_event" {% if filters.channel == 'his_event' %}selected{% endif %}>{% trans "HIS Event" %}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Status" %}</label>
|
|
<select name="status" 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">
|
|
<option value="">{% trans "All" %}</option>
|
|
<option value="success" {% if filters.status == 'success' %}selected{% endif %}>{% trans "Success" %}</option>
|
|
<option value="sent" {% if filters.status == 'sent' %}selected{% endif %}>{% trans "Sent" %}</option>
|
|
<option value="failed" {% if filters.status == 'failed' %}selected{% endif %}>{% trans "Failed" %}</option>
|
|
<option value="partial" {% if filters.status == 'partial' %}selected{% endif %}>{% trans "Partial" %}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Visit Type" %}</label>
|
|
<select name="visit_type" 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">
|
|
<option value="">{% trans "All" %}</option>
|
|
<option value="opd" {% if filters.visit_type == 'opd' %}selected{% endif %}>OPD</option>
|
|
<option value="inpatient" {% if filters.visit_type == 'inpatient' %}selected{% endif %}>Inpatient</option>
|
|
<option value="ems" {% if filters.visit_type == 'ems' %}selected{% endif %}>EMS</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Search" %}</label>
|
|
<input type="text" name="search" 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" placeholder="ID, MRN, recipient..." value="{{ filters.search }}">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Date From" %}</label>
|
|
<input type="date" name="date_from" 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" value="{{ filters.date_from }}">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">{% trans "Date To" %}</label>
|
|
<input type="date" name="date_to" 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" value="{{ filters.date_to }}">
|
|
</div>
|
|
<div class="lg:col-span-2 flex items-end">
|
|
<button type="submit" class="bg-light0 text-white px-6 py-2.5 rounded-xl font-bold hover:bg-navy transition flex items-center gap-2">
|
|
<i data-lucide="search" class="w-4 h-4"></i> {% trans "Search" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Table -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-50 overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<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 "Timestamp" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Channel" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Status" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Summary" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Details" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">{% trans "Process Time" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
{% for log in logs %}
|
|
<tr class="log-row transition">
|
|
<td class="px-6 py-4"><strong class="text-gray-800">{{ log.request_id }}</strong></td>
|
|
<td class="px-6 py-4 text-gray-600 text-sm">{{ log.timestamp|date:"Y-m-d H:i:s" }}</td>
|
|
<td class="px-6 py-4">
|
|
<span class="channel-badge channel-{{ log.channel }}">
|
|
{% if log.channel == 'email' %}📧 {% trans "Email" %}
|
|
{% elif log.channel == 'sms' %}📱 {% trans "SMS" %}
|
|
{% else %}🏥 {% trans "HIS Event" %}{% endif %}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="status-badge status-{{ log.status }}">
|
|
{{ log.get_status_display_with_icon }}
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<strong class="text-gray-800">{{ log.get_summary }}</strong>
|
|
{% if log.patient_id %}
|
|
<br><small class="text-gray-500">MRN: {{ log.patient_id }}</small>
|
|
{% endif %}
|
|
{% if log.journey_id %}
|
|
<br><small class="text-gray-500">Journey: {{ log.journey_id }}</small>
|
|
{% endif %}
|
|
{% if log.survey_id %}
|
|
<br><small class="text-gray-500">Survey: {{ log.survey_id }}</small>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
{% if log.message_preview %}
|
|
<div class="json-preview">{{ log.message_preview|truncatechars:100 }}</div>
|
|
{% elif log.subject %}
|
|
<div><strong class="text-gray-800">{{ log.subject|truncatechars:50 }}</strong></div>
|
|
{% elif log.event_type %}
|
|
<div><strong class="text-gray-800">{{ log.event_type }}</strong></div>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4 text-gray-600 text-sm">
|
|
{% if log.processing_time_ms %}
|
|
{{ log.processing_time_ms }}ms
|
|
{% else %}
|
|
-
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<a href="{% url 'simulator:log_detail' log.request_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>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="8" class="text-center py-12">
|
|
<i data-lucide="inbox" class="w-16 h-16 text-gray-300 mx-auto mb-4"></i>
|
|
<p class="text-gray-500">{% trans "No HIS logs found." %}</p>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if page_obj.has_other_pages %}
|
|
<div class="px-6 py-4 border-t border-gray-100 flex justify-center">
|
|
<nav>
|
|
<ul class="flex gap-1">
|
|
{% if page_obj.has_previous %}
|
|
<li>
|
|
<a class="px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition" href="?page=1{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
|
|
<i data-lucide="chevrons-left" class="w-4 h-4"></i>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition" href="?page={{ page_obj.previous_page_number }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
|
|
<i data-lucide="chevron-left" class="w-4 h-4"></i>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for num in page_obj.paginator.page_range %}
|
|
{% if page_obj.number == num %}
|
|
<li><span class="px-4 py-2 bg-light0 text-white rounded-lg font-bold">{{ num }}</span></li>
|
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
<li>
|
|
<a class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition" href="?page={{ num }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">{{ num }}</a>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if page_obj.has_next %}
|
|
<li>
|
|
<a class="px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition" href="?page={{ page_obj.next_page_number }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
|
|
<i data-lucide="chevron-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="px-3 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in filters.items %}&{{ key }}={{ value }}{% endfor %}">
|
|
<i data-lucide="chevrons-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
});
|
|
</script>
|
|
{% endblock %} |