399 lines
15 KiB
HTML
399 lines
15 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Patient Management - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="fas fa-users me-2"></i>Patient Management
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active">Patients</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-download me-2"></i>Export
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="?export=csv"><i class="fas fa-file-csv me-2"></i>CSV</a></li>
|
|
<li><a class="dropdown-item" href="?export=excel"><i class="fas fa-file-excel me-2"></i>Excel</a></li>
|
|
<li><a class="dropdown-item" href="?export=pdf"><i class="fas fa-file-pdf me-2"></i>PDF</a></li>
|
|
</ul>
|
|
<a href="{% url 'patients:patient_registration' %}" class="btn btn-primary">
|
|
<i class="fas fa-user-plus me-2"></i>Register Patient
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Patient Statistics -->
|
|
<div class="row mb-4" id="patient-stats"
|
|
hx-get="{% url 'patients:patient_stats' %}"
|
|
hx-trigger="load, every 60s">
|
|
<!-- Statistics will be loaded here -->
|
|
<div class="col-12 text-center">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading statistics...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search and Filters -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-filter me-2"></i>Search & Filter Patients
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3" id="filterForm">
|
|
<div class="col-lg-4 col-md-6">
|
|
<label for="search" class="form-label">Search Patients</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="text"
|
|
class="form-control"
|
|
id="search"
|
|
name="search"
|
|
value="{{ request.GET.search }}"
|
|
placeholder="Name, MRN, phone, email..."
|
|
hx-get="{% url 'patients:patient_search' %}"
|
|
hx-target="#patient-list-container"
|
|
hx-trigger="keyup changed delay:500ms"
|
|
hx-include="#filterForm">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-2 col-md-6">
|
|
<label for="gender" class="form-label">Gender</label>
|
|
<select class="form-select" id="gender" name="gender"
|
|
hx-get="{% url 'patients:patient_search' %}"
|
|
hx-target="#patient-list-container"
|
|
hx-trigger="change"
|
|
hx-include="#filterForm">
|
|
<option value="">All Genders</option>
|
|
<option value="MALE" {% if request.GET.gender == 'MALE' %}selected{% endif %}>Male</option>
|
|
<option value="FEMALE" {% if request.GET.gender == 'FEMALE' %}selected{% endif %}>Female</option>
|
|
<option value="OTHER" {% if request.GET.gender == 'OTHER' %}selected{% endif %}>Other</option>
|
|
<option value="UNKNOWN" {% if request.GET.gender == 'UNKNOWN' %}selected{% endif %}>Unknown</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-lg-2 col-md-6">
|
|
<label for="age_range" class="form-label">Age Range</label>
|
|
<select class="form-select" id="age_range" name="age_range"
|
|
hx-get="{% url 'patients:patient_search' %}"
|
|
hx-target="#patient-list-container"
|
|
hx-trigger="change"
|
|
hx-include="#filterForm">
|
|
<option value="">All Ages</option>
|
|
<option value="pediatric" {% if request.GET.age_range == 'pediatric' %}selected{% endif %}>Pediatric (<18)</option>
|
|
<option value="adult" {% if request.GET.age_range == 'adult' %}selected{% endif %}>Adult (18-64)</option>
|
|
<option value="geriatric" {% if request.GET.age_range == 'geriatric' %}selected{% endif %}>Geriatric (65+)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-lg-2 col-md-6">
|
|
<label for="status" class="form-label">Status</label>
|
|
<select class="form-select" id="status" name="status"
|
|
hx-get="{% url 'patients:patient_search' %}"
|
|
hx-target="#patient-list-container"
|
|
hx-trigger="change"
|
|
hx-include="#filterForm">
|
|
<option value="">All Status</option>
|
|
<option value="active" {% if request.GET.status == 'active' %}selected{% endif %}>Active</option>
|
|
<option value="inactive" {% if request.GET.status == 'inactive' %}selected{% endif %}>Inactive</option>
|
|
<option value="deceased" {% if request.GET.status == 'deceased' %}selected{% endif %}>Deceased</option>
|
|
<option value="vip" {% if request.GET.status == 'vip' %}selected{% endif %}>VIP</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-lg-2 col-md-6">
|
|
<label class="form-label"> </label>
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-search me-2"></i>Filter
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="clearFilters()">
|
|
<i class="fas fa-times me-2"></i>Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Patient List -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-list me-2"></i>Patient List
|
|
</h5>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<span class="badge bg-primary">{{ page_obj.paginator.count|default:patients.count }} total</span>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary" onclick="selectAll()">
|
|
<i class="fas fa-check-square me-1"></i>Select All
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary" onclick="clearSelection()">
|
|
<i class="fas fa-square me-1"></i>Clear
|
|
</button>
|
|
</div>
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="fas fa-cog me-1"></i>Actions
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#" onclick="bulkAction('export')">
|
|
<i class="fas fa-download me-2"></i>Export Selected
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="bulkAction('deactivate')">
|
|
<i class="fas fa-user-slash me-2"></i>Deactivate Selected
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="#" onclick="bulkAction('delete')">
|
|
<i class="fas fa-trash me-2"></i>Delete Selected
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body p-0" id="patient-list-container">
|
|
{% include 'patients/partials/patient_list.html' %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Clear filters function
|
|
function clearFilters() {
|
|
document.getElementById('filterForm').reset();
|
|
window.location.href = '{% url "patients:patient_list" %}';
|
|
}
|
|
|
|
// Bulk selection functions
|
|
function selectAll() {
|
|
const checkboxes = document.querySelectorAll('input[name="selected_patients"]');
|
|
checkboxes.forEach(cb => cb.checked = true);
|
|
updateBulkActionButtons();
|
|
}
|
|
|
|
function clearSelection() {
|
|
const checkboxes = document.querySelectorAll('input[name="selected_patients"]');
|
|
checkboxes.forEach(cb => cb.checked = false);
|
|
updateBulkActionButtons();
|
|
}
|
|
|
|
function updateBulkActionButtons() {
|
|
const selectedCount = document.querySelectorAll('input[name="selected_patients"]:checked').length;
|
|
const actionButtons = document.querySelectorAll('[onclick^="bulkAction"]');
|
|
|
|
actionButtons.forEach(btn => {
|
|
if (selectedCount > 0) {
|
|
btn.classList.remove('disabled');
|
|
btn.removeAttribute('disabled');
|
|
} else {
|
|
btn.classList.add('disabled');
|
|
btn.setAttribute('disabled', 'disabled');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Bulk actions
|
|
function bulkAction(action) {
|
|
const selectedPatients = Array.from(document.querySelectorAll('input[name="selected_patients"]:checked'))
|
|
.map(cb => cb.value);
|
|
|
|
if (selectedPatients.length === 0) {
|
|
alert('Please select at least one patient.');
|
|
return;
|
|
}
|
|
|
|
let confirmMessage = '';
|
|
switch (action) {
|
|
case 'export':
|
|
confirmMessage = `Export ${selectedPatients.length} selected patient(s)?`;
|
|
break;
|
|
case 'deactivate':
|
|
confirmMessage = `Deactivate ${selectedPatients.length} selected patient(s)?`;
|
|
break;
|
|
case 'delete':
|
|
confirmMessage = `Delete ${selectedPatients.length} selected patient(s)? This action cannot be undone.`;
|
|
break;
|
|
}
|
|
|
|
if (confirm(confirmMessage)) {
|
|
// Implement bulk action logic here
|
|
const form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = `{% url 'patients:patient_list' %}bulk_action/`;
|
|
|
|
// Add CSRF token
|
|
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
|
|
if (csrfToken) {
|
|
form.appendChild(csrfToken.cloneNode(true));
|
|
}
|
|
|
|
// Add action
|
|
const actionInput = document.createElement('input');
|
|
actionInput.type = 'hidden';
|
|
actionInput.name = 'action';
|
|
actionInput.value = action;
|
|
form.appendChild(actionInput);
|
|
|
|
// Add selected patients
|
|
selectedPatients.forEach(patientId => {
|
|
const input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = 'selected_patients';
|
|
input.value = patientId;
|
|
form.appendChild(input);
|
|
});
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
}
|
|
|
|
// Auto-refresh functionality
|
|
setInterval(function() {
|
|
htmx.trigger('#patient-stats', 'refresh');
|
|
}, 60000); // Refresh every minute
|
|
|
|
// Initialize bulk action buttons
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
updateBulkActionButtons();
|
|
|
|
// Add event listeners to checkboxes
|
|
document.addEventListener('change', function(e) {
|
|
if (e.target.name === 'selected_patients') {
|
|
updateBulkActionButtons();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', function(e) {
|
|
// Ctrl+A to select all
|
|
if (e.ctrlKey && e.key === 'a' && e.target.tagName !== 'INPUT') {
|
|
e.preventDefault();
|
|
selectAll();
|
|
}
|
|
|
|
// Escape to clear selection
|
|
if (e.key === 'Escape') {
|
|
clearSelection();
|
|
}
|
|
|
|
// Ctrl+N for new patient
|
|
if (e.ctrlKey && e.key === 'n') {
|
|
e.preventDefault();
|
|
window.location.href = '{% url "patients:patient_registration" %}';
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.card {
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
border: 1px solid rgba(0, 0, 0, 0.125);
|
|
}
|
|
|
|
.card-header {
|
|
background-color: rgba(13, 110, 253, 0.1);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
|
}
|
|
|
|
.btn {
|
|
border-radius: 0.375rem;
|
|
transition: all 0.15s ease-in-out;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.form-control, .form-select {
|
|
border-radius: 0.375rem;
|
|
border: 1px solid #ced4da;
|
|
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
|
}
|
|
|
|
.form-control:focus, .form-select:focus {
|
|
border-color: #86b7fe;
|
|
outline: 0;
|
|
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
|
}
|
|
|
|
.input-group-text {
|
|
background-color: #f8f9fa;
|
|
border: 1px solid #ced4da;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.dropdown-menu {
|
|
border-radius: 0.375rem;
|
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.htmx-indicator {
|
|
opacity: 0;
|
|
transition: opacity 0.3s ease-in-out;
|
|
}
|
|
|
|
.htmx-request .htmx-indicator {
|
|
opacity: 1;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.btn-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.card-header .d-flex {
|
|
flex-direction: column;
|
|
align-items: flex-start !important;
|
|
gap: 1rem;
|
|
}
|
|
}
|
|
|
|
/* Loading states */
|
|
.htmx-request {
|
|
opacity: 0.8;
|
|
transition: opacity 0.3s ease-in-out;
|
|
}
|
|
|
|
/* Selection highlighting */
|
|
tr.selected {
|
|
background-color: rgba(13, 110, 253, 0.1);
|
|
}
|
|
|
|
/* Responsive table */
|
|
@media (max-width: 992px) {
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|