1187 lines
39 KiB
HTML
1187 lines
39 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}Patients Dashboard{% endblock %}
|
|
|
|
{% block css %}
|
|
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.dashboard-header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: var(--card-color);
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 1rem;
|
|
color: white;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2.5rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.stat-change {
|
|
font-size: 0.75rem;
|
|
margin-top: 0.5rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stat-change.positive {
|
|
color: #28a745;
|
|
}
|
|
|
|
.stat-change.negative {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.stat-change.neutral {
|
|
color: #6c757d;
|
|
}
|
|
|
|
.dashboard-section {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.quick-actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.action-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
transition: all 0.2s;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
|
|
.action-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
|
|
.action-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 1rem;
|
|
color: white;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.action-title {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
color: #495057;
|
|
}
|
|
|
|
.action-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 300px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.recent-activity {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.activity-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #f1f3f4;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.activity-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.activity-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.activity-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: #007bff;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 1rem;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.activity-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.activity-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.activity-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.activity-time {
|
|
font-size: 0.75rem;
|
|
color: #adb5bd;
|
|
}
|
|
|
|
.activity-type {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.type-admission { background: #d4edda; color: #155724; }
|
|
.type-discharge { background: #f8d7da; color: #721c24; }
|
|
.type-appointment { background: #d1ecf1; color: #0c5460; }
|
|
.type-emergency { background: #f5c6cb; color: #721c24; }
|
|
.type-update { background: #fff3cd; color: #856404; }
|
|
|
|
.alerts-section {
|
|
background: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 0.5rem;
|
|
padding: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.alert-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0.75rem;
|
|
background: white;
|
|
border: 1px solid #ffeaa7;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.alert-item:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.alert-icon {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 1rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.alert-critical { background: #dc3545; color: white; }
|
|
.alert-warning { background: #ffc107; color: #212529; }
|
|
.alert-info { background: #17a2b8; color: white; }
|
|
|
|
.patient-status-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.status-item {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
border: 1px solid #dee2e6;
|
|
}
|
|
|
|
.status-number {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.status-label {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.search-widget {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.search-tabs {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.search-tab {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #dee2e6;
|
|
background: #f8f9fa;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.search-tab.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.search-content {
|
|
display: none;
|
|
}
|
|
|
|
.search-content.active {
|
|
display: block;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.dashboard-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.stats-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.stat-card {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.quick-actions {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.patient-status-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.quick-actions, .search-widget, .btn {
|
|
display: none !important;
|
|
}
|
|
|
|
.dashboard-section {
|
|
break-inside: avoid;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.section-header {
|
|
background: none;
|
|
border-bottom: 2px solid #000;
|
|
color: #000;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<!-- Dashboard Header -->
|
|
<div class="dashboard-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h1 class="mb-2">
|
|
<i class="fas fa-users me-3"></i>Patients Dashboard
|
|
</h1>
|
|
<p class="mb-0 fs-5">Comprehensive patient management and analytics</p>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="text-white-50 mb-1">Last Updated</div>
|
|
<div class="h5 mb-0">{% now "M d, Y g:i A" %}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Key Statistics -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card" style="--card-color: #007bff;">
|
|
<div class="stat-icon" style="background: #007bff;">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-number" id="total-patients">{{ stats.total_patients|default:0 }}</div>
|
|
<div class="stat-label">Total Patients</div>
|
|
<div class="stat-change positive">
|
|
<i class="fas fa-arrow-up me-1"></i>+{{ stats.new_patients_today|default:0 }} today
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #28a745;">
|
|
<div class="stat-icon" style="background: #28a745;">
|
|
<i class="fas fa-user-check"></i>
|
|
</div>
|
|
<div class="stat-number" id="active-patients">{{ stats.active_patients|default:0 }}</div>
|
|
<div class="stat-label">Active Patients</div>
|
|
<div class="stat-change positive">
|
|
<i class="fas fa-arrow-up me-1"></i>{{ stats.active_change|default:0 }}% this week
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #ffc107;">
|
|
<div class="stat-icon" style="background: #ffc107;">
|
|
<i class="fas fa-bed"></i>
|
|
</div>
|
|
<div class="stat-number" id="admitted-patients">{{ stats.admitted_patients|default:0 }}</div>
|
|
<div class="stat-label">Currently Admitted</div>
|
|
<div class="stat-change neutral">
|
|
<i class="fas fa-minus me-1"></i>{{ stats.admission_change|default:0 }} from yesterday
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #dc3545;">
|
|
<div class="stat-icon" style="background: #dc3545;">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
</div>
|
|
<div class="stat-number" id="critical-patients">{{ stats.critical_patients|default:0 }}</div>
|
|
<div class="stat-label">Critical Condition</div>
|
|
<div class="stat-change negative">
|
|
<i class="fas fa-arrow-down me-1"></i>{{ stats.critical_change|default:0 }} from last hour
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #17a2b8;">
|
|
<div class="stat-icon" style="background: #17a2b8;">
|
|
<i class="fas fa-calendar-check"></i>
|
|
</div>
|
|
<div class="stat-number" id="appointments-today">{{ stats.appointments_today|default:0 }}</div>
|
|
<div class="stat-label">Appointments Today</div>
|
|
<div class="stat-change positive">
|
|
<i class="fas fa-clock me-1"></i>{{ stats.completed_appointments|default:0 }} completed
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card" style="--card-color: #6f42c1;">
|
|
<div class="stat-icon" style="background: #6f42c1;">
|
|
<i class="fas fa-user-plus"></i>
|
|
</div>
|
|
<div class="stat-number" id="new-registrations">{{ stats.new_registrations|default:0 }}</div>
|
|
<div class="stat-label">New Registrations</div>
|
|
<div class="stat-change positive">
|
|
<i class="fas fa-calendar me-1"></i>This week
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alerts Section -->
|
|
{% if alerts %}
|
|
<div class="alerts-section">
|
|
<h6 class="mb-3">
|
|
<i class="fas fa-bell me-2"></i>Patient Alerts & Notifications
|
|
</h6>
|
|
{% for alert in alerts %}
|
|
<div class="alert-item">
|
|
<div class="alert-icon alert-{{ alert.priority }}">
|
|
<i class="fas fa-{{ alert.icon }}"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold">{{ alert.title }}</div>
|
|
<div class="text-muted small">{{ alert.message }}</div>
|
|
</div>
|
|
<div class="text-muted small">{{ alert.created_at|timesince }} ago</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="quick-actions">
|
|
<a href="{% url 'patients:patient_registration' %}" class="action-card">
|
|
<div class="action-icon" style="background: #007bff;">
|
|
<i class="fas fa-user-plus"></i>
|
|
</div>
|
|
<div class="action-title">Register New Patient</div>
|
|
<div class="action-description">Add a new patient to the system with complete registration</div>
|
|
</a>
|
|
|
|
<a href="{% url 'patients:patient_list' %}" class="action-card">
|
|
<div class="action-icon" style="background: #28a745;">
|
|
<i class="fas fa-list"></i>
|
|
</div>
|
|
<div class="action-title">View All Patients</div>
|
|
<div class="action-description">Browse and manage all registered patients</div>
|
|
</a>
|
|
|
|
<a href="{% url 'appointments:appointment_create' %}" class="action-card">
|
|
<div class="action-icon" style="background: #17a2b8;">
|
|
<i class="fas fa-calendar-plus"></i>
|
|
</div>
|
|
<div class="action-title">Schedule Appointment</div>
|
|
<div class="action-description">Book new appointments for existing patients</div>
|
|
</a>
|
|
|
|
<a href="{% url 'patients:emergency_admission' %}" class="action-card">
|
|
<div class="action-icon" style="background: #dc3545;">
|
|
<i class="fas fa-ambulance"></i>
|
|
</div>
|
|
<div class="action-title">Emergency Admission</div>
|
|
<div class="action-description">Quick admission for emergency cases</div>
|
|
</a>
|
|
|
|
<a href="{% url 'patients:discharge_management' %}" class="action-card">
|
|
<div class="action-icon" style="background: #ffc107;">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
</div>
|
|
<div class="action-title">Discharge Management</div>
|
|
<div class="action-description">Process patient discharges and follow-ups</div>
|
|
</a>
|
|
|
|
<a href="{% url 'patients:reports' %}" class="action-card">
|
|
<div class="action-icon" style="background: #6f42c1;">
|
|
<i class="fas fa-chart-bar"></i>
|
|
</div>
|
|
<div class="action-title">Patient Reports</div>
|
|
<div class="action-description">Generate comprehensive patient analytics</div>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Search Widget -->
|
|
<div class="search-widget">
|
|
<h6 class="mb-3">
|
|
<i class="fas fa-search me-2"></i>Quick Patient Search
|
|
</h6>
|
|
|
|
<div class="search-tabs">
|
|
<div class="search-tab active" onclick="switchSearchTab('name')">By Name</div>
|
|
<div class="search-tab" onclick="switchSearchTab('id')">By ID</div>
|
|
<div class="search-tab" onclick="switchSearchTab('phone')">By Phone</div>
|
|
<div class="search-tab" onclick="switchSearchTab('advanced')">Advanced</div>
|
|
</div>
|
|
|
|
<div class="search-content active" id="search-name">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Enter patient name..." id="search-name-input">
|
|
<button class="btn btn-primary" onclick="searchPatients('name')">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-content" id="search-id">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Enter patient ID..." id="search-id-input">
|
|
<button class="btn btn-primary" onclick="searchPatients('id')">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-content" id="search-phone">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" placeholder="Enter phone number..." id="search-phone-input">
|
|
<button class="btn btn-primary" onclick="searchPatients('phone')">
|
|
<i class="fas fa-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="search-content" id="search-advanced">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control mb-2" placeholder="First Name" id="search-fname">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="text" class="form-control mb-2" placeholder="Last Name" id="search-lname">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="date" class="form-control mb-2" placeholder="Date of Birth" id="search-dob">
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="searchPatients('advanced')">
|
|
<i class="fas fa-search me-1"></i>Advanced Search
|
|
</button>
|
|
</div>
|
|
|
|
<div id="search-results" class="mt-3" style="display: none;">
|
|
<!-- Search results will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<!-- Patient Status Overview -->
|
|
<div class="dashboard-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-chart-pie me-2"></i>Patient Status Overview
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-secondary" onclick="refreshCharts()">
|
|
<i class="fas fa-sync"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary" onclick="exportChart('status')">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="patient-status-grid">
|
|
<div class="status-item">
|
|
<div class="status-number text-success">{{ stats.outpatients|default:0 }}</div>
|
|
<div class="status-label">Outpatients</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-number text-primary">{{ stats.inpatients|default:0 }}</div>
|
|
<div class="status-label">Inpatients</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-number text-warning">{{ stats.emergency|default:0 }}</div>
|
|
<div class="status-label">Emergency</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-number text-info">{{ stats.icu|default:0 }}</div>
|
|
<div class="status-label">ICU</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-number text-secondary">{{ stats.discharged_today|default:0 }}</div>
|
|
<div class="status-label">Discharged Today</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-number text-danger">{{ stats.readmissions|default:0 }}</div>
|
|
<div class="status-label">Readmissions</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-container">
|
|
<canvas id="patientStatusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admission Trends -->
|
|
<div class="dashboard-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-chart-line me-2"></i>Admission Trends (Last 30 Days)
|
|
</div>
|
|
<div class="btn-group btn-group-sm">
|
|
<button class="btn btn-outline-secondary" onclick="changeTrendPeriod('7')">7D</button>
|
|
<button class="btn btn-outline-secondary active" onclick="changeTrendPeriod('30')">30D</button>
|
|
<button class="btn btn-outline-secondary" onclick="changeTrendPeriod('90')">90D</button>
|
|
</div>
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="chart-container">
|
|
<canvas id="admissionTrendsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<!-- Recent Activity -->
|
|
<div class="dashboard-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-clock me-2"></i>Recent Activity
|
|
</div>
|
|
<a href="{% url 'patients:activity_log' %}" class="btn btn-outline-primary btn-sm">
|
|
View All
|
|
</a>
|
|
</div>
|
|
<div class="section-content p-0">
|
|
<div class="recent-activity">
|
|
{% for activity in recent_activities %}
|
|
<div class="activity-item">
|
|
<div class="activity-avatar">
|
|
{{ activity.patient.first_name.0|default:"P" }}{{ activity.patient.last_name.0|default:"" }}
|
|
</div>
|
|
<div class="activity-content">
|
|
<div class="activity-title">{{ activity.patient.get_full_name }}</div>
|
|
<div class="activity-description">{{ activity.description }}</div>
|
|
<div class="activity-time">{{ activity.created_at|timesince }} ago</div>
|
|
</div>
|
|
<div class="activity-type type-{{ activity.activity_type }}">
|
|
{{ activity.get_activity_type_display }}
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="fas fa-clock fa-2x mb-2"></i>
|
|
<p>No recent activity</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Today's Appointments -->
|
|
<div class="dashboard-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-calendar-day me-2"></i>Today's Appointments
|
|
</div>
|
|
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-outline-primary btn-sm">
|
|
View All
|
|
</a>
|
|
</div>
|
|
<div class="section-content">
|
|
{% for appointment in todays_appointments %}
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="me-3">
|
|
<div class="fw-bold text-primary">{{ appointment.scheduled_time|time:"g:i A" }}</div>
|
|
<small class="text-muted">{{ appointment.duration }} min</small>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold">{{ appointment.patient.get_full_name }}</div>
|
|
<small class="text-muted">{{ appointment.appointment_type }} - {{ appointment.doctor.get_full_name }}</small>
|
|
</div>
|
|
<div>
|
|
<span class="badge bg-{{ appointment.status_color }}">
|
|
{{ appointment.get_status_display }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-3 text-muted">
|
|
<i class="fas fa-calendar fa-2x mb-2"></i>
|
|
<p>No appointments scheduled for today</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Critical Patients -->
|
|
<div class="dashboard-section">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-exclamation-triangle me-2 text-danger"></i>Critical Patients
|
|
</div>
|
|
<a href="{% url 'patients:critical_list' %}" class="btn btn-outline-danger btn-sm">
|
|
View All
|
|
</a>
|
|
</div>
|
|
<div class="section-content">
|
|
{% for patient in critical_patients %}
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="activity-avatar bg-danger me-3">
|
|
{{ patient.first_name.0 }}{{ patient.last_name.0 }}
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<div class="fw-bold">{{ patient.get_full_name }}</div>
|
|
<small class="text-muted">{{ patient.current_condition }}</small>
|
|
<div class="small text-danger">
|
|
<i class="fas fa-map-marker-alt me-1"></i>{{ patient.current_location }}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<a href="{% url 'patients:patient_detail' patient.pk %}" class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-3 text-muted">
|
|
<i class="fas fa-heart fa-2x mb-2 text-success"></i>
|
|
<p>No critical patients at this time</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="dashboard-section">
|
|
<div class="section-header">
|
|
<i class="fas fa-tachometer-alt me-2"></i>Quick Statistics
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="row text-center">
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-primary mb-1">{{ stats.avg_stay_duration|default:0 }}</div>
|
|
<small class="text-muted">Avg Stay (days)</small>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-success mb-1">{{ stats.satisfaction_rate|default:0 }}%</div>
|
|
<small class="text-muted">Satisfaction Rate</small>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-warning mb-1">{{ stats.readmission_rate|default:0 }}%</div>
|
|
<small class="text-muted">Readmission Rate</small>
|
|
</div>
|
|
<div class="col-6 mb-3">
|
|
<div class="h4 text-info mb-1">{{ stats.bed_occupancy|default:0 }}%</div>
|
|
<small class="text-muted">Bed Occupancy</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
|
|
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
|
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize charts
|
|
initializePatientStatusChart();
|
|
initializeAdmissionTrendsChart();
|
|
|
|
// Auto-refresh data every 5 minutes
|
|
setInterval(refreshDashboardData, 300000);
|
|
|
|
// Initialize search functionality
|
|
initializeSearch();
|
|
});
|
|
|
|
function initializePatientStatusChart() {
|
|
const ctx = document.getElementById('patientStatusChart').getContext('2d');
|
|
|
|
const data = {
|
|
labels: ['Outpatients', 'Inpatients', 'Emergency', 'ICU', 'Discharged', 'Readmissions'],
|
|
datasets: [{
|
|
data: [
|
|
{{ stats.outpatients|default:0 }},
|
|
{{ stats.inpatients|default:0 }},
|
|
{{ stats.emergency|default:0 }},
|
|
{{ stats.icu|default:0 }},
|
|
{{ stats.discharged_today|default:0 }},
|
|
{{ stats.readmissions|default:0 }}
|
|
],
|
|
backgroundColor: [
|
|
'#28a745',
|
|
'#007bff',
|
|
'#ffc107',
|
|
'#17a2b8',
|
|
'#6c757d',
|
|
'#dc3545'
|
|
],
|
|
borderWidth: 2,
|
|
borderColor: '#fff'
|
|
}]
|
|
};
|
|
|
|
new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: data,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
padding: 20,
|
|
usePointStyle: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function initializeAdmissionTrendsChart() {
|
|
const ctx = document.getElementById('admissionTrendsChart').getContext('2d');
|
|
|
|
// Generate sample data for the last 30 days
|
|
const labels = [];
|
|
const admissionData = [];
|
|
const dischargeData = [];
|
|
|
|
for (let i = 29; i >= 0; i--) {
|
|
const date = new Date();
|
|
date.setDate(date.getDate() - i);
|
|
labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }));
|
|
|
|
// Sample data - replace with actual data from backend
|
|
admissionData.push(Math.floor(Math.random() * 20) + 5);
|
|
dischargeData.push(Math.floor(Math.random() * 15) + 3);
|
|
}
|
|
|
|
new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Admissions',
|
|
data: admissionData,
|
|
borderColor: '#007bff',
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}, {
|
|
label: 'Discharges',
|
|
data: dischargeData,
|
|
borderColor: '#28a745',
|
|
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top'
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
stepSize: 5
|
|
}
|
|
},
|
|
x: {
|
|
ticks: {
|
|
maxTicksLimit: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function refreshDashboardData() {
|
|
fetch('/patients/dashboard/data/', {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Update statistics
|
|
document.getElementById('total-patients').textContent = data.total_patients || 0;
|
|
document.getElementById('active-patients').textContent = data.active_patients || 0;
|
|
document.getElementById('admitted-patients').textContent = data.admitted_patients || 0;
|
|
document.getElementById('critical-patients').textContent = data.critical_patients || 0;
|
|
document.getElementById('appointments-today').textContent = data.appointments_today || 0;
|
|
document.getElementById('new-registrations').textContent = data.new_registrations || 0;
|
|
|
|
// Show update indicator
|
|
showUpdateIndicator();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error refreshing dashboard data:', error);
|
|
});
|
|
}
|
|
|
|
function showUpdateIndicator() {
|
|
const indicator = document.createElement('div');
|
|
indicator.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
|
indicator.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 250px;';
|
|
indicator.innerHTML = `
|
|
<i class="fas fa-sync me-2"></i>Dashboard updated
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(indicator);
|
|
|
|
setTimeout(() => {
|
|
if (indicator.parentNode) {
|
|
indicator.remove();
|
|
}
|
|
}, 3000);
|
|
}
|
|
|
|
function refreshCharts() {
|
|
// Refresh chart data
|
|
location.reload();
|
|
}
|
|
|
|
function exportChart(chartType) {
|
|
// Export chart functionality
|
|
const canvas = document.getElementById(chartType === 'status' ? 'patientStatusChart' : 'admissionTrendsChart');
|
|
const url = canvas.toDataURL('image/png');
|
|
|
|
const link = document.createElement('a');
|
|
link.download = `${chartType}-chart.png`;
|
|
link.href = url;
|
|
link.click();
|
|
}
|
|
|
|
function changeTrendPeriod(days) {
|
|
// Update trend chart for different periods
|
|
document.querySelectorAll('.btn-group .btn').forEach(btn => btn.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
|
|
// Fetch new data for the selected period
|
|
fetch(`/patients/dashboard/trends/?days=${days}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Update chart with new data
|
|
// Implementation depends on your backend API
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating trend data:', error);
|
|
});
|
|
}
|
|
|
|
function switchSearchTab(tabType) {
|
|
// Hide all search contents
|
|
document.querySelectorAll('.search-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
|
|
// Remove active class from all tabs
|
|
document.querySelectorAll('.search-tab').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
|
|
// Show selected content and activate tab
|
|
document.getElementById(`search-${tabType}`).classList.add('active');
|
|
event.target.classList.add('active');
|
|
|
|
// Hide search results
|
|
document.getElementById('search-results').style.display = 'none';
|
|
}
|
|
|
|
function initializeSearch() {
|
|
// Add enter key support for search inputs
|
|
document.querySelectorAll('.search-content input').forEach(input => {
|
|
input.addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
const searchType = this.closest('.search-content').id.replace('search-', '');
|
|
searchPatients(searchType);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function searchPatients(searchType) {
|
|
let searchData = {};
|
|
|
|
switch (searchType) {
|
|
case 'name':
|
|
searchData.name = document.getElementById('search-name-input').value;
|
|
break;
|
|
case 'id':
|
|
searchData.patient_id = document.getElementById('search-id-input').value;
|
|
break;
|
|
case 'phone':
|
|
searchData.phone = document.getElementById('search-phone-input').value;
|
|
break;
|
|
case 'advanced':
|
|
searchData.first_name = document.getElementById('search-fname').value;
|
|
searchData.last_name = document.getElementById('search-lname').value;
|
|
searchData.date_of_birth = document.getElementById('search-dob').value;
|
|
break;
|
|
}
|
|
|
|
// Show loading state
|
|
const resultsDiv = document.getElementById('search-results');
|
|
resultsDiv.style.display = 'block';
|
|
resultsDiv.innerHTML = '<div class="text-center py-3"><i class="fas fa-spinner fa-spin"></i> Searching...</div>';
|
|
|
|
fetch('/patients/search/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
},
|
|
body: JSON.stringify(searchData)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
displaySearchResults(data.results);
|
|
})
|
|
.catch(error => {
|
|
resultsDiv.innerHTML = '<div class="alert alert-danger">Error performing search</div>';
|
|
});
|
|
}
|
|
|
|
function displaySearchResults(results) {
|
|
const resultsDiv = document.getElementById('search-results');
|
|
|
|
if (results.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="alert alert-info">No patients found matching your search criteria</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="list-group">';
|
|
results.forEach(patient => {
|
|
html += `
|
|
<a href="/patients/${patient.id}/" class="list-group-item list-group-item-action">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h6 class="mb-1">${patient.full_name}</h6>
|
|
<small class="text-muted">ID: ${patient.patient_id}</small>
|
|
</div>
|
|
<p class="mb-1">${patient.date_of_birth} • ${patient.gender}</p>
|
|
<small class="text-muted">${patient.phone || 'No phone'} • ${patient.email || 'No email'}</small>
|
|
</a>
|
|
`;
|
|
});
|
|
html += '</div>';
|
|
|
|
resultsDiv.innerHTML = html;
|
|
}
|
|
|
|
// Real-time updates for critical alerts
|
|
function checkCriticalAlerts() {
|
|
fetch('/patients/critical-alerts/', {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.alerts && data.alerts.length > 0) {
|
|
showCriticalAlert(data.alerts[0]);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error checking critical alerts:', error);
|
|
});
|
|
}
|
|
|
|
function showCriticalAlert(alert) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
|
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 350px;';
|
|
alertDiv.innerHTML = `
|
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>Critical Alert</h6>
|
|
<p class="mb-1"><strong>${alert.patient_name}</strong></p>
|
|
<p class="mb-0">${alert.message}</p>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(alertDiv);
|
|
|
|
// Auto-remove after 10 seconds
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.remove();
|
|
}
|
|
}, 10000);
|
|
}
|
|
|
|
// Check for critical alerts every 2 minutes
|
|
setInterval(checkCriticalAlerts, 120000);
|
|
</script>
|
|
{% endblock %}
|
|
|