513 lines
21 KiB
HTML
513 lines
21 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "PX Command Center" %} - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
<!-- Page Header -->
|
|
<header class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-2">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-navy flex items-center gap-3">
|
|
<i data-lucide="layout-dashboard" class="w-7 h-7"></i>
|
|
{% trans "PX Command Center" %}
|
|
</h1>
|
|
<p class="text-sm text-slate mt-1">{% trans "Real-time overview of your patient experience operations" %}</p>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="text-right">
|
|
<p class="text-xs text-slate uppercase tracking-wider">{% trans "Last Updated" %}</p>
|
|
<p class="text-sm font-bold text-navy">{% now "j M Y, H:i" %}</p>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Stat Cards -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Total Complaints" %}</p>
|
|
<p class="text-3xl font-bold text-navy">{{ stats.total_complaints }}</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="trending-up" class="w-4 h-4 text-green-500"></i>
|
|
<span class="text-sm font-bold text-green-500">+12.5%</span>
|
|
<span class="text-xs text-slate">vs last month</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-blue-50 rounded-xl">
|
|
<i data-lucide="message-square" class="w-6 h-6 text-blue"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Avg. Resolution" %}</p>
|
|
<p class="text-3xl font-bold text-navy">{{ stats.avg_resolution_time }}</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="trending-down" class="w-4 h-4 text-green-500"></i>
|
|
<span class="text-sm font-bold text-green-500">-8.3%</span>
|
|
<span class="text-xs text-slate">faster</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-green-50 rounded-xl">
|
|
<i data-lucide="clock" class="w-6 h-6 text-green-500"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Patient Satisfaction" %}</p>
|
|
<p class="text-3xl font-bold text-navy">{{ stats.satisfaction_score }}%</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="trending-up" class="w-4 h-4 text-green-500"></i>
|
|
<span class="text-sm font-bold text-green-500">+5.2%</span>
|
|
<span class="text-xs text-slate">improvement</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-purple-50 rounded-xl">
|
|
<i data-lucide="smile" class="w-6 h-6 text-purple-500"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card stat-card">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<p class="text-xs font-bold text-slate uppercase tracking-wider mb-1">{% trans "Active Actions" %}</p>
|
|
<p class="text-3xl font-bold text-navy">{{ stats.active_actions }}</p>
|
|
<div class="flex items-center gap-1.5 mt-2">
|
|
<i data-lucide="activity" class="w-4 h-4 text-blue"></i>
|
|
<span class="text-sm font-bold text-navy">{{ stats.new_today }}</span>
|
|
<span class="text-xs text-slate">new today</span>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-orange-50 rounded-xl">
|
|
<i data-lucide="check-circle" class="w-6 h-6 text-orange-500"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Complaints Trend -->
|
|
<div class="lg:col-span-2 card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="trending-up" class="w-4 h-4"></i>
|
|
{% trans "Complaints Trend" %}
|
|
</h3>
|
|
<div class="flex gap-2">
|
|
<button class="px-3 py-1.5 text-xs font-bold text-slate hover:text-navy hover:bg-light rounded-lg transition">Day</button>
|
|
<button class="px-3 py-1.5 text-xs font-bold bg-navy text-white rounded-lg">Week</button>
|
|
<button class="px-3 py-1.5 text-xs font-bold text-slate hover:text-navy hover:bg-light rounded-lg transition">Month</button>
|
|
</div>
|
|
</div>
|
|
<div id="complaintsTrendChart" class="h-[320px]"></div>
|
|
</div>
|
|
|
|
<!-- Survey Satisfaction -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="star" class="w-4 h-4"></i>
|
|
{% trans "Survey Satisfaction" %}
|
|
</h3>
|
|
</div>
|
|
<div class="text-center py-8">
|
|
<div class="text-6xl font-bold text-navy mb-2">{{ chart_data.survey_satisfaction|floatformat:1 }}</div>
|
|
<p class="text-sm text-slate mb-6">{% trans "Average Score (Last 30 Days)" %}</p>
|
|
<div class="w-full bg-slate-100 rounded-full h-3 overflow-hidden">
|
|
<div class="bg-gradient-to-r from-blue to-navy h-full transition-all duration-500"
|
|
style="width: {{ chart_data.survey_satisfaction|floatformat:0 }}%"></div>
|
|
</div>
|
|
<div class="mt-4 flex justify-between text-xs text-slate">
|
|
<span>0</span>
|
|
<span>50</span>
|
|
<span>100</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Feed Row -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Latest High Severity Complaints -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
|
|
{% trans "Latest High Severity Complaints" %}
|
|
</h3>
|
|
<a href="{% url 'complaints:complaint_list' %}?severity=high,critical"
|
|
class="text-xs font-bold text-navy hover:text-blue flex items-center gap-1 transition">
|
|
{% trans "View All" %}
|
|
<i data-lucide="arrow-right" class="w-3 h-3"></i>
|
|
</a>
|
|
|
|
</div>
|
|
<div class="divide-y divide-slate-100">
|
|
{% if latest_complaints %}
|
|
{% for complaint in latest_complaints %}
|
|
<a href="{% url 'complaints:complaint_detail' complaint.id %}"
|
|
class="block p-5 hover:bg-light transition group">
|
|
|
|
<div class="flex justify-between items-start gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-bold text-navy mb-2 truncate group-hover:text-blue transition">{{ complaint.title }}</h4>
|
|
<div class="flex items-center gap-3 text-sm text-slate mb-2">
|
|
<span class="flex items-center gap-1">
|
|
<i data-lucide="user" class="w-3 h-3"></i>
|
|
{{ complaint.patient.get_full_name }}
|
|
</span>
|
|
<span class="flex items-center gap-1">
|
|
<i data-lucide="building" class="w-3 h-3"></i>
|
|
{{ complaint.hospital.name }}
|
|
</span>
|
|
</div>
|
|
<div class="text-xs text-slate-500">
|
|
<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>
|
|
{{ complaint.created_at|timesince }} {% trans "ago" %}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col gap-2 items-end">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold
|
|
{% if complaint.severity == 'critical' %}bg-red-100 text-red-600
|
|
{% elif complaint.severity == 'high' %}bg-orange-100 text-orange-600
|
|
{% elif complaint.severity == 'medium' %}bg-yellow-100 text-yellow-600
|
|
{% else %}bg-blue-100 text-blue-600{% endif %}">
|
|
{{ complaint.get_severity_display }}
|
|
</span>
|
|
{% if complaint.is_overdue %}
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-red-500 text-white">
|
|
{% trans "OVERDUE" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="p-8 text-center">
|
|
<div class="bg-green-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i data-lucide="check-circle" class="w-8 h-8 text-green-500"></i>
|
|
</div>
|
|
<p class="text-sm text-slate">{% trans "No high severity complaints" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Latest Escalated Actions -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="arrow-up-circle" class="w-4 h-4"></i>
|
|
{% trans "Latest Escalated Actions" %}
|
|
</h3>
|
|
<a href="{% url 'actions:action_list' %}"
|
|
class="text-xs font-bold text-navy hover:text-blue flex items-center gap-1 transition">
|
|
{% trans "View All" %}
|
|
<i data-lucide="arrow-right" class="w-3 h-3"></i>
|
|
</a>
|
|
|
|
</div>
|
|
<div class="divide-y divide-slate-100">
|
|
{% if latest_actions %}
|
|
{% for action in latest_actions %}
|
|
<a href="{% url 'actions:action_detail' action.id %}"
|
|
class="block p-5 hover:bg-light transition group">
|
|
|
|
<div class="flex justify-between items-start gap-4">
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-bold text-navy mb-2 truncate group-hover:text-blue transition">{{ action.title }}</h4>
|
|
<div class="flex items-center gap-3 text-sm text-slate mb-2">
|
|
<span class="flex items-center gap-1">
|
|
<i data-lucide="building" class="w-3 h-3"></i>
|
|
{{ action.hospital.name }}
|
|
</span>
|
|
{% if action.assigned_to %}
|
|
<span class="flex items-center gap-1">
|
|
<i data-lucide="user" class="w-3 h-3"></i>
|
|
{{ action.assigned_to.get_full_name }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="text-xs text-slate-500">
|
|
<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>
|
|
{% trans "Escalated" %} {{ action.escalated_at|timesince }} {% trans "ago" %}
|
|
</div>
|
|
</div>
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-red-100 text-red-600">
|
|
{% trans "Level" %} {{ action.escalation_level }}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="p-8 text-center">
|
|
<div class="bg-green-50 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i data-lucide="check-circle" class="w-8 h-8 text-green-500"></i>
|
|
</div>
|
|
<p class="text-sm text-slate">{% trans "No escalated actions" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Physicians This Month -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="trophy" class="w-4 h-4"></i>
|
|
{% trans "Top Physicians This Month" %}
|
|
</h3>
|
|
<a href="{% url 'physicians:leaderboard' %}"
|
|
class="text-xs font-bold text-navy hover:text-blue flex items-center gap-1 transition">
|
|
{% trans "View Leaderboard" %}
|
|
<i data-lucide="arrow-right" class="w-3 h-3"></i>
|
|
</a>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="bg-light">
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider" style="width: 80px;">{% trans "Rank" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Physician" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Specialization" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Department" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Rating" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Surveys" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Sentiment" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for rating in top_physicians %}
|
|
<tr class="hover:bg-light transition cursor-pointer group">
|
|
<td class="px-6 py-4">
|
|
{% if forloop.counter == 1 %}
|
|
<div class="w-10 h-10 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="trophy" class="w-5 h-5 text-yellow-500 fill-current"></i>
|
|
</div>
|
|
{% elif forloop.counter == 2 %}
|
|
<div class="w-10 h-10 bg-slate-200 rounded-full flex items-center justify-center">
|
|
<i data-lucide="trophy" class="w-5 h-5 text-slate-500 fill-current"></i>
|
|
</div>
|
|
{% elif forloop.counter == 3 %}
|
|
<div class="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="trophy" class="w-5 h-5 text-amber-600 fill-current"></i>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-slate-400 font-bold">#{{ forloop.counter }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="font-bold text-navy">{{ rating.staff }}</div>
|
|
<div class="text-xs text-slate">{{ rating.staff.license_number }}</div>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-slate">{{ rating.staff.specialization }}</td>
|
|
<td class="px-6 py-4 text-sm text-slate">
|
|
{% if rating.staff.department %}
|
|
{{ rating.staff.department.name }}
|
|
{% else %}
|
|
<span class="text-slate-400">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="text-2xl font-bold text-green-500">{{ rating.average_rating|floatformat:2 }}</div>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-slate-100 text-slate">{{ rating.total_surveys }}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<div class="flex gap-1.5">
|
|
<span class="px-2 py-1 rounded-full text-xs font-bold bg-green-100 text-green-600" title="{% trans 'Positive' %}">{{ rating.positive_count }}</span>
|
|
<span class="px-2 py-1 rounded-full text-xs font-bold bg-yellow-100 text-yellow-600" title="{% trans 'Neutral' %}">{{ rating.neutral_count }}</span>
|
|
<span class="px-2 py-1 rounded-full text-xs font-bold bg-red-100 text-red-600" title="{% trans 'Negative' %}">{{ rating.negative_count }}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if physician_stats.total_physicians %}
|
|
<div class="bg-gradient-to-r from-light to-blue-50 px-6 py-4 border-t border-slate-100">
|
|
<div class="grid grid-cols-3 gap-6 text-center">
|
|
<div>
|
|
<div class="text-2xl font-bold text-navy">{{ physician_stats.total_physicians }}</div>
|
|
<div class="text-xs text-slate uppercase tracking-wider mt-1">{% trans "Physicians Rated" %}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-2xl font-bold text-navy">{{ physician_stats.avg_rating|floatformat:2 }}</div>
|
|
<div class="text-xs text-slate uppercase tracking-wider mt-1">{% trans "Average Rating" %}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-2xl font-bold text-navy">{{ physician_stats.total_surveys }}</div>
|
|
<div class="text-xs text-slate uppercase tracking-wider mt-1">{% trans "Total Surveys" %}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Latest Integration Events -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title flex items-center gap-2">
|
|
<i data-lucide="activity" class="w-4 h-4"></i>
|
|
{% trans "Latest Integration Events" %}
|
|
</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="bg-light">
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Source" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Event Code" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Encounter ID" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Status" %}</th>
|
|
<th class="px-6 py-4 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Processed At" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-slate-100">
|
|
{% for event in latest_events %}
|
|
<tr class="hover:bg-light transition">
|
|
<td class="px-6 py-4">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-light text-navy">{{ event.get_source_system_display }}</span>
|
|
</td>
|
|
<td class="px-6 py-4">
|
|
<code class="px-2 py-1 bg-slate-100 rounded text-sm">{{ event.event_code }}</code>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-slate">{{ event.encounter_id }}</td>
|
|
<td class="px-6 py-4">
|
|
<span class="px-3 py-1 rounded-full text-xs font-bold bg-green-100 text-green-600">{{ event.get_status_display }}</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-slate">
|
|
<i data-lucide="clock" class="w-3 h-3 inline mr-1"></i>
|
|
{{ event.processed_at|timesince }} {% trans "ago" %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if not latest_events %}
|
|
<div class="p-8 text-center">
|
|
<div class="bg-slate-100 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i data-lucide="inbox" class="w-8 h-8 text-slate-400"></i>
|
|
</div>
|
|
<p class="text-sm text-slate">{% trans "No recent events" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Complaints Trend Chart - ApexCharts
|
|
const chartElement = document.getElementById('complaintsTrendChart');
|
|
if (chartElement) {
|
|
const trendData = {{ chart_data.complaints_trend|safe }};
|
|
|
|
var options = {
|
|
series: [{
|
|
name: '{% trans "Complaints" %}',
|
|
data: trendData.map(d => d.count)
|
|
}],
|
|
chart: {
|
|
type: 'area',
|
|
height: 320,
|
|
toolbar: {
|
|
show: false
|
|
},
|
|
fontFamily: 'Inter, sans-serif'
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 3
|
|
},
|
|
colors: ['#005696'],
|
|
fill: {
|
|
type: 'gradient',
|
|
gradient: {
|
|
shadeIntensity: 1,
|
|
opacityFrom: 0.4,
|
|
opacityTo: 0.1,
|
|
stops: [0, 90, 100]
|
|
}
|
|
},
|
|
xaxis: {
|
|
categories: trendData.map(d => d.date),
|
|
labels: {
|
|
style: {
|
|
fontSize: '12px',
|
|
fontFamily: 'Inter, sans-serif',
|
|
colors: ['#64748b']
|
|
}
|
|
},
|
|
axisBorder: {
|
|
show: false
|
|
},
|
|
axisTicks: {
|
|
show: false
|
|
}
|
|
},
|
|
yaxis: {
|
|
min: 0,
|
|
forceNiceScale: true,
|
|
labels: {
|
|
style: {
|
|
fontSize: '12px',
|
|
fontFamily: 'Inter, sans-serif',
|
|
colors: ['#64748b']
|
|
}
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#f1f5f9',
|
|
strokeDashArray: 5,
|
|
yaxis: {
|
|
lines: {
|
|
show: true
|
|
}
|
|
}
|
|
},
|
|
tooltip: {
|
|
theme: 'light',
|
|
x: {
|
|
format: 'dd MMM yyyy'
|
|
},
|
|
style: {
|
|
fontSize: '12px',
|
|
fontFamily: 'Inter, sans-serif'
|
|
}
|
|
},
|
|
dataLabels: {
|
|
enabled: false
|
|
},
|
|
markers: {
|
|
size: 0,
|
|
hover: {
|
|
size: 6
|
|
}
|
|
}
|
|
};
|
|
|
|
var chart = new ApexCharts(chartElement, options);
|
|
chart.render();
|
|
}
|
|
|
|
// Initialize Lucide icons
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
});
|
|
</script>
|
|
{% endblock %} |