This commit is contained in:
Marwan Alwali 2025-09-11 19:01:55 +03:00
parent 43901b5bda
commit a710d1c4d8
702 changed files with 29353 additions and 1412 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -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
View File

@ -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

Binary file not shown.

Binary file not shown.

View File

@ -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
View 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.

View File

@ -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>

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View 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 %}

View 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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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