update
This commit is contained in:
parent
43901b5bda
commit
a710d1c4d8
1
.idea/inspectionProfiles/profiles_settings.xml
generated
1
.idea/inspectionProfiles/profiles_settings.xml
generated
@ -1,5 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="PROJECT_PROFILE" value="Default" />
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
|
||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@ -14,6 +14,9 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="uv (hospital_management_system_v4)" project-jdk-type="Python SDK" />
|
||||
<component name="PyPackaging">
|
||||
<option name="earlyReleasesAsUpgrades" value="true" />
|
||||
</component>
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
|
||||
BIN
appointments/.DS_Store
vendored
Normal file
BIN
appointments/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
appointments/__pycache__/mixins.cpython-312.pyc
Normal file
BIN
appointments/__pycache__/mixins.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -12,21 +12,24 @@ from .models import (
|
||||
)
|
||||
from patients.models import PatientProfile
|
||||
from accounts.models import User
|
||||
from hr.models import Employee
|
||||
|
||||
|
||||
class AppointmentRequestForm(forms.ModelForm):
|
||||
"""
|
||||
Form for appointment request management.
|
||||
Patient is set automatically in the view; it is NOT exposed here.
|
||||
"""
|
||||
class Meta:
|
||||
model = AppointmentRequest
|
||||
# do not include 'patient' so it cannot be tampered with
|
||||
exclude = ['patient','status']
|
||||
fields = [
|
||||
'patient', 'provider', 'appointment_type', 'specialty', 'preferred_date',
|
||||
'provider', 'appointment_type', 'specialty', 'preferred_date',
|
||||
'preferred_time', 'duration_minutes', 'priority', 'chief_complaint',
|
||||
'clinical_notes', 'status', 'is_telemedicine', 'location', 'room_number'
|
||||
'clinical_notes', 'is_telemedicine', 'location', 'room_number'
|
||||
]
|
||||
widgets = {
|
||||
'patient': forms.Select(attrs={'class': 'form-select'}),
|
||||
'provider': forms.Select(attrs={'class': 'form-select'}),
|
||||
'appointment_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'specialty': forms.Select(attrs={'class': 'form-select'}),
|
||||
@ -36,34 +39,31 @@ class AppointmentRequestForm(forms.ModelForm):
|
||||
'priority': forms.Select(attrs={'class': 'form-select'}),
|
||||
'chief_complaint': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'clinical_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'status': forms.Select(attrs={'class': 'form-select'}),
|
||||
'is_telemedicine': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||
'location': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'room_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
# patient is optional to receive; we don't render it, but can use if needed
|
||||
# self._patient = kwargs.pop('patient', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# Scope provider list to tenant
|
||||
if user and hasattr(user, 'tenant'):
|
||||
self.fields['patient'].queryset = PatientProfile.objects.filter(
|
||||
tenant=user.tenant,
|
||||
is_active=True
|
||||
).order_by('last_name', 'first_name')
|
||||
|
||||
self.fields['provider'].queryset = User.objects.filter(
|
||||
tenant=user.tenant,
|
||||
is_active=True,
|
||||
role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
||||
).order_by('last_name', 'first_name')
|
||||
|
||||
|
||||
def clean_preferred_date(self):
|
||||
preferred_date = self.cleaned_data.get('preferred_date')
|
||||
if preferred_date and preferred_date < timezone.now().date():
|
||||
raise ValidationError('Appointment cannot be scheduled in the past.')
|
||||
return preferred_date
|
||||
|
||||
|
||||
def clean_duration_minutes(self):
|
||||
duration = self.cleaned_data.get('duration_minutes')
|
||||
if duration and duration < 15:
|
||||
@ -148,11 +148,12 @@ class WaitingQueueForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop('user', None)
|
||||
tenant = kwargs.pop('tenant', None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user and hasattr(user, 'tenant'):
|
||||
if user and tenant:
|
||||
self.fields['provider'].queryset = User.objects.filter(
|
||||
tenant=user.tenant,
|
||||
tenant=tenant,
|
||||
is_active=True,
|
||||
role__in=['PHYSICIAN', 'NURSE', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT']
|
||||
).order_by('last_name', 'first_name')
|
||||
|
||||
49
appointments/mixins.py
Normal file
49
appointments/mixins.py
Normal file
@ -0,0 +1,49 @@
|
||||
# views.py
|
||||
import requests
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from patients.models import PatientProfile
|
||||
|
||||
|
||||
class PatientContextMixin:
|
||||
"""
|
||||
Resolve the patient automatically from:
|
||||
1) URL kwarg `patient_id`
|
||||
2) Query param `?patient=<uuid|pk>`
|
||||
3) Logged-in user's linked PatientProfile
|
||||
"""
|
||||
patient_kwarg_name = "patient_id"
|
||||
patient_query_param = "patient"
|
||||
|
||||
def get_patient(self):
|
||||
tenant = getattr(self.request, 'tenant', None)
|
||||
|
||||
# 1) URL kwarg
|
||||
patient_id = self.kwargs.get(self.patient_kwarg_name)
|
||||
if patient_id:
|
||||
patient = get_object_or_404(
|
||||
PatientProfile,
|
||||
pk=patient_id
|
||||
)
|
||||
print(patient)
|
||||
return patient
|
||||
|
||||
# 2) Query param
|
||||
patient_qp = self.request.GET.get(self.patient_query_param)
|
||||
if patient_qp:
|
||||
patient = get_object_or_404(
|
||||
PatientProfile,
|
||||
pk=patient_qp
|
||||
)
|
||||
print(patient)
|
||||
return patient
|
||||
|
||||
# 3) Current user's patient profile (self-service)
|
||||
# Adjust attribute access if your relation is named differently.
|
||||
# if hasattr(self.request.user, 'patientprofile'):
|
||||
# pp = self.request.user.patientprofile
|
||||
# if pp and pp.tenant == tenant and pp.is_active:
|
||||
# return pp
|
||||
|
||||
# If still not found, raise a clear 404 (or you can redirect with a message)
|
||||
# raise Http404("Patient not found or not accessible.")
|
||||
Binary file not shown.
BIN
appointments/templates/appointments/.DS_Store
vendored
Normal file
BIN
appointments/templates/appointments/.DS_Store
vendored
Normal file
Binary file not shown.
@ -20,7 +20,7 @@
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h1 class="page-header mb-0">Appointment Calendar</h1>
|
||||
<div>
|
||||
<a href="{% url 'appointments:appointment_create' %}" class="btn btn-success">
|
||||
<a href="#" class="btn btn-success">
|
||||
<i class="fa fa-plus me-2"></i>New Appointment
|
||||
</a>
|
||||
</div>
|
||||
@ -36,15 +36,21 @@
|
||||
|
||||
<div class="row">
|
||||
<!-- Today's Appointments -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
<div class="col-lg-6">
|
||||
<div class="panel panel-inverse mb-4" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-calendar-day"></i> Today's Appointments
|
||||
</h5>
|
||||
<span class="badge bg-primary">{{ todays_appointments|length }} scheduled</span>
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-xs btn-outline-primary me-2">View All</a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
@ -137,13 +143,20 @@
|
||||
|
||||
<!-- Active Queues -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-users"></i> Active Queues
|
||||
</h5>
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'appointments:queue_management' %}" class="btn btn-xs btn-outline-primary me-2">View All</a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel-body">
|
||||
{% for queue in active_queues %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 p-3 border rounded">
|
||||
<div>
|
||||
@ -171,15 +184,22 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<div class="panel panel-inverse mb-4" data-sortable-id="index-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-bolt"></i> Quick Actions
|
||||
</h5>
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'appointments:scheduling_calendar' %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-calendar-plus"></i> Schedule Appointment
|
||||
@ -0,0 +1,19 @@
|
||||
{% if slots %}
|
||||
|
||||
|
||||
<div class="row">
|
||||
{% for slot in slots %}
|
||||
<div class="col-md-3 mb-2">
|
||||
<p>{{ slot.date }}</p>
|
||||
<button
|
||||
class="btn btn-outline-primary btn-sm w-100 time-slot"
|
||||
data-time="{{ slot.start_time|time:'H:i' }}">
|
||||
{{ slot.start_time|time:"g:i A" }}
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted text-center mb-0">Select the date and provider for available slots.</p>
|
||||
|
||||
{% endif %}
|
||||
@ -0,0 +1,10 @@
|
||||
{% if schedules %}
|
||||
<div class="small mb-2"><strong>Working Hours:</strong></div>
|
||||
{% for schedule in schedules %}
|
||||
<div class="small text-muted mb-1">{{ schedule.schedule_type }}</div>
|
||||
{% endfor %}
|
||||
<hr class="my-2">
|
||||
<div class="small mb-2"><strong>Specializations:</strong></div>
|
||||
<div class="small text-muted">General Medicine, Cardiology</div>
|
||||
|
||||
{% endif %}
|
||||
@ -0,0 +1,451 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Remove from Queue - {{ entry.patient.get_full_name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-warning {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.patient-card {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.patient-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background: #dee2e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6c757d;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.impact-icon {
|
||||
color: #856404;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.removal-options {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
background: #f8f9fa;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
background: #e3f2fd;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.option-description {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.entry-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-waiting { background: #fff3cd; color: #856404; }
|
||||
.status-called { background: #d1ecf1; color: #0c5460; }
|
||||
.status-in-service { background: #cce5ff; color: #004085; }
|
||||
.status-completed { background: #d4edda; color: #155724; }
|
||||
.status-left { background: #f8d7da; color: #721c24; }
|
||||
.status-no-show { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-warning {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.patient-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:queueentry_list' %}">Queue Entries</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:queueentry_detail' entry.pk %}">{{ entry.patient.get_full_name }}</a></li>
|
||||
<li class="breadcrumb-item active">Remove</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-user-times me-2"></i>Remove from Queue
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'appointments:queueentry_detail' entry.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Entry
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Warning -->
|
||||
<div class="delete-warning text-center">
|
||||
<div class="warning-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h3 class="mb-3">Remove Patient from Queue?</h3>
|
||||
<p class="mb-0 opacity-75">
|
||||
This will remove the patient from the waiting queue and may affect their appointment.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Patient Information -->
|
||||
<div class="patient-card">
|
||||
<div class="patient-header">
|
||||
<div class="patient-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1">{{ entry.patient.get_full_name }}</h4>
|
||||
<p class="text-muted mb-0">Patient ID: {{ entry.patient.patient_id }}</p>
|
||||
<p class="text-muted mb-0">{{ entry.patient.date_of_birth|date:"M d, Y" }} • {{ entry.patient.gender }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Current Status:</span>
|
||||
<span class="entry-status status-{{ entry.status|lower }}">
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Position in Queue:</span>
|
||||
<span class="info-value">
|
||||
<span class="badge bg-primary">{{ entry.queue_position }}</span>
|
||||
of {{ entry.queue.current_queue_size }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Wait Time:</span>
|
||||
<span class="info-value">{{ entry.joined_at|timesince }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Queue:</span>
|
||||
<span class="info-value">{{ entry.queue.name }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Appointment:</span>
|
||||
<span class="info-value">{{ entry.appointment.appointment_type }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Scheduled Time:</span>
|
||||
<span class="info-value">{{ entry.appointment.scheduled_time|time:"g:i A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Warning -->
|
||||
<div class="impact-warning">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
|
||||
Removal Impact
|
||||
</h6>
|
||||
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-users impact-icon"></i>
|
||||
<span>Other patients behind this patient will move up in the queue</span>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-calendar-times impact-icon"></i>
|
||||
<span>The associated appointment may need to be rescheduled</span>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-bell impact-icon"></i>
|
||||
<span>Patient will not receive further queue notifications</span>
|
||||
</div>
|
||||
|
||||
{% if entry.assigned_provider %}
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-user-md impact-icon"></i>
|
||||
<span>Provider {{ entry.assigned_provider.get_full_name }} will be notified</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Removal Options -->
|
||||
<div class="removal-options">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-cog me-2"></i>Removal Reason
|
||||
</h6>
|
||||
|
||||
<div class="option-card" data-reason="completed">
|
||||
<div class="option-title">Service Completed</div>
|
||||
<div class="option-description">Patient has been served and the appointment is complete</div>
|
||||
</div>
|
||||
|
||||
<div class="option-card" data-reason="no_show">
|
||||
<div class="option-title">No Show</div>
|
||||
<div class="option-description">Patient did not show up for their appointment</div>
|
||||
</div>
|
||||
|
||||
<div class="option-card" data-reason="left">
|
||||
<div class="option-title">Patient Left</div>
|
||||
<div class="option-description">Patient chose to leave the queue voluntarily</div>
|
||||
</div>
|
||||
|
||||
<div class="option-card" data-reason="rescheduled">
|
||||
<div class="option-title">Rescheduled</div>
|
||||
<div class="option-description">Appointment has been rescheduled to a different time</div>
|
||||
</div>
|
||||
|
||||
<div class="option-card" data-reason="cancelled">
|
||||
<div class="option-title">Cancelled</div>
|
||||
<div class="option-description">Appointment has been cancelled by patient or provider</div>
|
||||
</div>
|
||||
|
||||
<div class="option-card" data-reason="other">
|
||||
<div class="option-title">Other Reason</div>
|
||||
<div class="option-description">Specify a custom reason for removal</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Notes -->
|
||||
<div class="entry-info">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-sticky-note me-2"></i>Additional Notes (Optional)
|
||||
</h6>
|
||||
|
||||
<div class="form-group">
|
||||
<textarea class="form-control" id="removal-notes" rows="3"
|
||||
placeholder="Add any additional notes about the removal..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
This action will be logged for audit purposes
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'appointments:queueentry_detail' entry.pk %}" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
<form method="post" class="d-inline" id="removeForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="removal-reason" name="removal_reason" value="">
|
||||
<input type="hidden" id="removal-notes-input" name="removal_notes" value="">
|
||||
<button type="submit" class="btn btn-danger" id="removeButton" disabled>
|
||||
<i class="fas fa-user-times me-1"></i>Remove from Queue
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let selectedReason = '';
|
||||
|
||||
// Handle option selection
|
||||
$('.option-card').on('click', function() {
|
||||
$('.option-card').removeClass('selected');
|
||||
$(this).addClass('selected');
|
||||
|
||||
selectedReason = $(this).data('reason');
|
||||
$('#removal-reason').val(selectedReason);
|
||||
|
||||
// Enable remove button if reason is selected
|
||||
$('#removeButton').prop('disabled', false);
|
||||
});
|
||||
|
||||
// Update notes input when textarea changes
|
||||
$('#removal-notes').on('input', function() {
|
||||
$('#removal-notes-input').val($(this).val());
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#removeForm').on('submit', function(e) {
|
||||
if (!selectedReason) {
|
||||
e.preventDefault();
|
||||
showAlert('error', 'Please select a reason for removal');
|
||||
return;
|
||||
}
|
||||
|
||||
// Confirm removal
|
||||
const reasonText = $('.option-card.selected .option-title').text();
|
||||
if (!confirm(`Are you sure you want to remove this patient from the queue?\n\nReason: ${reasonText}\n\nThis action cannot be undone.`)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
$('#removeButton').prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-1"></i>Removing...');
|
||||
});
|
||||
|
||||
// Prevent accidental navigation
|
||||
let formSubmitted = false;
|
||||
$('#removeForm').on('submit', function() {
|
||||
formSubmitted = true;
|
||||
});
|
||||
|
||||
$(window).on('beforeunload', function(e) {
|
||||
if (!formSubmitted && selectedReason) {
|
||||
const message = 'You have selected a removal reason. Are you sure you want to leave?';
|
||||
e.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,693 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Queue Entry Details - {{ entry.patient.get_full_name }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.entry-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.status-timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2rem;
|
||||
top: 0.5rem;
|
||||
width: 2px;
|
||||
height: calc(100% - 0.5rem);
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -2.5rem;
|
||||
top: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-marker.active {
|
||||
background: #007bff;
|
||||
box-shadow: 0 0 0 2px #007bff;
|
||||
}
|
||||
|
||||
.timeline-marker.completed {
|
||||
background: #28a745;
|
||||
box-shadow: 0 0 0 2px #28a745;
|
||||
}
|
||||
|
||||
.timeline-marker.pending {
|
||||
background: #6c757d;
|
||||
box-shadow: 0 0 0 2px #6c757d;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.timeline-description {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.entry-status {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-waiting { background: #fff3cd; color: #856404; }
|
||||
.status-called { background: #d1ecf1; color: #0c5460; }
|
||||
.status-in-service { background: #cce5ff; color: #004085; }
|
||||
.status-completed { background: #d4edda; color: #155724; }
|
||||
.status-left { background: #f8d7da; color: #721c24; }
|
||||
.status-no-show { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.priority-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.priority-low { background: #28a745; }
|
||||
.priority-medium { background: #ffc107; }
|
||||
.priority-high { background: #fd7e14; }
|
||||
.priority-critical { background: #dc3545; }
|
||||
|
||||
.wait-time-display {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wait-time-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.wait-time-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.entry-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.status-timeline {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
left: -2rem;
|
||||
}
|
||||
|
||||
.timeline-item::before {
|
||||
left: -1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:queue_entry_list' %}">Queue Entries</a></li>
|
||||
<li class="breadcrumb-item active">{{ entry.patient.get_full_name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-user-clock me-2"></i>Queue Entry Details
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'appointments:queue_entry_list' %}?queue={{ entry.queue.pk }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Queue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entry Header -->
|
||||
<div class="entry-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="avatar avatar-lg me-3">
|
||||
<i class="fas fa-user-circle fa-3x"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="mb-1">{{ entry.patient.get_full_name }}</h3>
|
||||
<p class="mb-0 opacity-75">Patient MRN: {{ entry.patient.mrn }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="entry-status status-{{ entry.status|lower }} me-3">
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
<div class="priority-indicator priority-{{ entry.priority_level }}"></div>
|
||||
<span class="opacity-75">Priority: {{ entry.priority_level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="wait-time-display">
|
||||
<div class="wait-time-number" id="wait-time-display">
|
||||
{{ entry.wait_time_minutes }}
|
||||
</div>
|
||||
<div class="wait-time-label">Minutes Waiting</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<!-- Queue Information -->
|
||||
<div class="info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-users me-2"></i>Queue Information
|
||||
</h5>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Queue Name:</span>
|
||||
<span class="info-value">
|
||||
<a href="" class="text-decoration-none">
|
||||
{{ entry.queue.name }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Queue Type:</span>
|
||||
<span class="info-value">{{ entry.queue.get_queue_type_display }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Position in Queue:</span>
|
||||
<span class="info-value">
|
||||
<span class="badge bg-primary">{{ entry.queue_position }}</span>
|
||||
of {{ entry.queue.current_queue_size }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Priority Score:</span>
|
||||
<span class="info-value">{{ entry.priority_score }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Estimated Service Time:</span>
|
||||
<span class="info-value">
|
||||
{% if entry.estimated_service_time %}
|
||||
{{ entry.estimated_service_time|time:"g:i A" }}
|
||||
{% else %}
|
||||
Not calculated
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appointment Information -->
|
||||
<div class="info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-calendar-check me-2"></i>Appointment Information
|
||||
</h5>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Appointment Type:</span>
|
||||
<span class="info-value">{{ entry.appointment.appointment_type }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Scheduled Time:</span>
|
||||
<span class="info-value">{{ entry.appointment.scheduled_time|date:"M d, Y g:i A" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Duration:</span>
|
||||
<span class="info-value">{{ entry.appointment.duration_minutes }} minutes</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Reason:</span>
|
||||
<span class="info-value">{{ entry.appointment.reason|default:"Not specified" }}</span>
|
||||
</div>
|
||||
|
||||
{% if entry.appointment.notes %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Notes:</span>
|
||||
<span class="info-value">{{ entry.appointment.notes }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Provider Information -->
|
||||
<div class="info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-user-md me-2"></i>Provider Information
|
||||
</h5>
|
||||
|
||||
{% if entry.assigned_provider %}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-md me-3">
|
||||
<i class="fas fa-user-circle fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ entry.assigned_provider.get_full_name }}</div>
|
||||
<div class="text-muted">{{ entry.assigned_provider.email }}</div>
|
||||
{% if entry.assigned_provider.phone %}
|
||||
<div class="text-muted">{{ entry.assigned_provider.phone }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-user-slash fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted mb-0">No provider assigned</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Status Timeline -->
|
||||
<div class="info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-history me-2"></i>Status Timeline
|
||||
</h5>
|
||||
|
||||
<div class="status-timeline">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker completed"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">{{ entry.joined_at|date:"M d, Y g:i A" }}</div>
|
||||
<div class="timeline-title">Joined Queue</div>
|
||||
<div class="timeline-description">Patient added to {{ entry.queue.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if entry.called_at %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker completed"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">{{ entry.called_at|date:"M d, Y g:i A" }}</div>
|
||||
<div class="timeline-title">Patient Called</div>
|
||||
<div class="timeline-description">Patient was called for service</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.served_at %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker completed"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">{{ entry.served_at|date:"M d, Y g:i A" }}</div>
|
||||
<div class="timeline-title">Service Started</div>
|
||||
<div class="timeline-description">Patient service began</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.status == 'COMPLETED' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker completed"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">{{ entry.updated_at|date:"M d, Y g:i A" }}</div>
|
||||
<div class="timeline-title">Service Completed</div>
|
||||
<div class="timeline-description">Patient service completed successfully</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif entry.status == 'WAITING' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker pending"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">Pending</div>
|
||||
<div class="timeline-title">Waiting for Call</div>
|
||||
<div class="timeline-description">Patient is waiting to be called</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif entry.status == 'CALLED' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker active"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">Current</div>
|
||||
<div class="timeline-title">Called - Waiting for Service</div>
|
||||
<div class="timeline-description">Patient has been called and is waiting for service</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif entry.status == 'IN_SERVICE' %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker active"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-time">Current</div>
|
||||
<div class="timeline-title">In Service</div>
|
||||
<div class="timeline-description">Patient is currently being served</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
{% if entry.notes %}
|
||||
<div class="info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-sticky-note me-2"></i>Notes
|
||||
</h5>
|
||||
<p class="mb-0">{{ entry.notes|linebreaks }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<div class="info-card">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-cogs me-2"></i>Quick Actions
|
||||
</h6>
|
||||
|
||||
{% if entry.status == 'WAITING' %}
|
||||
<button class="btn btn-primary w-100 mb-2" onclick="callPatient()">
|
||||
<i class="fas fa-phone me-1"></i>Call Patient
|
||||
</button>
|
||||
|
||||
<button class="btn btn-warning w-100 mb-2" onclick="moveToTop()">
|
||||
<i class="fas fa-arrow-up me-1"></i>Move to Top
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{% if entry.status in 'WAITING,CALLED' %}
|
||||
<a href="{% url 'appointments:queue_entry_update' entry.pk %}" class="btn btn-outline-warning w-100 mb-2">
|
||||
<i class="fas fa-edit me-1"></i>Edit Entry
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-outline-info w-100 mb-2" onclick="sendNotification()">
|
||||
<i class="fas fa-bell me-1"></i>Send Notification
|
||||
</button>
|
||||
|
||||
{% if entry.status != 'COMPLETED' %}
|
||||
<button class="btn btn-outline-success w-100 mb-2" onclick="markCompleted()">
|
||||
<i class="fas fa-check me-1"></i>Mark Completed
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-outline-danger w-100" onclick="removeFromQueue()">
|
||||
<i class="fas fa-times me-1"></i>Remove from Queue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Communication -->
|
||||
<div class="info-card">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-comments me-2"></i>Communication
|
||||
</h6>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Notification Sent:</span>
|
||||
<span class="info-value">
|
||||
{% if entry.notification_sent %}
|
||||
<i class="fas fa-check text-success"></i> Yes
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> No
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if entry.notification_method %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Method:</span>
|
||||
<span class="info-value">{{ entry.get_notification_method_display }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Entry Metadata -->
|
||||
<div class="info-card">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>Entry Details
|
||||
</h6>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Entry ID:</span>
|
||||
<span class="info-value">{{ entry.entry_id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Created:</span>
|
||||
<span class="info-value">{{ entry.joined_at|date:"M d, Y g:i A" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Updated:</span>
|
||||
<span class="info-value">{{ entry.updated_at|timesince }} ago</span>
|
||||
</div>
|
||||
|
||||
{% if entry.updated_by %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Updated By:</span>
|
||||
<span class="info-value">{{ entry.updated_by.get_full_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Update wait time every minute
|
||||
setInterval(updateWaitTime, 60000);
|
||||
|
||||
// Auto-refresh page every 5 minutes
|
||||
setInterval(function() {
|
||||
location.reload();
|
||||
}, 300000);
|
||||
});
|
||||
|
||||
function updateWaitTime() {
|
||||
const joinedAt = new Date('{{ entry.joined_at|date:"c" }}');
|
||||
const now = new Date();
|
||||
const diffMinutes = Math.floor((now - joinedAt) / (1000 * 60));
|
||||
|
||||
$('#wait-time-display').text(diffMinutes);
|
||||
}
|
||||
|
||||
{#function callPatient() {#}
|
||||
{# if (confirm('Call this patient?')) {#}
|
||||
{# $.post('{% url "appointments:call_patient" %}', {#}
|
||||
{# entry_id: '{{ entry.pk }}',#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient called successfully');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to call patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to call patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function moveToTop() {#}
|
||||
{# if (confirm('Move this patient to the top of the queue?')) {#}
|
||||
{# $.post('{% url "appointments:move_to_top" %}', {#}
|
||||
{# entry_id: '{{ entry.pk }}',#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient moved to top of queue');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to move patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to move patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function sendNotification() {#}
|
||||
{# if (confirm('Send notification to this patient?')) {#}
|
||||
{# $.post('{% url "appointments:send_notification" %}', {#}
|
||||
{# entry_id: '{{ entry.pk }}',#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Notification sent successfully');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to send notification');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to send notification');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function markCompleted() {#}
|
||||
{# if (confirm('Mark this entry as completed?')) {#}
|
||||
{# $.post('{% url "appointments:mark_completed" %}', {#}
|
||||
{# entry_id: '{{ entry.pk }}',#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Entry marked as completed');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to mark as completed');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to mark as completed');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function removeFromQueue() {#}
|
||||
{# if (confirm('Remove this patient from the queue? This action cannot be undone.')) {#}
|
||||
{# $.post('{% url "appointments:remove_from_queue" %}', {#}
|
||||
{# entry_id: '{{ entry.pk }}',#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient removed from queue');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# window.location.href = '{% url "appointments:queueentry_list" %}';#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to remove patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to remove patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
659
appointments/templates/appointments/queue/queue_entry_form.html
Normal file
659
appointments/templates/appointments/queue/queue_entry_form.html
Normal file
@ -0,0 +1,659 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if form.instance.pk %}Edit{% else %}Add to{% endif %} Queue Entry{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/select2-bootstrap5-theme/dist/select2-bootstrap-5-theme.min.css' %}" rel="stylesheet" />
|
||||
<style>
|
||||
.form-section {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
}
|
||||
|
||||
.queue-preview {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.patient-preview {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.patient-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: #dee2e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.priority-slider {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.priority-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.estimated-time {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
color: #666;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.queue-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:queueentry_list' %}">Queue Entries</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if form.instance.pk %}Edit Entry{% else %}Add to Queue{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-user-plus me-2"></i>
|
||||
{% if form.instance.pk %}Edit Queue Entry{% else %}Add Patient to Queue{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'appointments:queueentry_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="queueEntryForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Queue Selection -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-users me-2"></i>Queue Selection
|
||||
</h4>
|
||||
<p class="section-description">Select the queue to add the patient to</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.queue.id_for_label }}" class="form-label">
|
||||
Waiting Queue <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.queue }}
|
||||
{% if form.queue.help_text %}
|
||||
<div class="form-text">{{ form.queue.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.queue.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.queue.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="queue-preview" id="queue-preview" style="display: none;">
|
||||
<div class="queue-info">
|
||||
<div>
|
||||
<h6 class="mb-1" id="queue-name"></h6>
|
||||
<small class="text-muted" id="queue-type"></small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-primary" id="queue-capacity"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="queue-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="current-size">0</div>
|
||||
<div class="stat-label">Current</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="max-size">0</div>
|
||||
<div class="stat-label">Capacity</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number" id="avg-wait">0</div>
|
||||
<div class="stat-label">Avg Wait (min)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Patient Selection -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-user me-2"></i>Patient Selection
|
||||
</h4>
|
||||
<p class="section-description">Select the patient to add to the queue</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.patient.id_for_label }}" class="form-label">
|
||||
Patient <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.patient }}
|
||||
{% if form.patient.help_text %}
|
||||
<div class="form-text">{{ form.patient.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.patient.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.patient.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="patient-preview" id="patient-preview" style="display: none;">
|
||||
<div class="patient-info">
|
||||
<div class="patient-avatar">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1" id="patient-name"></h6>
|
||||
<small class="text-muted">ID: <span id="patient-id"></span></small>
|
||||
<div class="text-muted" id="patient-details"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appointment Selection -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-calendar-check me-2"></i>Appointment Selection
|
||||
</h4>
|
||||
<p class="section-description">Select the associated appointment</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.appointment.id_for_label }}" class="form-label">
|
||||
Appointment <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.appointment }}
|
||||
{% if form.appointment.help_text %}
|
||||
<div class="form-text">{{ form.appointment.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.appointment.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.appointment.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Priority Configuration -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-sort-amount-up me-2"></i>Priority Configuration
|
||||
</h4>
|
||||
<p class="section-description">Set the priority level for queue ordering</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.priority_score.id_for_label }}" class="form-label">
|
||||
Priority Score
|
||||
</label>
|
||||
{{ form.priority_score }}
|
||||
{% if form.priority_score.help_text %}
|
||||
<div class="form-text">{{ form.priority_score.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.priority_score.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.priority_score.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="priority-slider">
|
||||
<input type="range" class="form-range" id="priority-slider"
|
||||
min="0.1" max="10" step="0.1" value="1.0">
|
||||
<div class="priority-labels">
|
||||
<span>Low</span>
|
||||
<span>Medium</span>
|
||||
<span>High</span>
|
||||
<span>Critical</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Quick Priority Settings</label>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<button type="button" class="btn btn-outline-success" onclick="setPriority(1.0)">
|
||||
Low
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-warning" onclick="setPriority(3.0)">
|
||||
Medium
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="setPriority(7.0)">
|
||||
High
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="setPriority(10.0)">
|
||||
Critical
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider Assignment -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-user-md me-2"></i>Provider Assignment
|
||||
</h4>
|
||||
<p class="section-description">Optionally assign a specific provider</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.assigned_provider.id_for_label }}" class="form-label">
|
||||
Assigned Provider
|
||||
</label>
|
||||
{{ form.assigned_provider }}
|
||||
{% if form.assigned_provider.help_text %}
|
||||
<div class="form-text">{{ form.assigned_provider.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.assigned_provider.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.assigned_provider.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Communication Settings -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-comments me-2"></i>Communication Settings
|
||||
</h4>
|
||||
<p class="section-description">Configure notification preferences</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.notification_method.id_for_label }}" class="form-label">
|
||||
Notification Method
|
||||
</label>
|
||||
{{ form.notification_method }}
|
||||
{% if form.notification_method.help_text %}
|
||||
<div class="form-text">{{ form.notification_method.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.notification_method.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.notification_method.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notification Options</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="send-immediate-notification">
|
||||
<label class="form-check-label" for="send-immediate-notification">
|
||||
Send immediate notification
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="auto-notify-on-call">
|
||||
<label class="form-check-label" for="auto-notify-on-call">
|
||||
Auto-notify when called
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Notes -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-sticky-note me-2"></i>Additional Information
|
||||
</h4>
|
||||
<p class="section-description">Add any additional notes or special instructions</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.notes.id_for_label }}" class="form-label">Notes</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.help_text %}
|
||||
<div class="form-text">{{ form.notes.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.notes.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.notes.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimated Wait Time -->
|
||||
<div class="estimated-time" id="estimated-time" style="display: none;">
|
||||
<div class="time-display" id="estimated-minutes">0</div>
|
||||
<div class="time-label">Estimated Wait Time (minutes)</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-section">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{% if form.instance.pk %}
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Last updated: {{ form.instance.updated_at|date:"M d, Y g:i A" }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'appointments:queueentry_list' %}" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% if form.instance.pk %}Update Entry{% else %}Add to Queue{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2
|
||||
$('#id_queue, #id_patient, #id_appointment, #id_assigned_provider').select2({
|
||||
theme: 'bootstrap-5',
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
// Queue data for preview
|
||||
const queueData = {
|
||||
{% for queue in queues %}
|
||||
'{{ queue.pk }}': {
|
||||
name: '{{ queue.name }}',
|
||||
type: '{{ queue.get_queue_type_display }}',
|
||||
current_size: {{ queue.current_queue_size }},
|
||||
max_size: {{ queue.max_queue_size }},
|
||||
avg_wait: {{ queue.estimated_wait_time_minutes }}
|
||||
},
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
// Patient data for preview
|
||||
const patientData = {
|
||||
{% for patient in patients %}
|
||||
'{{ patient.pk }}': {
|
||||
name: '{{ patient.get_full_name }}',
|
||||
id: '{{ patient.patient_id }}',
|
||||
details: '{{ patient.date_of_birth|date:"M d, Y" }} • {{ patient.gender }}'
|
||||
},
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
// Queue selection change
|
||||
$('#id_queue').on('change', function() {
|
||||
const queueId = $(this).val();
|
||||
if (queueId && queueData[queueId]) {
|
||||
const queue = queueData[queueId];
|
||||
$('#queue-name').text(queue.name);
|
||||
$('#queue-type').text(queue.type);
|
||||
$('#queue-capacity').text(`${queue.current_size}/${queue.max_size}`);
|
||||
$('#current-size').text(queue.current_size);
|
||||
$('#max-size').text(queue.max_size);
|
||||
$('#avg-wait').text(queue.avg_wait);
|
||||
$('#queue-preview').show();
|
||||
|
||||
updateEstimatedWaitTime();
|
||||
} else {
|
||||
$('#queue-preview').hide();
|
||||
$('#estimated-time').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Patient selection change
|
||||
$('#id_patient').on('change', function() {
|
||||
const patientId = $(this).val();
|
||||
if (patientId && patientData[patientId]) {
|
||||
const patient = patientData[patientId];
|
||||
$('#patient-name').text(patient.name);
|
||||
$('#patient-id').text(patient.id);
|
||||
$('#patient-details').text(patient.details);
|
||||
$('#patient-preview').show();
|
||||
} else {
|
||||
$('#patient-preview').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Priority slider
|
||||
$('#priority-slider').on('input', function() {
|
||||
const value = parseFloat($(this).val());
|
||||
$('#id_priority_score').val(value);
|
||||
updateEstimatedWaitTime();
|
||||
});
|
||||
|
||||
// Priority score input
|
||||
$('#id_priority_score').on('input', function() {
|
||||
const value = parseFloat($(this).val()) || 1.0;
|
||||
$('#priority-slider').val(value);
|
||||
updateEstimatedWaitTime();
|
||||
});
|
||||
|
||||
// Trigger initial events
|
||||
$('#id_queue').trigger('change');
|
||||
$('#id_patient').trigger('change');
|
||||
});
|
||||
|
||||
function setPriority(score) {
|
||||
$('#id_priority_score').val(score);
|
||||
$('#priority-slider').val(score);
|
||||
updateEstimatedWaitTime();
|
||||
}
|
||||
|
||||
function updateEstimatedWaitTime() {
|
||||
const queueId = $('#id_queue').val();
|
||||
const priorityScore = parseFloat($('#id_priority_score').val()) || 1.0;
|
||||
|
||||
if (queueId && queueData[queueId]) {
|
||||
const queue = queueData[queueId];
|
||||
// Simple estimation: base wait time adjusted by priority
|
||||
const baseWait = queue.current_size * (queue.avg_wait / queue.max_size);
|
||||
const adjustedWait = Math.max(1, Math.round(baseWait / priorityScore));
|
||||
|
||||
$('#estimated-minutes').text(adjustedWait);
|
||||
$('#estimated-time').show();
|
||||
} else {
|
||||
$('#estimated-time').hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation
|
||||
$('#queueEntryForm').on('submit', function(e) {
|
||||
let isValid = true;
|
||||
const errors = [];
|
||||
|
||||
// Validate required fields
|
||||
if (!$('#id_queue').val()) {
|
||||
errors.push('Please select a queue');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#id_patient').val()) {
|
||||
errors.push('Please select a patient');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#id_appointment').val()) {
|
||||
errors.push('Please select an appointment');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
const priorityScore = parseFloat($('#id_priority_score').val());
|
||||
if (!priorityScore || priorityScore < 0.1 || priorityScore > 10) {
|
||||
errors.push('Priority score must be between 0.1 and 10');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Check queue capacity
|
||||
const queueId = $('#id_queue').val();
|
||||
if (queueId && queueData[queueId]) {
|
||||
const queue = queueData[queueId];
|
||||
if (queue.current_size >= queue.max_size) {
|
||||
if (!confirm('The selected queue is at capacity. Do you want to add the patient anyway?')) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
showAlert('error', errors.join('<br>'));
|
||||
}
|
||||
});
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 8 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 8000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
715
appointments/templates/appointments/queue/queue_entry_list.html
Normal file
715
appointments/templates/appointments/queue/queue_entry_list.html
Normal file
@ -0,0 +1,715 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Queue Entries{% 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>
|
||||
.queue-overview {
|
||||
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(200px, 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: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-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;
|
||||
}
|
||||
|
||||
.stat-icon.primary { background: #007bff; }
|
||||
.stat-icon.success { background: #28a745; }
|
||||
.stat-icon.warning { background: #ffc107; }
|
||||
.stat-icon.info { background: #17a2b8; }
|
||||
.stat-icon.danger { background: #dc3545; }
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.entry-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-waiting { background: #fff3cd; color: #856404; }
|
||||
.status-called { background: #d1ecf1; color: #0c5460; }
|
||||
.status-in-service { background: #cce5ff; color: #004085; }
|
||||
.status-completed { background: #d4edda; color: #155724; }
|
||||
.status-left { background: #f8d7da; color: #721c24; }
|
||||
.status-no-show { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.priority-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.priority-low { background: #28a745; }
|
||||
.priority-medium { background: #ffc107; }
|
||||
.priority-high { background: #fd7e14; }
|
||||
.priority-critical { background: #dc3545; }
|
||||
|
||||
.wait-time-indicator {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wait-normal { background: #d4edda; color: #155724; }
|
||||
.wait-moderate { background: #fff3cd; color: #856404; }
|
||||
.wait-long { background: #f8d7da; color: #721c24; }
|
||||
|
||||
.queue-selector {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
|
||||
<li class="breadcrumb-item active">Queue Entries</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-list me-2"></i>Queue Entries Management
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'appointments:queue_entry_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add to Queue
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="refreshEntries()">
|
||||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Selector -->
|
||||
{% if queues %}
|
||||
<div class="queue-selector">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label mb-2">
|
||||
<i class="fas fa-filter me-1"></i>Filter by Queue:
|
||||
</label>
|
||||
<select class="form-select" id="queue-filter" onchange="filterByQueue()">
|
||||
<option value="">All Queues</option>
|
||||
{% for queue in queues %}
|
||||
<option value="{{ queue.pk }}" {% if selected_queue.pk == queue.pk %}selected{% endif %}>
|
||||
{{ queue.name }} ({{ queue.current_queue_size }} patients)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{% if selected_queue %}
|
||||
<div class="text-md-end">
|
||||
<a href="{% url 'appointments:waiting_queue_detail' selected_queue.pk %}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View Queue Details
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Queue Overview -->
|
||||
{% if selected_queue %}
|
||||
<div class="queue-overview">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mb-2">{{ selected_queue.name }}</h3>
|
||||
<p class="mb-0 opacity-75">{{ selected_queue.description|default:"No description available" }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<div class="h4 mb-1">{{ selected_queue.current_queue_size }}/{{ selected_queue.max_queue_size }}</div>
|
||||
<div class="opacity-75">Current Capacity</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-user-clock"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.waiting_count|default:0 }}</div>
|
||||
<div class="stat-label">Waiting</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon info">
|
||||
<i class="fas fa-phone"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.called_count|default:0 }}</div>
|
||||
<div class="stat-label">Called</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.in_service_count|default:0 }}</div>
|
||||
<div class="stat-label">In Service</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.completed_today|default:0 }}</div>
|
||||
<div class="stat-label">Completed Today</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-user-times"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.no_shows_today|default:0 }}</div>
|
||||
<div class="stat-label">No Shows Today</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="WAITING">Waiting</option>
|
||||
<option value="CALLED">Called</option>
|
||||
<option value="IN_SERVICE">In Service</option>
|
||||
<option value="COMPLETED">Completed</option>
|
||||
<option value="LEFT">Left Queue</option>
|
||||
<option value="NO_SHOW">No Show</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Provider</label>
|
||||
<select class="form-select" id="provider-filter">
|
||||
<option value="">All Providers</option>
|
||||
{% for provider in providers %}
|
||||
<option value="{{ provider.id }}">{{ provider.get_full_name }} - {{ provider.department }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Date Range</label>
|
||||
<select class="form-select" id="date-filter">
|
||||
<option value="">All Dates</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="yesterday">Yesterday</option>
|
||||
<option value="week">This Week</option>
|
||||
<option value="month">This Month</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button class="btn btn-outline-primary me-2" onclick="applyFilters()">
|
||||
<i class="fas fa-filter me-1"></i>Apply
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Search</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="search-input"
|
||||
placeholder="Search by patient name, ID, or appointment details...">
|
||||
<button class="btn btn-outline-secondary" type="button" onclick="searchEntries()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Actions</label>
|
||||
<div class="btn-group w-100">
|
||||
<button class="btn btn-outline-success" onclick="callNextPatient()">
|
||||
<i class="fas fa-phone me-1"></i>Call Next
|
||||
</button>
|
||||
<button class="btn btn-outline-info" onclick="exportEntries()">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Entries Table -->
|
||||
<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>Queue Entries
|
||||
{% if selected_queue %}
|
||||
<span class="badge bg-primary ms-2">{{ entries.count }} entries</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="toggleView('card')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-primary" onclick="toggleView('table')">
|
||||
<i class="fas fa-table"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="entriesTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">Position</th>
|
||||
<th>Patient</th>
|
||||
<th>Queue</th>
|
||||
<th>Appointment</th>
|
||||
<th>Joined</th>
|
||||
<th>Wait Time</th>
|
||||
<th>Status</th>
|
||||
<th>Provider</th>
|
||||
<th width="15%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in entries %}
|
||||
<tr data-entry-id="{{ entry.pk }}" data-status="{{ entry.status }}">
|
||||
<td>
|
||||
<span class="badge bg-primary">{{ entry.queue_position }}</span>
|
||||
<div class="priority-indicator priority-{{ entry.priority_level|default:'medium' }}"
|
||||
title="Priority: {{ entry.priority_level|default:'Medium' }}"></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
<i class="fas fa-user-circle fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ entry.patient.get_full_name }}</div>
|
||||
<small class="text-muted">ID: {{ entry.patient.patient_id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ entry.queue.name }}</div>
|
||||
<small class="text-muted">{{ entry.queue.get_queue_type_display }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ entry.appointment.appointment_type }}</div>
|
||||
<small class="text-muted">
|
||||
Scheduled: {{ entry.appointment.scheduled_time|time:"g:i A" }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ entry.joined_at|time:"g:i A" }}</div>
|
||||
<small class="text-muted">{{ entry.joined_at|date:"M d" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{% with wait_minutes=entry.wait_time_minutes %}
|
||||
<span class="wait-time-indicator
|
||||
{% if wait_minutes < 30 %}wait-normal
|
||||
{% elif wait_minutes < 60 %}wait-moderate
|
||||
{% else %}wait-long{% endif %}">
|
||||
{{ entry.joined_at|timesince }}
|
||||
</span>
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="entry-status status-{{ entry.status|lower }}">
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if entry.assigned_provider %}
|
||||
<div>{{ entry.assigned_provider.get_full_name }}</div>
|
||||
<small class="text-muted">{{ entry.assigned_provider.email }}</small>
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if entry.status == 'WAITING' %}
|
||||
<button class="btn btn-outline-primary"
|
||||
onclick="callPatient('{{ entry.pk }}')" title="Call Patient">
|
||||
<i class="fas fa-phone"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url 'appointments:queue_entry_detail' entry.pk %}"
|
||||
class="btn btn-outline-info" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
|
||||
{% if entry.status in 'WAITING,CALLED' %}
|
||||
<a href="{% url 'appointments:queue_entry_update' entry.pk %}"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown" title="More Actions">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% if entry.status == 'WAITING' %}
|
||||
<li><a class="dropdown-item" href="#" onclick="moveToTop('{{ entry.pk }}')">
|
||||
<i class="fas fa-arrow-up me-2"></i>Move to Top
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="assignProvider('{{ entry.pk }}')">
|
||||
<i class="fas fa-user-md me-2"></i>Assign Provider
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#" onclick="sendNotification('{{ entry.pk }}')">
|
||||
<i class="fas fa-bell me-2"></i>Send Notification
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="removeFromQueue('{{ entry.pk }}')">
|
||||
<i class="fas fa-times me-2"></i>Remove from Queue
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<i class="fas fa-list fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No queue entries found</p>
|
||||
{% if selected_queue %}
|
||||
<a href="{% url 'appointments:add_to_queue' selected_queue.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add First Patient
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<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 src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
const table = $('#entriesTable').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[0, 'asc']], // Sort by position
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [8] } // Disable sorting for actions
|
||||
],
|
||||
language: {
|
||||
search: "",
|
||||
searchPlaceholder: "Search entries...",
|
||||
lengthMenu: "Show _MENU_ entries per page",
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ entries",
|
||||
infoEmpty: "No entries available",
|
||||
infoFiltered: "(filtered from _MAX_ total entries)"
|
||||
}
|
||||
});
|
||||
|
||||
// Custom search
|
||||
$('#search-input').on('keyup', function() {
|
||||
table.search(this.value).draw();
|
||||
});
|
||||
|
||||
// Auto-refresh every 60 seconds
|
||||
setInterval(function() {
|
||||
refreshEntries();
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
function filterByQueue() {
|
||||
const queueId = $('#queue-filter').val();
|
||||
const currentUrl = new URL(window.location);
|
||||
|
||||
if (queueId) {
|
||||
currentUrl.searchParams.set('queue', queueId);
|
||||
} else {
|
||||
currentUrl.searchParams.delete('queue');
|
||||
}
|
||||
|
||||
window.location.href = currentUrl.toString();
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const status = $('#status-filter').val();
|
||||
const provider = $('#provider-filter').val();
|
||||
const dateRange = $('#date-filter').val();
|
||||
|
||||
// Apply filters to DataTable
|
||||
const table = $('#entriesTable').DataTable();
|
||||
|
||||
// Status filter
|
||||
if (status) {
|
||||
table.column(6).search(status);
|
||||
} else {
|
||||
table.column(6).search('');
|
||||
}
|
||||
|
||||
table.draw();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#status-filter, #provider-filter, #date-filter').val('');
|
||||
$('#search-input').val('');
|
||||
|
||||
const table = $('#entriesTable').DataTable();
|
||||
table.search('').columns().search('').draw();
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
{#function callPatient(entryId) {#}
|
||||
{# if (confirm('Call this patient?')) {#}
|
||||
{# $.post('{% url "appointments:call_patient" %}', {#}
|
||||
{# entry_id: entryId,#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient called successfully');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to call patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to call patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function callNextPatient() {
|
||||
const queueId = $('#queue-filter').val();
|
||||
if (!queueId) {
|
||||
showAlert('error', 'Please select a queue first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('Call the next patient in the selected queue?')) {
|
||||
$.post(`{% url "appointments:call_next_patient" 0 %}`.replace('0', queueId), {
|
||||
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
|
||||
}).done(function(response) {
|
||||
if (response.success) {
|
||||
showAlert('success', 'Next patient called successfully');
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert('error', response.message || 'Failed to call next patient');
|
||||
}
|
||||
}).fail(function() {
|
||||
showAlert('error', 'Failed to call next patient');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{#function moveToTop(entryId) {#}
|
||||
{# if (confirm('Move this patient to the top of the queue?')) {#}
|
||||
{# $.post('{% url "appointments:move_to_top" %}', {#}
|
||||
{# entry_id: entryId,#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient moved to top of queue');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to move patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to move patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function assignProvider(entryId) {#}
|
||||
{# // This would typically open a modal or redirect to assignment form#}
|
||||
{# window.location.href = `{% url "appointments:assign_provider" 0 %}`.replace('0', entryId);#}
|
||||
{# }#}
|
||||
|
||||
{#function sendNotification(entryId) {#}
|
||||
{# if (confirm('Send notification to this patient?')) {#}
|
||||
{# $.post('{% url "appointments:send_notification" %}', {#}
|
||||
{# entry_id: entryId,#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Notification sent successfully');#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to send notification');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to send notification');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function removeFromQueue(entryId) {#}
|
||||
{# if (confirm('Remove this patient from the queue?')) {#}
|
||||
{# $.post('{% url "appointments:remove_from_queue" %}', {#}
|
||||
{# entry_id: entryId,#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient removed from queue');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to remove patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to remove patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function exportEntries() {#}
|
||||
{# const queueId = $('#queue-filter').val();#}
|
||||
{# let url = '{% url "appointments:export_queue_entries" %}';#}
|
||||
{# if (queueId) {#}
|
||||
{# url += `?queue=${queueId}`;#}
|
||||
{# }#}
|
||||
{# window.location.href = url;#}
|
||||
{# }#}
|
||||
|
||||
function toggleView(viewType) {
|
||||
// This would toggle between card and table views
|
||||
console.log('Toggle view to:', viewType);
|
||||
}
|
||||
|
||||
function searchEntries() {
|
||||
const searchTerm = $('#search-input').val();
|
||||
const table = $('#entriesTable').DataTable();
|
||||
table.search(searchTerm).draw();
|
||||
}
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,448 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Waiting Queue - {{ queue.name }}{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.delete-warning {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1.5rem;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.queue-info {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.impact-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.impact-icon {
|
||||
color: #856404;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.queue-type-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-provider { background: #d4edda; color: #155724; }
|
||||
.type-specialty { background: #d1ecf1; color: #0c5460; }
|
||||
.type-location { background: #fff3cd; color: #856404; }
|
||||
.type-procedure { background: #f8d7da; color: #721c24; }
|
||||
.type-emergency { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-active { background: #d4edda; color: #155724; }
|
||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-warning {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_detail' queue.pk %}">{{ queue.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash me-2"></i>Delete Waiting Queue
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Queue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Warning -->
|
||||
<div class="delete-warning text-center">
|
||||
<div class="warning-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
<h3 class="mb-3">Are you sure you want to delete this waiting queue?</h3>
|
||||
<p class="mb-0 opacity-75">
|
||||
This action cannot be undone. All queue entries and associated data will be permanently removed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Queue Information -->
|
||||
<div class="queue-info">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>Queue Information
|
||||
</h5>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Queue Name:</span>
|
||||
<span class="info-value fw-bold">{{ queue.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Queue Type:</span>
|
||||
<span class="queue-type-badge type-{{ queue.queue_type|lower }}">
|
||||
{{ queue.get_queue_type_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Status:</span>
|
||||
<span class="status-badge status-{% if queue.is_active %}active{% else %}inactive{% endif %}">
|
||||
{% if queue.is_active %}Active{% else %}Inactive{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if queue.specialty %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Specialty:</span>
|
||||
<span class="info-value">{{ queue.specialty }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if queue.location %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Location:</span>
|
||||
<span class="info-value">{{ queue.location }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Current Queue Size:</span>
|
||||
<span class="info-value">{{ queue.current_queue_size }} patients</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Maximum Capacity:</span>
|
||||
<span class="info-value">{{ queue.max_queue_size }} patients</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Assigned Providers:</span>
|
||||
<span class="info-value">{{ queue.providers.count }} provider{{ queue.providers.count|pluralize }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Created:</span>
|
||||
<span class="info-value">{{ queue.created_at|date:"M d, Y g:i A" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Updated:</span>
|
||||
<span class="info-value">{{ queue.updated_at|date:"M d, Y g:i A" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Warning -->
|
||||
{% if queue.current_queue_size > 0 or queue.providers.count > 0 %}
|
||||
<div class="impact-warning">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle text-warning me-2"></i>
|
||||
Deletion Impact
|
||||
</h6>
|
||||
|
||||
{% if queue.current_queue_size > 0 %}
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-users impact-icon"></i>
|
||||
<span>{{ queue.current_queue_size }} patient{{ queue.current_queue_size|pluralize }} currently in queue will be removed</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if queue.providers.count > 0 %}
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-user-md impact-icon"></i>
|
||||
<span>{{ queue.providers.count }} assigned provider{{ queue.providers.count|pluralize }} will be unassigned</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-chart-line impact-icon"></i>
|
||||
<span>All historical queue data and statistics will be permanently lost</span>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<i class="fas fa-clock impact-icon"></i>
|
||||
<span>Any scheduled queue operations will be cancelled</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Current Queue Entries -->
|
||||
{% if queue_entries %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>
|
||||
Current Queue Entries ({{ queue_entries|length }})
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Patient</th>
|
||||
<th>Joined</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in queue_entries %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-primary">{{ entry.queue_position }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ entry.patient.get_full_name }}</div>
|
||||
<small class="text-muted">ID: {{ entry.patient.patient_id }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ entry.joined_at|time:"g:i A" }}</div>
|
||||
<small class="text-muted">{{ entry.joined_at|timesince }} ago</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-warning">{{ entry.get_status_display }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Confirmation Section -->
|
||||
<div class="confirmation-section">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
Confirmation Required
|
||||
</h6>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="confirmUnderstand">
|
||||
<label class="form-check-label" for="confirmUnderstand">
|
||||
I understand that this action cannot be undone
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="confirmPatients">
|
||||
<label class="form-check-label" for="confirmPatients">
|
||||
I understand that all patients in the queue will be removed
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="confirmData">
|
||||
<label class="form-check-label" for="confirmData">
|
||||
I understand that all queue data and statistics will be permanently lost
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmationText" class="form-label">
|
||||
Type <strong>"{{ queue.name }}"</strong> to confirm deletion:
|
||||
</label>
|
||||
<input type="text" class="form-control" id="confirmationText"
|
||||
placeholder="Enter queue name to confirm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
This action requires administrator privileges
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
<form method="post" class="d-inline" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger" id="deleteButton" disabled>
|
||||
<i class="fas fa-trash me-1"></i>Delete Queue Permanently
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const queueName = "{{ queue.name }}";
|
||||
const $deleteButton = $('#deleteButton');
|
||||
const $confirmUnderstand = $('#confirmUnderstand');
|
||||
const $confirmPatients = $('#confirmPatients');
|
||||
const $confirmData = $('#confirmData');
|
||||
const $confirmationText = $('#confirmationText');
|
||||
|
||||
function checkConfirmation() {
|
||||
const allChecked = $confirmUnderstand.is(':checked') &&
|
||||
$confirmPatients.is(':checked') &&
|
||||
$confirmData.is(':checked');
|
||||
const textMatches = $confirmationText.val().trim() === queueName;
|
||||
|
||||
$deleteButton.prop('disabled', !(allChecked && textMatches));
|
||||
}
|
||||
|
||||
// Check confirmation status on any change
|
||||
$confirmUnderstand.on('change', checkConfirmation);
|
||||
$confirmPatients.on('change', checkConfirmation);
|
||||
$confirmData.on('change', checkConfirmation);
|
||||
$confirmationText.on('input', checkConfirmation);
|
||||
|
||||
// Form submission confirmation
|
||||
$('#deleteForm').on('submit', function(e) {
|
||||
if (!$deleteButton.prop('disabled')) {
|
||||
if (!confirm('Are you absolutely sure you want to delete this waiting queue? This action cannot be undone.')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else {
|
||||
e.preventDefault();
|
||||
showAlert('error', 'Please complete all confirmation requirements before deleting.');
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent accidental navigation
|
||||
{#let formSubmitted = false;#}
|
||||
{#$('#deleteForm').on('submit', function() {#}
|
||||
{# formSubmitted = true;#}
|
||||
{# });#}
|
||||
|
||||
$(window).on('beforeunload', function(e) {
|
||||
if (!formSubmitted && ($confirmUnderstand.is(':checked') ||
|
||||
$confirmPatients.is(':checked') ||
|
||||
$confirmData.is(':checked') ||
|
||||
$confirmationText.val().trim())) {
|
||||
const message = 'You have started the deletion process. Are you sure you want to leave?';
|
||||
e.returnValue = message;
|
||||
return message;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,617 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ queue.name }} - Queue Details{% 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>
|
||||
.queue-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(200px, 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: all 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.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-icon.primary { background: #007bff; }
|
||||
.stat-icon.success { background: #28a745; }
|
||||
.stat-icon.warning { background: #ffc107; }
|
||||
.stat-icon.info { background: #17a2b8; }
|
||||
.stat-icon.danger { background: #dc3545; }
|
||||
|
||||
.stat-number {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.queue-info-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.queue-type-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-provider { background: #d4edda; color: #155724; }
|
||||
.type-specialty { background: #d1ecf1; color: #0c5460; }
|
||||
.type-location { background: #fff3cd; color: #856404; }
|
||||
.type-procedure { background: #f8d7da; color: #721c24; }
|
||||
.type-emergency { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.status-badge {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-active { background: #d4edda; color: #155724; }
|
||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
||||
|
||||
.entry-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-waiting { background: #fff3cd; color: #856404; }
|
||||
.status-called { background: #d1ecf1; color: #0c5460; }
|
||||
.status-in-service { background: #cce5ff; color: #004085; }
|
||||
.status-completed { background: #d4edda; color: #155724; }
|
||||
.status-left { background: #f8d7da; color: #721c24; }
|
||||
.status-no-show { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
|
||||
<li class="breadcrumb-item active">{{ queue.name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-users me-2"></i>Queue Details
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-edit me-1"></i>Edit Queue
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="refreshQueue()">
|
||||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary dropdown-toggle" 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="callNextPatient()">
|
||||
<i class="fas fa-phone me-2"></i>Call Next Patient
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="pauseQueue()">
|
||||
<i class="fas fa-pause me-2"></i>Pause Queue
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="clearQueue()">
|
||||
<i class="fas fa-broom me-2"></i>Clear Queue
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportQueue()">
|
||||
<i class="fas fa-download me-2"></i>Export Data
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Header -->
|
||||
<div class="queue-header">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-2">{{ queue.name }}</h2>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<span class="queue-type-badge type-{{ queue.queue_type|lower }} me-3">
|
||||
{{ queue.get_queue_type_display }}
|
||||
</span>
|
||||
<span class="status-badge status-{% if queue.is_active %}active{% else %}inactive{% endif %}">
|
||||
{% if queue.is_active %}Active{% else %}Inactive{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if queue.description %}
|
||||
<p class="mb-0 opacity-75">{{ queue.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<div class="text-white">
|
||||
<div class="h4 mb-1">{{ queue.current_queue_size }}/{{ queue.max_queue_size }}</div>
|
||||
<div class="opacity-75">Current Capacity</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-user-clock"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ queue.current_queue_size }}</div>
|
||||
<div class="stat-label">Patients Waiting</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon info">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ queue.estimated_wait_time_minutes }}</div>
|
||||
<div class="stat-label">Est. Wait Time (min)</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.served_today|default:0 }}</div>
|
||||
<div class="stat-label">Served Today</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ queue.average_service_time_minutes }}</div>
|
||||
<div class="stat-label">Avg Service (min)</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger">
|
||||
<i class="fas fa-user-times"></i>
|
||||
</div>
|
||||
<div class="stat-number">{{ stats.no_shows_today|default:0 }}</div>
|
||||
<div class="stat-label">No Shows Today</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Information -->
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<div class="queue-info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>Queue Information
|
||||
</h5>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Queue ID:</span>
|
||||
<span class="info-value">{{ queue.queue_id }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Type:</span>
|
||||
<span class="info-value">{{ queue.get_queue_type_display }}</span>
|
||||
</div>
|
||||
|
||||
{% if queue.specialty %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Specialty:</span>
|
||||
<span class="info-value">{{ queue.specialty }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if queue.location %}
|
||||
<div class="info-item">
|
||||
<span class="info-label">Location:</span>
|
||||
<span class="info-value">{{ queue.location }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Max Capacity:</span>
|
||||
<span class="info-value">{{ queue.max_queue_size }} patients</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Accepting Patients:</span>
|
||||
<span class="info-value">
|
||||
{% if queue.is_accepting_patients %}
|
||||
<i class="fas fa-check text-success"></i> Yes
|
||||
{% else %}
|
||||
<i class="fas fa-times text-danger"></i> No
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Created:</span>
|
||||
<span class="info-value">{{ queue.created_at|date:"M d, Y g:i A" }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Last Updated:</span>
|
||||
<span class="info-value">{{ queue.updated_at|timesince }} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assigned Providers -->
|
||||
<div class="queue-info-card">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-user-md me-2"></i>Assigned Providers
|
||||
</h5>
|
||||
|
||||
{% for provider in queue.providers.all %}
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
<i class="fas fa-user-circle fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ provider.get_full_name }}</div>
|
||||
<small class="text-muted">{{ provider.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<p class="text-muted mb-0">No providers assigned</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<!-- Current Queue -->
|
||||
<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>Current Queue
|
||||
</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary" onclick="callNextPatient()">
|
||||
<i class="fas fa-phone me-1"></i>Call Next
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="addPatientToQueue()">
|
||||
<i class="fas fa-plus me-1"></i>Add Patient
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="queueTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Patient</th>
|
||||
<th>Appointment</th>
|
||||
<th>Joined</th>
|
||||
<th>Wait Time</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in queue_entries %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-primary">{{ entry.queue_position }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
<i class="fas fa-user-circle fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ entry.patient.get_full_name }}</div>
|
||||
<small class="text-muted">ID: {{ entry.patient.patient_id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ entry.appointment.appointment_type }}</div>
|
||||
<small class="text-muted">{{ entry.appointment.scheduled_time|time:"g:i A" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ entry.joined_at|time:"g:i A" }}</div>
|
||||
<small class="text-muted">{{ entry.joined_at|date:"M d" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold">{{ entry.joined_at|timesince }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="entry-status status-{{ entry.status|lower }}">
|
||||
{{ entry.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if entry.status == 'WAITING' %}
|
||||
<button class="btn btn-outline-primary"
|
||||
onclick="callPatient('{{ entry.pk }}')" title="Call Patient">
|
||||
<i class="fas fa-phone"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'appointments:queue_entry_detail' entry.pk %}"
|
||||
class="btn btn-outline-info" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-danger"
|
||||
onclick="removeFromQueue('{{ entry.pk }}')" title="Remove">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4">
|
||||
<i class="fas fa-users fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No patients in queue</p>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<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 src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
$('#queueTable').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[0, 'asc']], // Sort by position
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [6] } // Disable sorting for actions
|
||||
],
|
||||
language: {
|
||||
search: "",
|
||||
searchPlaceholder: "Search queue entries...",
|
||||
lengthMenu: "Show _MENU_ entries per page",
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ entries",
|
||||
infoEmpty: "No entries in queue",
|
||||
infoFiltered: "(filtered from _MAX_ total entries)"
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(function() {
|
||||
refreshQueue();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
function refreshQueue() {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
function callNextPatient() {
|
||||
if (confirm('Call the next patient in queue?')) {
|
||||
$.post('{% url "appointments:call_next_patient" queue.pk %}', {
|
||||
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
|
||||
}).done(function(response) {
|
||||
if (response.success) {
|
||||
showAlert('success', 'Next patient called successfully');
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showAlert('error', response.message || 'Failed to call next patient');
|
||||
}
|
||||
}).fail(function() {
|
||||
showAlert('error', 'Failed to call next patient');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
{#function callPatient(entryId) {#}
|
||||
{# if (confirm('Call this patient?')) {#}
|
||||
{# $.post('{% url "appointments:call_patient" %}', {#}
|
||||
{# entry_id: entryId,#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient called successfully');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to call patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to call patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function removeFromQueue(entryId) {#}
|
||||
{# if (confirm('Remove this patient from the queue?')) {#}
|
||||
{# $.post('{% url "appointments:remove_from_queue" %}', {#}
|
||||
{# entry_id: entryId,#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Patient removed from queue');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to remove patient');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to remove patient');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function addPatientToQueue() {#}
|
||||
{# // Redirect to add patient form#}
|
||||
{# window.location.href = '{% url "appointments:add_to_queue" queue.pk %}';#}
|
||||
{# }#}
|
||||
|
||||
{#function pauseQueue() {#}
|
||||
{# if (confirm('Pause this queue? No new patients will be accepted.')) {#}
|
||||
{# $.post('{% url "appointments:pause_queue" queue.pk %}', {#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Queue paused successfully');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to pause queue');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to pause queue');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function clearQueue() {#}
|
||||
{# if (confirm('Clear all patients from this queue? This action cannot be undone.')) {#}
|
||||
{# $.post('{% url "appointments:clear_queue" queue.pk %}', {#}
|
||||
{# csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()#}
|
||||
{# }).done(function(response) {#}
|
||||
{# if (response.success) {#}
|
||||
{# showAlert('success', 'Queue cleared successfully');#}
|
||||
{# setTimeout(function() {#}
|
||||
{# location.reload();#}
|
||||
{# }, 1000);#}
|
||||
{# } else {#}
|
||||
{# showAlert('error', response.message || 'Failed to clear queue');#}
|
||||
{# }#}
|
||||
{# }).fail(function() {#}
|
||||
{# showAlert('error', 'Failed to clear queue');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
{#function exportQueue() {#}
|
||||
{# window.location.href = '{% url "appointments:export_queue" queue.pk %}';#}
|
||||
{# }#}
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 5 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,608 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if form.instance.pk %}Edit{% else %}Create{% endif %} Waiting Queue{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<style>
|
||||
.form-section {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
border-bottom: 2px solid #f8f9fa;
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
margin: 0.25rem 0 0 0;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.queue-type-preview {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.operating-hours-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.day-schedule {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.time-inputs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.priority-weight-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.weight-label {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weight-input {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.operating-hours-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.time-inputs {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.priority-weight-item {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.weight-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_queue_list' %}">Waiting Queues</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if form.instance.pk %}Edit Queue{% else %}Create Queue{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-users me-2"></i>
|
||||
{% if form.instance.pk %}Edit Waiting Queue{% else %}Create Waiting Queue{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="queueForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
|
||||
</h4>
|
||||
<small class="text-light">Configure the basic details of the waiting queue</small>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
Queue Name <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.help_text %}
|
||||
<div class="form-text">{{ form.name.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.name.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.queue_type.id_for_label }}" class="form-label">
|
||||
Queue Type <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.queue_type }}
|
||||
{% if form.queue_type.help_text %}
|
||||
<div class="form-text">{{ form.queue_type.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.queue_type.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.queue_type.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="queue-type-preview" id="type-preview" style="display: none;">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-info-circle text-primary me-2"></i>
|
||||
<span id="type-description"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.help_text %}
|
||||
<div class="form-text">{{ form.description.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.description.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.description.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Queue Configuration -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-cogs me-2"></i>Queue Configuration
|
||||
</h4>
|
||||
<p class="section-description">Set capacity limits and service time estimates</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.max_queue_size.id_for_label }}" class="form-label">
|
||||
Maximum Queue Size <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.max_queue_size }}
|
||||
{% if form.max_queue_size.help_text %}
|
||||
<div class="form-text">{{ form.max_queue_size.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.max_queue_size.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.max_queue_size.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.average_service_time_minutes.id_for_label }}" class="form-label">
|
||||
Average Service Time (minutes) <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.average_service_time_minutes }}
|
||||
{% if form.average_service_time_minutes.help_text %}
|
||||
<div class="form-text">{{ form.average_service_time_minutes.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.average_service_time_minutes.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.average_service_time_minutes.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<div class="form-check">
|
||||
{{ form.is_active }}
|
||||
<label class="form-check-label" for="{{ form.is_active.id_for_label }}">
|
||||
Queue is active
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
{{ form.is_accepting_patients }}
|
||||
<label class="form-check-label" for="{{ form.is_accepting_patients.id_for_label }}">
|
||||
Accepting new patients
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Associations -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-link me-2"></i>Queue Associations
|
||||
</h4>
|
||||
<p class="section-description">Associate the queue with providers, specialties, and locations</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.specialty.id_for_label }}" class="form-label">Medical Specialty</label>
|
||||
{{ form.specialty }}
|
||||
{% if form.specialty.help_text %}
|
||||
<div class="form-text">{{ form.specialty.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.specialty.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.specialty.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.location.id_for_label }}" class="form-label">Location</label>
|
||||
{{ form.location }}
|
||||
{% if form.location.help_text %}
|
||||
<div class="form-text">{{ form.location.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.location.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.location.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.providers.id_for_label }}" class="form-label">Assigned Providers</label>
|
||||
{{ form.providers }}
|
||||
{% if form.providers.help_text %}
|
||||
<div class="form-text">{{ form.providers.help_text }}</div>
|
||||
{% endif %}
|
||||
{% if form.providers.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.providers.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operating Hours -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-clock me-2"></i>Operating Hours
|
||||
</h4>
|
||||
<p class="section-description">Set the operating hours for each day of the week</p>
|
||||
</div>
|
||||
|
||||
<div class="operating-hours-grid">
|
||||
{% for day in days_of_week %}
|
||||
<div class="day-schedule">
|
||||
<div class="day-header">{{ day.name }}</div>
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="day_{{ day.value }}_enabled"
|
||||
name="operating_hours_{{ day.value }}_enabled"
|
||||
{% if day.enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="day_{{ day.value }}_enabled">
|
||||
Open on {{ day.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="time-inputs" id="times_{{ day.value }}"
|
||||
{% if not day.enabled %}style="display: none;"{% endif %}>
|
||||
<input type="time" class="form-control form-control-sm"
|
||||
name="operating_hours_{{ day.value }}_start"
|
||||
value="{{ day.start_time|default:'09:00' }}">
|
||||
<span>to</span>
|
||||
<input type="time" class="form-control form-control-sm"
|
||||
name="operating_hours_{{ day.value }}_end"
|
||||
value="{{ day.end_time|default:'17:00' }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Priority Configuration -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-sort-amount-up me-2"></i>Priority Configuration
|
||||
</h4>
|
||||
<p class="section-description">Configure priority weights for queue ordering (higher values = higher priority)</p>
|
||||
</div>
|
||||
|
||||
<div class="priority-weight-item">
|
||||
<span class="weight-label">Emergency Cases</span>
|
||||
<input type="number" class="form-control weight-input"
|
||||
name="priority_weight_emergency"
|
||||
value="{{ priority_weights.emergency|default:10 }}"
|
||||
min="0" max="100" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="priority-weight-item">
|
||||
<span class="weight-label">Urgent Cases</span>
|
||||
<input type="number" class="form-control weight-input"
|
||||
name="priority_weight_urgent"
|
||||
value="{{ priority_weights.urgent|default:5 }}"
|
||||
min="0" max="100" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="priority-weight-item">
|
||||
<span class="weight-label">Elderly Patients (65+)</span>
|
||||
<input type="number" class="form-control weight-input"
|
||||
name="priority_weight_elderly"
|
||||
value="{{ priority_weights.elderly|default:2 }}"
|
||||
min="0" max="100" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="priority-weight-item">
|
||||
<span class="weight-label">Pregnant Patients</span>
|
||||
<input type="number" class="form-control weight-input"
|
||||
name="priority_weight_pregnant"
|
||||
value="{{ priority_weights.pregnant|default:3 }}"
|
||||
min="0" max="100" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="priority-weight-item">
|
||||
<span class="weight-label">Pediatric Patients</span>
|
||||
<input type="number" class="form-control weight-input"
|
||||
name="priority_weight_pediatric"
|
||||
value="{{ priority_weights.pediatric|default:2 }}"
|
||||
min="0" max="100" step="0.1">
|
||||
</div>
|
||||
|
||||
<div class="priority-weight-item">
|
||||
<span class="weight-label">Regular Appointments</span>
|
||||
<input type="number" class="form-control weight-input"
|
||||
name="priority_weight_regular"
|
||||
value="{{ priority_weights.regular|default:1 }}"
|
||||
min="0" max="100" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-section">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
{% if form.instance.pk %}
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Last updated: {{ form.instance.updated_at|date:"M d, Y g:i A" }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'appointments:waiting_queue_list' %}" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% if form.instance.pk %}Update Queue{% else %}Create Queue{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize Select2
|
||||
$('#id_providers').select2({
|
||||
theme: 'bootstrap-5',
|
||||
placeholder: 'Select providers...',
|
||||
allowClear: true
|
||||
});
|
||||
|
||||
// Queue type descriptions
|
||||
const typeDescriptions = {
|
||||
'PROVIDER': 'Queue managed by specific healthcare providers',
|
||||
'SPECIALTY': 'Queue for patients requiring specific medical specialty',
|
||||
'LOCATION': 'Queue for patients at a specific location or department',
|
||||
'PROCEDURE': 'Queue for patients requiring specific procedures',
|
||||
'EMERGENCY': 'Priority queue for emergency cases'
|
||||
};
|
||||
|
||||
// Show queue type description
|
||||
$('#id_queue_type').on('change', function() {
|
||||
const selectedType = $(this).val();
|
||||
if (selectedType && typeDescriptions[selectedType]) {
|
||||
$('#type-description').text(typeDescriptions[selectedType]);
|
||||
$('#type-preview').show();
|
||||
} else {
|
||||
$('#type-preview').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Trigger on page load
|
||||
$('#id_queue_type').trigger('change');
|
||||
|
||||
// Operating hours toggle
|
||||
$('[id^="day_"][id$="_enabled"]').on('change', function() {
|
||||
const dayValue = $(this).attr('id').replace('day_', '').replace('_enabled', '');
|
||||
const timesDiv = $('#times_' + dayValue);
|
||||
|
||||
if ($(this).is(':checked')) {
|
||||
timesDiv.show();
|
||||
} else {
|
||||
timesDiv.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Form validation
|
||||
$('#queueForm').on('submit', function(e) {
|
||||
let isValid = true;
|
||||
const errors = [];
|
||||
|
||||
// Validate required fields
|
||||
if (!$('#id_name').val().trim()) {
|
||||
errors.push('Queue name is required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!$('#id_queue_type').val()) {
|
||||
errors.push('Queue type is required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
const maxSize = parseInt($('#id_max_queue_size').val());
|
||||
if (!maxSize || maxSize < 1) {
|
||||
errors.push('Maximum queue size must be at least 1');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
const serviceTime = parseInt($('#id_average_service_time_minutes').val());
|
||||
if (!serviceTime || serviceTime < 1) {
|
||||
errors.push('Average service time must be at least 1 minute');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate operating hours
|
||||
let hasOperatingHours = false;
|
||||
$('[id^="day_"][id$="_enabled"]:checked').each(function() {
|
||||
hasOperatingHours = true;
|
||||
const dayValue = $(this).attr('id').replace('day_', '').replace('_enabled', '');
|
||||
const startTime = $(`[name="operating_hours_${dayValue}_start"]`).val();
|
||||
const endTime = $(`[name="operating_hours_${dayValue}_end"]`).val();
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
errors.push(`Please set operating hours for enabled days`);
|
||||
isValid = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startTime >= endTime) {
|
||||
errors.push(`End time must be after start time for all enabled days`);
|
||||
isValid = false;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasOperatingHours) {
|
||||
errors.push('Please enable at least one day of operation');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
showAlert('error', errors.join('<br>'));
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft functionality
|
||||
let autoSaveTimer;
|
||||
$('input, select, textarea').on('change input', function() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(function() {
|
||||
// Could implement auto-save here
|
||||
console.log('Auto-save triggered');
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
const alertHtml = `
|
||||
<div class="alert ${alertClass} alert-dismissible fade show" role="alert">
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing alerts
|
||||
$('.alert').remove();
|
||||
|
||||
// Add new alert at the top of content
|
||||
$('#content').prepend(alertHtml);
|
||||
|
||||
// Auto-dismiss after 8 seconds
|
||||
setTimeout(function() {
|
||||
$('.alert').fadeOut();
|
||||
}, 8000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,497 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Waiting Queues{% 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>
|
||||
.queue-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.queue-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.queue-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.queue-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.queue-type-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.type-provider { background: #d4edda; color: #155724; }
|
||||
.type-specialty { background: #d1ecf1; color: #0c5460; }
|
||||
.type-location { background: #fff3cd; color: #856404; }
|
||||
.type-procedure { background: #f8d7da; color: #721c24; }
|
||||
.type-emergency { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-active { background: #d4edda; color: #155724; }
|
||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
||||
.status-full { background: #fff3cd; color: #856404; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.queue-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
|
||||
<li class="breadcrumb-item active">Waiting Queues</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-users me-2"></i>Waiting Queues Management
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create Queue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Overview -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ stats.total_queues|default:0 }}</h4>
|
||||
<p class="mb-0">Total Queues</p>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-list fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ stats.active_queues|default:0 }}</h4>
|
||||
<p class="mb-0">Active Queues</p>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-check-circle fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ stats.total_patients|default:0 }}</h4>
|
||||
<p class="mb-0">Patients Waiting</p>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-user-clock fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<h4 class="mb-0">{{ stats.avg_wait_time|default:"0" }} min</h4>
|
||||
<p class="mb-0">Avg Wait Time</p>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<i class="fas fa-clock fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Queue Type</label>
|
||||
<select class="form-select" id="type-filter">
|
||||
<option value="">All Types</option>
|
||||
<option value="PROVIDER">Provider Queue</option>
|
||||
<option value="SPECIALTY">Specialty Queue</option>
|
||||
<option value="LOCATION">Location Queue</option>
|
||||
<option value="PROCEDURE">Procedure Queue</option>
|
||||
<option value="EMERGENCY">Emergency Queue</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="full">Full</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Specialty</label>
|
||||
<select class="form-select" id="specialty-filter">
|
||||
<option value="">All Specialties</option>
|
||||
{% for specialty in specialties %}
|
||||
<option value="{{ specialty }}">{{ specialty }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button class="btn btn-outline-primary me-2" onclick="applyFilters()">
|
||||
<i class="fas fa-filter me-1"></i>Apply
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- View Toggle -->
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<button class="btn btn-primary btn-sm me-2" id="card-view-btn" onclick="toggleView('cards')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm" id="table-view-btn" onclick="toggleView('table')">
|
||||
<i class="fas fa-table"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Queue Cards View -->
|
||||
<div class="row" id="queue-cards">
|
||||
{% for queue in queues %}
|
||||
<div class="col-lg-6 col-xl-4 "
|
||||
data-type="{{ queue.queue_type }}"
|
||||
data-status="{% if queue.is_active %}active{% else %}inactive{% endif %}"
|
||||
data-specialty="{{ queue.specialty }}">
|
||||
<div class="panel panel-inverse mb-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-users-rectangle me-2"></i> {{ queue.name }}
|
||||
{% if queue.queue_type == "PROVIDER" %}
|
||||
<span class="badge bg-secondary me-2">{{ queue.get_queue_type_display }}</span>
|
||||
{% elif queue.queue_type == "SPECIALTY" %}
|
||||
<span class="badge bg-primary me-2">{{ queue.get_queue_type_display }}</span>
|
||||
{% elif queue.queue_type == "LOCATION" %}
|
||||
<span class="badge bg-success me-2">{{ queue.get_queue_type_display }}</span>
|
||||
{% elif queue.queue_type == "PROCEDURE" %}
|
||||
<span class="badge bg-info me-2">{{ queue.get_queue_type_display }}</span>
|
||||
{% elif queue.queue_type == "EMERGENCY" %}
|
||||
<span class="badge bg-danger me-2">{{ queue.get_queue_type_display }}</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="badge bg-{% if queue.is_active %}success{% else %}danger{% endif %} me-2">
|
||||
{% if queue.is_active %}Active{% else %}Inactive{% endif %}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="queue-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ queue.current_queue_size }}</div>
|
||||
<div class="stat-label">Waiting</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ queue.max_queue_size }}</div>
|
||||
<div class="stat-label">Capacity</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ queue.estimated_wait_time_minutes }}</div>
|
||||
<div class="stat-label">Est. Wait (min)</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-number">{{ queue.average_service_time_minutes }}</div>
|
||||
<div class="stat-label">Avg Service (min)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
{% if queue.specialty %}
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Specialty:</small>
|
||||
<span class="fw-bold">{{ queue.specialty }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if queue.location %}
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Location:</small>
|
||||
<span class="fw-bold">{{ queue.location }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-3">
|
||||
<small class="text-muted">Providers:</small>
|
||||
<span class="fw-bold">{{ queue.providers.count }}</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
Updated: {{ queue.updated_at|timesince }} ago
|
||||
</small>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}"
|
||||
class="btn btn-outline-primary" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'appointments:queue_entry_list' %}?queue={{ queue.pk }}"
|
||||
class="btn btn-outline-info" title="View Entries">
|
||||
<i class="fas fa-list"></i>
|
||||
</a>
|
||||
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{# <a href="{% url 'appointments:waiting_queue_delete' queue.pk %}"#}
|
||||
{# class="btn btn-outline-danger" title="Delete">#}
|
||||
{# <i class="fas fa-trash"></i>#}
|
||||
{# </a>#}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-users fa-4x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">No Waiting Queues Found</h4>
|
||||
<p class="text-muted">Create your first waiting queue to start managing patient flow.</p>
|
||||
<a href="{% url 'appointments:waiting_queue_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create First Queue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Table View (Alternative) -->
|
||||
<div class="card d-none" id="table-view">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-table me-2"></i>Queue List
|
||||
</h5>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-success btn-sm" onclick="exportQueues('excel')">
|
||||
<i class="fas fa-file-excel me-1"></i>Excel
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="exportQueues('pdf')">
|
||||
<i class="fas fa-file-pdf me-1"></i>PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="queuesTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Queue Name</th>
|
||||
<th>Type</th>
|
||||
<th>Specialty</th>
|
||||
<th>Location</th>
|
||||
<th>Current Size</th>
|
||||
<th>Max Size</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for queue in queues %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-bold">{{ queue.name }}</div>
|
||||
<small class="text-muted">{{ queue.description|truncatechars:50 }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="queue-type-badge type-{{ queue.queue_type|lower }}">
|
||||
{{ queue.get_queue_type_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ queue.specialty }}</td>
|
||||
<td>{{ queue.location }}</td>
|
||||
<td>
|
||||
<span class="fw-bold">{{ queue.current_queue_size }}</span>
|
||||
</td>
|
||||
<td>{{ queue.max_queue_size }}</td>
|
||||
<td>
|
||||
<span class="status-badge status-{% if queue.is_active %}active{% else %}inactive{% endif %}">
|
||||
{% if queue.is_active %}Active{% else %}Inactive{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{% url 'appointments:waiting_queue_detail' queue.pk %}"
|
||||
class="btn btn-outline-primary" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'appointments:waiting_queue_update' queue.pk %}"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{# <a href="{% url 'appointments:waiting_queue_delete' queue.pk %}"#}
|
||||
{# class="btn btn-outline-danger" title="Delete">#}
|
||||
{# <i class="fas fa-trash"></i>#}
|
||||
{# </a>#}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<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 src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/sweetalert/dist/sweetalert.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
$('#queuesTable').DataTable({
|
||||
responsive: true,
|
||||
order: [[0, 'asc']],
|
||||
});
|
||||
|
||||
// Auto-refresh queue stats every 30 seconds
|
||||
setInterval(function() {
|
||||
location.reload();
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
const typeFilter = $('#type-filter').val();
|
||||
const statusFilter = $('#status-filter').val();
|
||||
const specialtyFilter = $('#specialty-filter').val();
|
||||
|
||||
$('.queue-item').each(function() {
|
||||
const $item = $(this);
|
||||
const type = $item.data('type');
|
||||
const status = $item.data('status');
|
||||
const specialty = $item.data('specialty');
|
||||
|
||||
let show = true;
|
||||
|
||||
if (typeFilter && type !== typeFilter) show = false;
|
||||
if (statusFilter && status !== statusFilter) show = false;
|
||||
if (specialtyFilter && specialty !== specialtyFilter) show = false;
|
||||
|
||||
if (show) {
|
||||
$item.show();
|
||||
} else {
|
||||
$item.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#type-filter, #status-filter, #specialty-filter').val('');
|
||||
$('.queue-item').show();
|
||||
}
|
||||
|
||||
function toggleView(view) {
|
||||
if (view === 'cards') {
|
||||
$('#queue-cards').removeClass('d-none');
|
||||
$('#table-view').addClass('d-none');
|
||||
$('#card-view-btn').removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
$('#table-view-btn').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
} else {
|
||||
$('#queue-cards').addClass('d-none');
|
||||
$('#table-view').removeClass('d-none');
|
||||
$('#table-view-btn').removeClass('btn-outline-primary').addClass('btn-primary');
|
||||
$('#card-view-btn').removeClass('btn-primary').addClass('btn-outline-primary');
|
||||
}
|
||||
}
|
||||
|
||||
{#function exportQueues(format) {#}
|
||||
{# window.location.href = `{% url 'appointments:waiting_queue_export' %}?format=${format}`;#}
|
||||
{# }#}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -5,12 +5,30 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-users-gear"></i> Queue<span class="fw-light">Management</span>
|
||||
</h1>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-calendar-plus"></i> Schedule
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-users"></i> Queue
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary">
|
||||
<i class="fas fa-video"></i> Telemedicine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-users me-2"></i>Queue Management
|
||||
<i class="fas fa-users me-2"></i>Active Queues
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
@ -25,8 +43,8 @@
|
||||
<div class="col-lg-6 col-xl-4 mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">{{ queue.queue.name }}</h5>
|
||||
<span class="badge bg-primary">{{ queue.queue.current_queue_size }}</span>
|
||||
<h5 class="mb-0">{{ queue.name }}</h5>
|
||||
<span class="badge bg-primary">{{ queue.current_queue_size }}</span>
|
||||
</div>
|
||||
<div class="card-body"
|
||||
hx-get="{% url 'appointments:queue_status' queue.pk %}"
|
||||
@ -38,7 +56,7 @@
|
||||
<div class="card-footer">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">
|
||||
Avg Wait: {{ queue.wait_time_minutes|default:"N/A" }}
|
||||
Avg Wait: {{ queue.wait_time_minutes }}
|
||||
</small>
|
||||
<button class="btn btn-sm btn-primary"
|
||||
hx-post="{% url 'appointments:call_next_patient' queue.id %}"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user