894 lines
37 KiB
HTML
894 lines
37 KiB
HTML
|
|
{% extends 'layouts/base.html' %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "PX Command Center" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* Card Styling with Borders */
|
|
.card {
|
|
background: white;
|
|
border-radius: 1rem;
|
|
border: 2px solid #e2e8f0;
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
.card:hover {
|
|
border-color: #005696;
|
|
box-shadow: 0 10px 25px -5px rgba(0, 86, 150, 0.15);
|
|
}
|
|
.card-header {
|
|
padding: 1rem 1.25rem;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
background: linear-gradient(to right, #f8fafc, #f1f5f9);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.card-title {
|
|
font-weight: 700;
|
|
color: #1e293b;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
/* Module Card Specific Styles */
|
|
.module-card {
|
|
border-left: 4px solid;
|
|
}
|
|
.module-card.complaints {
|
|
border-left-color: #ef4444;
|
|
}
|
|
.module-card.surveys {
|
|
border-left-color: #a855f7;
|
|
}
|
|
.module-card.actions {
|
|
border-left-color: #f97316;
|
|
}
|
|
.module-card.inquiries {
|
|
border-left-color: #3b82f6;
|
|
}
|
|
.module-card.observations {
|
|
border-left-color: #22c55e;
|
|
}
|
|
|
|
/* Section Containers */
|
|
.section-card {
|
|
background: white;
|
|
border-radius: 1rem;
|
|
border: 2px solid #e2e8f0;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
}
|
|
.section-header {
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
background: linear-gradient(to right, #f8fafc, #f1f5f9);
|
|
}
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
/* Table Styling */
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
}
|
|
.data-table th {
|
|
background: #f8fafc;
|
|
border-bottom: 2px solid #e2e8f0;
|
|
padding: 0.75rem 1rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
color: #64748b;
|
|
}
|
|
.data-table td {
|
|
border-bottom: 1px solid #e2e8f0;
|
|
padding: 0.75rem 1rem;
|
|
}
|
|
.data-table tr:hover td {
|
|
background: #f8fafc;
|
|
}
|
|
|
|
/* Chart Containers */
|
|
.chart-container {
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 0.75rem;
|
|
padding: 1rem;
|
|
background: #fafafa;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6" id="command-center-container">
|
|
<!-- 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-4">
|
|
<!-- Auto-refresh indicator -->
|
|
<div class="flex items-center gap-2 text-sm">
|
|
<div class="flex items-center gap-2 px-3 py-1.5 bg-green-50 rounded-full">
|
|
<span class="relative flex h-2 w-2">
|
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
|
</span>
|
|
<span class="text-green-700 font-medium">{% trans "Live" %}</span>
|
|
</div>
|
|
<span class="text-slate text-xs" id="refresh-timer">{% trans "Refreshes in 60s" %}</span>
|
|
</div>
|
|
<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" id="last-updated">{{ last_updated }}</p>
|
|
</div>
|
|
<button onclick="refreshCommandCenter()" class="p-2 bg-light rounded-lg hover:bg-slate-200 transition" title="{% trans 'Refresh Now' %}">
|
|
<i data-lucide="refresh-cw" class="w-5 h-5 text-navy" id="refresh-icon"></i>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- RED ALERT BANNER (5-Second Rule) -->
|
|
{% if has_red_alerts %}
|
|
<div class="bg-gradient-to-r from-red-500 to-red-600 rounded-xl p-4 shadow-lg animate-pulse-subtle" id="red-alert-banner">
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<div class="p-2 bg-white/20 rounded-lg">
|
|
<i data-lucide="alert-octagon" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-lg font-bold text-white">{% trans "⚠️ ATTENTION REQUIRED" %}</h2>
|
|
<p class="text-sm text-white/80">{% trans "Items requiring immediate action" %}</p>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
{% for alert in red_alerts %}
|
|
<a href="{{ alert.url }}" class="bg-white/10 hover:bg-white/20 rounded-lg p-3 transition group">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<i data-lucide="{{ alert.icon }}" class="w-4 h-4 text-white"></i>
|
|
<span class="text-xs font-medium text-white/80">{{ alert.label }}</span>
|
|
</div>
|
|
<div class="text-2xl font-bold text-white">{{ alert.value }}</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Core Module Cards Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<!-- Complaints Module Card -->
|
|
<div class="card module-card complaints hover:shadow-lg transition cursor-pointer" onclick="window.location.href='{% url 'complaints:complaint_list' %}'">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="p-2 bg-red-50 rounded-lg">
|
|
<i data-lucide="message-square-warning" class="w-5 h-5 text-red-500"></i>
|
|
</div>
|
|
<span class="text-xs font-bold px-2 py-1 rounded-full {{ complaints_module.variance_direction|yesno:'bg-green-100 text-green-600,bg-red-100 text-red-600,bg-slate-100 text-slate' }}">
|
|
{% if complaints_module.variance > 0 %}+{% endif %}{{ complaints_module.variance }}%
|
|
</span>
|
|
</div>
|
|
<h3 class="text-sm font-medium text-slate mb-1">{% trans "Complaints" %}</h3>
|
|
<p class="text-3xl font-bold text-navy">{{ complaints_module.total_active }}</p>
|
|
<div class="mt-2 flex items-center gap-2 text-xs text-slate">
|
|
<span class="flex items-center gap-1">
|
|
<span class="w-2 h-2 rounded-full bg-red-500"></span>
|
|
{{ complaints_module.by_severity.critical }} {% trans "critical" %}
|
|
</span>
|
|
</div>
|
|
{% if complaints_module.overdue > 0 %}
|
|
<div class="mt-2 text-xs font-bold text-red-500 flex items-center gap-1">
|
|
<i data-lucide="clock" class="w-3 h-3"></i>
|
|
{{ complaints_module.overdue }} {% trans "overdue" %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="px-4 py-2 bg-light border-t border-slate-100 flex justify-between items-center">
|
|
<span class="text-xs text-slate">{{ complaints_module.avg_resolution_hours }}h {% trans "avg resolution" %}</span>
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Survey Insights Module Card -->
|
|
<div class="card module-card surveys hover:shadow-lg transition cursor-pointer" onclick="window.location.href='{% url 'surveys:instance_list' %}'">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="p-2 bg-purple-50 rounded-lg">
|
|
<i data-lucide="clipboard-check" class="w-5 h-5 text-purple-500"></i>
|
|
</div>
|
|
<span class="text-xs font-bold px-2 py-1 rounded-full bg-purple-100 text-purple-600">
|
|
NPS: {{ survey_module.nps_score }}
|
|
</span>
|
|
</div>
|
|
<h3 class="text-sm font-medium text-slate mb-1">{% trans "Survey Insights" %}</h3>
|
|
<p class="text-3xl font-bold text-navy">{{ survey_module.avg_satisfaction }}%</p>
|
|
<div class="mt-2 flex items-center gap-2 text-xs text-slate">
|
|
<span class="flex items-center gap-1">
|
|
<i data-lucide="send" class="w-3 h-3"></i>
|
|
{{ survey_module.response_rate }}% {% trans "response" %}
|
|
</span>
|
|
</div>
|
|
{% if survey_module.negative_24h > 0 %}
|
|
<div class="mt-2 text-xs font-bold text-orange-500 flex items-center gap-1">
|
|
<i data-lucide="frown" class="w-3 h-3"></i>
|
|
{{ survey_module.negative_24h }} {% trans "negative (24h)" %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="px-4 py-2 bg-light border-t border-slate-100 flex justify-between items-center">
|
|
<span class="text-xs text-slate">{{ survey_module.total_completed }} {% trans "completed" %}</span>
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PX Actions Module Card -->
|
|
<div class="card module-card actions hover:shadow-lg transition cursor-pointer" onclick="window.location.href='{% url 'actions:action_list' %}'">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="p-2 bg-orange-50 rounded-lg">
|
|
<i data-lucide="check-circle-2" class="w-5 h-5 text-orange-500"></i>
|
|
</div>
|
|
{% if actions_module.escalated > 0 %}
|
|
<span class="text-xs font-bold px-2 py-1 rounded-full bg-red-100 text-red-600 animate-pulse">
|
|
{{ actions_module.escalated }} {% trans "escalated" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<h3 class="text-sm font-medium text-slate mb-1">{% trans "PX Actions" %}</h3>
|
|
<p class="text-3xl font-bold text-navy">{{ actions_module.open|add:actions_module.in_progress }}</p>
|
|
<div class="mt-2 flex items-center gap-3 text-xs text-slate">
|
|
<span>{{ actions_module.open }} {% trans "open" %}</span>
|
|
<span>{{ actions_module.in_progress }} {% trans "in progress" %}</span>
|
|
</div>
|
|
{% if actions_module.overdue > 0 %}
|
|
<div class="mt-2 text-xs font-bold text-red-500 flex items-center gap-1">
|
|
<i data-lucide="clock" class="w-3 h-3"></i>
|
|
{{ actions_module.overdue }} {% trans "overdue" %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="px-4 py-2 bg-light border-t border-slate-100 flex justify-between items-center">
|
|
<span class="text-xs text-slate">{{ actions_module.closed_30d }} {% trans "closed (30d)" %}</span>
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inquiries Module Card -->
|
|
<div class="card module-card inquiries hover:shadow-lg transition cursor-pointer" onclick="window.location.href='{% url 'complaints:complaint_list' %}?type=inquiry'">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="p-2 bg-blue-50 rounded-lg">
|
|
<i data-lucide="help-circle" class="w-5 h-5 text-blue"></i>
|
|
</div>
|
|
{% if inquiries_module.new_24h > 0 %}
|
|
<span class="text-xs font-bold px-2 py-1 rounded-full bg-blue-100 text-blue-600">
|
|
{{ inquiries_module.new_24h }} {% trans "new" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<h3 class="text-sm font-medium text-slate mb-1">{% trans "Inquiries" %}</h3>
|
|
<p class="text-3xl font-bold text-navy">{{ inquiries_module.total_active }}</p>
|
|
<div class="mt-2 flex items-center gap-3 text-xs text-slate">
|
|
<span>{{ inquiries_module.open }} {% trans "open" %}</span>
|
|
<span>{{ inquiries_module.in_progress }} {% trans "pending" %}</span>
|
|
</div>
|
|
</div>
|
|
<div class="px-4 py-2 bg-light border-t border-slate-100 flex justify-between items-center">
|
|
<span class="text-xs text-slate">{{ inquiries_module.resolved_30d }} {% trans "resolved (30d)" %}</span>
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Observations Module Card -->
|
|
<div class="card module-card observations hover:shadow-lg transition cursor-pointer" onclick="window.location.href='{% url 'observations:observation_list' %}'">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="p-2 bg-green-50 rounded-lg">
|
|
<i data-lucide="eye" class="w-5 h-5 text-green-500"></i>
|
|
</div>
|
|
{% if observations_module.critical > 0 %}
|
|
<span class="text-xs font-bold px-2 py-1 rounded-full bg-red-100 text-red-600">
|
|
{{ observations_module.critical }} {% trans "critical" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<h3 class="text-sm font-medium text-slate mb-1">{% trans "Observations" %}</h3>
|
|
<p class="text-3xl font-bold text-navy">{{ observations_module.total_active }}</p>
|
|
<div class="mt-2 flex items-center gap-3 text-xs text-slate">
|
|
<span>{{ observations_module.new }} {% trans "new" %}</span>
|
|
<span>{{ observations_module.in_progress }} {% trans "in progress" %}</span>
|
|
</div>
|
|
</div>
|
|
<div class="px-4 py-2 bg-light border-t border-slate-100 flex justify-between items-center">
|
|
<span class="text-xs text-slate">{{ observations_module.resolved_30d }} {% trans "resolved (30d)" %}</span>
|
|
<i data-lucide="chevron-right" class="w-4 h-4 text-slate"></i>
|
|
</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 section-card">
|
|
<div class="section-header flex justify-between items-center">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="trending-up" class="w-5 h-5 text-blue"></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 border border-slate-200">Day</button>
|
|
<button class="px-3 py-1.5 text-xs font-bold bg-navy text-white rounded-lg border border-navy">Week</button>
|
|
<button class="px-3 py-1.5 text-xs font-bold text-slate hover:text-navy hover:bg-light rounded-lg transition border border-slate-200">Month</button>
|
|
</div>
|
|
</div>
|
|
<div class="section-content">
|
|
<div id="complaintsTrendChart" class="h-[280px] chart-container"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- NPS Trend -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="smile" class="w-5 h-5 text-purple-500"></i>
|
|
{% trans "NPS Score Trend" %}
|
|
</h3>
|
|
</div>
|
|
<div class="section-content">
|
|
<div id="npsTrendChart" class="h-[200px] chart-container"></div>
|
|
<div class="mt-4 pt-4 border-t-2 border-slate-100 text-center">
|
|
<div class="text-3xl font-bold text-navy">{{ survey_module.nps_score }}</div>
|
|
<p class="text-sm text-slate">{% trans "Current NPS Score" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Severity Distribution & Actions Funnel -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Complaints by Severity -->
|
|
<div class="section-card">
|
|
<div class="section-header flex justify-between items-center">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="pie-chart" class="w-5 h-5 text-red-500"></i>
|
|
{% trans "Active Complaints by Severity" %}
|
|
</h3>
|
|
<a href="{% url 'complaints:complaint_list' %}" class="text-xs font-bold text-navy hover:text-blue border border-navy px-3 py-1 rounded-lg transition">
|
|
{% trans "View All" %} →
|
|
</a>
|
|
</div>
|
|
<div class="section-content">
|
|
<div id="severityDonutChart" class="h-[250px] chart-container"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions Funnel -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="git-branch" class="w-5 h-5 text-orange-500"></i>
|
|
{% trans "PX Actions Pipeline" %}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="space-y-4">
|
|
<div class="relative">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-sm font-medium text-slate">{% trans "Open" %}</span>
|
|
<span class="text-sm font-bold text-navy">{{ actions_module.open }}</span>
|
|
</div>
|
|
<div class="h-8 bg-blue-100 rounded-lg overflow-hidden">
|
|
<div class="h-full bg-blue-500 flex items-center justify-end pr-2" style="width: {% widthratio actions_module.open actions_module.open|add:actions_module.in_progress|add:actions_module.pending_approval|add:actions_module.closed_30d 100 %}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="relative">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-sm font-medium text-slate">{% trans "In Progress" %}</span>
|
|
<span class="text-sm font-bold text-navy">{{ actions_module.in_progress }}</span>
|
|
</div>
|
|
<div class="h-8 bg-orange-100 rounded-lg overflow-hidden">
|
|
<div class="h-full bg-orange-500 flex items-center justify-end pr-2" style="width: {% widthratio actions_module.in_progress actions_module.open|add:actions_module.in_progress|add:actions_module.pending_approval|add:actions_module.closed_30d 100 %}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="relative">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-sm font-medium text-slate">{% trans "Pending Approval" %}</span>
|
|
<span class="text-sm font-bold text-navy">{{ actions_module.pending_approval }}</span>
|
|
</div>
|
|
<div class="h-8 bg-yellow-100 rounded-lg overflow-hidden">
|
|
<div class="h-full bg-yellow-500 flex items-center justify-end pr-2" style="width: {% widthratio actions_module.pending_approval actions_module.open|add:actions_module.in_progress|add:actions_module.pending_approval|add:actions_module.closed_30d 100 %}%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="relative">
|
|
<div class="flex items-center justify-between mb-1">
|
|
<span class="text-sm font-medium text-slate">{% trans "Closed (30d)" %}</span>
|
|
<span class="text-sm font-bold text-green-500">{{ actions_module.closed_30d }}</span>
|
|
</div>
|
|
<div class="h-8 bg-green-100 rounded-lg overflow-hidden">
|
|
<div class="h-full bg-green-500 flex items-center justify-end pr-2" style="width: {% widthratio actions_module.closed_30d actions_module.open|add:actions_module.in_progress|add:actions_module.pending_approval|add:actions_module.closed_30d 100 %}%"></div>
|
|
</div>
|
|
</div>
|
|
</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="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy 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 max-h-[400px] overflow-y-auto">
|
|
{% if latest_complaints %}
|
|
{% for complaint in latest_complaints %}
|
|
<a href="{% url 'complaints:complaint_detail' complaint.id %}"
|
|
class="block p-4 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 text-sm mb-1 truncate group-hover:text-blue transition">{{ complaint.title }}</h4>
|
|
<div class="flex items-center gap-2 text-xs text-slate mb-1">
|
|
<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-1 items-end">
|
|
<span class="px-2 py-0.5 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-2 py-0.5 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-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
<i data-lucide="check-circle" class="w-6 h-6 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="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy 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 max-h-[400px] overflow-y-auto">
|
|
{% if latest_actions %}
|
|
{% for action in latest_actions %}
|
|
<a href="{% url 'actions:action_detail' action.id %}"
|
|
class="block p-4 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 text-sm mb-1 truncate group-hover:text-blue transition">{{ action.title }}</h4>
|
|
<div class="flex items-center gap-2 text-xs text-slate mb-1">
|
|
<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-2 py-0.5 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-12 h-12 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
<i data-lucide="check-circle" class="w-6 h-6 text-green-500"></i>
|
|
</div>
|
|
<p class="text-sm text-slate">{% trans "No escalated actions" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Inquiries & Observations Row -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Latest Inquiries -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="help-circle" class="w-4 h-4"></i>
|
|
{% trans "Recent Inquiries" %}
|
|
</h3>
|
|
<a href="{% url 'complaints:complaint_list' %}?type=inquiry"
|
|
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 max-h-[300px] overflow-y-auto">
|
|
{% if latest_inquiries %}
|
|
{% for inquiry in latest_inquiries %}
|
|
<a href="{% url 'complaints:inquiry_detail' inquiry.id %}"
|
|
class="block p-4 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 text-sm mb-1 truncate group-hover:text-blue transition">{{ inquiry.subject }}</h4>
|
|
<div class="flex items-center gap-2 text-xs text-slate">
|
|
<span class="flex items-center gap-1">
|
|
<i data-lucide="user" class="w-3 h-3"></i>
|
|
{{ inquiry.patient.get_full_name }}
|
|
</span>
|
|
<span>{{ inquiry.created_at|timesince }} {% trans "ago" %}</span>
|
|
</div>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold
|
|
{% if inquiry.status == 'open' %}bg-blue-100 text-blue-600
|
|
{% elif inquiry.status == 'in_progress' %}bg-yellow-100 text-yellow-600
|
|
{% else %}bg-green-100 text-green-600{% endif %}">
|
|
{{ inquiry.get_status_display }}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="p-6 text-center">
|
|
<p class="text-sm text-slate">{% trans "No active inquiries" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Latest Observations -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="eye" class="w-4 h-4"></i>
|
|
{% trans "Recent Observations" %}
|
|
</h3>
|
|
<a href="{% url 'observations:observation_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 max-h-[300px] overflow-y-auto">
|
|
{% if latest_observations %}
|
|
{% for observation in latest_observations %}
|
|
<a href="{% url 'observations:observation_detail' observation.id %}"
|
|
class="block p-4 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 text-sm mb-1 truncate group-hover:text-blue transition">{{ observation.title }}</h4>
|
|
<div class="flex items-center gap-2 text-xs text-slate">
|
|
{% if observation.category %}
|
|
<span class="px-2 py-0.5 bg-slate-100 rounded text-slate">{{ observation.category.name_en }}</span>
|
|
{% endif %}
|
|
<span>{{ observation.created_at|timesince }} {% trans "ago" %}</span>
|
|
</div>
|
|
</div>
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold
|
|
{% if observation.status == 'new' %}bg-blue-100 text-blue-600
|
|
{% elif observation.status == 'in_progress' %}bg-yellow-100 text-yellow-600
|
|
{% else %}bg-slate-100 text-slate{% endif %}">
|
|
{{ observation.get_status_display }}
|
|
</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="p-6 text-center">
|
|
<p class="text-sm text-slate">{% trans "No active observations" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top Physicians This Month -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3 class="font-bold text-navy 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-4 py-3 text-left text-xs font-bold text-slate uppercase tracking-wider" style="width: 60px;">{% trans "Rank" %}</th>
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Physician" %}</th>
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Department" %}</th>
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Rating" %}</th>
|
|
<th class="px-4 py-3 text-left text-xs font-bold text-slate uppercase tracking-wider">{% trans "Surveys" %}</th>
|
|
<th class="px-4 py-3 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-4 py-3">
|
|
{% if forloop.counter == 1 %}
|
|
<div class="w-8 h-8 bg-yellow-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="trophy" class="w-4 h-4 text-yellow-500 fill-current"></i>
|
|
</div>
|
|
{% elif forloop.counter == 2 %}
|
|
<div class="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center">
|
|
<i data-lucide="trophy" class="w-4 h-4 text-slate-500 fill-current"></i>
|
|
</div>
|
|
{% elif forloop.counter == 3 %}
|
|
<div class="w-8 h-8 bg-amber-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="trophy" class="w-4 h-4 text-amber-600 fill-current"></i>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-slate-400 font-bold text-sm">#{{ forloop.counter }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="font-bold text-navy text-sm">{{ rating.staff }}</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate">
|
|
{% if rating.staff.department %}{{ rating.staff.department.name }}{% else %}-{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="text-lg font-bold text-green-500">{{ rating.average_rating|floatformat:2 }}</div>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-slate-100 text-slate">{{ rating.total_surveys }}</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex gap-1">
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-green-100 text-green-600">{{ rating.positive_count }}</span>
|
|
<span class="px-2 py-0.5 rounded-full text-xs font-bold bg-red-100 text-red-600">{{ rating.negative_count }}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="6" class="px-4 py-8 text-center text-slate">{% trans "No physician ratings this month" %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// Auto-refresh configuration
|
|
let refreshInterval = 60; // seconds
|
|
let countdownTimer = refreshInterval;
|
|
|
|
// Countdown timer
|
|
function updateCountdown() {
|
|
countdownTimer--;
|
|
document.getElementById('refresh-timer').textContent =
|
|
`{% trans "Refreshes in" %} ${countdownTimer}s`;
|
|
|
|
if (countdownTimer <= 0) {
|
|
refreshCommandCenter();
|
|
countdownTimer = refreshInterval;
|
|
}
|
|
}
|
|
|
|
// Start countdown
|
|
setInterval(updateCountdown, 1000);
|
|
|
|
// Manual refresh function
|
|
function refreshCommandCenter() {
|
|
const refreshIcon = document.getElementById('refresh-icon');
|
|
refreshIcon.classList.add('animate-spin');
|
|
|
|
// Reload page to refresh data
|
|
window.location.reload();
|
|
}
|
|
|
|
// Complaints Trend Chart
|
|
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: 280,
|
|
toolbar: { show: false },
|
|
fontFamily: 'Inter, sans-serif',
|
|
animations: {
|
|
enabled: true,
|
|
dynamicAnimation: { speed: 1000 }
|
|
}
|
|
},
|
|
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: '11px', colors: ['#64748b'] }
|
|
},
|
|
axisBorder: { show: false },
|
|
axisTicks: { show: false }
|
|
},
|
|
yaxis: {
|
|
min: 0,
|
|
forceNiceScale: true,
|
|
labels: {
|
|
style: { fontSize: '11px', colors: ['#64748b'] }
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#f1f5f9',
|
|
strokeDashArray: 5
|
|
},
|
|
tooltip: {
|
|
theme: 'light',
|
|
x: { format: 'dd MMM yyyy' }
|
|
},
|
|
dataLabels: { enabled: false },
|
|
markers: {
|
|
size: 0,
|
|
hover: { size: 6 }
|
|
}
|
|
};
|
|
|
|
new ApexCharts(chartElement, options).render();
|
|
}
|
|
|
|
// NPS Trend Chart
|
|
const npsElement = document.getElementById('npsTrendChart');
|
|
if (npsElement) {
|
|
const npsData = {{ chart_data.nps_trend|safe }};
|
|
|
|
var npsOptions = {
|
|
series: [{
|
|
name: 'NPS',
|
|
data: npsData.map(d => d.nps)
|
|
}],
|
|
chart: {
|
|
type: 'line',
|
|
height: 200,
|
|
toolbar: { show: false },
|
|
fontFamily: 'Inter, sans-serif'
|
|
},
|
|
stroke: { curve: 'smooth', width: 2 },
|
|
colors: ['#10b981'],
|
|
xaxis: {
|
|
categories: npsData.map(d => d.date),
|
|
labels: { show: false }
|
|
},
|
|
yaxis: {
|
|
min: -100,
|
|
max: 100,
|
|
labels: {
|
|
style: { fontSize: '10px', colors: ['#64748b'] }
|
|
}
|
|
},
|
|
grid: {
|
|
borderColor: '#f1f5f9',
|
|
strokeDashArray: 3
|
|
},
|
|
tooltip: { theme: 'light' },
|
|
dataLabels: { enabled: false }
|
|
};
|
|
|
|
new ApexCharts(npsElement, npsOptions).render();
|
|
}
|
|
|
|
// Severity Donut Chart
|
|
const severityElement = document.getElementById('severityDonutChart');
|
|
if (severityElement) {
|
|
const severityData = {{ chart_data.complaints_by_severity|safe }};
|
|
|
|
var severityOptions = {
|
|
series: [severityData.critical, severityData.high, severityData.medium, severityData.low],
|
|
chart: {
|
|
type: 'donut',
|
|
height: 250,
|
|
fontFamily: 'Inter, sans-serif'
|
|
},
|
|
colors: ['#ef4444', '#f97316', '#eab308', '#3b82f6'],
|
|
labels: ['{% trans "Critical" %}', '{% trans "High" %}', '{% trans "Medium" %}', '{% trans "Low" %}'],
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%',
|
|
labels: {
|
|
show: true,
|
|
total: {
|
|
show: true,
|
|
label: '{% trans "Total" %}',
|
|
fontSize: '14px',
|
|
fontWeight: 'bold'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
fontSize: '12px'
|
|
},
|
|
dataLabels: { enabled: false }
|
|
};
|
|
|
|
new ApexCharts(severityElement, severityOptions).render();
|
|
}
|
|
|
|
// Initialize Lucide icons
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
});
|
|
</script>
|
|
{% endblock %} |