This commit is contained in:
Marwan Alwali 2025-09-11 20:32:09 +03:00
parent a710d1c4d8
commit beba30e532
46 changed files with 13478 additions and 52 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -5,10 +5,7 @@ Admin configuration for appointments app.
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html from django.utils.html import format_html
from django.utils import timezone from django.utils import timezone
from .models import ( from .models import *
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
TelemedicineSession, AppointmentTemplate
)
class QueueEntryInline(admin.TabularInline): class QueueEntryInline(admin.TabularInline):
@ -459,3 +456,151 @@ class AppointmentTemplateAdmin(admin.ModelAdmin):
'tenant', 'created_by' 'tenant', 'created_by'
) )
@admin.register(WaitingList)
class WaitingListAdmin(admin.ModelAdmin):
"""
Admin configuration for WaitingList model.
"""
list_display = [
'patient', 'specialty', 'priority', 'urgency_score',
'status', 'position', 'days_waiting', 'last_contacted'
]
list_filter = [
'tenant', 'department', 'specialty', 'priority', 'status',
'appointment_type', 'authorization_required', 'requires_interpreter'
]
search_fields = [
'patient__first_name', 'patient__last_name', 'patient__mrn',
'clinical_indication', 'referring_provider'
]
ordering = ['priority', 'urgency_score', 'created_at']
fieldsets = (
('Patient Information', {
'fields': ('tenant', 'patient', 'department', 'provider')
}),
('Service Request', {
'fields': ('appointment_type', 'specialty', 'clinical_indication')
}),
('Priority and Urgency', {
'fields': ('priority', 'urgency_score', 'diagnosis_codes')
}),
('Patient Preferences', {
'fields': (
'preferred_date', 'preferred_time', 'flexible_scheduling',
'earliest_acceptable_date', 'latest_acceptable_date'
)
}),
('Contact Information', {
'fields': ('contact_method', 'contact_phone', 'contact_email')
}),
('Status and Position', {
'fields': ('status', 'position', 'estimated_wait_time')
}),
('Contact History', {
'fields': (
'last_contacted', 'contact_attempts', 'max_contact_attempts',
'appointments_offered', 'appointments_declined'
)
}),
('Requirements', {
'fields': (
'requires_interpreter', 'interpreter_language',
'accessibility_requirements', 'transportation_needed'
),
'classes': ('collapse',)
}),
('Insurance and Authorization', {
'fields': (
'insurance_verified', 'authorization_required',
'authorization_status', 'authorization_number'
),
'classes': ('collapse',)
}),
('Referral Information', {
'fields': (
'referring_provider', 'referral_date', 'referral_urgency'
),
'classes': ('collapse',)
}),
('Outcome', {
'fields': (
'scheduled_appointment', 'removal_reason', 'removal_notes',
'removed_at', 'removed_by'
),
'classes': ('collapse',)
}),
('Notes', {
'fields': ('notes',)
}),
('Metadata', {
'fields': ('waiting_list_id', 'created_at', 'updated_at', 'created_by'),
'classes': ('collapse',)
}),
)
readonly_fields = ['waiting_list_id', 'created_at', 'updated_at', 'days_waiting']
def get_queryset(self, request):
return super().get_queryset(request).select_related(
'tenant', 'patient', 'department', 'provider', 'created_by',
'removed_by', 'scheduled_appointment'
)
def days_waiting(self, obj):
return obj.days_waiting
days_waiting.short_description = 'Days Waiting'
@admin.register(WaitingListContactLog)
class WaitingListContactLogAdmin(admin.ModelAdmin):
"""
Admin configuration for WaitingListContactLog model.
"""
list_display = [
'waiting_list_entry', 'contact_date', 'contact_method',
'contact_outcome', 'appointment_offered', 'patient_response'
]
list_filter = [
'contact_method', 'contact_outcome', 'appointment_offered',
'patient_response', 'contact_date'
]
search_fields = [
'waiting_list_entry__patient__first_name',
'waiting_list_entry__patient__last_name',
'notes'
]
ordering = ['-contact_date']
fieldsets = (
('Contact Information', {
'fields': ('waiting_list_entry', 'contact_method', 'contact_outcome')
}),
('Appointment Offer', {
'fields': (
'appointment_offered', 'offered_date', 'offered_time',
'patient_response'
)
}),
('Follow-up', {
'fields': ('next_contact_date',)
}),
('Notes', {
'fields': ('notes',)
}),
('Metadata', {
'fields': ('contact_date', 'contacted_by'),
'classes': ('collapse',)
}),
)
readonly_fields = ['contact_date']
def get_queryset(self, request):
return super().get_queryset(request).select_related(
'waiting_list_entry__patient', 'contacted_by'
)

View File

@ -5,15 +5,11 @@ Forms for Appointments app CRUD operations.
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils import timezone from django.utils import timezone
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta, date
from .models import ( from .models import *
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
TelemedicineSession, AppointmentTemplate
)
from patients.models import PatientProfile from patients.models import PatientProfile
from accounts.models import User from accounts.models import User
from hr.models import Employee from hr.models import Employee, Department
class AppointmentRequestForm(forms.ModelForm): class AppointmentRequestForm(forms.ModelForm):
""" """
@ -361,8 +357,6 @@ class AppointmentSearchForm(forms.Form):
).order_by('last_name', 'first_name') ).order_by('last_name', 'first_name')
class QueueSearchForm(forms.Form): class QueueSearchForm(forms.Form):
""" """
Form for searching queues. Form for searching queues.
@ -441,6 +435,437 @@ class SlotSearchForm(forms.Form):
).order_by('last_name', 'first_name') ).order_by('last_name', 'first_name')
class WaitingListForm(forms.ModelForm):
"""
Form for creating and updating waiting list entries.
"""
class Meta:
model = WaitingList
fields = [
'patient', 'department', 'provider', 'appointment_type', 'specialty',
'priority', 'urgency_score', 'clinical_indication', 'diagnosis_codes',
'preferred_date', 'preferred_time', 'flexible_scheduling',
'earliest_acceptable_date', 'latest_acceptable_date',
'acceptable_days', 'acceptable_times',
'contact_method', 'contact_phone', 'contact_email',
'requires_interpreter', 'interpreter_language',
'accessibility_requirements', 'transportation_needed',
'insurance_verified', 'authorization_required',
'referring_provider', 'referral_date', 'referral_urgency',
'notes'
]
widgets = {
'patient': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'department': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'provider': forms.Select(attrs={
'class': 'form-select'
}),
'appointment_type': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'specialty': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'priority': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'urgency_score': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
'max': 10,
'required': True
}),
'clinical_indication': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'required': True,
'placeholder': 'Describe the clinical reason for this appointment request...'
}),
'diagnosis_codes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Enter ICD-10 codes separated by commas'
}),
'preferred_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'min': date.today().isoformat()
}),
'preferred_time': forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
'flexible_scheduling': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'earliest_acceptable_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'min': date.today().isoformat()
}),
'latest_acceptable_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'acceptable_days': forms.CheckboxSelectMultiple(attrs={
'class': 'form-check-input'
}),
'contact_method': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'contact_phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': '(555) 123-4567'
}),
'contact_email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'patient@example.com'
}),
'requires_interpreter': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'interpreter_language': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Spanish, Mandarin, ASL'
}),
'accessibility_requirements': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Describe any accessibility needs...'
}),
'transportation_needed': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'insurance_verified': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'authorization_required': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'referring_provider': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Dr. Smith, Internal Medicine'
}),
'referral_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'referral_urgency': forms.Select(attrs={
'class': 'form-select'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Additional notes and comments...'
}),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Filter choices based on tenant
if self.tenant:
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(
tenant=self.tenant
)
self.fields['department'].queryset = self.fields['department'].queryset.filter(
tenant=self.tenant
)
if 'provider' in self.fields:
self.fields['provider'].queryset = self.fields['provider'].queryset.filter(
tenant=self.tenant
)
def clean(self):
cleaned_data = super().clean()
# Validate date ranges
preferred_date = cleaned_data.get('preferred_date')
earliest_date = cleaned_data.get('earliest_acceptable_date')
latest_date = cleaned_data.get('latest_acceptable_date')
if preferred_date and preferred_date < date.today():
raise ValidationError("Preferred date cannot be in the past.")
if earliest_date and latest_date and earliest_date > latest_date:
raise ValidationError("Earliest acceptable date must be before latest acceptable date.")
if preferred_date and earliest_date and preferred_date < earliest_date:
raise ValidationError("Preferred date must be within acceptable date range.")
if preferred_date and latest_date and preferred_date > latest_date:
raise ValidationError("Preferred date must be within acceptable date range.")
# Validate contact information
contact_method = cleaned_data.get('contact_method')
contact_phone = cleaned_data.get('contact_phone')
contact_email = cleaned_data.get('contact_email')
if contact_method == 'PHONE' and not contact_phone:
raise ValidationError("Phone number is required when phone is selected as contact method.")
if contact_method == 'EMAIL' and not contact_email:
raise ValidationError("Email address is required when email is selected as contact method.")
# Validate interpreter requirements
requires_interpreter = cleaned_data.get('requires_interpreter')
interpreter_language = cleaned_data.get('interpreter_language')
if requires_interpreter and not interpreter_language:
raise ValidationError("Interpreter language is required when interpreter services are needed.")
return cleaned_data
class WaitingListContactLogForm(forms.ModelForm):
"""
Form for logging contact attempts with waiting list patients.
"""
class Meta:
model = WaitingListContactLog
fields = [
'contact_method', 'contact_outcome', 'appointment_offered',
'offered_date', 'offered_time', 'patient_response',
'notes', 'next_contact_date'
]
widgets = {
'contact_method': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'contact_outcome': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'appointment_offered': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'offered_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'offered_time': forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
'patient_response': forms.Select(attrs={
'class': 'form-select'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Notes from contact attempt...'
}),
'next_contact_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'min': date.today().isoformat()
}),
}
def clean(self):
cleaned_data = super().clean()
appointment_offered = cleaned_data.get('appointment_offered')
offered_date = cleaned_data.get('offered_date')
offered_time = cleaned_data.get('offered_time')
patient_response = cleaned_data.get('patient_response')
if appointment_offered:
if not offered_date:
raise ValidationError("Offered date is required when appointment is offered.")
if not offered_time:
raise ValidationError("Offered time is required when appointment is offered.")
if not patient_response:
raise ValidationError("Patient response is required when appointment is offered.")
next_contact_date = cleaned_data.get('next_contact_date')
if next_contact_date and next_contact_date < date.today():
raise ValidationError("Next contact date cannot be in the past.")
return cleaned_data
class WaitingListFilterForm(forms.Form):
"""
Form for filtering waiting list entries.
"""
PRIORITY_CHOICES = [
('', 'All Priorities'),
('EMERGENCY', 'Emergency'),
('STAT', 'STAT'),
('URGENT', 'Urgent'),
('ROUTINE', 'Routine'),
]
STATUS_CHOICES = [
('', 'All Status'),
('ACTIVE', 'Active'),
('CONTACTED', 'Contacted'),
('OFFERED', 'Appointment Offered'),
('SCHEDULED', 'Scheduled'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
]
department = forms.ModelChoiceField(
queryset=None,
required=False,
empty_label="All Departments",
widget=forms.Select(attrs={'class': 'form-select'})
)
specialty = forms.ChoiceField(
choices=[('', 'All Specialties')] + WaitingList._meta.get_field('specialty').choices,
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
priority = forms.ChoiceField(
choices=PRIORITY_CHOICES,
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
status = forms.ChoiceField(
choices=STATUS_CHOICES,
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
provider = forms.ModelChoiceField(
queryset=None,
required=False,
empty_label="All Providers",
widget=forms.Select(attrs={'class': 'form-select'})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
urgency_min = forms.IntegerField(
required=False,
min_value=1,
max_value=10,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Min urgency (1-10)'
})
)
urgency_max = forms.IntegerField(
required=False,
min_value=1,
max_value=10,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'placeholder': 'Max urgency (1-10)'
})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
self.fields['department'].queryset = Department.objects.filter(tenant=tenant)
self.fields['provider'].queryset = User.objects.filter(
tenant=tenant
)
class WaitingListBulkActionForm(forms.Form):
"""
Form for bulk actions on waiting list entries.
"""
ACTION_CHOICES = [
('', 'Select Action'),
('contact', 'Mark as Contacted'),
('cancel', 'Cancel Entries'),
('update_priority', 'Update Priority'),
('transfer_provider', 'Transfer to Provider'),
('export', 'Export Selected'),
]
action = forms.ChoiceField(
choices=ACTION_CHOICES,
required=True,
widget=forms.Select(attrs={'class': 'form-select'})
)
# Fields for specific actions
new_priority = forms.ChoiceField(
choices=WaitingList._meta.get_field('priority').choices,
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
transfer_provider = forms.ModelChoiceField(
queryset=None,
required=False,
widget=forms.Select(attrs={'class': 'form-select'})
)
contact_notes = forms.CharField(
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Notes for contact action...'
})
)
cancellation_reason = forms.CharField(
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Reason for cancellation...'
})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
from django.contrib.auth import get_user_model
User = get_user_model()
self.fields['transfer_provider'].queryset = User.objects.filter(
tenant=tenant
)
# from django import forms # from django import forms
# from django.core.exceptions import ValidationError # from django.core.exceptions import ValidationError
# from django.utils import timezone # from django.utils import timezone

View File

@ -0,0 +1,688 @@
# Generated by Django 5.2.6 on 2025-09-11 17:03
import django.core.validators
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("appointments", "0002_initial"),
("core", "0001_initial"),
("hr", "0001_initial"),
("patients", "0003_remove_insuranceinfo_subscriber_ssn_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="WaitingList",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"waiting_list_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique waiting list entry identifier",
unique=True,
),
),
(
"appointment_type",
models.CharField(
choices=[
("CONSULTATION", "Consultation"),
("FOLLOW_UP", "Follow-up"),
("PROCEDURE", "Procedure"),
("SURGERY", "Surgery"),
("DIAGNOSTIC", "Diagnostic"),
("THERAPY", "Therapy"),
("VACCINATION", "Vaccination"),
("SCREENING", "Screening"),
("EMERGENCY", "Emergency"),
("TELEMEDICINE", "Telemedicine"),
("OTHER", "Other"),
],
help_text="Type of appointment requested",
max_length=50,
),
),
(
"specialty",
models.CharField(
choices=[
("FAMILY_MEDICINE", "Family Medicine"),
("INTERNAL_MEDICINE", "Internal Medicine"),
("PEDIATRICS", "Pediatrics"),
("CARDIOLOGY", "Cardiology"),
("DERMATOLOGY", "Dermatology"),
("ENDOCRINOLOGY", "Endocrinology"),
("GASTROENTEROLOGY", "Gastroenterology"),
("NEUROLOGY", "Neurology"),
("ONCOLOGY", "Oncology"),
("ORTHOPEDICS", "Orthopedics"),
("PSYCHIATRY", "Psychiatry"),
("RADIOLOGY", "Radiology"),
("SURGERY", "Surgery"),
("UROLOGY", "Urology"),
("GYNECOLOGY", "Gynecology"),
("OPHTHALMOLOGY", "Ophthalmology"),
("ENT", "Ear, Nose & Throat"),
("EMERGENCY", "Emergency Medicine"),
("OTHER", "Other"),
],
help_text="Medical specialty required",
max_length=100,
),
),
(
"priority",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("STAT", "STAT"),
("EMERGENCY", "Emergency"),
],
default="ROUTINE",
help_text="Clinical priority level",
max_length=20,
),
),
(
"urgency_score",
models.PositiveIntegerField(
default=1,
help_text="Clinical urgency score (1-10, 10 being most urgent)",
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(10),
],
),
),
(
"clinical_indication",
models.TextField(
help_text="Clinical reason for appointment request"
),
),
(
"diagnosis_codes",
models.JSONField(
blank=True, default=list, help_text="ICD-10 diagnosis codes"
),
),
(
"preferred_date",
models.DateField(
blank=True,
help_text="Patient preferred appointment date",
null=True,
),
),
(
"preferred_time",
models.TimeField(
blank=True,
help_text="Patient preferred appointment time",
null=True,
),
),
(
"flexible_scheduling",
models.BooleanField(
default=True,
help_text="Patient accepts alternative dates/times",
),
),
(
"earliest_acceptable_date",
models.DateField(
blank=True,
help_text="Earliest acceptable appointment date",
null=True,
),
),
(
"latest_acceptable_date",
models.DateField(
blank=True,
help_text="Latest acceptable appointment date",
null=True,
),
),
(
"acceptable_days",
models.JSONField(
blank=True,
default=list,
help_text="Acceptable days of week (0=Monday, 6=Sunday)",
),
),
(
"acceptable_times",
models.JSONField(
blank=True, default=list, help_text="Acceptable time ranges"
),
),
(
"contact_method",
models.CharField(
choices=[
("PHONE", "Phone"),
("EMAIL", "Email"),
("SMS", "SMS"),
("PORTAL", "Patient Portal"),
("MAIL", "Mail"),
],
default="PHONE",
help_text="Preferred contact method",
max_length=20,
),
),
(
"contact_phone",
models.CharField(
blank=True,
help_text="Contact phone number",
max_length=20,
null=True,
),
),
(
"contact_email",
models.EmailField(
blank=True,
help_text="Contact email address",
max_length=254,
null=True,
),
),
(
"status",
models.CharField(
choices=[
("ACTIVE", "Active"),
("CONTACTED", "Contacted"),
("OFFERED", "Appointment Offered"),
("SCHEDULED", "Scheduled"),
("CANCELLED", "Cancelled"),
("EXPIRED", "Expired"),
("TRANSFERRED", "Transferred"),
],
default="ACTIVE",
help_text="Waiting list status",
max_length=20,
),
),
(
"position",
models.PositiveIntegerField(
blank=True,
help_text="Position in waiting list queue",
null=True,
),
),
(
"estimated_wait_time",
models.PositiveIntegerField(
blank=True, help_text="Estimated wait time in days", null=True
),
),
(
"last_contacted",
models.DateTimeField(
blank=True,
help_text="Last contact attempt date/time",
null=True,
),
),
(
"contact_attempts",
models.PositiveIntegerField(
default=0, help_text="Number of contact attempts made"
),
),
(
"max_contact_attempts",
models.PositiveIntegerField(
default=3, help_text="Maximum contact attempts before expiring"
),
),
(
"appointments_offered",
models.PositiveIntegerField(
default=0, help_text="Number of appointments offered"
),
),
(
"appointments_declined",
models.PositiveIntegerField(
default=0, help_text="Number of appointments declined"
),
),
(
"last_offer_date",
models.DateTimeField(
blank=True,
help_text="Date of last appointment offer",
null=True,
),
),
(
"requires_interpreter",
models.BooleanField(
default=False, help_text="Patient requires interpreter services"
),
),
(
"interpreter_language",
models.CharField(
blank=True,
help_text="Required interpreter language",
max_length=50,
null=True,
),
),
(
"accessibility_requirements",
models.TextField(
blank=True,
help_text="Special accessibility requirements",
null=True,
),
),
(
"transportation_needed",
models.BooleanField(
default=False,
help_text="Patient needs transportation assistance",
),
),
(
"insurance_verified",
models.BooleanField(
default=False, help_text="Insurance coverage verified"
),
),
(
"authorization_required",
models.BooleanField(
default=False, help_text="Prior authorization required"
),
),
(
"authorization_status",
models.CharField(
choices=[
("NOT_REQUIRED", "Not Required"),
("PENDING", "Pending"),
("APPROVED", "Approved"),
("DENIED", "Denied"),
("EXPIRED", "Expired"),
],
default="NOT_REQUIRED",
help_text="Authorization status",
max_length=20,
),
),
(
"authorization_number",
models.CharField(
blank=True,
help_text="Authorization number",
max_length=100,
null=True,
),
),
(
"referring_provider",
models.CharField(
blank=True,
help_text="Referring provider name",
max_length=200,
null=True,
),
),
(
"referral_date",
models.DateField(
blank=True, help_text="Date of referral", null=True
),
),
(
"referral_urgency",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("STAT", "STAT"),
],
default="ROUTINE",
help_text="Referral urgency level",
max_length=20,
),
),
(
"removal_reason",
models.CharField(
blank=True,
choices=[
("SCHEDULED", "Appointment Scheduled"),
("PATIENT_CANCELLED", "Patient Cancelled"),
("PROVIDER_CANCELLED", "Provider Cancelled"),
("NO_RESPONSE", "No Response to Contact"),
("INSURANCE_ISSUE", "Insurance Issue"),
("TRANSFERRED", "Transferred to Another Provider"),
("EXPIRED", "Entry Expired"),
("DUPLICATE", "Duplicate Entry"),
("OTHER", "Other"),
],
help_text="Reason for removal from waiting list",
max_length=50,
null=True,
),
),
(
"removal_notes",
models.TextField(
blank=True,
help_text="Additional notes about removal",
null=True,
),
),
(
"removed_at",
models.DateTimeField(
blank=True,
help_text="Date/time removed from waiting list",
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"notes",
models.TextField(
blank=True, help_text="Additional notes and comments", null=True
),
),
(
"created_by",
models.ForeignKey(
blank=True,
help_text="User who created the waiting list entry",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
(
"department",
models.ForeignKey(
help_text="Department for appointment",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="hr.department",
),
),
(
"patient",
models.ForeignKey(
help_text="Patient on waiting list",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="patients.patientprofile",
),
),
(
"provider",
models.ForeignKey(
blank=True,
help_text="Preferred healthcare provider",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="provider_waiting_list",
to=settings.AUTH_USER_MODEL,
),
),
(
"removed_by",
models.ForeignKey(
blank=True,
help_text="User who removed entry from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="removed_waiting_list_entries",
to=settings.AUTH_USER_MODEL,
),
),
(
"scheduled_appointment",
models.ForeignKey(
blank=True,
help_text="Scheduled appointment from waiting list",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="waiting_list_entry",
to="appointments.appointmentrequest",
),
),
(
"tenant",
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="waiting_list_entries",
to="core.tenant",
),
),
],
options={
"verbose_name": "Waiting List Entry",
"verbose_name_plural": "Waiting List Entries",
"db_table": "appointments_waiting_list",
"ordering": ["priority", "urgency_score", "created_at"],
},
),
migrations.CreateModel(
name="WaitingListContactLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"contact_date",
models.DateTimeField(
auto_now_add=True, help_text="Date and time of contact attempt"
),
),
(
"contact_method",
models.CharField(
choices=[
("PHONE", "Phone Call"),
("EMAIL", "Email"),
("SMS", "SMS"),
("PORTAL", "Patient Portal Message"),
("MAIL", "Mail"),
("IN_PERSON", "In Person"),
],
help_text="Method of contact used",
max_length=20,
),
),
(
"contact_outcome",
models.CharField(
choices=[
("SUCCESSFUL", "Successful Contact"),
("NO_ANSWER", "No Answer"),
("BUSY", "Line Busy"),
("VOICEMAIL", "Left Voicemail"),
("EMAIL_SENT", "Email Sent"),
("EMAIL_BOUNCED", "Email Bounced"),
("SMS_SENT", "SMS Sent"),
("SMS_FAILED", "SMS Failed"),
("WRONG_NUMBER", "Wrong Number"),
("DECLINED", "Patient Declined"),
],
help_text="Outcome of contact attempt",
max_length=20,
),
),
(
"appointment_offered",
models.BooleanField(
default=False,
help_text="Appointment was offered during contact",
),
),
(
"offered_date",
models.DateField(
blank=True, help_text="Date of offered appointment", null=True
),
),
(
"offered_time",
models.TimeField(
blank=True, help_text="Time of offered appointment", null=True
),
),
(
"patient_response",
models.CharField(
blank=True,
choices=[
("ACCEPTED", "Accepted Appointment"),
("DECLINED", "Declined Appointment"),
("REQUESTED_DIFFERENT", "Requested Different Time"),
("WILL_CALL_BACK", "Will Call Back"),
("NO_LONGER_NEEDED", "No Longer Needed"),
("INSURANCE_ISSUE", "Insurance Issue"),
("NO_RESPONSE", "No Response"),
],
help_text="Patient response to contact",
max_length=20,
null=True,
),
),
(
"notes",
models.TextField(
blank=True, help_text="Notes from contact attempt", null=True
),
),
(
"next_contact_date",
models.DateField(
blank=True,
help_text="Scheduled date for next contact attempt",
null=True,
),
),
(
"contacted_by",
models.ForeignKey(
blank=True,
help_text="Staff member who made contact",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to=settings.AUTH_USER_MODEL,
),
),
(
"waiting_list_entry",
models.ForeignKey(
help_text="Associated waiting list entry",
on_delete=django.db.models.deletion.CASCADE,
related_name="contact_logs",
to="appointments.waitinglist",
),
),
],
options={
"verbose_name": "Waiting List Contact Log",
"verbose_name_plural": "Waiting List Contact Logs",
"db_table": "appointments_waiting_list_contact_log",
"ordering": ["-contact_date"],
},
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["tenant", "status"], name="appointment_tenant__a558da_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["patient", "status"], name="appointment_patient_73f03d_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["department", "specialty", "status"],
name="appointment_departm_78fd70_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["priority", "urgency_score"],
name="appointment_priorit_30fb90_idx",
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["status", "created_at"], name="appointment_status_cfe551_idx"
),
),
migrations.AddIndex(
model_name="waitinglist",
index=models.Index(
fields=["provider", "status"], name="appointment_provide_dd6c2b_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["waiting_list_entry", "contact_date"],
name="appointment_waiting_50d8ac_idx",
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["contact_outcome"], name="appointment_contact_ad9c45_idx"
),
),
migrations.AddIndex(
model_name="waitinglistcontactlog",
index=models.Index(
fields=["next_contact_date"], name="appointment_next_co_b29984_idx"
),
),
]

View File

@ -1176,3 +1176,640 @@ class AppointmentTemplate(models.Model):
def __str__(self): def __str__(self):
return f"{self.name} ({self.specialty})" return f"{self.name} ({self.specialty})"
class WaitingList(models.Model):
"""
Patient waiting list for appointment scheduling.
Follows healthcare industry standards for patient queue management.
"""
APPOINTMENT_TYPE_CHOICES = [
('CONSULTATION', 'Consultation'),
('FOLLOW_UP', 'Follow-up'),
('PROCEDURE', 'Procedure'),
('SURGERY', 'Surgery'),
('DIAGNOSTIC', 'Diagnostic'),
('THERAPY', 'Therapy'),
('VACCINATION', 'Vaccination'),
('SCREENING', 'Screening'),
('EMERGENCY', 'Emergency'),
('TELEMEDICINE', 'Telemedicine'),
('OTHER', 'Other'),
]
SPECIALTY_CHOICES = [
('FAMILY_MEDICINE', 'Family Medicine'),
('INTERNAL_MEDICINE', 'Internal Medicine'),
('PEDIATRICS', 'Pediatrics'),
('CARDIOLOGY', 'Cardiology'),
('DERMATOLOGY', 'Dermatology'),
('ENDOCRINOLOGY', 'Endocrinology'),
('GASTROENTEROLOGY', 'Gastroenterology'),
('NEUROLOGY', 'Neurology'),
('ONCOLOGY', 'Oncology'),
('ORTHOPEDICS', 'Orthopedics'),
('PSYCHIATRY', 'Psychiatry'),
('RADIOLOGY', 'Radiology'),
('SURGERY', 'Surgery'),
('UROLOGY', 'Urology'),
('GYNECOLOGY', 'Gynecology'),
('OPHTHALMOLOGY', 'Ophthalmology'),
('ENT', 'Ear, Nose & Throat'),
('EMERGENCY', 'Emergency Medicine'),
('OTHER', 'Other'),
]
PRIORITY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
]
CONTACT_METHOD_CHOICES = [
('PHONE', 'Phone'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PORTAL', 'Patient Portal'),
('MAIL', 'Mail'),
]
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('CONTACTED', 'Contacted'),
('OFFERED', 'Appointment Offered'),
('SCHEDULED', 'Scheduled'),
('CANCELLED', 'Cancelled'),
('EXPIRED', 'Expired'),
('TRANSFERRED', 'Transferred'),
]
AUTHORIZATION_STATUS_CHOICES = [
('NOT_REQUIRED', 'Not Required'),
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('DENIED', 'Denied'),
('EXPIRED', 'Expired'),
]
REFERRAL_URGENCY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
]
REMOVAL_REASON_CHOICES = [
('SCHEDULED', 'Appointment Scheduled'),
('PATIENT_CANCELLED', 'Patient Cancelled'),
('PROVIDER_CANCELLED', 'Provider Cancelled'),
('NO_RESPONSE', 'No Response to Contact'),
('INSURANCE_ISSUE', 'Insurance Issue'),
('TRANSFERRED', 'Transferred to Another Provider'),
('EXPIRED', 'Entry Expired'),
('DUPLICATE', 'Duplicate Entry'),
('OTHER', 'Other'),
]
# Basic Identifiers
waiting_list_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique waiting list entry identifier'
)
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='waiting_list_entries',
help_text='Organization tenant'
)
# Patient Information
patient = models.ForeignKey(
'patients.PatientProfile',
on_delete=models.CASCADE,
related_name='waiting_list_entries',
help_text='Patient on waiting list'
)
# Provider and Service Information
provider = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='provider_waiting_list',
blank=True,
null=True,
help_text='Preferred healthcare provider'
)
department = models.ForeignKey(
'hr.Department',
on_delete=models.CASCADE,
related_name='waiting_list_entries',
help_text='Department for appointment'
)
appointment_type = models.CharField(
max_length=50,
choices=APPOINTMENT_TYPE_CHOICES,
help_text='Type of appointment requested'
)
specialty = models.CharField(
max_length=100,
choices=SPECIALTY_CHOICES,
help_text='Medical specialty required'
)
# Priority and Clinical Information
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default='ROUTINE',
help_text='Clinical priority level'
)
urgency_score = models.PositiveIntegerField(
default=1,
validators=[MinValueValidator(1), MaxValueValidator(10)],
help_text='Clinical urgency score (1-10, 10 being most urgent)'
)
clinical_indication = models.TextField(
help_text='Clinical reason for appointment request'
)
diagnosis_codes = models.JSONField(
default=list,
blank=True,
help_text='ICD-10 diagnosis codes'
)
# Patient Preferences
preferred_date = models.DateField(
blank=True,
null=True,
help_text='Patient preferred appointment date'
)
preferred_time = models.TimeField(
blank=True,
null=True,
help_text='Patient preferred appointment time'
)
flexible_scheduling = models.BooleanField(
default=True,
help_text='Patient accepts alternative dates/times'
)
earliest_acceptable_date = models.DateField(
blank=True,
null=True,
help_text='Earliest acceptable appointment date'
)
latest_acceptable_date = models.DateField(
blank=True,
null=True,
help_text='Latest acceptable appointment date'
)
acceptable_days = models.JSONField(
default=list,
blank=True,
help_text='Acceptable days of week (0=Monday, 6=Sunday)'
)
acceptable_times = models.JSONField(
default=list,
blank=True,
help_text='Acceptable time ranges'
)
# Communication Preferences
contact_method = models.CharField(
max_length=20,
choices=CONTACT_METHOD_CHOICES,
default='PHONE',
help_text='Preferred contact method'
)
contact_phone = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Contact phone number'
)
contact_email = models.EmailField(
blank=True,
null=True,
help_text='Contact email address'
)
# Status and Workflow
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='ACTIVE',
help_text='Waiting list status'
)
# Position and Timing
position = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Position in waiting list queue'
)
estimated_wait_time = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Estimated wait time in days'
)
# Contact History
last_contacted = models.DateTimeField(
blank=True,
null=True,
help_text='Last contact attempt date/time'
)
contact_attempts = models.PositiveIntegerField(
default=0,
help_text='Number of contact attempts made'
)
max_contact_attempts = models.PositiveIntegerField(
default=3,
help_text='Maximum contact attempts before expiring'
)
# Appointment Offers
appointments_offered = models.PositiveIntegerField(
default=0,
help_text='Number of appointments offered'
)
appointments_declined = models.PositiveIntegerField(
default=0,
help_text='Number of appointments declined'
)
last_offer_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date of last appointment offer'
)
# Scheduling Constraints
requires_interpreter = models.BooleanField(
default=False,
help_text='Patient requires interpreter services'
)
interpreter_language = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Required interpreter language'
)
accessibility_requirements = models.TextField(
blank=True,
null=True,
help_text='Special accessibility requirements'
)
transportation_needed = models.BooleanField(
default=False,
help_text='Patient needs transportation assistance'
)
# Insurance and Authorization
insurance_verified = models.BooleanField(
default=False,
help_text='Insurance coverage verified'
)
authorization_required = models.BooleanField(
default=False,
help_text='Prior authorization required'
)
authorization_status = models.CharField(
max_length=20,
choices=AUTHORIZATION_STATUS_CHOICES,
default='NOT_REQUIRED',
help_text='Authorization status'
)
authorization_number = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Authorization number'
)
# Referral Information
referring_provider = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Referring provider name'
)
referral_date = models.DateField(
blank=True,
null=True,
help_text='Date of referral'
)
referral_urgency = models.CharField(
max_length=20,
choices=REFERRAL_URGENCY_CHOICES,
default='ROUTINE',
help_text='Referral urgency level'
)
# Outcome Tracking
scheduled_appointment = models.ForeignKey(
'AppointmentRequest',
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='waiting_list_entry',
help_text='Scheduled appointment from waiting list'
)
removal_reason = models.CharField(
max_length=50,
choices=REMOVAL_REASON_CHOICES,
blank=True,
null=True,
help_text='Reason for removal from waiting list'
)
removal_notes = models.TextField(
blank=True,
null=True,
help_text='Additional notes about removal'
)
removed_at = models.DateTimeField(
blank=True,
null=True,
help_text='Date/time removed from waiting list'
)
removed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name='removed_waiting_list_entries',
help_text='User who removed entry from waiting list'
)
# Audit Trail
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_waiting_list_entries',
help_text='User who created the waiting list entry'
)
# Notes and Comments
notes = models.TextField(
blank=True,
null=True,
help_text='Additional notes and comments'
)
class Meta:
db_table = 'appointments_waiting_list'
verbose_name = 'Waiting List Entry'
verbose_name_plural = 'Waiting List Entries'
ordering = ['priority', 'urgency_score', 'created_at']
indexes = [
models.Index(fields=['tenant', 'status']),
models.Index(fields=['patient', 'status']),
models.Index(fields=['department', 'specialty', 'status']),
models.Index(fields=['priority', 'urgency_score']),
models.Index(fields=['status', 'created_at']),
models.Index(fields=['provider', 'status']),
]
def __str__(self):
return f"{self.patient.get_full_name()} - {self.specialty} ({self.status})"
@property
def days_waiting(self):
"""Calculate number of days patient has been waiting."""
return (timezone.now().date() - self.created_at.date()).days
@property
def is_overdue_contact(self):
"""Check if contact is overdue based on priority."""
if not self.last_contacted:
return self.days_waiting > 1
days_since_contact = (timezone.now().date() - self.last_contacted.date()).days
if self.priority == 'EMERGENCY':
return days_since_contact > 0 # Same day contact required
elif self.priority == 'STAT':
return days_since_contact > 1 # Next day contact required
elif self.priority == 'URGENT':
return days_since_contact > 3 # 3 day contact window
else:
return days_since_contact > 7 # Weekly contact for routine
@property
def should_expire(self):
"""Check if waiting list entry should expire."""
if self.contact_attempts >= self.max_contact_attempts:
return True
# Expire after 90 days for routine, 30 days for urgent
max_days = 30 if self.priority in ['URGENT', 'STAT', 'EMERGENCY'] else 90
return self.days_waiting > max_days
def calculate_position(self):
"""Calculate position in waiting list queue."""
# Priority-based position calculation
priority_weights = {
'EMERGENCY': 1000,
'STAT': 800,
'URGENT': 600,
'ROUTINE': 400,
}
base_score = priority_weights.get(self.priority, 400)
urgency_bonus = self.urgency_score * 10
wait_time_bonus = min(self.days_waiting, 30) # Cap at 30 days
total_score = base_score + urgency_bonus + wait_time_bonus
# Count entries with higher scores
higher_priority = WaitingList.objects.filter(
department=self.department,
specialty=self.specialty,
status='ACTIVE',
tenant=self.tenant
).exclude(id=self.id)
position = 1
for entry in higher_priority:
entry_score = (
priority_weights.get(entry.priority, 400) +
entry.urgency_score * 10 +
min(entry.days_waiting, 30)
)
if entry_score > total_score:
position += 1
return position
def update_position(self):
"""Update position in waiting list."""
self.position = self.calculate_position()
self.save(update_fields=['position'])
def estimate_wait_time(self):
"""Estimate wait time based on historical data and current queue."""
# This would typically use historical scheduling data
# For now, provide basic estimation
base_wait = {
'EMERGENCY': 1,
'STAT': 3,
'URGENT': 7,
'ROUTINE': 14,
}
estimated_days = base_wait.get(self.priority, 14)
# Adjust based on queue position
if self.position:
estimated_days += max(0, (self.position - 1) * 2)
return estimated_days
class WaitingListContactLog(models.Model):
"""
Contact log for waiting list entries.
Tracks all communication attempts with patients on waiting list.
"""
CONTACT_METHOD_CHOICES = [
('PHONE', 'Phone Call'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PORTAL', 'Patient Portal Message'),
('MAIL', 'Mail'),
('IN_PERSON', 'In Person'),
]
CONTACT_OUTCOME_CHOICES = [
('SUCCESSFUL', 'Successful Contact'),
('NO_ANSWER', 'No Answer'),
('BUSY', 'Line Busy'),
('VOICEMAIL', 'Left Voicemail'),
('EMAIL_SENT', 'Email Sent'),
('EMAIL_BOUNCED', 'Email Bounced'),
('SMS_SENT', 'SMS Sent'),
('SMS_FAILED', 'SMS Failed'),
('WRONG_NUMBER', 'Wrong Number'),
('DECLINED', 'Patient Declined'),
]
PATIENT_RESPONSE_CHOICES = [
('ACCEPTED', 'Accepted Appointment'),
('DECLINED', 'Declined Appointment'),
('REQUESTED_DIFFERENT', 'Requested Different Time'),
('WILL_CALL_BACK', 'Will Call Back'),
('NO_LONGER_NEEDED', 'No Longer Needed'),
('INSURANCE_ISSUE', 'Insurance Issue'),
('NO_RESPONSE', 'No Response'),
]
waiting_list_entry = models.ForeignKey(
WaitingList,
on_delete=models.CASCADE,
related_name='contact_logs',
help_text='Associated waiting list entry'
)
contact_date = models.DateTimeField(
auto_now_add=True,
help_text='Date and time of contact attempt'
)
contact_method = models.CharField(
max_length=20,
choices=CONTACT_METHOD_CHOICES,
help_text='Method of contact used'
)
contact_outcome = models.CharField(
max_length=20,
choices=CONTACT_OUTCOME_CHOICES,
help_text='Outcome of contact attempt'
)
appointment_offered = models.BooleanField(
default=False,
help_text='Appointment was offered during contact'
)
offered_date = models.DateField(
blank=True,
null=True,
help_text='Date of offered appointment'
)
offered_time = models.TimeField(
blank=True,
null=True,
help_text='Time of offered appointment'
)
patient_response = models.CharField(
max_length=20,
choices=PATIENT_RESPONSE_CHOICES,
blank=True,
null=True,
help_text='Patient response to contact'
)
notes = models.TextField(
blank=True,
null=True,
help_text='Notes from contact attempt'
)
next_contact_date = models.DateField(
blank=True,
null=True,
help_text='Scheduled date for next contact attempt'
)
contacted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text='Staff member who made contact'
)
class Meta:
db_table = 'appointments_waiting_list_contact_log'
verbose_name = 'Waiting List Contact Log'
verbose_name_plural = 'Waiting List Contact Logs'
ordering = ['-contact_date']
indexes = [
models.Index(fields=['waiting_list_entry', 'contact_date']),
models.Index(fields=['contact_outcome']),
models.Index(fields=['next_contact_date']),
]
def __str__(self):
return f"{self.waiting_list_entry.patient.get_full_name()} - {self.contact_method} ({self.contact_outcome})"

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,39 @@
{% for log in contact_logs %}
<div class="contact-log-item">
<div class="d-flex justify-content-between align-items-center mb-1">
<h6 class="mb-0">
<i class="fas fa-{{ log.contact_method|lower }} me-1"></i>
{{ log.get_contact_method_display }} -
<span class="badge bg-{% if log.contact_outcome == 'SUCCESSFUL' %}success{% elif log.contact_outcome == 'DECLINED' %}danger{% else %}info{% endif %}">
{{ log.get_contact_outcome_display }}
</span>
</h6>
<small class="text-muted">{{ log.contact_date|date:"M d, Y H:i" }}</small>
</div>
<p class="mb-1">{{ log.notes|default:"No notes." }}</p>
{% if log.appointment_offered %}
<p class="mb-1 text-primary">
<i class="fas fa-calendar-check me-1"></i>Appointment Offered:
{{ log.offered_date|date:"M d, Y" }} at {{ log.offered_time|time:"g:i A" }}
</p>
<p class="mb-0 text-primary">
<i class="fas fa-reply me-1"></i>Patient Response:
<span class="badge bg-{% if log.patient_response == 'ACCEPTED' %}success{% elif log.patient_response == 'DECLINED' %}danger{% else %}secondary{% endif %}">
{{ log.get_patient_response_display }}
</span>
</p>
{% endif %}
{% if log.next_contact_date %}
<p class="mb-0 text-info">
<i class="fas fa-calendar-alt me-1"></i>Next Contact: {{ log.next_contact_date|date:"M d, Y" }}
</p>
{% endif %}
<small class="text-muted">Contacted by: {{ log.contacted_by.get_full_name|default:"N/A" }}</small>
</div>
{% empty %}
<div class="text-center text-muted py-3">
<i class="fas fa-comment-slash fa-2x mb-2"></i>
<p class="mb-0">No contact logs available for this entry.</p>
</div>
{% endfor %}

View File

@ -151,22 +151,15 @@
{% csrf_token %} {% csrf_token %}
<!-- Basic Information --> <!-- Basic Information -->
<div class="panel panel-inverse"> <div class="form-section">
<div class="panel-heading"> <div class="section-header">
<h4 class="panel-title"> <h4 class="section-title">
<i class="fas fa-info-circle me-2"></i>Basic Information <i class="fas fa-info-circle me-2"></i>Basic Information
</h4> </h4>
<small class="text-light">Configure the basic details of the waiting queue</small> <p class="section-description">Configure the basic details of the waiting queue</p>
<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>
<div class="panel-body">
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<label for="{{ form.name.id_for_label }}" class="form-label"> <label for="{{ form.name.id_for_label }}" class="form-label">
@ -207,6 +200,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label> <label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
@ -221,8 +215,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
</div>
<!-- Queue Configuration --> <!-- Queue Configuration -->
<div class="form-section"> <div class="form-section">
<div class="section-header"> <div class="section-header">

View File

@ -0,0 +1,525 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Patient Waiting List Management{% 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" />
<link href="{% static 'plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.priority-emergency { border-left: 4px solid #dc3545; }
.priority-stat { border-left: 4px solid #fd7e14; }
.priority-urgent { border-left: 4px solid #ffc107; }
.priority-routine { border-left: 4px solid #28a745; }
.overdue-contact { background-color: #fff3cd; }
.patient-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item active">Waiting List</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-clock text-primary me-2"></i>
Patient Waiting List Management
<small class="text-muted ms-2">Manage appointment waiting list and patient queue</small>
</h1>
<!-- END page-header -->
<!-- BEGIN statistics cards -->
<div class="row mb-4">
<div class="col-xl-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">
<h6 class="card-title mb-1">Total Waiting</h6>
<h3 class="mb-0" id="total-waiting">{{ stats.total }}</h3>
<small class="opacity-75">Active entries</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-users fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<h6 class="card-title mb-1">Urgent Cases</h6>
<h3 class="mb-0" id="urgent-waiting">{{ stats.urgent }}</h3>
<small class="opacity-75">High priority</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<h6 class="card-title mb-1">Contacted</h6>
<h3 class="mb-0" id="contacted-waiting">{{ stats.contacted }}</h3>
<small class="opacity-75">Recently contacted</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-phone fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<h6 class="card-title mb-1">Avg. Wait</h6>
<h3 class="mb-0" id="avg-wait">{{ stats.avg_wait_days }}</h3>
<small class="opacity-75">Days waiting</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-chart-line fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END statistics cards -->
<!-- BEGIN filter panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-filter me-2"></i>Filter Waiting List
</h4>
<div class="panel-heading-btn">
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-collapse">
<i class="fa fa-minus"></i>
</a>
</div>
</div>
<div class="panel-body">
{# <form method="get" class="row g-3">#}
{# <div class="col-md-2">#}
{# {{ filter_form.department.label_tag }}#}
{# {{ filter_form.department }}#}
{# </div>#}
{# <div class="col-md-2">#}
{# {{ filter_form.specialty.label_tag }}#}
{# {{ filter_form.specialty }}#}
{# </div>#}
{# <div class="col-md-2">#}
{# {{ filter_form.priority.label_tag }}#}
{# {{ filter_form.priority }}#}
{# </div>#}
{# <div class="col-md-2">#}
{# {{ filter_form.status.label_tag }}#}
{# {{ filter_form.status }}#}
{# </div>#}
{# <div class="col-md-2">#}
{# {{ filter_form.provider.label_tag }}#}
{# {{ filter_form.provider }}#}
{# </div>#}
{# <div class="col-md-2">#}
{# <label class="form-label">Actions</label>#}
{# <div class="d-flex gap-2">#}
{# <button type="submit" class="btn btn-primary">#}
{# <i class="fas fa-search"></i> Filter#}
{# </button>#}
{# <a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary">#}
{# <i class="fas fa-times"></i> Clear#}
{# </a>#}
{# </div>#}
{# </div>#}
{# </form>#}
</div>
</div>
<!-- END filter panel -->
<!-- BEGIN main panel -->
<div class="panel panel-inverse">
<!-- BEGIN panel-heading -->
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-list me-2"></i>Waiting List Entries
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:waiting_list_create' %}" class="btn btn-primary btn-sm me-2">
<i class="fas fa-plus me-1"></i>Add to Waiting List
</a>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshStats()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<!-- END panel-heading -->
<!-- BEGIN panel-body -->
<div class="panel-body">
<!-- BEGIN bulk actions -->
<div class="row mb-3">
<div class="col-md-12">
<form method="post" action="{% url 'appointments:waiting_list_bulk_action' %}" id="bulk-action-form">
{% csrf_token %}
<div class="d-flex align-items-center gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="select-all">
<label class="form-check-label" for="select-all">
Select All
</label>
</div>
<div class="flex-grow-1">
{{ bulk_action_form.action }}
</div>
<button type="submit" class="btn btn-outline-primary" disabled id="bulk-action-btn">
<i class="fas fa-cogs"></i> Apply Action
</button>
</div>
</form>
</div>
</div>
<!-- END bulk actions -->
<!-- BEGIN table -->
<div class="table-responsive">
<table id="waitingListTable" class="table table-striped table-bordered align-middle">
<thead class="table-dark">
<tr>
<th width="3%">
<input type="checkbox" class="form-check-input" id="header-checkbox">
</th>
<th width="5%">Pos.</th>
<th width="20%">Patient</th>
<th width="12%">Department</th>
<th width="12%">Specialty</th>
<th width="8%">Priority</th>
<th width="8%">Urgency</th>
<th width="10%">Status</th>
<th width="8%">Wait Time</th>
<th width="8%">Last Contact</th>
<th width="6%">Actions</th>
</tr>
</thead>
<tbody>
{% for entry in waiting_list %}
<tr class="{% if entry.priority == 'EMERGENCY' %}priority-emergency{% elif entry.priority == 'STAT' %}priority-stat{% elif entry.priority == 'URGENT' %}priority-urgent{% else %}priority-routine{% endif %} {% if entry.is_overdue_contact %}overdue-contact{% endif %}">
<td>
<input type="checkbox" class="form-check-input entry-checkbox" name="selected_entries" value="{{ entry.id }}">
</td>
<td>
<span class="badge bg-secondary">{{ entry.position|default:"-" }}</span>
</td>
<td>
<div class="d-flex align-items-center">
<div class="patient-avatar bg-primary me-2">
{{ entry.patient.first_name|first }}{{ entry.patient.last_name|first }}
</div>
<div>
<div class="fw-bold">{{ entry.patient.get_full_name }}</div>
<small class="text-muted">
MRN: {{ entry.patient.mrn|default:"N/A" }}
{% if entry.requires_interpreter %}
<i class="fas fa-language text-info ms-1" title="Interpreter needed: {{ entry.interpreter_language }}"></i>
{% endif %}
</small>
</div>
</div>
</td>
<td>
<span class="badge bg-info">{{ entry.department.name }}</span>
</td>
<td>
{{ entry.get_specialty_display }}
</td>
<td>
{% if entry.priority == 'EMERGENCY' %}
<span class="badge bg-danger">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'STAT' %}
<span class="badge bg-warning">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'URGENT' %}
<span class="badge bg-warning">{{ entry.get_priority_display }}</span>
{% else %}
<span class="badge bg-success">{{ entry.get_priority_display }}</span>
{% endif %}
</td>
<td>
<div class="d-flex align-items-center">
<span class="me-1">{{ entry.urgency_score }}</span>
<div class="progress flex-grow-1" style="height: 6px;">
<div class="progress-bar bg-{% if entry.urgency_score >= 8 %}danger{% elif entry.urgency_score >= 6 %}warning{% else %}success{% endif %}"
style="width: {{ entry.urgency_score }}0%"></div>
</div>
</div>
</td>
<td>
{% if entry.status == 'ACTIVE' %}
<span class="badge bg-primary">Active</span>
{% elif entry.status == 'CONTACTED' %}
<span class="badge bg-info">Contacted</span>
{% elif entry.status == 'OFFERED' %}
<span class="badge bg-warning">Offered</span>
{% elif entry.status == 'SCHEDULED' %}
<span class="badge bg-success">Scheduled</span>
{% elif entry.status == 'CANCELLED' %}
<span class="badge bg-danger">Cancelled</span>
{% else %}
<span class="badge bg-secondary">{{ entry.get_status_display }}</span>
{% endif %}
</td>
<td>
<div class="text-center">
<div class="fw-bold">{{ entry.days_waiting }}</div>
<small class="text-muted">days</small>
</div>
</td>
<td>
{% if entry.last_contacted %}
<div class="text-center">
<div class="small">{{ entry.last_contacted|date:"M d" }}</div>
<small class="text-muted">{{ entry.last_contacted|timesince }} ago</small>
</div>
{% else %}
<span class="text-muted">Never</span>
{% endif %}
{% if entry.is_overdue_contact %}
<i class="fas fa-exclamation-triangle text-warning ms-1" title="Contact overdue"></i>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'appointments:waiting_list_detail' entry.pk %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}"
class="btn btn-outline-warning btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-success btn-sm"
onclick="quickContact({{ entry.pk }})" title="Quick Contact">
<i class="fas fa-phone"></i>
</button>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center py-5">
<div class="text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p class="mb-0">No patients currently on waiting list</p>
<a href="{% url 'appointments:waiting_list_create' %}" class="btn btn-primary mt-3">
<i class="fas fa-plus me-1"></i>Add First Patient
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- END table -->
<!-- BEGIN pagination -->
{% if is_paginated %}
{% include 'partial/pagination.html' %}
{% endif %}
<!-- END pagination -->
</div>
<!-- END panel-body -->
</div>
<!-- END main panel -->
<!-- Quick Contact Modal -->
<div class="modal fade" id="quickContactModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Quick Contact</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="quick-contact-form">
{% csrf_token %}
<input type="hidden" id="contact-entry-id" name="entry_id">
<div class="mb-3">
<label class="form-label">Contact Method</label>
<select class="form-select" name="contact_method" required>
<option value="PHONE">Phone Call</option>
<option value="EMAIL">Email</option>
<option value="SMS">SMS</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Contact Outcome</label>
<select class="form-select" name="contact_outcome" required>
<option value="SUCCESSFUL">Successful Contact</option>
<option value="NO_ANSWER">No Answer</option>
<option value="VOICEMAIL">Left Voicemail</option>
<option value="EMAIL_SENT">Email Sent</option>
<option value="WRONG_NUMBER">Wrong Number</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea class="form-control" name="notes" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitQuickContact()">Save Contact</button>
</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/datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons/js/buttons.html5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons/js/buttons.print.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
$('#waitingListTable').DataTable({
responsive: true,
dom: 'Bfrtip',
buttons: [
'copy', 'csv', 'excel', 'pdf', 'print'
],
order: [[5, 'desc'], [6, 'desc'], [8, 'desc']], // Priority, Urgency, Wait time
pageLength: 25,
language: {
search: "Search waiting list:",
lengthMenu: "Show _MENU_ entries per page",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
infoEmpty: "No entries available",
infoFiltered: "(filtered from _MAX_ total entries)"
},
columnDefs: [
{ orderable: false, targets: [0, 10] } // Checkbox and actions columns
]
});
// Select all functionality
$('#select-all, #header-checkbox').change(function() {
const isChecked = $(this).prop('checked');
$('.entry-checkbox').prop('checked', isChecked);
toggleBulkActionButton();
});
// Individual checkbox change
$('.entry-checkbox').change(function() {
toggleBulkActionButton();
// Update select all checkbox
const totalCheckboxes = $('.entry-checkbox').length;
const checkedCheckboxes = $('.entry-checkbox:checked').length;
$('#select-all, #header-checkbox').prop('checked', totalCheckboxes === checkedCheckboxes);
});
// Auto-refresh stats every 30 seconds
setInterval(refreshStats, 30000);
});
function toggleBulkActionButton() {
const checkedCount = $('.entry-checkbox:checked').length;
$('#bulk-action-btn').prop('disabled', checkedCount === 0);
}
function refreshStats() {
$.get('{% url "appointments:waiting_list_stats" %}', function(data) {
$('#total-waiting').text(data.total);
$('#urgent-waiting').text(data.urgent);
$('#contacted-waiting').text(data.contacted);
$('#avg-wait').text(data.avg_wait_days);
});
}
function quickContact(entryId) {
$('#contact-entry-id').val(entryId);
$('#quickContactModal').modal('show');
}
function submitQuickContact() {
const formData = new FormData($('#quick-contact-form')[0]);
const entryId = $('#contact-entry-id').val();
$.ajax({
url: `/appointments/waiting-list/${entryId}/contact/`,
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
$('#quickContactModal').modal('hide');
location.reload(); // Refresh page to show updated data
},
error: function() {
alert('Error saving contact log. Please try again.');
}
});
}
// Bulk action form submission
$('#bulk-action-form').submit(function(e) {
const action = $('select[name="action"]').val();
const selectedCount = $('.entry-checkbox:checked').length;
if (!action) {
e.preventDefault();
alert('Please select an action.');
return;
}
if (selectedCount === 0) {
e.preventDefault();
alert('Please select at least one entry.');
return;
}
const confirmMessage = `Are you sure you want to ${action} ${selectedCount} selected entries?`;
if (!confirm(confirmMessage)) {
e.preventDefault();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Confirm Cancellation{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_list' %}">Waiting List</a></li>
<li class="breadcrumb-item active">Confirm Cancellation</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-trash-alt text-danger me-2"></i>
Confirm Waiting List Entry Cancellation
<small class="text-muted ms-2">Permanently remove this patient from the waiting list</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-exclamation-triangle me-2"></i>Warning: This action cannot be undone!
</h4>
</div>
<div class="panel-body">
<div class="alert alert-danger mb-4">
<h4><i class="fas fa-exclamation-circle me-2"></i>Are you absolutely sure you want to cancel this waiting list entry?</h4>
<p class="mb-0">Cancelling this entry will remove <strong>{{ object.patient.get_full_name }}</strong> from the waiting list for <strong>{{ object.get_specialty_display }}</strong>.</p>
<p class="mb-0">This action is usually taken when the patient no longer requires the appointment, has been scheduled through other means, or has been transferred.</p>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Patient Name:</label>
<p class="form-control-static"><strong>{{ object.patient.get_full_name }}</strong></p>
</div>
<div class="form-group mb-3">
<label class="form-label">Specialty:</label>
<p class="form-control-static">{{ object.get_specialty_display }}</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Priority:</label>
<p class="form-control-static">{{ object.get_priority_display }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label">Days Waiting:</label>
<p class="form-control-static">{{ object.days_waiting }} days</p>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'appointments:waiting_list_detail' object.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash-alt me-1"></i>Confirm Cancellation
</button>
</div>
</form>
</div>
</div>
<!-- END panel -->
{% endblock %}

View File

@ -0,0 +1,427 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Waiting List Entry Details{% endblock %}
{% block extra_css %}
<style>
.detail-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
.detail-section h5 {
color: #007bff;
}
.priority-badge {
font-size: 0.9rem;
padding: 0.4em 0.6em;
}
.status-badge {
font-size: 0.9rem;
padding: 0.4em 0.6em;
}
.contact-log-item {
border-bottom: 1px dashed #eee;
padding-bottom: 10px;
margin-bottom: 10px;
}
.contact-log-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_list' %}">Waiting List</a></li>
<li class="breadcrumb-item active">Entry Details</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-info-circle text-primary me-2"></i>
Waiting List Entry Details
<small class="text-muted ms-2">Comprehensive view of patient waiting list entry</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-tag me-2"></i>Patient: {{ entry.patient.get_full_name }}
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:waiting_list_edit' entry.pk %}" class="btn btn-warning btn-sm me-2">
<i class="fas fa-edit me-1"></i>Edit Entry
</a>
<a href="{% url 'appointments:waiting_list_delete' entry.pk %}" class="btn btn-danger btn-sm me-2">
<i class="fas fa-trash me-1"></i>Delete Entry
</a>
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-8">
<!-- Patient & Service Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-user me-2"></i>Patient & Service Information</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Patient Name:</strong></div>
<div class="col-md-8">{{ entry.patient.get_full_name }} (MRN: {{ entry.patient.mrn|default:'N/A' }})</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Department:</strong></div>
<div class="col-md-8">{{ entry.department.name }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Provider:</strong></div>
<div class="col-md-8">{{ entry.provider.get_full_name|default:'Any' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Appointment Type:</strong></div>
<div class="col-md-8">{{ entry.get_appointment_type_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Medical Specialty:</strong></div>
<div class="col-md-8">{{ entry.get_specialty_display }}</div>
</div>
</div>
<!-- Clinical Priority & Urgency -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-exclamation-triangle me-2"></i>Clinical Priority & Urgency</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Priority Level:</strong></div>
<div class="col-md-8">
{% if entry.priority == 'EMERGENCY' %}
<span class="badge bg-danger priority-badge">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'STAT' %}
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
{% elif entry.priority == 'URGENT' %}
<span class="badge bg-warning priority-badge">{{ entry.get_priority_display }}</span>
{% else %}
<span class="badge bg-success priority-badge">{{ entry.get_priority_display }}</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Urgency Score:</strong></div>
<div class="col-md-8">{{ entry.urgency_score }} / 10</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Clinical Indication:</strong></div>
<div class="col-md-8">{{ entry.clinical_indication|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Diagnosis Codes:</strong></div>
<div class="col-md-8">{{ entry.diagnosis_codes|join:", "|default:'N/A' }}</div>
</div>
</div>
<!-- Patient Scheduling Preferences -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-calendar-alt me-2"></i>Patient Scheduling Preferences</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Date:</strong></div>
<div class="col-md-8">{{ entry.preferred_date|date:"M d, Y"|default:'Any' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Time:</strong></div>
<div class="col-md-8">{{ entry.preferred_time|time:"g:i A"|default:'Any' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Flexible Scheduling:</strong></div>
<div class="col-md-8">{% if entry.flexible_scheduling %}Yes{% else %}No{% endif %}</div>
</div>
{% if entry.flexible_scheduling %}
<div class="row mb-2">
<div class="col-md-4"><strong>Acceptable Date Range:</strong></div>
<div class="col-md-8">
{% if entry.earliest_acceptable_date %}{{ entry.earliest_acceptable_date|date:"M d, Y" }}{% else %}Any{% endif %}
to
{% if entry.latest_acceptable_date %}{{ entry.latest_acceptable_date|date:"M d, Y" }}{% else %}Any{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- Contact Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-phone me-2"></i>Contact Information</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Preferred Method:</strong></div>
<div class="col-md-8">{{ entry.get_contact_method_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Phone:</strong></div>
<div class="col-md-8">{{ entry.contact_phone|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Email:</strong></div>
<div class="col-md-8">{{ entry.contact_email|default:'N/A' }}</div>
</div>
</div>
<!-- Special Requirements -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-universal-access me-2"></i>Special Requirements & Accommodations</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Interpreter Needed:</strong></div>
<div class="col-md-8">{% if entry.requires_interpreter %}Yes ({{ entry.interpreter_language|default:'N/A' }}){% else %}No{% endif %}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Transportation Needed:</strong></div>
<div class="col-md-8">{% if entry.transportation_needed %}Yes{% else %}No{% endif %}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Accessibility:</strong></div>
<div class="col-md-8">{{ entry.accessibility_requirements|default:'None'|linebreaksbr }}</div>
</div>
</div>
<!-- Insurance & Authorization -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-shield-alt me-2"></i>Insurance & Authorization</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Insurance Verified:</strong></div>
<div class="col-md-8">{% if entry.insurance_verified %}Yes{% else %}No{% endif %}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Required:</strong></div>
<div class="col-md-8">{% if entry.authorization_required %}Yes{% else %}No{% endif %}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Status:</strong></div>
<div class="col-md-8">{{ entry.get_authorization_status_display }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Authorization Number:</strong></div>
<div class="col-md-8">{{ entry.authorization_number|default:'N/A' }}</div>
</div>
</div>
<!-- Referral Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-user-md me-2"></i>Referral Information</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Referring Provider:</strong></div>
<div class="col-md-8">{{ entry.referring_provider|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Referral Date:</strong></div>
<div class="col-md-8">{{ entry.referral_date|date:"M d, Y"|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Referral Urgency:</strong></div>
<div class="col-md-8">{{ entry.get_referral_urgency_display }}</div>
</div>
</div>
<!-- Additional Notes -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-sticky-note me-2"></i>Additional Notes</h5>
<div class="row mb-2">
<div class="col-md-12">{{ entry.notes|default:'No additional notes.'|linebreaksbr }}</div>
</div>
</div>
<!-- Outcome Tracking -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-check-circle me-2"></i>Outcome Tracking</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Status:</strong></div>
<div class="col-md-8">
{% if entry.status == 'ACTIVE' %}
<span class="badge bg-primary status-badge">{{ entry.get_status_display }}</span>
{% elif entry.status == 'CONTACTED' %}
<span class="badge bg-info status-badge">{{ entry.get_status_display }}</span>
{% elif entry.status == 'OFFERED' %}
<span class="badge bg-warning status-badge">{{ entry.get_status_display }}</span>
{% elif entry.status == 'SCHEDULED' %}
<span class="badge bg-success status-badge">{{ entry.get_status_display }}</span>
{% elif entry.status == 'CANCELLED' %}
<span class="badge bg-danger status-badge">{{ entry.get_status_display }}</span>
{% else %}
<span class="badge bg-secondary status-badge">{{ entry.get_status_display }}</span>
{% endif %}
</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Scheduled Appointment:</strong></div>
<div class="col-md-8">
{% if entry.scheduled_appointment %}
<a href="{% url 'appointments:appointment_detail' entry.scheduled_appointment.pk %}">
{{ entry.scheduled_appointment.patient.get_full_name }} - {{ entry.scheduled_appointment.get_appointment_type_display }}
</a>
{% else %}
N/A
{% endif %}
</div>
</div>
{% if entry.status == 'CANCELLED' or entry.status == 'EXPIRED' or entry.status == 'TRANSFERRED' %}
<div class="row mb-2">
<div class="col-md-4"><strong>Removal Reason:</strong></div>
<div class="col-md-8">{{ entry.get_removal_reason_display|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Removal Notes:</strong></div>
<div class="col-md-8">{{ entry.removal_notes|default:'None'|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Removed At:</strong></div>
<div class="col-md-8">{{ entry.removed_at|date:"M d, Y H:i"|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Removed By:</strong></div>
<div class="col-md-8">{{ entry.removed_by.get_full_name|default:'N/A' }}</div>
</div>
{% endif %}
</div>
<!-- Metadata -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-database me-2"></i>Metadata</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Entry ID:</strong></div>
<div class="col-md-8">{{ entry.waiting_list_id }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Created At:</strong></div>
<div class="col-md-8">{{ entry.created_at|date:"M d, Y H:i" }} by {{ entry.created_by.get_full_name|default:'N/A' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Last Updated:</strong></div>
<div class="col-md-8">{{ entry.updated_at|date:"M d, Y H:i" }}</div>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Waiting List Metrics -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0"><i class="fas fa-chart-bar me-2"></i>Waiting List Metrics</h5>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Current Position:</strong></div>
<div class="fs-4 text-primary">{{ entry.position|default:'N/A' }}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Days Waiting:</strong></div>
<div class="fs-4 text-info">{{ entry.days_waiting }} days</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Estimated Wait Time:</strong></div>
<div class="fs-4 text-warning">{{ estimated_wait_time }} days</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Contact Attempts:</strong></div>
<div class="fs-4 text-secondary">{{ entry.contact_attempts }}</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div><strong>Overdue Contact:</strong></div>
<div class="fs-4">{% if entry.is_overdue_contact %}<span class="badge bg-danger">Yes</span>{% else %}<span class="badge bg-success">No</span>{% endif %}</div>
</div>
</div>
</div>
<!-- Contact Log -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="card-title mb-0"><i class="fas fa-history me-2"></i>Contact Log</h5>
</div>
<div class="card-body" id="contact-log-container">
{% include 'appointments/partials/contact_log_list.html' %}
</div>
<div class="card-footer">
<button type="button" class="btn btn-success btn-sm" data-bs-toggle="modal" data-bs-target="#addContactLogModal">
<i class="fas fa-plus me-1"></i>Add Contact Log
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END panel -->
<!-- Add Contact Log Modal -->
<div class="modal fade" id="addContactLogModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Contact Log for {{ entry.patient.get_full_name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="{% url 'appointments:add_contact_log' entry.pk %}" hx-post="{% url 'appointments:add_contact_log' entry.pk %}" hx-target="#contact-log-container" hx-swap="innerHTML">
{% csrf_token %}
<div class="modal-body">
{% for field in contact_form %}
<div class="mb-3">
<label class="form-label">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text }}</small>{% endif %}
{% for error in field.errors %}
<div class="invalid-feedback d-block">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Contact Log</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize date picker for offered_date and next_contact_date
$("#addContactLogModal input[type='date']").datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true,
startDate: 'today'
});
// Toggle offered date/time and patient response based on appointment_offered checkbox
$("input[name='appointment_offered']").change(function() {
const isChecked = $(this).is(':checked');
$("input[name='offered_date']").prop('required', isChecked);
$("input[name='offered_time']").prop('required', isChecked);
$("select[name='patient_response']").prop('required', isChecked);
}).trigger('change'); // Trigger on load for initial state
// Handle HTMX after swap to re-initialize datepickers if needed
document.body.addEventListener('htmx:afterSwap', function(event) {
if (event.detail.target.id === 'contact-log-container') {
$("#addContactLogModal input[type='date']").datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true,
startDate: 'today'
});
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,561 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{% if object %}Edit{% else %}Add{% endif %} Waiting List Entry{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<style>
.required-field::after {
content: " *";
color: #dc3545;
}
.form-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
.priority-indicator {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:waiting_list' %}">Waiting List</a></li>
<li class="breadcrumb-item active">{% if object %}Edit Entry{% else %}Add Entry{% endif %}</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-{% if object %}edit{% else %}plus{% endif %} text-primary me-2"></i>
{% if object %}Edit Waiting List Entry{% else %}Add Patient to Waiting List{% endif %}
<small class="text-muted ms-2">{% if object %}Update patient information{% else %}Register new waiting list entry{% endif %}</small>
</h1>
<!-- END page-header -->
<!-- BEGIN form panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-plus me-2"></i>Patient Information & Request Details
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
<div class="panel-body">
<form method="post" id="waiting-list-form" novalidate>
{% csrf_token %}
<!-- Patient & Service Information -->
<div class="form-section">
<h5 class="text-primary mb-3">
<i class="fas fa-user me-2"></i>Patient & Service Information
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Patient</label>
{{ form.patient }}
{% if form.patient.errors %}
<div class="invalid-feedback d-block">{{ form.patient.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Department</label>
{{ form.department }}
{% if form.department.errors %}
<div class="invalid-feedback d-block">{{ form.department.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Preferred Provider</label>
{{ form.provider }}
<small class="form-text text-muted">Leave blank for any available provider</small>
{% if form.provider.errors %}
<div class="invalid-feedback d-block">{{ form.provider.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Appointment Type</label>
{{ form.appointment_type }}
{% if form.appointment_type.errors %}
<div class="invalid-feedback d-block">{{ form.appointment_type.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label class="form-label required-field">Medical Specialty</label>
{{ form.specialty }}
{% if form.specialty.errors %}
<div class="invalid-feedback d-block">{{ form.specialty.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Clinical Priority -->
<div class="form-section">
<h5 class="text-warning mb-3">
<i class="fas fa-exclamation-triangle me-2"></i>Clinical Priority & Urgency
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Priority Level</label>
{{ form.priority }}
<div class="mt-2">
<div class="priority-indicator bg-success text-white" id="priority-indicator">
Select priority level
</div>
</div>
{% if form.priority.errors %}
<div class="invalid-feedback d-block">{{ form.priority.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Urgency Score (1-10)</label>
{{ form.urgency_score }}
<div class="progress mt-2" style="height: 8px;">
<div class="progress-bar bg-success" id="urgency-progress" style="width: 10%"></div>
</div>
<small class="form-text text-muted">1 = Routine, 10 = Most Urgent</small>
{% if form.urgency_score.errors %}
<div class="invalid-feedback d-block">{{ form.urgency_score.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label class="form-label required-field">Clinical Indication</label>
{{ form.clinical_indication }}
{% if form.clinical_indication.errors %}
<div class="invalid-feedback d-block">{{ form.clinical_indication.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label class="form-label">ICD-10 Diagnosis Codes</label>
{{ form.diagnosis_codes }}
<small class="form-text text-muted">Enter diagnosis codes separated by commas (e.g., M25.511, Z51.11)</small>
{% if form.diagnosis_codes.errors %}
<div class="invalid-feedback d-block">{{ form.diagnosis_codes.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Patient Preferences -->
<div class="form-section">
<h5 class="text-info mb-3">
<i class="fas fa-calendar-alt me-2"></i>Patient Scheduling Preferences
</h5>
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Preferred Date</label>
{{ form.preferred_date }}
{% if form.preferred_date.errors %}
<div class="invalid-feedback d-block">{{ form.preferred_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Preferred Time</label>
{{ form.preferred_time }}
{% if form.preferred_time.errors %}
<div class="invalid-feedback d-block">{{ form.preferred_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<div class="form-check mt-4">
{{ form.flexible_scheduling }}
<label class="form-check-label" for="{{ form.flexible_scheduling.id_for_label }}">
Flexible scheduling (accepts alternative times)
</label>
</div>
</div>
</div>
</div>
<div class="row" id="flexible-options" style="display: none;">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Earliest Acceptable Date</label>
{{ form.earliest_acceptable_date }}
{% if form.earliest_acceptable_date.errors %}
<div class="invalid-feedback d-block">{{ form.earliest_acceptable_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Latest Acceptable Date</label>
{{ form.latest_acceptable_date }}
{% if form.latest_acceptable_date.errors %}
<div class="invalid-feedback d-block">{{ form.latest_acceptable_date.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="form-section">
<h5 class="text-success mb-3">
<i class="fas fa-phone me-2"></i>Contact Information
</h5>
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label class="form-label required-field">Preferred Contact Method</label>
{{ form.contact_method }}
{% if form.contact_method.errors %}
<div class="invalid-feedback d-block">{{ form.contact_method.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Contact Phone</label>
{{ form.contact_phone }}
{% if form.contact_phone.errors %}
<div class="invalid-feedback d-block">{{ form.contact_phone.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label class="form-label">Contact Email</label>
{{ form.contact_email }}
{% if form.contact_email.errors %}
<div class="invalid-feedback d-block">{{ form.contact_email.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Special Requirements -->
<div class="form-section">
<h5 class="text-secondary mb-3">
<i class="fas fa-universal-access me-2"></i>Special Requirements & Accommodations
</h5>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.requires_interpreter }}
<label class="form-check-label" for="{{ form.requires_interpreter.id_for_label }}">
Interpreter services required
</label>
</div>
<div class="mb-3" id="interpreter-language" style="display: none;">
<label class="form-label">Interpreter Language</label>
{{ form.interpreter_language }}
{% if form.interpreter_language.errors %}
<div class="invalid-feedback d-block">{{ form.interpreter_language.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.transportation_needed }}
<label class="form-check-label" for="{{ form.transportation_needed.id_for_label }}">
Transportation assistance needed
</label>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label class="form-label">Accessibility Requirements</label>
{{ form.accessibility_requirements }}
<small class="form-text text-muted">Describe any special accessibility needs (wheelchair access, hearing assistance, etc.)</small>
{% if form.accessibility_requirements.errors %}
<div class="invalid-feedback d-block">{{ form.accessibility_requirements.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Insurance & Authorization -->
<div class="form-section">
<h5 class="text-warning mb-3">
<i class="fas fa-shield-alt me-2"></i>Insurance & Authorization
</h5>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.insurance_verified }}
<label class="form-check-label" for="{{ form.insurance_verified.id_for_label }}">
Insurance coverage verified
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.authorization_required }}
<label class="form-check-label" for="{{ form.authorization_required.id_for_label }}">
Prior authorization required
</label>
</div>
</div>
</div>
</div>
<!-- Referral Information -->
<div class="form-section">
<h5 class="text-info mb-3">
<i class="fas fa-user-md me-2"></i>Referral Information
</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Referring Provider</label>
{{ form.referring_provider }}
{% if form.referring_provider.errors %}
<div class="invalid-feedback d-block">{{ form.referring_provider.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Referral Date</label>
{{ form.referral_date }}
{% if form.referral_date.errors %}
<div class="invalid-feedback d-block">{{ form.referral_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label class="form-label">Referral Urgency</label>
{{ form.referral_urgency }}
{% if form.referral_urgency.errors %}
<div class="invalid-feedback d-block">{{ form.referral_urgency.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Additional Notes -->
<div class="form-section">
<h5 class="text-secondary mb-3">
<i class="fas fa-sticky-note me-2"></i>Additional Notes
</h5>
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label class="form-label">Notes & Comments</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="invalid-feedback d-block">{{ form.notes.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between">
<a href="{% url 'appointments:waiting_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>
{% if object %}Update Entry{% else %}Add to Waiting List{% endif %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- END form panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2 for dropdowns
$('.form-select').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Initialize date picker
$('input[type="date"]').datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true,
startDate: 'today'
});
// Priority indicator update
$('select[name="priority"]').change(function() {
const priority = $(this).val();
const indicator = $('#priority-indicator');
indicator.removeClass('bg-success bg-warning bg-danger bg-dark');
switch(priority) {
case 'EMERGENCY':
indicator.addClass('bg-danger').text('EMERGENCY - Immediate attention required');
break;
case 'STAT':
indicator.addClass('bg-danger').text('STAT - Within 24 hours');
break;
case 'URGENT':
indicator.addClass('bg-warning').text('URGENT - Within 1 week');
break;
case 'ROUTINE':
indicator.addClass('bg-success').text('ROUTINE - Standard scheduling');
break;
default:
indicator.addClass('bg-secondary').text('Select priority level');
}
});
// Urgency score progress bar
$('input[name="urgency_score"]').on('input', function() {
const score = parseInt($(this).val()) || 1;
const percentage = score * 10;
const progressBar = $('#urgency-progress');
progressBar.css('width', percentage + '%');
if (score >= 8) {
progressBar.removeClass('bg-success bg-warning').addClass('bg-danger');
} else if (score >= 6) {
progressBar.removeClass('bg-success bg-danger').addClass('bg-warning');
} else {
progressBar.removeClass('bg-warning bg-danger').addClass('bg-success');
}
});
// Flexible scheduling toggle
$('input[name="flexible_scheduling"]').change(function() {
if ($(this).is(':checked')) {
$('#flexible-options').show();
} else {
$('#flexible-options').hide();
}
});
// Interpreter language toggle
$('input[name="requires_interpreter"]').change(function() {
if ($(this).is(':checked')) {
$('#interpreter-language').show();
} else {
$('#interpreter-language').hide();
}
});
// Contact method validation
$('select[name="contact_method"]').change(function() {
const method = $(this).val();
const phoneField = $('input[name="contact_phone"]');
const emailField = $('input[name="contact_email"]');
// Reset required attributes
phoneField.removeAttr('required');
emailField.removeAttr('required');
// Set required based on contact method
if (method === 'PHONE' || method === 'SMS') {
phoneField.attr('required', true);
} else if (method === 'EMAIL') {
emailField.attr('required', true);
}
});
// Form validation
$('#waiting-list-form').submit(function(e) {
let isValid = true;
const requiredFields = $(this).find('[required]');
requiredFields.each(function() {
if (!$(this).val()) {
$(this).addClass('is-invalid');
isValid = false;
} else {
$(this).removeClass('is-invalid');
}
});
if (!isValid) {
e.preventDefault();
alert('Please fill in all required fields.');
}
});
// Initialize form state
$('select[name="priority"]').trigger('change');
$('input[name="urgency_score"]').trigger('input');
$('input[name="flexible_scheduling"]').trigger('change');
$('input[name="requires_interpreter"]').trigger('change');
$('select[name="contact_method"]').trigger('change');
});
</script>
{% endblock %}

View File

@ -0,0 +1,299 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Waiting List Management{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item active">Waiting List</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-clock text-primary me-2"></i>
Waiting List Management
<small class="text-muted ms-2">Manage patient waiting list entries</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<!-- BEGIN panel-heading -->
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-list me-2"></i>Waiting List Entries
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:waitinglist:waitinglistentry-create' %}" class="btn btn-primary btn-sm">
<i class="fas fa-plus me-1"></i>Add to Waiting List
</a>
</div>
</div>
<!-- END panel-heading -->
<!-- BEGIN panel-body -->
<div class="panel-body">
<!-- BEGIN stats cards -->
<div class="row mb-4">
<div class="col-xl-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">
<h5 class="card-title mb-1">Total Waiting</h5>
<h3 class="mb-0">{{ waitinglistentry_list|length }}</h3>
</div>
<div class="flex-shrink-0">
<i class="fas fa-clock fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<h5 class="card-title mb-1">Pending</h5>
<h3 class="mb-0">{{ waitinglistentry_list|length }}</h3>
</div>
<div class="flex-shrink-0">
<i class="fas fa-hourglass-half fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<h5 class="card-title mb-1">Priority</h5>
<h3 class="mb-0">0</h3>
</div>
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-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">
<h5 class="card-title mb-1">Avg. Wait Time</h5>
<h3 class="mb-0">5 days</h3>
</div>
<div class="flex-shrink-0">
<i class="fas fa-chart-line fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END stats cards -->
<!-- BEGIN filter section -->
<div class="row mb-3">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<form method="get" class="row g-3">
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="">All Status</option>
<option value="pending">Pending</option>
<option value="contacted">Contacted</option>
<option value="scheduled">Scheduled</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Appointment Type</label>
<select name="appointment_type" class="form-select">
<option value="">All Types</option>
<!-- Add appointment types dynamically -->
</select>
</div>
<div class="col-md-3">
<label class="form-label">Date Range</label>
<input type="date" name="date_from" class="form-control" placeholder="From">
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<input type="date" name="date_to" class="form-control" placeholder="To">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- END filter section -->
<!-- BEGIN table -->
<div class="table-responsive">
<table id="waitingListTable" class="table table-striped table-bordered align-middle">
<thead class="table-dark">
<tr>
<th width="5%">#</th>
<th width="20%">Patient</th>
<th width="15%">Appointment Type</th>
<th width="12%">Desired Date</th>
<th width="10%">Desired Time</th>
<th width="10%">Status</th>
<th width="10%">Wait Time</th>
<th width="8%">Priority</th>
<th width="10%">Actions</th>
</tr>
</thead>
<tbody>
{% for entry in waitinglistentry_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar avatar-sm me-2">
<div class="avatar-initial bg-primary rounded-circle">
{{ entry.patient.first_name|first }}{{ entry.patient.last_name|first }}
</div>
</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>
<span class="badge bg-info">{{ entry.appointment_type.name }}</span>
</td>
<td>
{% if entry.desired_date %}
{{ entry.desired_date|date:"M d, Y" }}
{% else %}
<span class="text-muted">Any date</span>
{% endif %}
</td>
<td>
{% if entry.desired_time %}
{{ entry.desired_time|time:"g:i A" }}
{% else %}
<span class="text-muted">Any time</span>
{% endif %}
</td>
<td>
{% if entry.status == 'pending' %}
<span class="badge bg-warning">Pending</span>
{% elif entry.status == 'contacted' %}
<span class="badge bg-info">Contacted</span>
{% elif entry.status == 'scheduled' %}
<span class="badge bg-success">Scheduled</span>
{% elif entry.status == 'cancelled' %}
<span class="badge bg-danger">Cancelled</span>
{% else %}
<span class="badge bg-secondary">{{ entry.status|title }}</span>
{% endif %}
</td>
<td>
<span class="text-muted">{{ entry.created_at|timesince }}</span>
</td>
<td>
<span class="badge bg-secondary">Normal</span>
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'appointments:waitinglist:waitinglistentry-detail' entry.pk %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'appointments:waitinglist:waitinglistentry-update' entry.pk %}"
class="btn btn-outline-warning btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'appointments:waitinglist:waitinglistentry-delete' entry.pk %}"
class="btn btn-outline-danger btn-sm" title="Delete">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center py-4">
<div class="text-muted">
<i class="fas fa-inbox fa-3x mb-3"></i>
<p class="mb-0">No waiting list entries found</p>
<a href="{% url 'appointments:waitinglist:waitinglistentry-create' %}" class="btn btn-primary mt-2">
<i class="fas fa-plus me-1"></i>Add First Entry
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- END table -->
</div>
<!-- END panel-body -->
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons/js/buttons.html5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons/js/buttons.print.min.js' %}"></script>
<script>
$(document).ready(function() {
$('#waitingListTable').DataTable({
responsive: true,
dom: 'Bfrtip',
buttons: [
'copy', 'csv', 'excel', 'pdf', 'print'
],
order: [[6, 'desc']], // Order by wait time (created_at) descending
pageLength: 25,
language: {
search: "Search waiting list:",
lengthMenu: "Show _MENU_ entries per page",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
infoEmpty: "No entries available",
infoFiltered: "(filtered from _MAX_ total entries)"
}
});
// Auto-refresh every 30 seconds
setInterval(function() {
$('#waitingListTable').DataTable().ajax.reload(null, false);
}, 30000);
});
</script>
{% endblock %}

View File

@ -4,8 +4,8 @@
{% block title %}Waitlist Management{% endblock %} {% block title %}Waitlist Management{% endblock %}
{% block css %} {% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" /> <link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" /> <link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -163,15 +163,6 @@
<h4 class="card-title">Current Waitlist</h4> <h4 class="card-title">Current Waitlist</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<div class="table-responsive"> <div class="table-responsive">
<table id="waitlistTable" class="table table-striped table-bordered align-middle"> <table id="waitlistTable" class="table table-striped table-bordered align-middle">
<thead> <thead>
@ -405,10 +396,10 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script> <script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script> <script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script> <script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script> <script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function() {

View File

@ -65,6 +65,15 @@ urlpatterns = [
path('templates/<int:pk>/delete/', views.AppointmentTemplateDeleteView.as_view(), name='appointment_template_delete'), path('templates/<int:pk>/delete/', views.AppointmentTemplateDeleteView.as_view(), name='appointment_template_delete'),
# path('get_doctor_schedule/', views.get_provider_schedule, name='get_doctor_schedule') # path('get_doctor_schedule/', views.get_provider_schedule, name='get_doctor_schedule')
# Waiting list management
path('waiting-list/', views.WaitingListView.as_view(), name='waiting_list'),
path('waiting-list/create/', views.WaitingListCreateView.as_view(), name='waiting_list_create'),
path('waiting-list/<int:pk>/', views.WaitingListDetailView.as_view(), name='waiting_list_detail'),
path('waiting-list/<int:pk>/edit/', views.WaitingListUpdateView.as_view(), name='waiting_list_edit'),
path('waiting-list/<int:pk>/delete/', views.WaitingListDeleteView.as_view(), name='waiting_list_delete'),
path('waiting-list/<int:pk>/contact/', views.add_contact_log, name='add_contact_log'),
path('waiting-list/bulk-action/', views.waiting_list_bulk_action, name='waiting_list_bulk_action'),
path('waiting-list/stats/', views.waiting_list_stats, name='waiting_list_stats'),
# API endpoints # API endpoints
# path('api/', include('appointments.api.urls')), # path('api/', include('appointments.api.urls')),

View File

@ -18,15 +18,8 @@ from django.core.paginator import Paginator
from datetime import timedelta, datetime, time, date from datetime import timedelta, datetime, time, date
from hr.models import Schedule, Employee from hr.models import Schedule, Employee
from .models import ( from .models import *
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry, from .forms import *
TelemedicineSession, AppointmentTemplate
)
from .forms import (
AppointmentRequestForm, SlotAvailabilityForm, WaitingQueueForm,
QueueEntryForm, TelemedicineSessionForm, AppointmentTemplateForm,
AppointmentSearchForm, QueueSearchForm, SlotSearchForm
)
from patients.models import PatientProfile from patients.models import PatientProfile
from accounts.models import User from accounts.models import User
from core.utils import AuditLogger from core.utils import AuditLogger
@ -1216,7 +1209,349 @@ class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
return super().delete(request, *args, **kwargs) return super().delete(request, *args, **kwargs)
class WaitingListView(LoginRequiredMixin, ListView):
"""
List view for waiting list entries.
"""
model = WaitingList
template_name = 'appointments/waiting_list/waiting_list.html'
context_object_name = 'waiting_list'
paginate_by = 25
def get_queryset(self):
tenant = self.request.user.tenant
queryset = WaitingList.objects.filter(
tenant=tenant
).select_related(
'patient', 'department', 'provider', 'scheduled_appointment'
).order_by('priority', 'urgency_score', 'created_at')
# Apply filters
form = WaitingListFilterForm(
self.request.GET,
tenant=tenant
)
if form.is_valid():
if form.cleaned_data.get('department'):
queryset = queryset.filter(department=form.cleaned_data['department'])
if form.cleaned_data.get('specialty'):
queryset = queryset.filter(specialty=form.cleaned_data['specialty'])
if form.cleaned_data.get('priority'):
queryset = queryset.filter(priority=form.cleaned_data['priority'])
if form.cleaned_data.get('status'):
queryset = queryset.filter(status=form.cleaned_data['status'])
if form.cleaned_data.get('provider'):
queryset = queryset.filter(provider=form.cleaned_data['provider'])
if form.cleaned_data.get('date_from'):
queryset = queryset.filter(created_at__date__gte=form.cleaned_data['date_from'])
if form.cleaned_data.get('date_to'):
queryset = queryset.filter(created_at__date__lte=form.cleaned_data['date_to'])
if form.cleaned_data.get('urgency_min'):
queryset = queryset.filter(urgency_score__gte=form.cleaned_data['urgency_min'])
if form.cleaned_data.get('urgency_max'):
queryset = queryset.filter(urgency_score__lte=form.cleaned_data['urgency_max'])
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['filter_form'] = WaitingListFilterForm
context['bulk_action_form'] = WaitingListBulkActionForm
# Statistics
waiting_list = self.get_queryset()
context['stats'] = {
'total': waiting_list.count(),
'active': waiting_list.filter(status='ACTIVE').count(),
'contacted': waiting_list.filter(status='CONTACTED').count(),
'urgent': waiting_list.filter(priority__in=['URGENT', 'STAT', 'EMERGENCY']).count(),
# 'avg_wait_days': waiting_list.aggregate(
# avg_days=Avg('created_at')
# )['avg_days'] or 0,
}
return context
class WaitingListDetailView(LoginRequiredMixin, DetailView):
"""
Detail view for waiting list entry.
"""
model = WaitingList
template_name = 'appointments/waiting_list/waiting_list_detail.html'
context_object_name = 'entry'
def get_queryset(self):
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'current_tenant', None)
).select_related(
'patient', 'department', 'provider', 'scheduled_appointment',
'created_by', 'removed_by'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Get contact logs
context['contact_logs'] = self.object.contact_logs.all().order_by('-contact_date')
# Contact log form
context['contact_form'] = WaitingListContactLogForm()
# Calculate position and wait time
self.object.update_position()
context['estimated_wait_time'] = self.object.estimate_wait_time()
return context
class WaitingListCreateView(LoginRequiredMixin, CreateView):
"""
Create view for waiting list entry.
"""
model = WaitingList
form_class = WaitingListForm
template_name = 'appointments/waiting_list/waiting_list_form.html'
success_url = reverse_lazy('appointments:waiting_list')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request.user, 'current_tenant', None)
return kwargs
def form_valid(self, form):
form.instance.tenant = getattr(self.request.user, 'current_tenant', None)
form.instance.created_by = self.request.user
# Calculate initial position and estimated wait time
response = super().form_valid(form)
self.object.update_position()
self.object.estimated_wait_time = self.object.estimate_wait_time()
self.object.save(update_fields=['position', 'estimated_wait_time'])
messages.success(
self.request,
f"Patient {self.object.patient.get_full_name()} has been added to the waiting list."
)
return response
class WaitingListUpdateView(LoginRequiredMixin, UpdateView):
"""
Update view for waiting list entry.
"""
model = WaitingList
form_class = WaitingListForm
template_name = 'appointments/waiting_list/waiting_list_form.html'
def get_queryset(self):
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'current_tenant', None)
)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request.user, 'current_tenant', None)
return kwargs
def get_success_url(self):
return reverse('appointments:waiting_list_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
# Update position if priority or urgency changed
old_priority = WaitingList.objects.get(pk=self.object.pk).priority
old_urgency = WaitingList.objects.get(pk=self.object.pk).urgency_score
response = super().form_valid(form)
if (form.instance.priority != old_priority or
form.instance.urgency_score != old_urgency):
self.object.update_position()
self.object.estimated_wait_time = self.object.estimate_wait_time()
self.object.save(update_fields=['position', 'estimated_wait_time'])
messages.success(self.request, "Waiting list entry has been updated.")
return response
class WaitingListDeleteView(LoginRequiredMixin, DeleteView):
"""
Delete view for waiting list entry.
"""
model = WaitingList
template_name = 'appointments/waiting_list/waiting_list_confirm_delete.html'
success_url = reverse_lazy('appointments:waiting_list')
def get_queryset(self):
return WaitingList.objects.filter(
tenant=getattr(self.request.user, 'current_tenant', None)
)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
patient_name = self.object.patient.get_full_name()
# Mark as removed instead of deleting
self.object.status = 'CANCELLED'
self.object.removal_reason = 'PROVIDER_CANCELLED'
self.object.removed_at = timezone.now()
self.object.removed_by = request.user
self.object.save()
messages.success(
request,
f"Waiting list entry for {patient_name} has been cancelled."
)
return redirect(self.success_url)
@login_required
def add_contact_log(request, pk):
"""
Add contact log entry for waiting list.
"""
entry = get_object_or_404(
WaitingList,
pk=pk,
tenant=getattr(request.user, 'current_tenant', None)
)
if request.method == 'POST':
form = WaitingListContactLogForm(request.POST)
if form.is_valid():
contact_log = form.save(commit=False)
contact_log.waiting_list_entry = entry
contact_log.contacted_by = request.user
contact_log.save()
# Update waiting list entry
entry.last_contacted = timezone.now()
entry.contact_attempts += 1
if contact_log.appointment_offered:
entry.appointments_offered += 1
entry.last_offer_date = timezone.now()
if contact_log.patient_response == 'DECLINED':
entry.appointments_declined += 1
elif contact_log.patient_response == 'ACCEPTED':
entry.status = 'SCHEDULED'
if contact_log.contact_outcome == 'SUCCESSFUL':
entry.status = 'CONTACTED'
entry.save()
messages.success(request, "Contact log has been added.")
if request.headers.get('HX-Request'):
return render(request, 'appointments/partials/contact_log_list.html', {
'contact_logs': entry.contact_logs.all().order_by('-contact_date')
})
else:
messages.error(request, "Please correct the errors below.")
return redirect('appointments:waiting_list_detail', pk=pk)
@login_required
def waiting_list_bulk_action(request):
"""
Handle bulk actions on waiting list entries.
"""
if request.method == 'POST':
form = WaitingListBulkActionForm(
request.POST,
tenant=getattr(request.user, 'current_tenant', None)
)
if form.is_valid():
action = form.cleaned_data['action']
entry_ids = request.POST.getlist('selected_entries')
if not entry_ids:
messages.error(request, "No entries selected.")
return redirect('appointments:waiting_list')
entries = WaitingList.objects.filter(
id__in=entry_ids,
tenant=getattr(request.user, 'current_tenant', None)
)
if action == 'contact':
entries.update(
status='CONTACTED',
last_contacted=timezone.now(),
contact_attempts=F('contact_attempts') + 1
)
messages.success(request, f"{entries.count()} entries marked as contacted.")
elif action == 'cancel':
entries.update(
status='CANCELLED',
removal_reason='PROVIDER_CANCELLED',
removed_at=timezone.now(),
removed_by=request.user
)
messages.success(request, f"{entries.count()} entries cancelled.")
elif action == 'update_priority':
new_priority = form.cleaned_data.get('new_priority')
if new_priority:
entries.update(priority=new_priority)
# Update positions for affected entries
for entry in entries:
entry.update_position()
messages.success(request, f"{entries.count()} entries priority updated.")
elif action == 'transfer_provider':
transfer_provider = form.cleaned_data.get('transfer_provider')
if transfer_provider:
entries.update(provider=transfer_provider)
messages.success(request, f"{entries.count()} entries transferred.")
elif action == 'export':
# Export functionality would be implemented here
messages.info(request, "Export functionality coming soon.")
return redirect('appointments:waiting_list')
@login_required
def waiting_list_stats(request):
"""
HTMX endpoint for waiting list statistics.
"""
tenant = getattr(request.user, 'current_tenant', None)
if not tenant:
return JsonResponse({'error': 'No tenant'})
waiting_list = WaitingList.objects.filter(tenant=tenant)
stats = {
'total': waiting_list.count(),
'active': waiting_list.filter(status='ACTIVE').count(),
'contacted': waiting_list.filter(status='CONTACTED').count(),
'scheduled': waiting_list.filter(status='SCHEDULED').count(),
'urgent': waiting_list.filter(priority__in=['URGENT', 'STAT', 'EMERGENCY']).count(),
'overdue_contact': sum(1 for entry in waiting_list.filter(status='ACTIVE') if entry.is_overdue_contact),
'avg_wait_days': int(waiting_list.aggregate(
avg_days=Avg(timezone.now().date() - F('created_at__date'))
)['avg_days'] or 0),
}
return JsonResponse(stats)
@login_required @login_required

Binary file not shown.

View File

@ -213761,3 +213761,434 @@ INFO 2025-09-11 18:59:16,679 basehttp 3788 6219984896 "GET /en/htmx/system-notif
INFO 2025-09-11 18:59:37,618 basehttp 3788 6219984896 "GET /en/appointments/queue/create/ HTTP/1.1" 200 36012 INFO 2025-09-11 18:59:37,618 basehttp 3788 6219984896 "GET /en/appointments/queue/create/ HTTP/1.1" 200 36012
INFO 2025-09-11 18:59:37,651 basehttp 3788 6219984896 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675 INFO 2025-09-11 18:59:37,651 basehttp 3788 6219984896 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:00:37,663 basehttp 3788 6219984896 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675 INFO 2025-09-11 19:00:37,663 basehttp 3788 6219984896 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:03:46,536 autoreload 61052 8682856640 Watching for file changes with StatReloader
INFO 2025-09-11 19:03:49,201 basehttp 61052 6164623360 "GET /en/appointments/queue/create/ HTTP/1.1" 200 36062
INFO 2025-09-11 19:03:49,239 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:04:30,138 basehttp 61052 6164623360 "GET /en/appointments/queue/create/ HTTP/1.1" 200 36075
INFO 2025-09-11 19:04:30,179 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:04:50,242 basehttp 61052 6164623360 "GET /en/appointments/queue/create/ HTTP/1.1" 200 36084
INFO 2025-09-11 19:04:50,276 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:05:05,242 basehttp 61052 6164623360 "GET /en/appointments/queue/create/ HTTP/1.1" 200 36065
INFO 2025-09-11 19:05:05,275 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:06:05,275 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:06:22,958 basehttp 61052 6164623360 "GET /en/appointments/queue/create/ HTTP/1.1" 200 35453
INFO 2025-09-11 19:06:22,989 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:07:23,000 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:08:23,255 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:09:23,607 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:10:24,600 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:11:25,587 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:12:26,597 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:13:28,597 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:15:24,606 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:17:24,607 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 19:18:24,610 basehttp 61052 6164623360 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:04:09,816 autoreload 88212 8682856640 Watching for file changes with StatReloader
INFO 2025-09-11 20:04:12,613 basehttp 88212 6196752384 "GET /en/appointments/ HTTP/1.1" 200 57564
INFO 2025-09-11 20:04:12,667 basehttp 88212 6196752384 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:04:12,681 basehttp 88212 6213578752 "GET /en/appointments/stats/ HTTP/1.1" 200 3132
WARNING 2025-09-11 20:04:24,458 log 88212 6213578752 Not Found: /en/appointments/waitinglist
WARNING 2025-09-11 20:04:24,458 basehttp 88212 6213578752 "GET /en/appointments/waitinglist HTTP/1.1" 404 43667
INFO 2025-09-11 20:04:31,741 basehttp 88212 6213578752 "GET /en/appointments/waiting-list HTTP/1.1" 301 0
ERROR 2025-09-11 20:04:31,766 log 88212 6196752384 Internal Server Error: /en/appointments/waiting-list/
Traceback (most recent call last):
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/contrib/auth/mixins.py", line 73, in dispatch
return super().dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 144, in dispatch
return handler(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/list.py", line 178, in get
context = self.get_context_data()
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py", line 1281, in get_context_data
'avg_wait_days': waiting_list.aggregate(
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 588, in aggregate
return self.query.chain().get_aggregation(self.db, kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 626, in get_aggregation
result = compiler.execute_sql(SINGLE)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1610, in execute_sql
sql, params = self.as_sql()
^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 766, in as_sql
extra_select, order_by, group_by = self.pre_sql_setup(
^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 85, in pre_sql_setup
self.setup_query(with_col_aliases=with_col_aliases)
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 74, in setup_query
self.select, self.klass_info, self.annotation_col_map = self.get_select(
^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 316, in get_select
sql, params = self.compile(col)
^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 575, in compile
sql, params = vendor_impl(self, self.connection)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/expressions.py", line 29, in as_sqlite
sql, params = self.as_sql(compiler, connection, **extra_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/aggregates.py", line 141, in as_sql
return super().as_sql(compiler, connection, **extra_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/expressions.py", line 1100, in as_sql
connection.ops.check_expression_support(self)
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/backends/sqlite3/operations.py", line 69, in check_expression_support
raise NotSupportedError(
django.db.utils.NotSupportedError: You cannot use Sum, Avg, StdDev, and Variance aggregations on date/time fields in sqlite3 since date/time is saved as text.
ERROR 2025-09-11 20:04:31,768 basehttp 88212 6196752384 "GET /en/appointments/waiting-list/ HTTP/1.1" 500 147337
INFO 2025-09-11 20:06:56,048 autoreload 88212 8682856640 /Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py changed, reloading.
INFO 2025-09-11 20:06:56,432 autoreload 89466 8682856640 Watching for file changes with StatReloader
ERROR 2025-09-11 20:06:56,918 log 89466 6170865664 Internal Server Error: /en/appointments/waiting-list/
Traceback (most recent call last):
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/contrib/auth/mixins.py", line 73, in dispatch
return super().dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 144, in dispatch
return handler(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/list.py", line 178, in get
context = self.get_context_data()
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py", line 1281, in get_context_data
'avg_wait_days': waiting_list.aggregate(
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 588, in aggregate
return self.query.chain().get_aggregation(self.db, kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 626, in get_aggregation
result = compiler.execute_sql(SINGLE)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1610, in execute_sql
sql, params = self.as_sql()
^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 766, in as_sql
extra_select, order_by, group_by = self.pre_sql_setup(
^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 85, in pre_sql_setup
self.setup_query(with_col_aliases=with_col_aliases)
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 74, in setup_query
self.select, self.klass_info, self.annotation_col_map = self.get_select(
^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 316, in get_select
sql, params = self.compile(col)
^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 575, in compile
sql, params = vendor_impl(self, self.connection)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/expressions.py", line 29, in as_sqlite
sql, params = self.as_sql(compiler, connection, **extra_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/aggregates.py", line 141, in as_sql
return super().as_sql(compiler, connection, **extra_context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/expressions.py", line 1100, in as_sql
connection.ops.check_expression_support(self)
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/backends/sqlite3/operations.py", line 69, in check_expression_support
raise NotSupportedError(
django.db.utils.NotSupportedError: You cannot use Sum, Avg, StdDev, and Variance aggregations on date/time fields in sqlite3 since date/time is saved as text.
ERROR 2025-09-11 20:06:56,922 basehttp 89466 6170865664 "GET /en/appointments/waiting-list/ HTTP/1.1" 500 147474
INFO 2025-09-11 20:07:36,355 autoreload 89466 8682856640 /Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py changed, reloading.
INFO 2025-09-11 20:07:36,703 autoreload 89776 8682856640 Watching for file changes with StatReloader
ERROR 2025-09-11 20:07:39,271 log 89776 6155333632 Internal Server Error: /en/appointments/waiting-list/
Traceback (most recent call last):
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 220, in _get_response
response = response.render()
^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 114, in render
self.content = self.rendered_content
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 92, in rendered_content
return template.render(context, self._request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/backends/django.py", line 107, in render
return self.template.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 171, in render
return self._render(context)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
return self.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 159, in render
return compiled_parent._render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
return self.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 65, in render
result = block.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1081, in render
return render_value_in_context(output, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1058, in render_value_in_context
value = str(value)
^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/utils.py", line 79, in __str__
return self.as_widget()
^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/boundfield.py", line 108, in as_widget
return widget.render(
^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 329, in render
context = self.get_context(name, value, attrs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 830, in get_context
context = super().get_context(name, value, attrs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 781, in get_context
context["widget"]["optgroups"] = self.optgroups(
^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 721, in optgroups
for index, (option_value, option_label) in enumerate(self.choices):
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/models.py", line 1422, in __iter__
if not queryset._prefetch_related_lookups:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute '_prefetch_related_lookups'
ERROR 2025-09-11 20:07:39,283 basehttp 89776 6155333632 "GET /en/appointments/waiting-list/ HTTP/1.1" 500 201102
INFO 2025-09-11 20:09:19,217 autoreload 89776 8682856640 /Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py changed, reloading.
INFO 2025-09-11 20:09:19,566 autoreload 90554 8682856640 Watching for file changes with StatReloader
ERROR 2025-09-11 20:09:22,187 log 90554 6164951040 Internal Server Error: /en/appointments/waiting-list/
Traceback (most recent call last):
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/contrib/auth/mixins.py", line 73, in dispatch
return super().dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 144, in dispatch
return handler(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/list.py", line 158, in get
self.object_list = self.get_queryset()
^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py", line 1230, in get_queryset
form = WaitingListFilterForm(
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/forms.py", line 802, in __init__
from core.models import Department
ImportError: cannot import name 'Department' from 'core.models' (/Users/marwanalwali/manus_project/hospital_management_system_v4/core/models.py)
ERROR 2025-09-11 20:09:22,190 basehttp 90554 6164951040 "GET /en/appointments/waiting-list/ HTTP/1.1" 500 90821
INFO 2025-09-11 20:13:01,739 autoreload 90554 8682856640 /Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/forms.py changed, reloading.
INFO 2025-09-11 20:13:02,062 autoreload 92195 8682856640 Watching for file changes with StatReloader
ERROR 2025-09-11 20:13:04,557 log 92195 6159020032 Internal Server Error: /en/appointments/waiting-list/
Traceback (most recent call last):
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
response = wrapped_callback(request, *callback_args, **callback_kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 105, in view
return self.dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/contrib/auth/mixins.py", line 73, in dispatch
return super().dispatch(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/base.py", line 144, in dispatch
return handler(request, *args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/views/generic/list.py", line 158, in get
self.object_list = self.get_queryset()
^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/views.py", line 1230, in get_queryset
form = WaitingListFilterForm(
^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/forms.py", line 801, in __init__
self.fields['provider'].queryset = User.objects.filter(
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/manager.py", line 87, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1493, in filter
return self._filter_or_exclude(False, args, kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1511, in _filter_or_exclude
clone._filter_or_exclude_inplace(negate, args, kwargs)
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1518, in _filter_or_exclude_inplace
self._query.add_q(Q(*args, **kwargs))
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 1646, in add_q
clause, _ = self._add_q(q_object, can_reuse)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 1678, in _add_q
child_clause, needed_inner = self.build_filter(
^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 1526, in build_filter
lookups, parts, reffed_expression = self.solve_lookup_type(arg, summarize)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 1333, in solve_lookup_type
_, field, _, lookup_parts = self.names_to_path(lookup_splitted, self.get_meta())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/query.py", line 1806, in names_to_path
raise FieldError(
django.core.exceptions.FieldError: Cannot resolve keyword 'tenant_memberships' into field. Choices are: accessible_dashboards, acknowledged_alerts, administered_medications, administered_transfusions, admitted_patients, alertrule, analyzed_results, anesthesia_cases, approval_date, approved_by, approved_by_id, approved_care_plans, approved_purchase_orders, approved_schedules, approved_time_entries, approved_transfers, approved_users, assigned_incidents, assigned_queue_entries, assistant_or_blocks, assistant_surgeries, assistant_surgical_cases, attending_bills, attending_patients, attending_wards, audit_logs, audit_team_memberships, authored_notes, availability_slots, billing_provider_bills, bio, blocked_beds, blood_requests, bloodtest, cancelled_appointments, cancelled_requests, care_team_plans, checked_in_appointments, circulating_cases, claim_status_updates, claimdocument, claimstatushistory, cleaned_beds, co_signed_notes, collected_specimens, collected_units, communicationchannel, completed_transfers, completed_transfusions, conducted_reviews, consulting_patients, created_admissions, created_alert_rules, created_appointment_templates, created_appointments, created_at, created_audit_plans, created_availability_slots, created_beds, created_billing_configurations, created_care_plans, created_claims, created_consent_forms, created_consent_templates, created_data_mappings, created_discharge_summaries, created_donors, created_drug_interactions, created_employees, created_encounters, created_external_systems, created_findings, created_hr_departments, created_improvement_projects, created_insurance_claims, created_integration_endpoints, created_inventory_items, created_inventory_locations, created_lab_tests, created_measurements, created_medical_bills, created_medications, created_note_templates, created_notifications, created_operating_rooms, created_or_blocks, created_patient_notes, created_pharmacy_inventory_items, created_problems, created_purchase_orders, created_reference_ranges, created_report_templates, created_risk_assessments, created_schedules, created_studies, created_suppliers, created_surgeries, created_surgical_cases, created_surgical_note_templates, created_telemedicine_sessions, created_training_records, created_waiting_list_entries, created_waiting_queues, created_wards, created_webhooks, crossmatch, dashboard, datasource, date_joined, dea_number, department, diagnosed_problems, dictated_reports, discharge_planning_cases, dispensed_medications, double_checked_administrations, email, emailaddress, employee_id, employee_profile, encounters, failed_login_attempts, first_name, force_password_change, groups, id, initiated_capas, inpatient_anesthesia_cases, inpatient_circulating_cases, inpatient_scrub_cases, integration_logs, interpreted_studies, investigated_reactions, is_active, is_approved, is_staff, is_superuser, is_verified, issued_units, job_title, language, last_login, last_name, last_password_change, led_audits, license_expiry, license_number, license_state, locked_until, logentry, managed_locations, managed_problems, managed_projects, managed_wards, max_concurrent_sessions, messagerecipient, metricdefinition, middle_name, mobile_number, notificationtemplate, npi_number, ordered_imaging_studies, ordered_lab_tests, password, password_expires_at, password_history, performed_qc, phone_number, physician_discharges, planned_discharges, preferred_name, prescribed_medications, primary_care_plans, primary_nurse_discharges, primary_or_blocks, primary_surgeries, primary_surgical_cases, processed_payments, processed_requests, profile_picture, project_team_memberships, provider_appointments, provider_waiting_list, qc_tests, radiology_reports, received_payments, received_specimens, received_units, recorded_equipment_usage, referred_studies, registered_patients, removed_waiting_list_entries, rendered_line_items, report, reported_incidents, reported_reactions, reportexecution, requested_purchase_orders, requested_transfers, resolved_alerts, responsible_findings, responsible_quality_indicators, responsible_risks, reviewed_qc, reviewed_qc_tests, revoked_consents, role, scrub_cases, sent_messages, session_timeout_minutes, signed_encounters, social_accounts, sponsored_projects, stopped_transfusions, supervised_line_items, surgeon_surgical_notes, targeted_notifications, task, tenant, tenant_id, theme, transcribed_reports, transport_assignments, triggered_integrations, two_factor_devices, two_factor_enabled, updated_at, updated_configurations, updated_queue_entries, user_id, user_permissions, user_sessions, user_timezone, username, verified_crossmatches, verified_dispenses, verified_findings, verified_insurance, verified_measurements, verified_prescriptions, verified_problems, verified_results, verified_tests, verified_vital_signs, vital_signs_measurements, waiting_queues, waitinglistcontactlog, witnessed_administrations, witnessed_transfusions
ERROR 2025-09-11 20:13:04,566 basehttp 92195 6159020032 "GET /en/appointments/waiting-list/ HTTP/1.1" 500 163345
INFO 2025-09-11 20:14:09,037 autoreload 92195 8682856640 /Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/forms.py changed, reloading.
INFO 2025-09-11 20:14:09,374 autoreload 92662 8682856640 Watching for file changes with StatReloader
ERROR 2025-09-11 20:14:11,734 log 92662 6131511296 Internal Server Error: /en/appointments/waiting-list/
Traceback (most recent call last):
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
response = get_response(request)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 220, in _get_response
response = response.render()
^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 114, in render
self.content = self.rendered_content
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 92, in rendered_content
return template.render(context, self._request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/backends/django.py", line 107, in render
return self.template.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 171, in render
return self._render(context)
^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
return self.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 159, in render
return compiled_parent._render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
return self.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 65, in render
result = block.nodelist.render(context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
return SafeString("".join([node.render_annotated(context) for node in self]))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
return self.render(context)
^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1081, in render
return render_value_in_context(output, context)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1058, in render_value_in_context
value = str(value)
^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/utils.py", line 79, in __str__
return self.as_widget()
^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/boundfield.py", line 108, in as_widget
return widget.render(
^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 329, in render
context = self.get_context(name, value, attrs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 830, in get_context
context = super().get_context(name, value, attrs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 781, in get_context
context["widget"]["optgroups"] = self.optgroups(
^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/widgets.py", line 721, in optgroups
for index, (option_value, option_label) in enumerate(self.choices):
^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/forms/models.py", line 1422, in __iter__
if not queryset._prefetch_related_lookups:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute '_prefetch_related_lookups'
ERROR 2025-09-11 20:14:11,750 basehttp 92662 6131511296 "GET /en/appointments/waiting-list/ HTTP/1.1" 500 198762
INFO 2025-09-11 20:15:33,325 autoreload 92662 8682856640 /Users/marwanalwali/manus_project/hospital_management_system_v4/appointments/forms.py changed, reloading.
INFO 2025-09-11 20:15:33,696 autoreload 93282 8682856640 Watching for file changes with StatReloader
INFO 2025-09-11 20:18:12,169 basehttp 93282 6341865472 "GET /en/appointments/waiting-list/ HTTP/1.1" 200 32200
INFO 2025-09-11 20:18:12,185 basehttp 93282 6375518208 "GET /static/plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js HTTP/1.1" 200 1627
INFO 2025-09-11 20:18:12,186 basehttp 93282 6341865472 "GET /static/plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css HTTP/1.1" 200 8136
INFO 2025-09-11 20:18:12,186 basehttp 93282 6358691840 "GET /static/plugins/datatables.net-buttons/js/dataTables.buttons.min.js HTTP/1.1" 200 27926
INFO 2025-09-11 20:18:12,186 basehttp 93282 6409170944 "GET /static/plugins/datatables.net-buttons/js/buttons.print.min.js HTTP/1.1" 200 3073
INFO 2025-09-11 20:18:12,187 basehttp 93282 6392344576 "GET /static/plugins/datatables.net-buttons/js/buttons.html5.min.js HTTP/1.1" 200 26043
INFO 2025-09-11 20:18:12,223 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:18:45,413 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:19:12,408 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:19:15,402 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:19:45,402 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:20:13,409 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:20:15,408 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:20:45,416 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:21:14,421 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:21:24,410 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:22:15,409 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:22:24,409 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:23:16,418 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:23:24,404 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:24:17,402 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:24:24,407 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:25:24,311 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:25:24,315 basehttp 93282 6341865472 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:26:24,321 basehttp 93282 6341865472 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:26:24,322 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:27:24,294 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:28:24,323 basehttp 93282 6341865472 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:28:24,324 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:29:24,293 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:30:06,694 basehttp 93282 6341865472 "GET /en/appointments/waiting-list/stats/ HTTP/1.1" 200 22
INFO 2025-09-11 20:30:06,695 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:30:08,547 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/create/ HTTP/1.1" 200 44843
INFO 2025-09-11 20:30:08,616 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:31:08,628 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675
INFO 2025-09-11 20:31:38,073 basehttp 93282 6392344576 "GET /en/appointments/waiting-list/ HTTP/1.1" 200 32200
INFO 2025-09-11 20:31:38,126 basehttp 93282 6392344576 "GET /en/htmx/system-notifications/ HTTP/1.1" 200 4675

Binary file not shown.

BIN
operating_theatre/templates/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Confirm Delete Surgical Note{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_list' %}">Surgical Notes</a></li>
<li class="breadcrumb-item active">Confirm Delete</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-trash-alt text-danger me-2"></i>
Confirm Delete Surgical Note
<small class="text-muted ms-2">Permanently remove this surgical record</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-exclamation-triangle me-2"></i>Warning: This action cannot be undone!
</h4>
</div>
<div class="panel-body">
<div class="alert alert-danger mb-4">
<h4><i class="fas fa-exclamation-circle me-2"></i>Are you absolutely sure you want to delete this surgical note?</h4>
<p class="mb-0">Deleting this note for <strong>{{ object.patient.get_full_name }}</strong> regarding the procedure <strong>{{ object.procedure_name }}</strong> will permanently remove it from the system.</p>
<p class="mb-0">Consider archiving or marking as inactive instead of permanent deletion if there's any possibility of future reference.</p>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Patient Name:</label>
<p class="form-control-static"><strong>{{ object.patient.get_full_name }}</strong></p>
</div>
<div class="form-group mb-3">
<label class="form-label">Procedure Name:</label>
<p class="form-control-static">{{ object.procedure_name }}</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Procedure Date:</label>
<p class="form-control-static">{{ object.procedure_date|date:"M d, Y" }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label">Surgeon:</label>
<p class="form-control-static">{{ object.surgeon.get_full_name }}</p>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'operating_theatre:surgical_note_detail' object.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash-alt me-1"></i>Confirm Delete
</button>
</div>
</form>
</div>
</div>
<!-- END panel -->
{% endblock %}

View File

@ -0,0 +1,503 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Delete Surgical Note{% endblock %}
{% block extra_css %}
<style>
.delete-confirmation {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin: 2rem auto;
max-width: 600px;
padding: 2rem;
}
.warning-icon {
align-items: center;
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
border-radius: 50%;
color: white;
display: flex;
font-size: 2rem;
height: 80px;
justify-content: center;
margin: 0 auto 1.5rem;
width: 80px;
}
.delete-title {
color: #dc3545;
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
text-align: center;
}
.delete-message {
color: #6c757d;
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 2rem;
text-align: center;
}
.note-details {
background: #f8f9fa;
border-left: 4px solid #dc3545;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.detail-row {
align-items: center;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
color: #495057;
font-weight: 500;
}
.detail-value {
color: #212529;
font-weight: 400;
}
.consequences-list {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.consequences-title {
color: #856404;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
.consequences-list ul {
color: #856404;
margin: 0;
padding-left: 1.5rem;
}
.consequences-list li {
margin-bottom: 0.5rem;
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 2rem;
}
.btn-delete {
background: linear-gradient(135deg, #dc3545, #c82333);
border: none;
color: white;
font-weight: 500;
padding: 0.75rem 2rem;
transition: all 0.3s ease;
}
.btn-delete:hover {
background: linear-gradient(135deg, #c82333, #bd2130);
color: white;
transform: translateY(-1px);
}
.btn-cancel {
background: #6c757d;
border: none;
color: white;
font-weight: 500;
padding: 0.75rem 2rem;
transition: all 0.3s ease;
}
.btn-cancel:hover {
background: #5a6268;
color: white;
transform: translateY(-1px);
}
.security-notice {
background: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 0.375rem;
color: #0c5460;
font-size: 0.9rem;
margin-top: 1.5rem;
padding: 1rem;
text-align: center;
}
.audit-info {
background: #e2e3e5;
border-radius: 0.375rem;
color: #495057;
font-size: 0.85rem;
margin-top: 1rem;
padding: 0.75rem;
text-align: center;
}
@media (max-width: 768px) {
.delete-confirmation {
margin: 1rem;
padding: 1.5rem;
}
.action-buttons {
flex-direction: column;
}
.btn-delete,
.btn-cancel {
width: 100%;
}
.warning-icon {
height: 60px;
width: 60px;
font-size: 1.5rem;
}
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="row">
<div class="col-12">
<div class="page-header">
<h1 class="page-header-title">
<i class="fas fa-trash-alt text-danger me-2"></i>
Delete Surgical Note
</h1>
<div class="page-header-subtitle">
Confirm deletion of surgical note record
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Card -->
<div class="delete-confirmation fade-in">
<div class="warning-icon pulse">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h2 class="delete-title">Are you sure you want to delete this surgical note?</h2>
<p class="delete-message">
This action cannot be undone. The surgical note and all associated data will be permanently removed from the system.
</p>
<!-- Note Details -->
<div class="note-details">
<h5 class="mb-3">
<i class="fas fa-file-medical text-primary me-2"></i>
Surgical Note Details
</h5>
<div class="detail-row">
<span class="detail-label">
<i class="fas fa-user me-2"></i>Patient:
</span>
<span class="detail-value">{{ object.patient.get_full_name }}</span>
</div>
<div class="detail-row">
<span class="detail-label">
<i class="fas fa-procedures me-2"></i>Procedure:
</span>
<span class="detail-value">{{ object.procedure_name }}</span>
</div>
<div class="detail-row">
<span class="detail-label">
<i class="fas fa-calendar me-2"></i>Surgery Date:
</span>
<span class="detail-value">{{ object.surgery_date|date:"F d, Y" }}</span>
</div>
<div class="detail-row">
<span class="detail-label">
<i class="fas fa-user-md me-2"></i>Primary Surgeon:
</span>
<span class="detail-value">{{ object.primary_surgeon.get_full_name }}</span>
</div>
<div class="detail-row">
<span class="detail-label">
<i class="fas fa-clock me-2"></i>Created:
</span>
<span class="detail-value">{{ object.created_at|date:"F d, Y g:i A" }}</span>
</div>
{% if object.is_signed %}
<div class="detail-row">
<span class="detail-label">
<i class="fas fa-signature me-2"></i>Status:
</span>
<span class="detail-value">
<span class="badge bg-success">
<i class="fas fa-check me-1"></i>Signed
</span>
</span>
</div>
{% endif %}
</div>
<!-- Consequences Warning -->
<div class="consequences-list">
<h6 class="consequences-title">
<i class="fas fa-exclamation-circle me-2"></i>
Deletion Consequences
</h6>
<ul>
<li>All surgical documentation will be permanently lost</li>
<li>Electronic signatures will be invalidated</li>
<li>Associated billing records may be affected</li>
<li>Audit trail will record this deletion</li>
<li>Patient medical history will be incomplete</li>
{% if object.is_signed %}
<li><strong>Warning:</strong> This is a signed document - deletion may have legal implications</li>
{% endif %}
</ul>
</div>
<!-- Confirmation Form -->
<form method="post" id="deleteForm">
{% csrf_token %}
<!-- Additional Confirmation -->
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmDeletion" required>
<label class="form-check-label" for="confirmDeletion">
I understand that this action cannot be undone and confirm the deletion
</label>
</div>
</div>
<div class="mb-3">
<label for="deletionReason" class="form-label">
<i class="fas fa-comment me-2"></i>Reason for Deletion (Required)
</label>
<textarea class="form-control" id="deletionReason" name="deletion_reason" rows="3"
placeholder="Please provide a reason for deleting this surgical note..." required></textarea>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a href="{% url 'operating_theatre:surgical_note_detail' object.pk %}" class="btn btn-cancel">
<i class="fas fa-arrow-left me-2"></i>Cancel
</a>
<button type="submit" class="btn btn-delete" id="confirmDeleteBtn" disabled>
<i class="fas fa-trash-alt me-2"></i>Delete Surgical Note
</button>
</div>
</form>
<!-- Security Notice -->
<div class="security-notice">
<i class="fas fa-shield-alt me-2"></i>
<strong>Security Notice:</strong> This action will be logged for audit purposes and may be subject to review by hospital administration.
</div>
<!-- Audit Information -->
<div class="audit-info">
<i class="fas fa-info-circle me-2"></i>
Deletion will be performed by: <strong>{{ user.get_full_name }}</strong> ({{ user.username }})
on {{ "now"|date:"F d, Y g:i A" }}
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="finalConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2"></i>
Final Confirmation
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<div class="mb-3">
<i class="fas fa-trash-alt text-danger" style="font-size: 3rem;"></i>
</div>
<h5>This is your final warning!</h5>
<p class="mb-0">Are you absolutely certain you want to delete this surgical note?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancel
</button>
<button type="button" class="btn btn-danger" id="finalDeleteBtn">
<i class="fas fa-trash-alt me-1"></i>Yes, Delete It
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Enable/disable delete button based on checkbox
$('#confirmDeletion').on('change', function() {
const isChecked = $(this).is(':checked');
const hasReason = $('#deletionReason').val().trim().length > 0;
$('#confirmDeleteBtn').prop('disabled', !(isChecked && hasReason));
});
// Enable/disable delete button based on reason text
$('#deletionReason').on('input', function() {
const hasReason = $(this).val().trim().length > 0;
const isChecked = $('#confirmDeletion').is(':checked');
$('#confirmDeleteBtn').prop('disabled', !(isChecked && hasReason));
});
// Show final confirmation modal
$('#deleteForm').on('submit', function(e) {
e.preventDefault();
$('#finalConfirmModal').modal('show');
});
// Handle final deletion
$('#finalDeleteBtn').on('click', function() {
// Show loading state
$(this).html('<i class="fas fa-spinner fa-spin me-1"></i>Deleting...');
$(this).prop('disabled', true);
// Add deletion timestamp and user info
const form = $('#deleteForm');
form.append('<input type="hidden" name="deleted_by" value="{{ user.id }}">');
form.append('<input type="hidden" name="deleted_at" value="' + new Date().toISOString() + '">');
// Submit the form
setTimeout(function() {
form.off('submit').submit();
}, 1000);
});
// Auto-resize textarea
$('#deletionReason').on('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
// Prevent accidental navigation
let formSubmitted = false;
$('#deleteForm').on('submit', function() {
formSubmitted = true;
});
$(window).on('beforeunload', function(e) {
if (!formSubmitted && ($('#confirmDeletion').is(':checked') || $('#deletionReason').val().trim().length > 0)) {
const message = 'You have unsaved changes. Are you sure you want to leave?';
e.returnValue = message;
return message;
}
});
// Focus on reason textarea when checkbox is checked
$('#confirmDeletion').on('change', function() {
if ($(this).is(':checked')) {
$('#deletionReason').focus();
}
});
// Character counter for deletion reason
$('#deletionReason').on('input', function() {
const length = $(this).val().length;
const maxLength = 500;
if (!$('#charCounter').length) {
$(this).after('<small id="charCounter" class="form-text text-muted"></small>');
}
$('#charCounter').text(`${length}/${maxLength} characters`);
if (length > maxLength) {
$(this).addClass('is-invalid');
$('#charCounter').addClass('text-danger');
} else {
$(this).removeClass('is-invalid');
$('#charCounter').removeClass('text-danger');
}
});
// Add confirmation sound effect (optional)
function playWarningSound() {
// Create audio context for warning sound
if (typeof(AudioContext) !== "undefined" || typeof(webkitAudioContext) !== "undefined") {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
}
}
// Play warning sound when modal opens
$('#finalConfirmModal').on('shown.bs.modal', function() {
playWarningSound();
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,649 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Surgical Note - {{ note.patient.get_full_name }}{% endblock %}
{% block extra_css %}
<style>
.note-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.note-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
}
.section-content {
padding: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.info-item {
display: flex;
flex-direction: column;
}
.info-label {
font-size: 0.875rem;
color: #6c757d;
font-weight: 600;
margin-bottom: 0.25rem;
}
.info-value {
color: #495057;
font-weight: 500;
}
.note-status {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
display: inline-block;
}
.status-draft { background: #f8f9fa; color: #6c757d; }
.status-in-progress { background: #fff3cd; color: #856404; }
.status-completed { background: #d4edda; color: #155724; }
.status-signed { background: #d1ecf1; color: #0c5460; }
.status-amended { background: #f8d7da; color: #721c24; }
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
display: inline-block;
}
.priority-low { background: #d4edda; color: #155724; }
.priority-medium { background: #fff3cd; color: #856404; }
.priority-high { background: #f8d7da; color: #721c24; }
.priority-critical { background: #f5c6cb; color: #721c24; }
.signature-section {
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 0.375rem;
padding: 2rem;
text-align: center;
margin-top: 2rem;
}
.signature-box {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
margin: 1rem 0;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline-item {
position: relative;
padding-bottom: 1.5rem;
}
.timeline-item:before {
content: '';
position: absolute;
left: -2rem;
top: 0;
width: 2px;
height: 100%;
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%;
background: #007bff;
border: 2px solid white;
box-shadow: 0 0 0 2px #dee2e6;
}
.timeline-content {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
.print-section {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
@media print {
.no-print {
display: none !important;
}
.note-header {
background: #f8f9fa !important;
color: #495057 !important;
border: 1px solid #dee2e6 !important;
}
.section-header {
background: #f8f9fa !important;
}
}
@media (max-width: 768px) {
.note-header {
padding: 1.5rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.timeline {
padding-left: 1rem;
}
.timeline-item:before {
left: -1rem;
}
.timeline-marker {
left: -1.5rem;
}
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3 no-print">
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_list' %}">Surgical Notes</a></li>
<li class="breadcrumb-item active">{{ note.patient.get_full_name }}</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-file-medical me-2"></i>Surgical Note Details
</h1>
</div>
<div class="ms-auto">
<div class="btn-group">
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
{% if note.status != 'signed' %}
<a href="{% url 'operating_theatre:surgical_note_edit' note.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-1"></i>Edit
</a>
{% endif %}
<button class="btn btn-success" onclick="printNote()">
<i class="fas fa-print me-1"></i>Print
</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="exportNote('pdf')">
<i class="fas fa-file-pdf me-2"></i>Export as PDF
</a></li>
<li><a class="dropdown-item" href="#" onclick="exportNote('word')">
<i class="fas fa-file-word me-2"></i>Export as Word
</a></li>
<li><hr class="dropdown-divider"></li>
{% if note.status == 'completed' and not note.is_signed %}
<li><a class="dropdown-item" href="#" onclick="signNote()">
<i class="fas fa-signature me-2"></i>Sign Note
</a></li>
{% endif %}
{% if note.status == 'signed' %}
<li><a class="dropdown-item" href="#" onclick="amendNote()">
<i class="fas fa-edit me-2"></i>Create Amendment
</a></li>
{% endif %}
<li><a class="dropdown-item" href="#" onclick="duplicateNote()">
<i class="fas fa-copy me-2"></i>Duplicate Note
</a></li>
</ul>
</div>
</div>
</div>
</div>
<!-- Note Header -->
<div class="note-header">
<div class="row">
<div class="col-md-8">
<h2 class="mb-3">{{ note.procedure_name }}</h2>
<div class="row">
<div class="col-md-6">
<p class="mb-1"><strong>Patient:</strong> {{ note.patient.get_full_name }}</p>
<p class="mb-1"><strong>Patient ID:</strong> {{ note.patient.patient_id }}</p>
<p class="mb-1"><strong>Date of Birth:</strong> {{ note.patient.date_of_birth|date:"M d, Y" }}</p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>Surgery Date:</strong> {{ note.surgery_date|date:"M d, Y" }}</p>
<p class="mb-1"><strong>Surgeon:</strong> {{ note.surgeon.get_full_name }}</p>
<p class="mb-1"><strong>Operating Room:</strong> {{ note.operating_room.name }}</p>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<div class="mb-3">
<span class="note-status status-{{ note.status }}">
{{ note.get_status_display }}
</span>
</div>
<div class="mb-2">
<span class="priority-badge priority-{{ note.priority }}">
{{ note.get_priority_display }} Priority
</span>
</div>
<p class="mb-0"><small>Note ID: {{ note.id }}</small></p>
</div>
</div>
</div>
<!-- Pre-operative Information -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-clipboard-list me-2"></i>Pre-operative Information
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Pre-operative Diagnosis</div>
<div class="info-value">{{ note.preoperative_diagnosis|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Planned Procedure</div>
<div class="info-value">{{ note.planned_procedure|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Anesthesia Type</div>
<div class="info-value">{{ note.get_anesthesia_type_display|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">ASA Classification</div>
<div class="info-value">{{ note.asa_classification|default:"Not specified" }}</div>
</div>
</div>
{% if note.preoperative_notes %}
<div class="mt-3">
<div class="info-label">Pre-operative Notes</div>
<div class="info-value">{{ note.preoperative_notes|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Operative Procedure -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-procedures me-2"></i>Operative Procedure
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Actual Procedure</div>
<div class="info-value">{{ note.actual_procedure|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Procedure Duration</div>
<div class="info-value">{{ note.procedure_duration|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Incision Type</div>
<div class="info-value">{{ note.incision_type|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Closure Method</div>
<div class="info-value">{{ note.closure_method|default:"Not specified" }}</div>
</div>
</div>
{% if note.operative_findings %}
<div class="mt-3">
<div class="info-label">Operative Findings</div>
<div class="info-value">{{ note.operative_findings|linebreaks }}</div>
</div>
{% endif %}
{% if note.procedure_description %}
<div class="mt-3">
<div class="info-label">Detailed Procedure Description</div>
<div class="info-value">{{ note.procedure_description|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Post-operative Information -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-heartbeat me-2"></i>Post-operative Information
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Post-operative Diagnosis</div>
<div class="info-value">{{ note.postoperative_diagnosis|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Estimated Blood Loss</div>
<div class="info-value">{{ note.estimated_blood_loss|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Complications</div>
<div class="info-value">{{ note.complications|default:"None reported" }}</div>
</div>
<div class="info-item">
<div class="info-label">Condition at End</div>
<div class="info-value">{{ note.condition_at_end|default:"Not specified" }}</div>
</div>
</div>
{% if note.postoperative_instructions %}
<div class="mt-3">
<div class="info-label">Post-operative Instructions</div>
<div class="info-value">{{ note.postoperative_instructions|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Surgical Team -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-users me-2"></i>Surgical Team
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Primary Surgeon</div>
<div class="info-value">{{ note.surgeon.get_full_name }}</div>
</div>
{% if note.assistant_surgeon %}
<div class="info-item">
<div class="info-label">Assistant Surgeon</div>
<div class="info-value">{{ note.assistant_surgeon.get_full_name }}</div>
</div>
{% endif %}
{% if note.anesthesiologist %}
<div class="info-item">
<div class="info-label">Anesthesiologist</div>
<div class="info-value">{{ note.anesthesiologist.get_full_name }}</div>
</div>
{% endif %}
{% if note.scrub_nurse %}
<div class="info-item">
<div class="info-label">Scrub Nurse</div>
<div class="info-value">{{ note.scrub_nurse.get_full_name }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Revision History -->
{% if note.revisions.exists %}
<div class="note-section">
<div class="section-header">
<i class="fas fa-history me-2"></i>Revision History
</div>
<div class="section-content">
<div class="timeline">
{% for revision in note.revisions.all %}
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-0">{{ revision.get_action_display }}</h6>
<small class="text-muted">{{ revision.created_at|date:"M d, Y g:i A" }}</small>
</div>
<p class="mb-1"><strong>By:</strong> {{ revision.created_by.get_full_name }}</p>
{% if revision.reason %}
<p class="mb-0"><strong>Reason:</strong> {{ revision.reason }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Electronic Signature -->
{% if note.status == 'signed' %}
<div class="note-section">
<div class="section-header">
<i class="fas fa-signature me-2"></i>Electronic Signature
</div>
<div class="section-content">
<div class="signature-box">
<div class="text-center">
<i class="fas fa-certificate fa-3x text-success mb-3"></i>
<h5 class="text-success">Electronically Signed</h5>
<p class="mb-1"><strong>Signed by:</strong> {{ note.signed_by.get_full_name }}</p>
<p class="mb-1"><strong>Date:</strong> {{ note.signed_at|date:"M d, Y g:i A" }}</p>
<p class="mb-0"><strong>IP Address:</strong> {{ note.signature_ip|default:"Not recorded" }}</p>
</div>
</div>
</div>
</div>
{% elif note.status == 'completed' %}
<div class="signature-section no-print">
<h5 class="mb-3">
<i class="fas fa-signature me-2"></i>Electronic Signature Required
</h5>
<p class="text-muted mb-3">This note is complete and ready for electronic signature.</p>
<button class="btn btn-primary btn-lg" onclick="signNote()">
<i class="fas fa-signature me-2"></i>Sign Note
</button>
</div>
{% endif %}
</div>
<!-- Sign Note Modal -->
<div class="modal fade" id="signNoteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-signature me-2"></i>Electronic Signature
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
By signing this note, you confirm that all information is accurate and complete.
</div>
<form id="signatureForm">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Password Confirmation</label>
<input type="password" class="form-control" name="password" required
placeholder="Enter your password to confirm signature">
</div>
<div class="mb-3">
<label class="form-label">Signature Comments (Optional)</label>
<textarea class="form-control" name="signature_comments" rows="3"
placeholder="Any additional comments about this signature..."></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitSignature()">
<i class="fas fa-signature me-1"></i>Sign Note
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function printNote() {
window.print();
}
function exportNote(format) {
const url = `{% url "operating_theatre:surgical_note_export" note.pk %}?format=${format}`;
window.open(url, '_blank');
}
function signNote() {
$('#signNoteModal').modal('show');
}
function submitSignature() {
const form = document.getElementById('signatureForm');
const formData = new FormData(form);
$.ajax({
url: '{% url "operating_theatre:surgical_note_sign" note.pk %}',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$('#signNoteModal').modal('hide');
alert('Note signed successfully!');
location.reload();
} else {
alert('Error signing note: ' + response.error);
}
},
error: function() {
alert('Error signing note');
}
});
}
function amendNote() {
const reason = prompt('Please provide a reason for the amendment:');
if (reason) {
$.ajax({
url: '{% url "operating_theatre:surgical_note_amend" note.pk %}',
method: 'POST',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'reason': reason
},
success: function(response) {
if (response.success) {
alert('Amendment created successfully!');
window.location.href = response.amendment_url;
} else {
alert('Error creating amendment: ' + response.error);
}
},
error: function() {
alert('Error creating amendment');
}
});
}
}
function duplicateNote() {
if (confirm('Create a duplicate of this note?')) {
$.ajax({
url: '{% url "operating_theatre:surgical_note_duplicate" note.pk %}',
method: 'POST',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
if (response.success) {
alert('Note duplicated successfully!');
window.location.href = response.duplicate_url;
} else {
alert('Error duplicating note: ' + response.error);
}
},
error: function() {
alert('Error duplicating note');
}
});
}
}
// Keyboard shortcuts
$(document).keydown(function(e) {
// Ctrl+P for print
if (e.ctrlKey && e.keyCode === 80) {
e.preventDefault();
printNote();
}
// Ctrl+E for edit (if not signed)
{% if note.status != 'signed' %}
if (e.ctrlKey && e.keyCode === 69) {
e.preventDefault();
window.location.href = '{% url "operating_theatre:surgical_note_edit" note.pk %}';
}
{% endif %}
// Ctrl+S for sign (if completed)
{% if note.status == 'completed' %}
if (e.ctrlKey && e.keyCode === 83) {
e.preventDefault();
signNote();
}
{% endif %}
});
</script>
{% endblock %}

View File

@ -0,0 +1,649 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Surgical Note - {{ note.patient.get_full_name }}{% endblock %}
{% block extra_css %}
<style>
.note-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.note-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
}
.section-content {
padding: 1.5rem;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
.info-item {
display: flex;
flex-direction: column;
}
.info-label {
font-size: 0.875rem;
color: #6c757d;
font-weight: 600;
margin-bottom: 0.25rem;
}
.info-value {
color: #495057;
font-weight: 500;
}
.note-status {
padding: 0.5rem 1rem;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
display: inline-block;
}
.status-draft { background: #f8f9fa; color: #6c757d; }
.status-in-progress { background: #fff3cd; color: #856404; }
.status-completed { background: #d4edda; color: #155724; }
.status-signed { background: #d1ecf1; color: #0c5460; }
.status-amended { background: #f8d7da; color: #721c24; }
.priority-badge {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
display: inline-block;
}
.priority-low { background: #d4edda; color: #155724; }
.priority-medium { background: #fff3cd; color: #856404; }
.priority-high { background: #f8d7da; color: #721c24; }
.priority-critical { background: #f5c6cb; color: #721c24; }
.signature-section {
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 0.375rem;
padding: 2rem;
text-align: center;
margin-top: 2rem;
}
.signature-box {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
margin: 1rem 0;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline-item {
position: relative;
padding-bottom: 1.5rem;
}
.timeline-item:before {
content: '';
position: absolute;
left: -2rem;
top: 0;
width: 2px;
height: 100%;
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%;
background: #007bff;
border: 2px solid white;
box-shadow: 0 0 0 2px #dee2e6;
}
.timeline-content {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.25rem;
padding: 1rem;
}
.print-section {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
@media print {
.no-print {
display: none !important;
}
.note-header {
background: #f8f9fa !important;
color: #495057 !important;
border: 1px solid #dee2e6 !important;
}
.section-header {
background: #f8f9fa !important;
}
}
@media (max-width: 768px) {
.note-header {
padding: 1.5rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.timeline {
padding-left: 1rem;
}
.timeline-item:before {
left: -1rem;
}
.timeline-marker {
left: -1.5rem;
}
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<!-- Page Header -->
<div class="d-flex align-items-center mb-3 no-print">
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_list' %}">Surgical Notes</a></li>
<li class="breadcrumb-item active">{{ note.patient.get_full_name }}</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-file-medical me-2"></i>Surgical Note Details
</h1>
</div>
<div class="ms-auto">
<div class="btn-group">
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
{% if note.status != 'signed' %}
<a href="{% url 'operating_theatre:surgical_note_edit' note.pk %}" class="btn btn-warning">
<i class="fas fa-edit me-1"></i>Edit
</a>
{% endif %}
<button class="btn btn-success" onclick="printNote()">
<i class="fas fa-print me-1"></i>Print
</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="exportNote('pdf')">
<i class="fas fa-file-pdf me-2"></i>Export as PDF
</a></li>
<li><a class="dropdown-item" href="#" onclick="exportNote('word')">
<i class="fas fa-file-word me-2"></i>Export as Word
</a></li>
<li><hr class="dropdown-divider"></li>
{% if note.status == 'completed' and not note.is_signed %}
<li><a class="dropdown-item" href="#" onclick="signNote()">
<i class="fas fa-signature me-2"></i>Sign Note
</a></li>
{% endif %}
{% if note.status == 'signed' %}
<li><a class="dropdown-item" href="#" onclick="amendNote()">
<i class="fas fa-edit me-2"></i>Create Amendment
</a></li>
{% endif %}
<li><a class="dropdown-item" href="#" onclick="duplicateNote()">
<i class="fas fa-copy me-2"></i>Duplicate Note
</a></li>
</ul>
</div>
</div>
</div>
</div>
<!-- Note Header -->
<div class="note-header">
<div class="row">
<div class="col-md-8">
<h2 class="mb-3">{{ note.procedure_name }}</h2>
<div class="row">
<div class="col-md-6">
<p class="mb-1"><strong>Patient:</strong> {{ note.patient.get_full_name }}</p>
<p class="mb-1"><strong>Patient ID:</strong> {{ note.patient.patient_id }}</p>
<p class="mb-1"><strong>Date of Birth:</strong> {{ note.patient.date_of_birth|date:"M d, Y" }}</p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>Surgery Date:</strong> {{ note.surgery_date|date:"M d, Y" }}</p>
<p class="mb-1"><strong>Surgeon:</strong> {{ note.surgeon.get_full_name }}</p>
<p class="mb-1"><strong>Operating Room:</strong> {{ note.operating_room.name }}</p>
</div>
</div>
</div>
<div class="col-md-4 text-end">
<div class="mb-3">
<span class="note-status status-{{ note.status }}">
{{ note.get_status_display }}
</span>
</div>
<div class="mb-2">
<span class="priority-badge priority-{{ note.priority }}">
{{ note.get_priority_display }} Priority
</span>
</div>
<p class="mb-0"><small>Note ID: {{ note.id }}</small></p>
</div>
</div>
</div>
<!-- Pre-operative Information -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-clipboard-list me-2"></i>Pre-operative Information
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Pre-operative Diagnosis</div>
<div class="info-value">{{ note.preoperative_diagnosis|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Planned Procedure</div>
<div class="info-value">{{ note.planned_procedure|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Anesthesia Type</div>
<div class="info-value">{{ note.get_anesthesia_type_display|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">ASA Classification</div>
<div class="info-value">{{ note.asa_classification|default:"Not specified" }}</div>
</div>
</div>
{% if note.preoperative_notes %}
<div class="mt-3">
<div class="info-label">Pre-operative Notes</div>
<div class="info-value">{{ note.preoperative_notes|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Operative Procedure -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-procedures me-2"></i>Operative Procedure
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Actual Procedure</div>
<div class="info-value">{{ note.actual_procedure|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Procedure Duration</div>
<div class="info-value">{{ note.procedure_duration|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Incision Type</div>
<div class="info-value">{{ note.incision_type|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Closure Method</div>
<div class="info-value">{{ note.closure_method|default:"Not specified" }}</div>
</div>
</div>
{% if note.operative_findings %}
<div class="mt-3">
<div class="info-label">Operative Findings</div>
<div class="info-value">{{ note.operative_findings|linebreaks }}</div>
</div>
{% endif %}
{% if note.procedure_description %}
<div class="mt-3">
<div class="info-label">Detailed Procedure Description</div>
<div class="info-value">{{ note.procedure_description|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Post-operative Information -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-heartbeat me-2"></i>Post-operative Information
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Post-operative Diagnosis</div>
<div class="info-value">{{ note.postoperative_diagnosis|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Estimated Blood Loss</div>
<div class="info-value">{{ note.estimated_blood_loss|default:"Not specified" }}</div>
</div>
<div class="info-item">
<div class="info-label">Complications</div>
<div class="info-value">{{ note.complications|default:"None reported" }}</div>
</div>
<div class="info-item">
<div class="info-label">Condition at End</div>
<div class="info-value">{{ note.condition_at_end|default:"Not specified" }}</div>
</div>
</div>
{% if note.postoperative_instructions %}
<div class="mt-3">
<div class="info-label">Post-operative Instructions</div>
<div class="info-value">{{ note.postoperative_instructions|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
<!-- Surgical Team -->
<div class="note-section">
<div class="section-header">
<i class="fas fa-users me-2"></i>Surgical Team
</div>
<div class="section-content">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Primary Surgeon</div>
<div class="info-value">{{ note.surgeon.get_full_name }}</div>
</div>
{% if note.assistant_surgeon %}
<div class="info-item">
<div class="info-label">Assistant Surgeon</div>
<div class="info-value">{{ note.assistant_surgeon.get_full_name }}</div>
</div>
{% endif %}
{% if note.anesthesiologist %}
<div class="info-item">
<div class="info-label">Anesthesiologist</div>
<div class="info-value">{{ note.anesthesiologist.get_full_name }}</div>
</div>
{% endif %}
{% if note.scrub_nurse %}
<div class="info-item">
<div class="info-label">Scrub Nurse</div>
<div class="info-value">{{ note.scrub_nurse.get_full_name }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Revision History -->
{% if note.revisions.exists %}
<div class="note-section">
<div class="section-header">
<i class="fas fa-history me-2"></i>Revision History
</div>
<div class="section-content">
<div class="timeline">
{% for revision in note.revisions.all %}
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-0">{{ revision.get_action_display }}</h6>
<small class="text-muted">{{ revision.created_at|date:"M d, Y g:i A" }}</small>
</div>
<p class="mb-1"><strong>By:</strong> {{ revision.created_by.get_full_name }}</p>
{% if revision.reason %}
<p class="mb-0"><strong>Reason:</strong> {{ revision.reason }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Electronic Signature -->
{% if note.status == 'signed' %}
<div class="note-section">
<div class="section-header">
<i class="fas fa-signature me-2"></i>Electronic Signature
</div>
<div class="section-content">
<div class="signature-box">
<div class="text-center">
<i class="fas fa-certificate fa-3x text-success mb-3"></i>
<h5 class="text-success">Electronically Signed</h5>
<p class="mb-1"><strong>Signed by:</strong> {{ note.signed_by.get_full_name }}</p>
<p class="mb-1"><strong>Date:</strong> {{ note.signed_at|date:"M d, Y g:i A" }}</p>
<p class="mb-0"><strong>IP Address:</strong> {{ note.signature_ip|default:"Not recorded" }}</p>
</div>
</div>
</div>
</div>
{% elif note.status == 'completed' %}
<div class="signature-section no-print">
<h5 class="mb-3">
<i class="fas fa-signature me-2"></i>Electronic Signature Required
</h5>
<p class="text-muted mb-3">This note is complete and ready for electronic signature.</p>
<button class="btn btn-primary btn-lg" onclick="signNote()">
<i class="fas fa-signature me-2"></i>Sign Note
</button>
</div>
{% endif %}
</div>
<!-- Sign Note Modal -->
<div class="modal fade" id="signNoteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-signature me-2"></i>Electronic Signature
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
By signing this note, you confirm that all information is accurate and complete.
</div>
<form id="signatureForm">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">Password Confirmation</label>
<input type="password" class="form-control" name="password" required
placeholder="Enter your password to confirm signature">
</div>
<div class="mb-3">
<label class="form-label">Signature Comments (Optional)</label>
<textarea class="form-control" name="signature_comments" rows="3"
placeholder="Any additional comments about this signature..."></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="submitSignature()">
<i class="fas fa-signature me-1"></i>Sign Note
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
function printNote() {
window.print();
}
function exportNote(format) {
const url = `{% url "operating_theatre:surgical_note_export" note.pk %}?format=${format}`;
window.open(url, '_blank');
}
function signNote() {
$('#signNoteModal').modal('show');
}
function submitSignature() {
const form = document.getElementById('signatureForm');
const formData = new FormData(form);
$.ajax({
url: '{% url "operating_theatre:surgical_note_sign" note.pk %}',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.success) {
$('#signNoteModal').modal('hide');
alert('Note signed successfully!');
location.reload();
} else {
alert('Error signing note: ' + response.error);
}
},
error: function() {
alert('Error signing note');
}
});
}
function amendNote() {
const reason = prompt('Please provide a reason for the amendment:');
if (reason) {
$.ajax({
url: '{% url "operating_theatre:surgical_note_amend" note.pk %}',
method: 'POST',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}',
'reason': reason
},
success: function(response) {
if (response.success) {
alert('Amendment created successfully!');
window.location.href = response.amendment_url;
} else {
alert('Error creating amendment: ' + response.error);
}
},
error: function() {
alert('Error creating amendment');
}
});
}
}
function duplicateNote() {
if (confirm('Create a duplicate of this note?')) {
$.ajax({
url: '{% url "operating_theatre:surgical_note_duplicate" note.pk %}',
method: 'POST',
data: {
'csrfmiddlewaretoken': '{{ csrf_token }}'
},
success: function(response) {
if (response.success) {
alert('Note duplicated successfully!');
window.location.href = response.duplicate_url;
} else {
alert('Error duplicating note: ' + response.error);
}
},
error: function() {
alert('Error duplicating note');
}
});
}
}
// Keyboard shortcuts
$(document).keydown(function(e) {
// Ctrl+P for print
if (e.ctrlKey && e.keyCode === 80) {
e.preventDefault();
printNote();
}
// Ctrl+E for edit (if not signed)
{% if note.status != 'signed' %}
if (e.ctrlKey && e.keyCode === 69) {
e.preventDefault();
window.location.href = '{% url "operating_theatre:surgical_note_edit" note.pk %}';
}
{% endif %}
// Ctrl+S for sign (if completed)
{% if note.status == 'completed' %}
if (e.ctrlKey && e.keyCode === 83) {
e.preventDefault();
signNote();
}
{% endif %}
});
</script>
{% endblock %}

View File

@ -0,0 +1,318 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{% if object %}Edit Surgical Note{% else %}Create Surgical Note{% endif %}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<style>
.form-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
.form-section h5 {
color: #007bff;
}
.required-field::after {
content: " *";
color: #dc3545;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_list' %}">Surgical Notes</a></li>
<li class="breadcrumb-item active">{% if object %}Edit{% else %}Create{% endif %}</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-{% if object %}edit{% else %}plus{% endif %} text-primary me-2"></i>
{% if object %}Edit Surgical Note{% else %}Create New Surgical Note{% endif %}
<small class="text-muted ms-2">{% if object %}Update existing surgical record{% else %}Document a new surgical procedure{% endif %}</small>
</h1>
<!-- END page-header -->
<!-- BEGIN form panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-file-medical me-2"></i>Surgical Note Details
</h4>
<div class="panel-heading-btn">
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
<div class="panel-body">
<form method="post" id="surgical-note-form" novalidate>
{% csrf_token %}
<!-- Patient & Procedure Information -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-user-injured me-2"></i>Patient & Procedure Information</h5>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Patient</label>
{{ form.patient }}
{% if form.patient.errors %}
<div class="invalid-feedback d-block">{{ form.patient.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Surgeon</label>
{{ form.surgeon }}
{% if form.surgeon.errors %}
<div class="invalid-feedback d-block">{{ form.surgeon.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Procedure Name</label>
{{ form.procedure_name }}
{% if form.procedure_name.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_name.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Procedure Code (CPT/ICD)</label>
{{ form.procedure_code }}
{% if form.procedure_code.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_code.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Date of Procedure</label>
{{ form.procedure_date }}
{% if form.procedure_date.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label required-field">Time of Procedure</label>
{{ form.procedure_time }}
{% if form.procedure_time.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_time.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Pre-operative Details -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-notes-medical me-2"></i>Pre-operative Details</h5>
<div class="mb-3">
<label class="form-label">Pre-operative Diagnosis</label>
{{ form.pre_operative_diagnosis }}
{% if form.pre_operative_diagnosis.errors %}
<div class="invalid-feedback d-block">{{ form.pre_operative_diagnosis.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Anesthesia Type</label>
{{ form.anesthesia_type }}
{% if form.anesthesia_type.errors %}
<div class="invalid-feedback d-block">{{ form.anesthesia_type.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Anesthesiologist</label>
{{ form.anesthesiologist }}
{% if form.anesthesiologist.errors %}
<div class="invalid-feedback d-block">{{ form.anesthesiologist.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Intra-operative Details -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-cut me-2"></i>Intra-operative Details</h5>
<div class="mb-3">
<label class="form-label required-field">Post-operative Diagnosis</label>
{{ form.post_operative_diagnosis }}
{% if form.post_operative_diagnosis.errors %}
<div class="invalid-feedback d-block">{{ form.post_operative_diagnosis.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label required-field">Procedure Description</label>
{{ form.procedure_description }}
{% if form.procedure_description.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_description.errors.0 }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Estimated Blood Loss (ml)</label>
{{ form.estimated_blood_loss }}
{% if form.estimated_blood_loss.errors %}
<div class="invalid-feedback d-block">{{ form.estimated_blood_loss.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Complications</label>
{{ form.complications }}
{% if form.complications.errors %}
<div class="invalid-feedback d-block">{{ form.complications.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Specimens Sent</label>
{{ form.specimens_sent }}
{% if form.specimens_sent.errors %}
<div class="invalid-feedback d-block">{{ form.specimens_sent.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Post-operative Details -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-heartbeat me-2"></i>Post-operative Details</h5>
<div class="mb-3">
<label class="form-label">Post-operative Orders</label>
{{ form.post_operative_orders }}
{% if form.post_operative_orders.errors %}
<div class="invalid-feedback d-block">{{ form.post_operative_orders.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Condition on Discharge from OR</label>
{{ form.condition_on_discharge }}
{% if form.condition_on_discharge.errors %}
<div class="invalid-feedback d-block">{{ form.condition_on_discharge.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Follow-up Plan</label>
{{ form.follow_up_plan }}
{% if form.follow_up_plan.errors %}
<div class="invalid-feedback d-block">{{ form.follow_up_plan.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Signature -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-signature me-2"></i>Signature</h5>
<div class="mb-3">
<label class="form-label required-field">Signed By</label>
{{ form.signed_by }}
{% if form.signed_by.errors %}
<div class="invalid-feedback d-block">{{ form.signed_by.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Signature Date</label>
{{ form.signature_date }}
{% if form.signature_date.errors %}
<div class="invalid-feedback d-block">{{ form.signature_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Form Actions -->
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between">
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>
{% if object %}Update Surgical Note{% else %}Create Surgical Note{% endif %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- END form panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2 for dropdowns
$(".form-select").select2({
theme: "bootstrap-5",
width: "100%"
});
// Initialize date pickers
$("input[type='date']").datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true
});
// Form validation (basic example, Django's form validation is primary)
$("#surgical-note-form").submit(function(e) {
let isValid = true;
$(this).find("[required]").each(function() {
if (!$(this).val()) {
$(this).addClass("is-invalid");
isValid = false;
} else {
$(this).removeClass("is-invalid");
}
});
if (!isValid) {
e.preventDefault();
alert("Please fill in all required fields.");
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,745 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{% if object %}Edit Surgical Note{% else %}Create Surgical Note{% endif %}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/summernote/dist/summernote-bs4.min.css' %}" rel="stylesheet" />
<style>
.form-section {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
padding: 1.5rem;
}
.section-header {
border-bottom: 2px solid #f8f9fa;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
}
.section-title {
color: #495057;
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
color: #495057;
font-weight: 500;
margin-bottom: 0.5rem;
}
.required-field::after {
color: #dc3545;
content: " *";
}
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn-group-actions {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
position: sticky;
bottom: 1rem;
z-index: 100;
}
.signature-pad {
border: 2px dashed #dee2e6;
border-radius: 0.375rem;
height: 200px;
position: relative;
width: 100%;
}
.signature-placeholder {
align-items: center;
color: #6c757d;
display: flex;
height: 100%;
justify-content: center;
position: absolute;
width: 100%;
}
.template-selector {
background: #f8f9fa;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
padding: 1rem;
}
.validation-feedback {
display: block;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.is-invalid {
border-color: #dc3545;
}
.is-valid {
border-color: #28a745;
}
.progress-indicator {
background: #f8f9fa;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
padding: 1rem;
}
.progress-steps {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
.progress-step {
align-items: center;
display: flex;
flex-direction: column;
flex: 1;
position: relative;
}
.progress-step:not(:last-child)::after {
background: #dee2e6;
content: '';
height: 2px;
left: 50%;
position: absolute;
top: 15px;
width: 100%;
z-index: 1;
}
.progress-step.active:not(:last-child)::after {
background: #007bff;
}
.step-circle {
align-items: center;
background: #dee2e6;
border-radius: 50%;
color: #6c757d;
display: flex;
font-size: 0.875rem;
font-weight: 600;
height: 30px;
justify-content: center;
position: relative;
width: 30px;
z-index: 2;
}
.progress-step.active .step-circle {
background: #007bff;
color: white;
}
.progress-step.completed .step-circle {
background: #28a745;
color: white;
}
.step-label {
font-size: 0.75rem;
margin-top: 0.5rem;
text-align: center;
}
@media (max-width: 768px) {
.form-section {
margin-bottom: 1rem;
padding: 1rem;
}
.btn-group-actions {
padding: 1rem;
}
.progress-steps {
flex-direction: column;
gap: 1rem;
}
.progress-step:not(:last-child)::after {
display: none;
}
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="row">
<div class="col-12">
<div class="page-header">
<h1 class="page-header-title">
<i class="fas fa-file-medical text-primary me-2"></i>
{% if object %}Edit Surgical Note{% else %}Create Surgical Note{% endif %}
</h1>
<div class="page-header-subtitle">
{% if object %}
Update surgical note for {{ object.patient.get_full_name }}
{% else %}
Create a new surgical note with comprehensive documentation
{% endif %}
</div>
</div>
</div>
</div>
<!-- Progress Indicator -->
<div class="progress-indicator">
<div class="progress-steps">
<div class="progress-step active">
<div class="step-circle">1</div>
<div class="step-label">Basic Info</div>
</div>
<div class="progress-step">
<div class="step-circle">2</div>
<div class="step-label">Procedure</div>
</div>
<div class="progress-step">
<div class="step-circle">3</div>
<div class="step-label">Documentation</div>
</div>
<div class="progress-step">
<div class="step-circle">4</div>
<div class="step-label">Review</div>
</div>
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 25%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
<form method="post" enctype="multipart/form-data" id="surgicalNoteForm">
{% csrf_token %}
<!-- Template Selector -->
{% if not object %}
<div class="template-selector">
<h5><i class="fas fa-clipboard-list me-2"></i>Use Template</h5>
<div class="row">
<div class="col-md-8">
<select class="form-select" id="templateSelector">
<option value="">Select a template to pre-fill form...</option>
<option value="general">General Surgery Template</option>
<option value="cardiac">Cardiac Surgery Template</option>
<option value="orthopedic">Orthopedic Surgery Template</option>
<option value="neurosurgery">Neurosurgery Template</option>
<option value="plastic">Plastic Surgery Template</option>
</select>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-outline-primary" id="loadTemplate">
<i class="fas fa-download me-1"></i>Load Template
</button>
</div>
</div>
</div>
{% endif %}
<!-- Basic Information -->
<div class="form-section" id="basicInfoSection">
<div class="section-header">
<h4 class="section-title">
<i class="fas fa-user-injured text-primary me-2"></i>
Basic Information
</h4>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label required-field">Patient</label>
{{ form.patient }}
{% if form.patient.errors %}
<div class="invalid-feedback">{{ form.patient.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label required-field">Surgical Case</label>
{{ form.surgical_case }}
{% if form.surgical_case.errors %}
<div class="invalid-feedback">{{ form.surgical_case.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label class="form-label required-field">Surgery Date</label>
{{ form.surgery_date }}
{% if form.surgery_date.errors %}
<div class="invalid-feedback">{{ form.surgery_date.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label required-field">Start Time</label>
{{ form.start_time }}
{% if form.start_time.errors %}
<div class="invalid-feedback">{{ form.start_time.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label">End Time</label>
{{ form.end_time }}
{% if form.end_time.errors %}
<div class="invalid-feedback">{{ form.end_time.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label required-field">Primary Surgeon</label>
{{ form.primary_surgeon }}
{% if form.primary_surgeon.errors %}
<div class="invalid-feedback">{{ form.primary_surgeon.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Assistant Surgeons</label>
{{ form.assistant_surgeons }}
{% if form.assistant_surgeons.errors %}
<div class="invalid-feedback">{{ form.assistant_surgeons.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Procedure Details -->
<div class="form-section" id="procedureSection">
<div class="section-header">
<h4 class="section-title">
<i class="fas fa-procedures text-success me-2"></i>
Procedure Details
</h4>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label required-field">Procedure Name</label>
{{ form.procedure_name }}
{% if form.procedure_name.errors %}
<div class="invalid-feedback">{{ form.procedure_name.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">CPT Code</label>
{{ form.cpt_code }}
{% if form.cpt_code.errors %}
<div class="invalid-feedback">{{ form.cpt_code.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="form-group">
<label class="form-label required-field">Preoperative Diagnosis</label>
{{ form.preoperative_diagnosis }}
{% if form.preoperative_diagnosis.errors %}
<div class="invalid-feedback">{{ form.preoperative_diagnosis.errors.0 }}</div>
{% endif %}
</div>
<div class="form-group">
<label class="form-label required-field">Postoperative Diagnosis</label>
{{ form.postoperative_diagnosis }}
{% if form.postoperative_diagnosis.errors %}
<div class="invalid-feedback">{{ form.postoperative_diagnosis.errors.0 }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Anesthesia Type</label>
{{ form.anesthesia_type }}
{% if form.anesthesia_type.errors %}
<div class="invalid-feedback">{{ form.anesthesia_type.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Anesthesiologist</label>
{{ form.anesthesiologist }}
{% if form.anesthesiologist.errors %}
<div class="invalid-feedback">{{ form.anesthesiologist.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Surgical Documentation -->
<div class="form-section" id="documentationSection">
<div class="section-header">
<h4 class="section-title">
<i class="fas fa-file-alt text-info me-2"></i>
Surgical Documentation
</h4>
</div>
<div class="form-group">
<label class="form-label required-field">Operative Technique</label>
{{ form.operative_technique }}
{% if form.operative_technique.errors %}
<div class="invalid-feedback">{{ form.operative_technique.errors.0 }}</div>
{% endif %}
</div>
<div class="form-group">
<label class="form-label">Findings</label>
{{ form.findings }}
{% if form.findings.errors %}
<div class="invalid-feedback">{{ form.findings.errors.0 }}</div>
{% endif %}
</div>
<div class="form-group">
<label class="form-label">Complications</label>
{{ form.complications }}
{% if form.complications.errors %}
<div class="invalid-feedback">{{ form.complications.errors.0 }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Estimated Blood Loss (mL)</label>
{{ form.estimated_blood_loss }}
{% if form.estimated_blood_loss.errors %}
<div class="invalid-feedback">{{ form.estimated_blood_loss.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Specimens Sent</label>
{{ form.specimens_sent }}
{% if form.specimens_sent.errors %}
<div class="invalid-feedback">{{ form.specimens_sent.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="form-group">
<label class="form-label">Postoperative Instructions</label>
{{ form.postoperative_instructions }}
{% if form.postoperative_instructions.errors %}
<div class="invalid-feedback">{{ form.postoperative_instructions.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Electronic Signature -->
<div class="form-section" id="signatureSection">
<div class="section-header">
<h4 class="section-title">
<i class="fas fa-signature text-warning me-2"></i>
Electronic Signature
</h4>
</div>
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label class="form-label">Digital Signature</label>
<div class="signature-pad" id="signaturePad">
<div class="signature-placeholder">
<div class="text-center">
<i class="fas fa-pen-nib fa-2x mb-2"></i>
<div>Click to sign or upload signature image</div>
</div>
</div>
</div>
<div class="mt-2">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" id="clearSignature">
<i class="fas fa-eraser me-1"></i>Clear
</button>
<input type="file" class="d-none" id="signatureUpload" accept="image/*">
<button type="button" class="btn btn-sm btn-outline-primary" id="uploadSignature">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="form-label">Signature Date</label>
<input type="datetime-local" class="form-control" id="signatureDate" readonly>
</div>
<div class="form-group">
<label class="form-label">Signed By</label>
<input type="text" class="form-control" value="{{ user.get_full_name }}" readonly>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="btn-group-actions">
<div class="d-flex justify-content-between align-items-center">
<div>
<button type="button" class="btn btn-outline-secondary me-2" id="saveDraft">
<i class="fas fa-save me-1"></i>Save as Draft
</button>
<button type="button" class="btn btn-outline-info me-2" id="previewNote">
<i class="fas fa-eye me-1"></i>Preview
</button>
</div>
<div>
<a href="{% url 'operating_theatre:surgical_note_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-check me-1"></i>
{% if object %}Update Note{% else %}Create Note{% endif %}
</button>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- Preview Modal -->
<div class="modal fade" id="previewModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-eye me-2"></i>Surgical Note Preview
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="previewContent">
<!-- Preview content will be loaded here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="printPreview">
<i class="fas fa-print me-1"></i>Print
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script src="{% static 'assets/plugins/summernote/dist/summernote-bs4.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2
$('.form-select').select2({
theme: 'bootstrap-5',
width: '100%'
});
// Initialize date picker
$('input[type="date"]').datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true
});
// Initialize rich text editor
$('textarea').summernote({
height: 150,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link']],
['view', ['fullscreen', 'codeview', 'help']]
]
});
// Set current date/time for signature
$('#signatureDate').val(new Date().toISOString().slice(0, 16));
// Form validation
$('#surgicalNoteForm').on('submit', function(e) {
let isValid = true;
// Check required fields
$(this).find('[required]').each(function() {
if (!$(this).val()) {
$(this).addClass('is-invalid');
isValid = false;
} else {
$(this).removeClass('is-invalid').addClass('is-valid');
}
});
if (!isValid) {
e.preventDefault();
showAlert('Please fill in all required fields.', 'error');
return false;
}
});
// Template loader
$('#loadTemplate').on('click', function() {
const template = $('#templateSelector').val();
if (template) {
loadTemplate(template);
}
});
// Save as draft
$('#saveDraft').on('click', function() {
const formData = new FormData($('#surgicalNoteForm')[0]);
formData.append('save_as_draft', 'true');
$.ajax({
url: window.location.href,
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
showAlert('Draft saved successfully!', 'success');
},
error: function() {
showAlert('Error saving draft.', 'error');
}
});
});
// Preview functionality
$('#previewNote').on('click', function() {
generatePreview();
});
// Progress tracking
updateProgress();
// Auto-save functionality
let autoSaveTimer;
$('#surgicalNoteForm input, #surgicalNoteForm textarea, #surgicalNoteForm select').on('change', function() {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(function() {
$('#saveDraft').click();
}, 30000); // Auto-save after 30 seconds of inactivity
});
function loadTemplate(templateType) {
// Template loading logic would go here
showAlert('Template loaded successfully!', 'success');
}
function generatePreview() {
const formData = new FormData($('#surgicalNoteForm')[0]);
// Generate preview content
let previewHtml = `
<div class="surgical-note-preview">
<div class="text-center mb-4">
<h3>SURGICAL NOTE</h3>
<hr>
</div>
<!-- Preview content would be generated here -->
<p><strong>Patient:</strong> ${$('#id_patient option:selected').text()}</p>
<p><strong>Procedure:</strong> ${$('#id_procedure_name').val()}</p>
<p><strong>Date:</strong> ${$('#id_surgery_date').val()}</p>
<!-- More preview content... -->
</div>
`;
$('#previewContent').html(previewHtml);
$('#previewModal').modal('show');
}
function updateProgress() {
const totalSections = 4;
let completedSections = 0;
// Check each section for completion
if ($('#id_patient').val() && $('#id_surgery_date').val()) completedSections++;
if ($('#id_procedure_name').val()) completedSections++;
if ($('#id_operative_technique').val()) completedSections++;
if ($('#signaturePad').hasClass('signed')) completedSections++;
const progress = (completedSections / totalSections) * 100;
$('.progress-bar').css('width', progress + '%').attr('aria-valuenow', progress);
// Update step indicators
$('.progress-step').each(function(index) {
if (index < completedSections) {
$(this).addClass('completed');
} else if (index === completedSections) {
$(this).addClass('active');
}
});
}
function showAlert(message, type) {
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>
`;
if ($('.alert').length) {
$('.alert').replaceWith(alertHtml);
} else {
$('.container-fluid').prepend(alertHtml);
}
setTimeout(function() {
$('.alert').fadeOut();
}, 5000);
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,516 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Surgical Notes{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-item {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1.5rem;
text-align: center;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
color: white;
}
.stat-icon.primary { background: #007bff; }
.stat-icon.success { background: #28a745; }
.stat-icon.warning { background: #ffc107; }
.stat-icon.info { background: #17a2b8; }
.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;
}
.filter-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.note-status {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-draft { background: #f8f9fa; color: #6c757d; }
.status-in-progress { background: #fff3cd; color: #856404; }
.status-completed { background: #d4edda; color: #155724; }
.status-signed { background: #d1ecf1; color: #0c5460; }
.status-amended { background: #f8d7da; color: #721c24; }
.priority-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
}
.priority-low { background: #d4edda; color: #155724; }
.priority-medium { background: #fff3cd; color: #856404; }
.priority-high { background: #f8d7da; color: #721c24; }
.priority-critical { background: #f5c6cb; color: #721c24; }
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.stat-item {
padding: 1rem;
}
.stat-number {
font-size: 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 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item active">Surgical Notes</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-file-medical me-2"></i>Surgical Notes Management
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create Note
</a>
</div>
</div>
<!-- Statistics Cards -->
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon primary">
<i class="fas fa-file-medical"></i>
</div>
<div class="stat-number" id="total-notes">{{ stats.total_notes|default:0 }}</div>
<div class="stat-label">Total Notes</div>
</div>
<div class="stat-item">
<div class="stat-icon warning">
<i class="fas fa-edit"></i>
</div>
<div class="stat-number" id="draft-notes">{{ stats.draft_notes|default:0 }}</div>
<div class="stat-label">Draft Notes</div>
</div>
<div class="stat-item">
<div class="stat-icon success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-number" id="completed-notes">{{ stats.completed_notes|default:0 }}</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat-item">
<div class="stat-icon info">
<i class="fas fa-signature"></i>
</div>
<div class="stat-number" id="signed-notes">{{ stats.signed_notes|default:0 }}</div>
<div class="stat-label">Signed</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="draft">Draft</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="signed">Signed</option>
<option value="amended">Amended</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Surgeon</label>
<select class="form-select" id="surgeon-filter">
<option value="">All Surgeons</option>
{% for surgeon in surgeons %}
<option value="{{ surgeon.id }}">{{ surgeon.get_full_name }}</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="week">This Week</option>
<option value="month">This Month</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Priority</label>
<select class="form-select" id="priority-filter">
<option value="">All Priorities</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div class="row mt-3" id="custom-date-range" style="display: none;">
<div class="col-md-6">
<label class="form-label">From Date</label>
<input type="date" class="form-control" id="from-date">
</div>
<div class="col-md-6">
<label class="form-label">To Date</label>
<input type="date" class="form-control" id="to-date">
</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, procedure, or note content...">
<button class="btn btn-outline-secondary" type="button" id="search-btn">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="col-md-4 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>
<!-- Notes 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>Surgical Notes
</h5>
<div class="btn-group">
<button class="btn btn-outline-success btn-sm" onclick="exportNotes('excel')">
<i class="fas fa-file-excel me-1"></i>Excel
</button>
<button class="btn btn-outline-danger btn-sm" onclick="exportNotes('pdf')">
<i class="fas fa-file-pdf me-1"></i>PDF
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="printNotes()">
<i class="fas fa-print me-1"></i>Print
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="notesTable" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th width="5%">
<input type="checkbox" id="select-all">
</th>
<th>Patient</th>
<th>Procedure</th>
<th>Surgeon</th>
<th>Date</th>
<th>Status</th>
<th>Priority</th>
<th>Last Modified</th>
<th width="15%">Actions</th>
</tr>
</thead>
<tbody>
{% for note in notes %}
<tr>
<td>
<input type="checkbox" class="note-checkbox" value="{{ note.id }}">
</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">{{ note.patient.get_full_name }}</div>
<small class="text-muted">ID: {{ note.patient.patient_id }}</small>
</div>
</div>
</td>
<td>
<div class="fw-bold">{{ note.procedure_name }}</div>
<small class="text-muted">{{ note.procedure_code|default:"" }}</small>
</td>
<td>
<div>{{ note.surgeon.get_full_name }}</div>
<small class="text-muted">{{ note.surgeon.specialization|default:"" }}</small>
</td>
<td>
<div>{{ note.surgery_date|date:"M d, Y" }}</div>
<small class="text-muted">{{ note.surgery_date|time:"g:i A" }}</small>
</td>
<td>
<span class="note-status status-{{ note.status }}">
{{ note.get_status_display }}
</span>
</td>
<td>
<span class="priority-badge priority-{{ note.priority }}">
{{ note.get_priority_display }}
</span>
</td>
<td>
<div>{{ note.updated_at|date:"M d, Y" }}</div>
<small class="text-muted">{{ note.updated_at|time:"g:i A" }}</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}"
class="btn btn-outline-primary" title="View">
<i class="fas fa-eye"></i>
</a>
{% if note.status != 'signed' %}
<a href="{% url 'operating_theatre:surgical_note_edit' note.pk %}"
class="btn btn-outline-warning" title="Edit">
<i class="fas fa-edit"></i>
</a>
{% endif %}
<button class="btn btn-outline-success"
onclick="printNote('{{ note.pk }}')" title="Print">
<i class="fas fa-print"></i>
</button>
{% if note.status != 'signed' %}
<a href="{% url 'operating_theatre:surgical_note_delete' note.pk %}"
class="btn btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center py-4">
<i class="fas fa-file-medical fa-3x text-muted mb-3"></i>
<p class="text-muted">No surgical notes found</p>
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create First Note
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
const table = $('#notesTable').DataTable({
responsive: true,
pageLength: 25,
order: [[4, 'desc']], // Sort by date descending
columnDefs: [
{ orderable: false, targets: [0, 8] } // Disable sorting for checkbox and actions
],
language: {
search: "",
searchPlaceholder: "Search notes...",
lengthMenu: "Show _MENU_ notes per page",
info: "Showing _START_ to _END_ of _TOTAL_ notes",
infoEmpty: "No notes available",
infoFiltered: "(filtered from _MAX_ total notes)"
}
});
// Custom search
$('#search-input').on('keyup', function() {
table.search(this.value).draw();
});
// Select all checkbox
$('#select-all').on('change', function() {
$('.note-checkbox').prop('checked', this.checked);
});
// Date filter change
$('#date-filter').on('change', function() {
if ($(this).val() === 'custom') {
$('#custom-date-range').show();
} else {
$('#custom-date-range').hide();
}
});
// Load statistics
loadStats();
});
function loadStats() {
$.ajax({
url: '{% url "operating_theatre:surgical_note_stats" %}',
success: function(data) {
$('#total-notes').text(data.total_notes);
$('#draft-notes').text(data.draft_notes);
$('#completed-notes').text(data.completed_notes);
$('#signed-notes').text(data.signed_notes);
}
});
}
function applyFilters() {
const filters = {
status: $('#status-filter').val(),
surgeon: $('#surgeon-filter').val(),
date_range: $('#date-filter').val(),
priority: $('#priority-filter').val(),
from_date: $('#from-date').val(),
to_date: $('#to-date').val(),
search: $('#search-input').val()
};
// Apply filters via AJAX
$.ajax({
url: '{% url "operating_theatre:surgical_note_list" %}',
data: filters,
success: function(response) {
// Reload table data
location.reload();
}
});
}
function clearFilters() {
$('#status-filter').val('');
$('#surgeon-filter').val('');
$('#date-filter').val('');
$('#priority-filter').val('');
$('#from-date').val('');
$('#to-date').val('');
$('#search-input').val('');
$('#custom-date-range').hide();
// Clear DataTable search
$('#notesTable').DataTable().search('').draw();
}
function exportNotes(format) {
const selectedNotes = $('.note-checkbox:checked').map(function() {
return this.value;
}).get();
const params = new URLSearchParams({
format: format,
notes: selectedNotes.join(',')
});
window.open(`{% url "operating_theatre:surgical_note_export" %}?${params}`, '_blank');
}
function printNotes() {
const selectedNotes = $('.note-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedNotes.length === 0) {
alert('Please select notes to print');
return;
}
const params = new URLSearchParams({
notes: selectedNotes.join(',')
});
window.open(`{% url "operating_theatre:surgical_note_print" %}?${params}`, '_blank');
}
function printNote(noteId) {
window.open(`{% url "operating_theatre:surgical_note_print" %}?notes=${noteId}`, '_blank');
}
// Keyboard shortcuts
$(document).keydown(function(e) {
// Ctrl+N for new note
if (e.ctrlKey && e.keyCode === 78) {
e.preventDefault();
window.location.href = '{% url "operating_theatre:surgical_note_create" %}';
}
// Ctrl+F for search
if (e.ctrlKey && e.keyCode === 70) {
e.preventDefault();
$('#search-input').focus();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,516 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Surgical Notes{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-item {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1.5rem;
text-align: center;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
color: white;
}
.stat-icon.primary { background: #007bff; }
.stat-icon.success { background: #28a745; }
.stat-icon.warning { background: #ffc107; }
.stat-icon.info { background: #17a2b8; }
.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;
}
.filter-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.note-status {
padding: 0.25rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-draft { background: #f8f9fa; color: #6c757d; }
.status-in-progress { background: #fff3cd; color: #856404; }
.status-completed { background: #d4edda; color: #155724; }
.status-signed { background: #d1ecf1; color: #0c5460; }
.status-amended { background: #f8d7da; color: #721c24; }
.priority-badge {
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
}
.priority-low { background: #d4edda; color: #155724; }
.priority-medium { background: #fff3cd; color: #856404; }
.priority-high { background: #f8d7da; color: #721c24; }
.priority-critical { background: #f5c6cb; color: #721c24; }
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.stat-item {
padding: 1rem;
}
.stat-number {
font-size: 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 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item active">Surgical Notes</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-file-medical me-2"></i>Surgical Notes Management
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create Note
</a>
</div>
</div>
<!-- Statistics Cards -->
<div class="stats-grid">
<div class="stat-item">
<div class="stat-icon primary">
<i class="fas fa-file-medical"></i>
</div>
<div class="stat-number" id="total-notes">{{ stats.total_notes|default:0 }}</div>
<div class="stat-label">Total Notes</div>
</div>
<div class="stat-item">
<div class="stat-icon warning">
<i class="fas fa-edit"></i>
</div>
<div class="stat-number" id="draft-notes">{{ stats.draft_notes|default:0 }}</div>
<div class="stat-label">Draft Notes</div>
</div>
<div class="stat-item">
<div class="stat-icon success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-number" id="completed-notes">{{ stats.completed_notes|default:0 }}</div>
<div class="stat-label">Completed</div>
</div>
<div class="stat-item">
<div class="stat-icon info">
<i class="fas fa-signature"></i>
</div>
<div class="stat-number" id="signed-notes">{{ stats.signed_notes|default:0 }}</div>
<div class="stat-label">Signed</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="draft">Draft</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
<option value="signed">Signed</option>
<option value="amended">Amended</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Surgeon</label>
<select class="form-select" id="surgeon-filter">
<option value="">All Surgeons</option>
{% for surgeon in surgeons %}
<option value="{{ surgeon.id }}">{{ surgeon.get_full_name }}</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="week">This Week</option>
<option value="month">This Month</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Priority</label>
<select class="form-select" id="priority-filter">
<option value="">All Priorities</option>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
<option value="critical">Critical</option>
</select>
</div>
</div>
<div class="row mt-3" id="custom-date-range" style="display: none;">
<div class="col-md-6">
<label class="form-label">From Date</label>
<input type="date" class="form-control" id="from-date">
</div>
<div class="col-md-6">
<label class="form-label">To Date</label>
<input type="date" class="form-control" id="to-date">
</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, procedure, or note content...">
<button class="btn btn-outline-secondary" type="button" id="search-btn">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="col-md-4 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>
<!-- Notes 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>Surgical Notes
</h5>
<div class="btn-group">
<button class="btn btn-outline-success btn-sm" onclick="exportNotes('excel')">
<i class="fas fa-file-excel me-1"></i>Excel
</button>
<button class="btn btn-outline-danger btn-sm" onclick="exportNotes('pdf')">
<i class="fas fa-file-pdf me-1"></i>PDF
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="printNotes()">
<i class="fas fa-print me-1"></i>Print
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table id="notesTable" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th width="5%">
<input type="checkbox" id="select-all">
</th>
<th>Patient</th>
<th>Procedure</th>
<th>Surgeon</th>
<th>Date</th>
<th>Status</th>
<th>Priority</th>
<th>Last Modified</th>
<th width="15%">Actions</th>
</tr>
</thead>
<tbody>
{% for note in notes %}
<tr>
<td>
<input type="checkbox" class="note-checkbox" value="{{ note.id }}">
</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">{{ note.patient.get_full_name }}</div>
<small class="text-muted">ID: {{ note.patient.patient_id }}</small>
</div>
</div>
</td>
<td>
<div class="fw-bold">{{ note.procedure_name }}</div>
<small class="text-muted">{{ note.procedure_code|default:"" }}</small>
</td>
<td>
<div>{{ note.surgeon.get_full_name }}</div>
<small class="text-muted">{{ note.surgeon.specialization|default:"" }}</small>
</td>
<td>
<div>{{ note.surgery_date|date:"M d, Y" }}</div>
<small class="text-muted">{{ note.surgery_date|time:"g:i A" }}</small>
</td>
<td>
<span class="note-status status-{{ note.status }}">
{{ note.get_status_display }}
</span>
</td>
<td>
<span class="priority-badge priority-{{ note.priority }}">
{{ note.get_priority_display }}
</span>
</td>
<td>
<div>{{ note.updated_at|date:"M d, Y" }}</div>
<small class="text-muted">{{ note.updated_at|time:"g:i A" }}</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}"
class="btn btn-outline-primary" title="View">
<i class="fas fa-eye"></i>
</a>
{% if note.status != 'signed' %}
<a href="{% url 'operating_theatre:surgical_note_edit' note.pk %}"
class="btn btn-outline-warning" title="Edit">
<i class="fas fa-edit"></i>
</a>
{% endif %}
<button class="btn btn-outline-success"
onclick="printNote('{{ note.pk }}')" title="Print">
<i class="fas fa-print"></i>
</button>
{% if note.status != 'signed' %}
<a href="{% url 'operating_theatre:surgical_note_delete' note.pk %}"
class="btn btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="9" class="text-center py-4">
<i class="fas fa-file-medical fa-3x text-muted mb-3"></i>
<p class="text-muted">No surgical notes found</p>
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create First Note
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
const table = $('#notesTable').DataTable({
responsive: true,
pageLength: 25,
order: [[4, 'desc']], // Sort by date descending
columnDefs: [
{ orderable: false, targets: [0, 8] } // Disable sorting for checkbox and actions
],
language: {
search: "",
searchPlaceholder: "Search notes...",
lengthMenu: "Show _MENU_ notes per page",
info: "Showing _START_ to _END_ of _TOTAL_ notes",
infoEmpty: "No notes available",
infoFiltered: "(filtered from _MAX_ total notes)"
}
});
// Custom search
$('#search-input').on('keyup', function() {
table.search(this.value).draw();
});
// Select all checkbox
$('#select-all').on('change', function() {
$('.note-checkbox').prop('checked', this.checked);
});
// Date filter change
$('#date-filter').on('change', function() {
if ($(this).val() === 'custom') {
$('#custom-date-range').show();
} else {
$('#custom-date-range').hide();
}
});
// Load statistics
loadStats();
});
function loadStats() {
$.ajax({
url: '{% url "operating_theatre:surgical_note_stats" %}',
success: function(data) {
$('#total-notes').text(data.total_notes);
$('#draft-notes').text(data.draft_notes);
$('#completed-notes').text(data.completed_notes);
$('#signed-notes').text(data.signed_notes);
}
});
}
function applyFilters() {
const filters = {
status: $('#status-filter').val(),
surgeon: $('#surgeon-filter').val(),
date_range: $('#date-filter').val(),
priority: $('#priority-filter').val(),
from_date: $('#from-date').val(),
to_date: $('#to-date').val(),
search: $('#search-input').val()
};
// Apply filters via AJAX
$.ajax({
url: '{% url "operating_theatre:surgical_note_list" %}',
data: filters,
success: function(response) {
// Reload table data
location.reload();
}
});
}
function clearFilters() {
$('#status-filter').val('');
$('#surgeon-filter').val('');
$('#date-filter').val('');
$('#priority-filter').val('');
$('#from-date').val('');
$('#to-date').val('');
$('#search-input').val('');
$('#custom-date-range').hide();
// Clear DataTable search
$('#notesTable').DataTable().search('').draw();
}
function exportNotes(format) {
const selectedNotes = $('.note-checkbox:checked').map(function() {
return this.value;
}).get();
const params = new URLSearchParams({
format: format,
notes: selectedNotes.join(',')
});
window.open(`{% url "operating_theatre:surgical_note_export" %}?${params}`, '_blank');
}
function printNotes() {
const selectedNotes = $('.note-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedNotes.length === 0) {
alert('Please select notes to print');
return;
}
const params = new URLSearchParams({
notes: selectedNotes.join(',')
});
window.open(`{% url "operating_theatre:surgical_note_print" %}?${params}`, '_blank');
}
function printNote(noteId) {
window.open(`{% url "operating_theatre:surgical_note_print" %}?notes=${noteId}`, '_blank');
}
// Keyboard shortcuts
$(document).keydown(function(e) {
// Ctrl+N for new note
if (e.ctrlKey && e.keyCode === 78) {
e.preventDefault();
window.location.href = '{% url "operating_theatre:surgical_note_create" %}';
}
// Ctrl+F for search
if (e.ctrlKey && e.keyCode === 70) {
e.preventDefault();
$('#search-input').focus();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Confirm Delete Surgical Note Template{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_template_list' %}">Surgical Note Templates</a></li>
<li class="breadcrumb-item active">Confirm Delete</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-trash-alt text-danger me-2"></i>
Confirm Delete Surgical Note Template
<small class="text-muted ms-2">Permanently remove this reusable template</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-exclamation-triangle me-2"></i>Warning: This action cannot be undone!
</h4>
</div>
<div class="panel-body">
<div class="alert alert-danger mb-4">
<h4><i class="fas fa-exclamation-circle me-2"></i>Are you absolutely sure you want to delete this surgical note template?</h4>
<p class="mb-0">Deleting the template <strong>{{ object.name }}</strong> will permanently remove it from the system.</p>
<p class="mb-0">Any surgical notes created using this template will retain their content, but you will no longer be able to use this template for new notes.</p>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Template Name:</label>
<p class="form-control-static"><strong>{{ object.name }}</strong></p>
</div>
<div class="form-group mb-3">
<label class="form-label">Specialty:</label>
<p class="form-control-static">{{ object.specialty }}</p>
</div>
</div>
<div class="col-md-6">
<div class="form-group mb-3">
<label class="form-label">Procedure Type:</label>
<p class="form-control-static">{{ object.procedure_type }}</p>
</div>
<div class="form-group mb-3">
<label class="form-label">Last Updated:</label>
<p class="form-control-static">{{ object.updated_at|date:"M d, Y H:i" }}</p>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-between">
<a href="{% url 'operating_theatre:surgical_note_template_detail' object.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash-alt me-1"></i>Confirm Delete
</button>
</div>
</form>
</div>
</div>
<!-- END panel -->
{% endblock %}

View File

@ -0,0 +1,837 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Delete Template - {{ object.name }}{% endblock %}
{% block extra_css %}
<style>
.delete-confirmation {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin: 2rem auto;
max-width: 700px;
padding: 2rem;
}
.warning-icon {
align-items: center;
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
border-radius: 50%;
color: white;
display: flex;
font-size: 2.5rem;
height: 100px;
justify-content: center;
margin: 0 auto 1.5rem;
width: 100px;
}
.delete-title {
color: #dc3545;
font-size: 1.75rem;
font-weight: 700;
margin-bottom: 1rem;
text-align: center;
}
.delete-message {
color: #6c757d;
font-size: 1.1rem;
line-height: 1.6;
margin-bottom: 2rem;
text-align: center;
}
.template-details {
background: #f8f9fa;
border-left: 4px solid #dc3545;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.detail-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.detail-item {
background: white;
border-radius: 0.375rem;
padding: 1rem;
}
.detail-label {
color: #6c757d;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.5rem;
text-transform: uppercase;
}
.detail-value {
color: #212529;
font-size: 1rem;
font-weight: 600;
}
.usage-stats {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.stats-title {
color: #856404;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
.stats-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.stat-item {
background: white;
border-radius: 0.375rem;
padding: 1rem;
text-align: center;
}
.stat-number {
color: #856404;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.stat-label {
color: #856404;
font-size: 0.8rem;
font-weight: 500;
}
.impact-warning {
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.impact-title {
color: #721c24;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
.impact-list {
color: #721c24;
margin: 0;
padding-left: 1.5rem;
}
.impact-list li {
margin-bottom: 0.5rem;
}
.impact-list .critical {
font-weight: 600;
}
.dependent-templates {
background: #d1ecf1;
border: 1px solid #bee5eb;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.dependent-title {
color: #0c5460;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
.dependent-item {
align-items: center;
background: white;
border-radius: 0.375rem;
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
padding: 0.75rem 1rem;
}
.dependent-name {
color: #0c5460;
font-weight: 500;
}
.dependent-count {
background: #0c5460;
border-radius: 1rem;
color: white;
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
}
.confirmation-section {
background: #e2e3e5;
border-radius: 0.375rem;
margin-bottom: 2rem;
padding: 1.5rem;
}
.confirmation-title {
color: #495057;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
.confirmation-steps {
counter-reset: step-counter;
list-style: none;
padding: 0;
}
.confirmation-steps li {
align-items: flex-start;
counter-increment: step-counter;
display: flex;
margin-bottom: 1rem;
position: relative;
}
.confirmation-steps li::before {
align-items: center;
background: #6c757d;
border-radius: 50%;
color: white;
content: counter(step-counter);
display: flex;
flex-shrink: 0;
font-size: 0.8rem;
font-weight: 600;
height: 24px;
justify-content: center;
margin-right: 1rem;
width: 24px;
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 2rem;
}
.btn-delete {
background: linear-gradient(135deg, #dc3545, #c82333);
border: none;
color: white;
font-weight: 600;
padding: 0.75rem 2rem;
transition: all 0.3s ease;
}
.btn-delete:hover {
background: linear-gradient(135deg, #c82333, #bd2130);
color: white;
transform: translateY(-1px);
}
.btn-delete:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
}
.btn-cancel {
background: #6c757d;
border: none;
color: white;
font-weight: 500;
padding: 0.75rem 2rem;
transition: all 0.3s ease;
}
.btn-cancel:hover {
background: #5a6268;
color: white;
transform: translateY(-1px);
}
.security-notice {
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 0.375rem;
color: #155724;
font-size: 0.9rem;
margin-top: 1.5rem;
padding: 1rem;
text-align: center;
}
.audit-info {
background: #e2e3e5;
border-radius: 0.375rem;
color: #495057;
font-size: 0.85rem;
margin-top: 1rem;
padding: 0.75rem;
text-align: center;
}
.template-preview {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1.4;
max-height: 200px;
overflow-y: auto;
padding: 1rem;
white-space: pre-wrap;
}
@media (max-width: 768px) {
.delete-confirmation {
margin: 1rem;
padding: 1.5rem;
}
.action-buttons {
flex-direction: column;
}
.btn-delete,
.btn-cancel {
width: 100%;
}
.warning-icon {
height: 80px;
width: 80px;
font-size: 2rem;
}
.detail-grid,
.stats-grid {
grid-template-columns: 1fr;
}
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.countdown {
color: #dc3545;
font-size: 1.2rem;
font-weight: 700;
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="row">
<div class="col-12">
<div class="page-header">
<h1 class="page-header-title">
<i class="fas fa-trash-alt text-danger me-2"></i>
Delete Template
</h1>
<div class="page-header-subtitle">
Confirm deletion of surgical note template
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Card -->
<div class="delete-confirmation fade-in">
<div class="warning-icon pulse">
<i class="fas fa-exclamation-triangle"></i>
</div>
<h2 class="delete-title">Delete "{{ object.name }}"?</h2>
<p class="delete-message">
You are about to permanently delete this surgical note template. This action cannot be undone and may have significant impact on your hospital's documentation workflow.
</p>
<!-- Template Details -->
<div class="template-details">
<h5 class="mb-3">
<i class="fas fa-file-medical text-primary me-2"></i>
Template Information
</h5>
<div class="detail-grid">
<div class="detail-item">
<div class="detail-label">Template Name</div>
<div class="detail-value">{{ object.name }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Specialty</div>
<div class="detail-value">{{ object.get_specialty_display }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Created By</div>
<div class="detail-value">{{ object.created_by.get_full_name }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Created Date</div>
<div class="detail-value">{{ object.created_at|date:"F d, Y" }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Last Modified</div>
<div class="detail-value">{{ object.updated_at|date:"F d, Y g:i A" }}</div>
</div>
<div class="detail-item">
<div class="detail-label">Status</div>
<div class="detail-value">
{% if object.is_active %}
<span class="badge bg-success">Active</span>
{% elif object.is_draft %}
<span class="badge bg-warning">Draft</span>
{% else %}
<span class="badge bg-secondary">Archived</span>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Usage Statistics -->
<div class="usage-stats">
<h6 class="stats-title">
<i class="fas fa-chart-bar me-2"></i>
Usage Statistics
</h6>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">{{ object.usage_count|default:0 }}</div>
<div class="stat-label">Times Used</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ object.active_notes_count|default:0 }}</div>
<div class="stat-label">Active Notes</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ object.users_count|default:0 }}</div>
<div class="stat-label">Users</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ object.departments_count|default:0 }}</div>
<div class="stat-label">Departments</div>
</div>
</div>
</div>
<!-- Impact Warning -->
<div class="impact-warning">
<h6 class="impact-title">
<i class="fas fa-exclamation-circle me-2"></i>
Deletion Impact
</h6>
<ul class="impact-list">
<li class="critical">All template content and structure will be permanently lost</li>
<li>{{ object.usage_count|default:0 }} existing surgical notes using this template will lose their template reference</li>
<li>Users who have bookmarked this template will lose access</li>
<li>Template sharing permissions will be revoked</li>
<li>Version history and audit trail will be preserved for compliance</li>
{% if object.is_active %}
<li class="critical"><strong>Warning:</strong> This is an active template currently in use</li>
{% endif %}
{% if object.usage_count > 10 %}
<li class="critical"><strong>High Impact:</strong> This template is heavily used ({{ object.usage_count }} times)</li>
{% endif %}
</ul>
</div>
<!-- Dependent Templates -->
{% if object.dependent_templates.exists %}
<div class="dependent-templates">
<h6 class="dependent-title">
<i class="fas fa-link me-2"></i>
Dependent Templates
</h6>
<p class="mb-3">The following templates are based on this template and may be affected:</p>
{% for dependent in object.dependent_templates.all %}
<div class="dependent-item">
<span class="dependent-name">{{ dependent.name }}</span>
<span class="dependent-count">{{ dependent.usage_count|default:0 }} uses</span>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Template Preview -->
<div class="mb-3">
<h6 class="mb-2">
<i class="fas fa-eye me-2"></i>
Template Preview (Last 10 lines)
</h6>
<div class="template-preview">
{{ object.content|default:"No content available"|truncatewords:50 }}
---
Template Fields: {{ object.field_count|default:0 }}
Last Modified: {{ object.updated_at|date:"F d, Y g:i A" }}
Version: {{ object.version|default:"1.0" }}
</div>
</div>
<!-- Confirmation Steps -->
<div class="confirmation-section">
<h6 class="confirmation-title">
<i class="fas fa-clipboard-check me-2"></i>
Before You Delete
</h6>
<ol class="confirmation-steps">
<li>
<div>
<strong>Backup Important Data</strong><br>
<small class="text-muted">Export the template if you might need it later</small>
</div>
</li>
<li>
<div>
<strong>Notify Affected Users</strong><br>
<small class="text-muted">Inform users who regularly use this template</small>
</div>
</li>
<li>
<div>
<strong>Check Dependencies</strong><br>
<small class="text-muted">Ensure no critical workflows depend on this template</small>
</div>
</li>
<li>
<div>
<strong>Provide Alternative</strong><br>
<small class="text-muted">Suggest alternative templates for users</small>
</div>
</li>
</ol>
</div>
<!-- Confirmation Form -->
<form method="post" id="deleteForm">
{% csrf_token %}
<!-- Confirmation Checkboxes -->
<div class="mb-3">
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="confirmBackup" required>
<label class="form-check-label" for="confirmBackup">
I have backed up any important data from this template
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="confirmNotification" required>
<label class="form-check-label" for="confirmNotification">
I have notified affected users about this deletion
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" id="confirmImpact" required>
<label class="form-check-label" for="confirmImpact">
I understand the impact of deleting this template
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="confirmPermanent" required>
<label class="form-check-label" for="confirmPermanent">
I understand this action is permanent and cannot be undone
</label>
</div>
</div>
<!-- Deletion Reason -->
<div class="mb-3">
<label for="deletionReason" class="form-label">
<i class="fas fa-comment me-2"></i>Reason for Deletion (Required)
</label>
<textarea class="form-control" id="deletionReason" name="deletion_reason" rows="3"
placeholder="Please provide a detailed reason for deleting this template..." required></textarea>
<div class="form-text">This information will be logged for audit purposes</div>
</div>
<!-- Type Template Name Confirmation -->
<div class="mb-3">
<label for="templateNameConfirm" class="form-label">
<i class="fas fa-keyboard me-2"></i>Type the template name to confirm deletion
</label>
<input type="text" class="form-control" id="templateNameConfirm"
placeholder="Type: {{ object.name }}" required>
<div class="form-text">Type the exact template name: <strong>{{ object.name }}</strong></div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<a href="{% url 'operating_theatre:surgical_note_template_detail' object.pk %}" class="btn btn-cancel">
<i class="fas fa-arrow-left me-2"></i>Cancel
</a>
<button type="submit" class="btn btn-delete" id="confirmDeleteBtn" disabled>
<i class="fas fa-trash-alt me-2"></i>Delete Template Permanently
</button>
</div>
</form>
<!-- Security Notice -->
<div class="security-notice">
<i class="fas fa-shield-alt me-2"></i>
<strong>Security Notice:</strong> This action will be logged and may be subject to review by hospital administration. The deletion will be recorded in the audit trail with your user information and timestamp.
</div>
<!-- Audit Information -->
<div class="audit-info">
<i class="fas fa-info-circle me-2"></i>
Deletion will be performed by: <strong>{{ user.get_full_name }}</strong> ({{ user.username }})
on <span id="currentDateTime"></span>
</div>
</div>
</div>
</div>
<!-- Final Confirmation Modal -->
<div class="modal fade" id="finalConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2"></i>
Final Warning
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<div class="mb-3">
<i class="fas fa-trash-alt text-danger" style="font-size: 4rem;"></i>
</div>
<h4 class="text-danger">This is your final warning!</h4>
<p class="mb-3">You are about to permanently delete the template:</p>
<div class="alert alert-danger">
<strong>"{{ object.name }}"</strong>
</div>
<p class="mb-0">This action cannot be undone. Are you absolutely certain?</p>
<div class="mt-3">
<div class="countdown" id="countdown">5</div>
<small class="text-muted">Please wait <span id="countdownText">5</span> seconds before confirming</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-1"></i>Cancel
</button>
<button type="button" class="btn btn-danger" id="finalDeleteBtn" disabled>
<i class="fas fa-trash-alt me-1"></i>Yes, Delete Forever
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Update current date/time
$('#currentDateTime').text(new Date().toLocaleString());
// Enable/disable delete button based on form validation
function validateForm() {
const allChecked = $('#confirmBackup, #confirmNotification, #confirmImpact, #confirmPermanent').length ===
$('#confirmBackup:checked, #confirmNotification:checked, #confirmImpact:checked, #confirmPermanent:checked').length;
const hasReason = $('#deletionReason').val().trim().length > 10;
const nameMatches = $('#templateNameConfirm').val() === '{{ object.name }}';
$('#confirmDeleteBtn').prop('disabled', !(allChecked && hasReason && nameMatches));
}
// Validate form on input changes
$('#confirmBackup, #confirmNotification, #confirmImpact, #confirmPermanent, #deletionReason, #templateNameConfirm').on('change input', validateForm);
// Show final confirmation modal
$('#deleteForm').on('submit', function(e) {
e.preventDefault();
$('#finalConfirmModal').modal('show');
startCountdown();
});
// Handle final deletion
$('#finalDeleteBtn').on('click', function() {
// Show loading state
$(this).html('<i class="fas fa-spinner fa-spin me-1"></i>Deleting...');
$(this).prop('disabled', true);
// Add deletion metadata
const form = $('#deleteForm');
form.append('<input type="hidden" name="deleted_by" value="{{ user.id }}">');
form.append('<input type="hidden" name="deleted_at" value="' + new Date().toISOString() + '">');
form.append('<input type="hidden" name="confirmed" value="true">');
// Submit the form
setTimeout(function() {
form.off('submit').submit();
}, 1000);
});
// Auto-resize textarea
$('#deletionReason').on('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
// Character counter for deletion reason
$('#deletionReason').on('input', function() {
const length = $(this).val().length;
const minLength = 10;
if (!$('#charCounter').length) {
$(this).after('<small id="charCounter" class="form-text"></small>');
}
if (length < minLength) {
$('#charCounter').text(`${length}/${minLength} characters minimum`).addClass('text-danger');
$(this).addClass('is-invalid');
} else {
$('#charCounter').text(`${length} characters`).removeClass('text-danger').addClass('text-success');
$(this).removeClass('is-invalid').addClass('is-valid');
}
});
// Template name validation with real-time feedback
$('#templateNameConfirm').on('input', function() {
const entered = $(this).val();
const required = '{{ object.name }}';
if (entered === required) {
$(this).removeClass('is-invalid').addClass('is-valid');
} else {
$(this).removeClass('is-valid').addClass('is-invalid');
}
});
// Prevent accidental navigation
let formSubmitted = false;
$('#deleteForm').on('submit', function() {
formSubmitted = true;
});
$(window).on('beforeunload', function(e) {
if (!formSubmitted && ($('#deletionReason').val().trim().length > 0 || $('#templateNameConfirm').val().length > 0)) {
const message = 'You have unsaved changes. Are you sure you want to leave?';
e.returnValue = message;
return message;
}
});
// Focus management
$('#templateNameConfirm').on('focus', function() {
$(this).attr('placeholder', 'Type exactly: {{ object.name }}');
});
$('#templateNameConfirm').on('blur', function() {
$(this).attr('placeholder', 'Type: {{ object.name }}');
});
function startCountdown() {
let count = 5;
const countdownEl = $('#countdown');
const countdownTextEl = $('#countdownText');
const finalBtn = $('#finalDeleteBtn');
const timer = setInterval(function() {
count--;
countdownEl.text(count);
countdownTextEl.text(count);
if (count <= 0) {
clearInterval(timer);
countdownEl.parent().hide();
finalBtn.prop('disabled', false).removeClass('btn-secondary').addClass('btn-danger');
}
}, 1000);
}
// Add warning sound effect
function playWarningSound() {
if (typeof(AudioContext) !== "undefined" || typeof(webkitAudioContext) !== "undefined") {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
oscillator.type = 'sine';
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
}
}
// Play warning sound when final modal opens
$('#finalConfirmModal').on('shown.bs.modal', function() {
playWarningSound();
});
// Export template before deletion
window.exportBeforeDelete = function() {
window.location.href = '{% url "operating_theatre:surgical_note_template_export" object.pk %}';
};
// Add export button dynamically
$('.action-buttons').prepend(`
<button type="button" class="btn btn-outline-info" onclick="exportBeforeDelete()">
<i class="fas fa-download me-2"></i>Export First
</button>
`);
});
</script>
{% endblock %}

View File

@ -0,0 +1,136 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Surgical Note Template Details{% endblock %}
{% block extra_css %}
<style>
.detail-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
.detail-section h5 {
color: #007bff;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_template_list' %}">Surgical Note Templates</a></li>
<li class="breadcrumb-item active">Template Details</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-info-circle text-primary me-2"></i>
Surgical Note Template Details
<small class="text-muted ms-2">Comprehensive view of surgical note template</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-file-alt me-2"></i>Template: {{ object.name }}
</h4>
<div class="panel-heading-btn">
<a href="{% url 'operating_theatre:surgical_note_template_edit' object.pk %}" class="btn btn-warning btn-sm me-2">
<i class="fas fa-edit me-1"></i>Edit Template
</a>
<a href="{% url 'operating_theatre:surgical_note_template_delete' object.pk %}" class="btn btn-danger btn-sm me-2">
<i class="fas fa-trash me-1"></i>Delete Template
</a>
<a href="{% url 'operating_theatre:surgical_note_template_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-12">
<!-- Template Information -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-info-circle me-2"></i>Template Information</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Template Name:</strong></div>
<div class="col-md-8">{{ object.name }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Specialty:</strong></div>
<div class="col-md-8">{{ object.specialty }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Procedure Type:</strong></div>
<div class="col-md-8">{{ object.procedure_type }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Description:</strong></div>
<div class="col-md-8">{{ object.description|default:\'N/A\'|linebreaksbr }}</div>
</div>
</div>
<!-- Template Content -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-file-medical me-2"></i>Template Content</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Pre-operative Diagnosis:</strong></div>
<div class="col-md-8">{{ object.pre_operative_diagnosis|default:\'N/A\'|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Anesthesia Type:</strong></div>
<div class="col-md-8">{{ object.anesthesia_type|default:
\'N/A\' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Post-operative Diagnosis:</strong></div>
<div class="col-md-8">{{ object.post_operative_diagnosis|default:
\'N/A\'|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Procedure Description:</strong></div>
<div class="col-md-8">{{ object.procedure_description|default:
\'N/A\'|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Post-operative Orders:</strong></div>
<div class="col-md-8">{{ object.post_operative_orders|default:
\'N/A\'|linebreaksbr }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Follow-up Plan:</strong></div>
<div class="col-md-8">{{ object.follow_up_plan|default:
\'N/A\'|linebreaksbr }}</div>
</div>
</div>
<!-- Metadata -->
<div class="detail-section mb-4">
<h5 class="mb-3"><i class="fas fa-database me-2"></i>Metadata</h5>
<div class="row mb-2">
<div class="col-md-4"><strong>Created At:</strong></div>
<div class="col-md-8">{{ object.created_at|date:"M d, Y H:i" }} by {{ object.created_by.get_full_name|default:
\'N/A\' }}</div>
</div>
<div class="row mb-2">
<div class="col-md-4"><strong>Last Updated:</strong></div>
<div class="col-md-8">{{ object.updated_at|date:"M d, Y H:i" }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
{% endblock %}

View File

@ -0,0 +1,937 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}{{ object.name }} - Template Details{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/prismjs/themes/prism.min.css' %}" rel="stylesheet" />
<style>
.template-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 0.5rem;
color: white;
margin-bottom: 2rem;
padding: 2rem;
}
.template-title {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.template-subtitle {
font-size: 1.1rem;
margin-bottom: 1rem;
opacity: 0.9;
}
.template-meta {
display: flex;
flex-wrap: wrap;
gap: 2rem;
}
.meta-item {
align-items: center;
display: flex;
gap: 0.5rem;
}
.meta-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.info-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: 2fr 1fr;
margin-bottom: 2rem;
}
.info-card {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
}
.card-header {
border-bottom: 2px solid #f8f9fa;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
}
.card-title {
color: #495057;
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.template-content {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.6;
max-height: 400px;
overflow-y: auto;
padding: 1rem;
white-space: pre-wrap;
}
.template-fields {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.field-item {
background: #f8f9fa;
border-left: 4px solid #007bff;
border-radius: 0.375rem;
padding: 1rem;
}
.field-name {
color: #495057;
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.field-type {
background: #e9ecef;
border-radius: 0.25rem;
color: #6c757d;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.field-description {
color: #6c757d;
font-size: 0.85rem;
margin-top: 0.5rem;
}
.stats-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
margin-bottom: 1.5rem;
}
.stat-item {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
text-align: center;
}
.stat-number {
color: #495057;
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6c757d;
font-size: 0.9rem;
font-weight: 500;
}
.usage-chart {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
padding: 1.5rem;
}
.chart-container {
height: 300px;
position: relative;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.template-tag {
background: #e9ecef;
border-radius: 1rem;
color: #495057;
font-size: 0.8rem;
padding: 0.5rem 1rem;
}
.template-tag.primary { background: #cce5ff; color: #0056b3; }
.template-tag.success { background: #d4edda; color: #155724; }
.template-tag.warning { background: #fff3cd; color: #856404; }
.template-tag.info { background: #d1ecf1; color: #0c5460; }
.action-buttons {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
margin-bottom: 2rem;
padding: 1.5rem;
}
.btn-action {
border-radius: 0.375rem;
font-weight: 500;
padding: 0.75rem 1.5rem;
transition: all 0.3s ease;
}
.btn-action:hover {
transform: translateY(-2px);
}
.version-history {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
padding: 1.5rem;
}
.version-item {
border-left: 3px solid #dee2e6;
margin-bottom: 1rem;
padding-left: 1rem;
}
.version-item.current {
border-left-color: #28a745;
}
.version-header {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.version-number {
color: #495057;
font-weight: 600;
}
.version-date {
color: #6c757d;
font-size: 0.85rem;
}
.version-changes {
color: #6c757d;
font-size: 0.9rem;
line-height: 1.5;
}
.preview-section {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
padding: 1.5rem;
}
.preview-container {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
max-height: 500px;
overflow-y: auto;
padding: 1.5rem;
}
.preview-form {
background: white;
border-radius: 0.375rem;
padding: 1.5rem;
}
.form-section {
border-bottom: 1px solid #dee2e6;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
}
.form-section:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.section-title {
color: #495057;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
color: #495057;
font-weight: 500;
margin-bottom: 0.5rem;
}
.form-control-preview {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
color: #6c757d;
padding: 0.5rem 0.75rem;
}
@media (max-width: 768px) {
.template-header {
padding: 1.5rem;
text-align: center;
}
.template-meta {
flex-direction: column;
gap: 1rem;
}
.info-grid {
grid-template-columns: 1fr;
}
.action-buttons {
flex-direction: column;
}
.btn-action {
width: 100%;
}
.template-fields {
grid-template-columns: 1fr;
}
}
.fade-in {
animation: fadeIn 0.6s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.code-block {
background: #2d3748;
border-radius: 0.375rem;
color: #e2e8f0;
font-family: 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.5;
overflow-x: auto;
padding: 1rem;
}
.highlight {
background: #fff3cd;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Template Header -->
<div class="template-header fade-in">
<div class="row align-items-center">
<div class="col-md-8">
<h1 class="template-title">{{ object.name }}</h1>
<p class="template-subtitle">{{ object.description }}</p>
<div class="template-meta">
<div class="meta-item">
<div class="meta-icon">
<i class="fas fa-stethoscope"></i>
</div>
<div>
<div class="fw-bold">{{ object.get_specialty_display }}</div>
<small>Specialty</small>
</div>
</div>
<div class="meta-item">
<div class="meta-icon">
<i class="fas fa-user"></i>
</div>
<div>
<div class="fw-bold">{{ object.created_by.get_full_name }}</div>
<small>Created by</small>
</div>
</div>
<div class="meta-item">
<div class="meta-icon">
<i class="fas fa-calendar"></i>
</div>
<div>
<div class="fw-bold">{{ object.created_at|date:"M d, Y" }}</div>
<small>Created</small>
</div>
</div>
<div class="meta-item">
<div class="meta-icon">
<i class="fas fa-chart-line"></i>
</div>
<div>
<div class="fw-bold">{{ object.usage_count|default:0 }}</div>
<small>Times Used</small>
</div>
</div>
</div>
</div>
<div class="col-md-4 text-md-end">
{% if object.is_active %}
<span class="badge bg-success fs-6 mb-2">
<i class="fas fa-check-circle me-1"></i>Active
</span>
{% elif object.is_draft %}
<span class="badge bg-warning fs-6 mb-2">
<i class="fas fa-edit me-1"></i>Draft
</span>
{% else %}
<span class="badge bg-secondary fs-6 mb-2">
<i class="fas fa-archive me-1"></i>Archived
</span>
{% endif %}
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons fade-in">
<a href="{% url 'operating_theatre:surgical_note_create' %}?template={{ object.id }}"
class="btn btn-primary btn-action">
<i class="fas fa-plus me-2"></i>Use This Template
</a>
<a href="{% url 'operating_theatre:surgical_note_template_edit' object.pk %}"
class="btn btn-outline-primary btn-action">
<i class="fas fa-edit me-2"></i>Edit Template
</a>
<button class="btn btn-outline-info btn-action" onclick="duplicateTemplate()">
<i class="fas fa-copy me-2"></i>Duplicate
</button>
<button class="btn btn-outline-success btn-action" onclick="exportTemplate()">
<i class="fas fa-download me-2"></i>Export
</button>
<button class="btn btn-outline-warning btn-action" onclick="shareTemplate()">
<i class="fas fa-share me-2"></i>Share
</button>
<a href="{% url 'operating_theatre:surgical_note_template_list' %}"
class="btn btn-outline-secondary btn-action">
<i class="fas fa-arrow-left me-2"></i>Back to Templates
</a>
</div>
<!-- Statistics -->
<div class="stats-grid fade-in">
<div class="stat-item">
<div class="stat-number">{{ object.usage_count|default:0 }}</div>
<div class="stat-label">Total Usage</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ object.field_count|default:0 }}</div>
<div class="stat-label">Form Fields</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ object.version|default:"1.0" }}</div>
<div class="stat-label">Version</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ object.rating|default:"4.5" }}</div>
<div class="stat-label">Rating</div>
</div>
</div>
<!-- Main Content Grid -->
<div class="info-grid fade-in">
<!-- Template Details -->
<div class="info-card">
<div class="card-header">
<h4 class="card-title">
<i class="fas fa-info-circle text-primary me-2"></i>
Template Details
</h4>
</div>
<!-- Tags -->
<div class="mb-3">
<h6 class="mb-2">Tags</h6>
<div class="tags-container">
{% for tag in object.tags.all %}
<span class="template-tag primary">{{ tag.name }}</span>
{% empty %}
<span class="template-tag">General</span>
<span class="template-tag">Surgery</span>
<span class="template-tag">Documentation</span>
{% endfor %}
</div>
</div>
<!-- Description -->
<div class="mb-3">
<h6 class="mb-2">Description</h6>
<p class="text-muted">{{ object.description|default:"No description provided." }}</p>
</div>
<!-- Template Fields -->
<div class="mb-3">
<h6 class="mb-2">Template Fields</h6>
<div class="template-fields">
<div class="field-item">
<div class="field-name">Patient Information</div>
<span class="field-type">Required</span>
<div class="field-description">Basic patient demographics and identifiers</div>
</div>
<div class="field-item">
<div class="field-name">Procedure Details</div>
<span class="field-type">Required</span>
<div class="field-description">Surgical procedure name and codes</div>
</div>
<div class="field-item">
<div class="field-name">Preoperative Diagnosis</div>
<span class="field-type">Required</span>
<div class="field-description">Diagnosis before surgery</div>
</div>
<div class="field-item">
<div class="field-name">Postoperative Diagnosis</div>
<span class="field-type">Required</span>
<div class="field-description">Diagnosis after surgery</div>
</div>
<div class="field-item">
<div class="field-name">Operative Technique</div>
<span class="field-type">Required</span>
<div class="field-description">Detailed surgical procedure description</div>
</div>
<div class="field-item">
<div class="field-name">Complications</div>
<span class="field-type">Optional</span>
<div class="field-description">Any complications during surgery</div>
</div>
</div>
</div>
<!-- Template Content -->
<div class="mb-3">
<h6 class="mb-2">Template Structure</h6>
<div class="template-content">
SURGICAL NOTE TEMPLATE - {{ object.name|upper }}
PATIENT INFORMATION:
- Name: [Patient Name]
- DOB: [Date of Birth]
- MRN: [Medical Record Number]
- Date of Surgery: [Surgery Date]
PROCEDURE INFORMATION:
- Primary Surgeon: [Surgeon Name]
- Assistant Surgeon(s): [Assistant Names]
- Anesthesiologist: [Anesthesiologist Name]
- Anesthesia Type: [Anesthesia Type]
DIAGNOSES:
- Preoperative Diagnosis: [Pre-op Diagnosis]
- Postoperative Diagnosis: [Post-op Diagnosis]
PROCEDURE PERFORMED:
[Procedure Name and Details]
OPERATIVE TECHNIQUE:
[Detailed description of surgical technique and steps]
FINDINGS:
[Surgical findings and observations]
COMPLICATIONS:
[Any complications encountered]
ESTIMATED BLOOD LOSS: [Amount] mL
SPECIMENS SENT:
[Specimens sent to pathology]
POSTOPERATIVE INSTRUCTIONS:
[Post-operative care instructions]
SURGEON SIGNATURE: ________________________
Date: _______________
</div>
</div>
</div>
<!-- Sidebar -->
<div>
<!-- Usage Chart -->
<div class="usage-chart">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-chart-bar text-success me-2"></i>
Usage Statistics
</h5>
</div>
<div class="chart-container">
<canvas id="usageChart"></canvas>
</div>
</div>
<!-- Version History -->
<div class="version-history">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-history text-info me-2"></i>
Version History
</h5>
</div>
<div class="version-item current">
<div class="version-header">
<span class="version-number">v{{ object.version|default:"1.0" }}</span>
<span class="version-date">{{ object.updated_at|date:"M d, Y" }}</span>
</div>
<div class="version-changes">
Current version - {{ object.description|truncatewords:10 }}
</div>
</div>
<div class="version-item">
<div class="version-header">
<span class="version-number">v0.9</span>
<span class="version-date">{{ object.created_at|date:"M d, Y" }}</span>
</div>
<div class="version-changes">
Initial template creation with basic fields
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="info-card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-tachometer-alt text-warning me-2"></i>
Quick Stats
</h5>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-2">
<span>Completion Rate</span>
<span class="fw-bold">95%</span>
</div>
<div class="progress">
<div class="progress-bar bg-success" style="width: 95%"></div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-2">
<span>User Satisfaction</span>
<span class="fw-bold">4.8/5</span>
</div>
<div class="progress">
<div class="progress-bar bg-info" style="width: 96%"></div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between mb-2">
<span>Error Rate</span>
<span class="fw-bold">2%</span>
</div>
<div class="progress">
<div class="progress-bar bg-danger" style="width: 2%"></div>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">Based on last 30 days usage</small>
</div>
</div>
</div>
</div>
<!-- Template Preview -->
<div class="preview-section fade-in">
<div class="card-header">
<h4 class="card-title">
<i class="fas fa-eye text-primary me-2"></i>
Template Preview
</h4>
</div>
<div class="preview-container">
<div class="preview-form">
<div class="form-section">
<h5 class="section-title">Patient Information</h5>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Patient Name</label>
<input type="text" class="form-control form-control-preview"
placeholder="Enter patient name" readonly>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Medical Record Number</label>
<input type="text" class="form-control form-control-preview"
placeholder="Enter MRN" readonly>
</div>
</div>
</div>
</div>
<div class="form-section">
<h5 class="section-title">Procedure Details</h5>
<div class="form-group">
<label class="form-label">Procedure Name</label>
<input type="text" class="form-control form-control-preview"
placeholder="Enter procedure name" readonly>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Primary Surgeon</label>
<select class="form-control form-control-preview" disabled>
<option>Select surgeon...</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label">Surgery Date</label>
<input type="date" class="form-control form-control-preview" readonly>
</div>
</div>
</div>
</div>
<div class="form-section">
<h5 class="section-title">Surgical Documentation</h5>
<div class="form-group">
<label class="form-label">Operative Technique</label>
<textarea class="form-control form-control-preview" rows="4"
placeholder="Describe the surgical technique..." readonly></textarea>
</div>
<div class="form-group">
<label class="form-label">Findings</label>
<textarea class="form-control form-control-preview" rows="3"
placeholder="Document surgical findings..." readonly></textarea>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Share Template Modal -->
<div class="modal fade" id="shareTemplateModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-share me-2"></i>Share Template
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Share with Users</label>
<select class="form-select" multiple>
<option>Dr. John Smith</option>
<option>Dr. Sarah Johnson</option>
<option>Dr. Michael Brown</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">Share with Departments</label>
<select class="form-select" multiple>
<option>General Surgery</option>
<option>Cardiac Surgery</option>
<option>Orthopedic Surgery</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="allowEditing">
<label class="form-check-label" for="allowEditing">
Allow recipients to edit template
</label>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary">Share Template</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/chart.js/dist/chart.min.js' %}"></script>
<script src="{% static 'assets/plugins/prismjs/components/prism-core.min.js' %}"></script>
<script src="{% static 'assets/plugins/prismjs/plugins/autoloader/prism-autoloader.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize usage chart
const ctx = document.getElementById('usageChart').getContext('2d');
new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Template Usage',
data: [12, 19, 8, 15, 22, 18],
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
grid: {
color: '#f8f9fa'
}
},
x: {
grid: {
color: '#f8f9fa'
}
}
}
}
});
// Smooth scrolling for anchor links
$('a[href^="#"]').on('click', function(e) {
e.preventDefault();
const target = $($(this).attr('href'));
if (target.length) {
$('html, body').animate({
scrollTop: target.offset().top - 100
}, 500);
}
});
});
function duplicateTemplate() {
if (confirm('Are you sure you want to duplicate this template?')) {
$.ajax({
url: `{% url 'operating_theatre:surgical_note_template_duplicate' object.pk %}`,
method: 'POST',
headers: {
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
},
success: function(response) {
showAlert('Template duplicated successfully!', 'success');
setTimeout(() => {
window.location.href = response.redirect_url;
}, 1500);
},
error: function() {
showAlert('Error duplicating template.', 'error');
}
});
}
}
function exportTemplate() {
window.location.href = `{% url 'operating_theatre:surgical_note_template_export' object.pk %}`;
}
function shareTemplate() {
$('#shareTemplateModal').modal('show');
}
function showAlert(message, type) {
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>
`;
$('.container-fluid').prepend(alertHtml);
setTimeout(() => $('.alert').fadeOut(), 5000);
}
// Print functionality
function printTemplate() {
window.print();
}
// Copy template URL
function copyTemplateUrl() {
navigator.clipboard.writeText(window.location.href).then(function() {
showAlert('Template URL copied to clipboard!', 'success');
});
}
// Add keyboard shortcuts
$(document).keydown(function(e) {
// Ctrl+E to edit
if (e.ctrlKey && e.keyCode === 69) {
e.preventDefault();
window.location.href = '{% url "operating_theatre:surgical_note_template_edit" object.pk %}';
}
// Ctrl+D to duplicate
if (e.ctrlKey && e.keyCode === 68) {
e.preventDefault();
duplicateTemplate();
}
// Ctrl+P to print
if (e.ctrlKey && e.keyCode === 80) {
e.preventDefault();
printTemplate();
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,207 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{% if object %}Edit Surgical Note Template{% else %}Create Surgical Note Template{% endif %}{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<style>
.form-section {
border-left: 4px solid #007bff;
padding-left: 1rem;
margin-bottom: 2rem;
}
.form-section h5 {
color: #007bff;
}
.required-field::after {
content: " *";
color: #dc3545;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_template_list' %}">Surgical Note Templates</a></li>
<li class="breadcrumb-item active">{% if object %}Edit{% else %}Create{% endif %}</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-{% if object %}edit{% else %}plus{% endif %} text-primary me-2"></i>
{% if object %}Edit Surgical Note Template{% else %}Create New Surgical Note Template{% endif %}
<small class="text-muted ms-2">{% if object %}Update existing template{% else %}Define a new reusable template for surgical notes{% endif %}</small>
</h1>
<!-- END page-header -->
<!-- BEGIN form panel -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-file-medical me-2"></i>Template Details
</h4>
<div class="panel-heading-btn">
<a href="{% url 'operating_theatre:surgical_note_template_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-arrow-left me-1"></i>Back to List
</a>
</div>
</div>
<div class="panel-body">
<form method="post" id="surgical-note-template-form" novalidate>
{% csrf_token %}
<!-- Template Information -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-info-circle me-2"></i>Template Information</h5>
<div class="mb-3">
<label class="form-label required-field">Template Name</label>
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">{{ form.name.errors.0 }}</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Specialty</label>
{{ form.specialty }}
{% if form.specialty.errors %}
<div class="invalid-feedback d-block">{{ form.specialty.errors.0 }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Procedure Type</label>
{{ form.procedure_type }}
{% if form.procedure_type.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_type.errors.0 }}</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">{{ form.description.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Template Content -->
<div class="form-section">
<h5 class="mb-3"><i class="fas fa-file-medical me-2"></i>Template Content (Pre-fill fields for new notes)</h5>
<div class="mb-3">
<label class="form-label">Pre-operative Diagnosis</label>
{{ form.pre_operative_diagnosis }}
{% if form.pre_operative_diagnosis.errors %}
<div class="invalid-feedback d-block">{{ form.pre_operative_diagnosis.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Anesthesia Type</label>
{{ form.anesthesia_type }}
{% if form.anesthesia_type.errors %}
<div class="invalid-feedback d-block">{{ form.anesthesia_type.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Post-operative Diagnosis</label>
{{ form.post_operative_diagnosis }}
{% if form.post_operative_diagnosis.errors %}
<div class="invalid-feedback d-block">{{ form.post_operative_diagnosis.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Procedure Description</label>
{{ form.procedure_description }}
{% if form.procedure_description.errors %}
<div class="invalid-feedback d-block">{{ form.procedure_description.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Post-operative Orders</label>
{{ form.post_operative_orders }}
{% if form.post_operative_orders.errors %}
<div class="invalid-feedback d-block">{{ form.post_operative_orders.errors.0 }}</div>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label">Follow-up Plan</label>
{{ form.follow_up_plan }}
{% if form.follow_up_plan.errors %}
<div class="invalid-feedback d-block">{{ form.follow_up_plan.errors.0 }}</div>
{% endif %}
</div>
</div>
<!-- Form Actions -->
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between">
<a href="{% url 'operating_theatre:surgical_note_template_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-1"></i>
{% if object %}Update Template{% else %}Create Template{% endif %}
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<!-- END form panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Select2 for dropdowns
$(".form-select").select2({
theme: "bootstrap-5",
width: "100%"
});
// Form validation (basic example, Django's form validation is primary)
$("#surgical-note-template-form").submit(function(e) {
let isValid = true;
$(this).find("[required]").each(function() {
if (!$(this).val()) {
$(this).addClass("is-invalid");
isValid = false;
} else {
$(this).removeClass("is-invalid");
}
});
if (!isValid) {
e.preventDefault();
alert("Please fill in all required fields.");
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,135 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Surgical Note Templates{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
<li class="breadcrumb-item active">Surgical Note Templates</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-file-alt text-primary me-2"></i>
Surgical Note Templates
<small class="text-muted ms-2">Manage reusable templates for surgical notes</small>
</h1>
<!-- END page-header -->
<!-- BEGIN panel -->
<div class="panel panel-inverse">
<!-- BEGIN panel-heading -->
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-list me-2"></i>Available Templates
</h4>
<div class="panel-heading-btn">
<a href="{% url 'operating_theatre:surgical_note_template_create' %}" class="btn btn-primary btn-sm">
<i class="fas fa-plus me-1"></i>Add New Template
</a>
</div>
</div>
<!-- END panel-heading -->
<!-- BEGIN panel-body -->
<div class="panel-body">
<div class="table-responsive">
<table id="data-table-default" class="table table-striped table-bordered align-middle">
<thead class="table-dark">
<tr>
<th width="1%">#</th>
<th width="30%">Template Name</th>
<th width="20%">Specialty</th>
<th width="20%">Procedure Type</th>
<th width="15%">Last Updated</th>
<th width="14%">Actions</th>
</tr>
</thead>
<tbody>
{% for template in object_list %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ template.name }}</td>
<td>{{ template.specialty }}</td>
<td>{{ template.procedure_type }}</td>
<td>{{ template.updated_at|date:"M d, Y H:i" }}</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'operating_theatre:surgical_note_template_edit' template.pk %}"
class="btn btn-outline-warning btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
<a href="{% url 'operating_theatre:surgical_note_template_delete' template.pk %}"
class="btn btn-outline-danger btn-sm" title="Delete">
<i class="fas fa-trash"></i>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-4">
<div class="text-muted">
<i class="fas fa-folder-open fa-3x mb-3"></i>
<p class="mb-0">No surgical note templates found.</p>
<a href="{% url 'operating_theatre:surgical_note_template_create' %}" class="btn btn-primary mt-2">
<i class="fas fa-plus me-1"></i>Create First Template
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- END panel-body -->
</div>
<!-- END panel -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons/js/buttons.html5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-buttons/js/buttons.print.min.js' %}"></script>
<script>
$(document).ready(function() {
$('#data-table-default').DataTable({
responsive: true,
dom: 'Bfrtip',
buttons: [
'copy', 'csv', 'excel', 'pdf', 'print'
],
pageLength: 10,
language: {
search: "Search templates:",
lengthMenu: "Show _MENU_ entries per page",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
infoEmpty: "No templates available",
infoFiltered: "(filtered from _MAX_ total entries)"
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,947 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Surgical Note Templates{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.template-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 0.5rem;
color: white;
margin-bottom: 2rem;
padding: 2rem;
}
.template-stats {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
text-align: center;
transition: transform 0.2s ease;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-icon {
align-items: center;
border-radius: 50%;
color: white;
display: flex;
font-size: 1.5rem;
height: 60px;
justify-content: center;
margin: 0 auto 1rem;
width: 60px;
}
.stat-icon.primary { background: linear-gradient(135deg, #007bff, #0056b3); }
.stat-icon.success { background: linear-gradient(135deg, #28a745, #1e7e34); }
.stat-icon.warning { background: linear-gradient(135deg, #ffc107, #e0a800); }
.stat-icon.info { background: linear-gradient(135deg, #17a2b8, #117a8b); }
.stat-number {
color: #495057;
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6c757d;
font-size: 0.9rem;
font-weight: 500;
}
.templates-table-container {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
padding: 1.5rem;
}
.table-header {
align-items: center;
border-bottom: 2px solid #f8f9fa;
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
}
.table-title {
color: #495057;
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.template-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
margin-bottom: 1rem;
overflow: hidden;
transition: all 0.3s ease;
}
.template-card:hover {
border-color: #007bff;
box-shadow: 0 0.25rem 0.5rem rgba(0, 123, 255, 0.15);
transform: translateY(-2px);
}
.template-card-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
}
.template-title {
color: #495057;
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.template-specialty {
color: #6c757d;
font-size: 0.9rem;
margin-top: 0.25rem;
}
.template-card-body {
padding: 1.5rem;
}
.template-description {
color: #6c757d;
line-height: 1.6;
margin-bottom: 1rem;
}
.template-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
}
.meta-item {
align-items: center;
color: #6c757d;
display: flex;
font-size: 0.85rem;
}
.meta-item i {
margin-right: 0.5rem;
}
.template-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.template-tag {
background: #e9ecef;
border-radius: 1rem;
color: #495057;
font-size: 0.75rem;
padding: 0.25rem 0.75rem;
}
.template-actions {
border-top: 1px solid #f8f9fa;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
padding-top: 1rem;
}
.btn-template {
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
padding: 0.5rem 1rem;
transition: all 0.2s ease;
}
.btn-template:hover {
transform: translateY(-1px);
}
.usage-indicator {
align-items: center;
display: flex;
gap: 0.5rem;
}
.usage-bar {
background: #e9ecef;
border-radius: 0.25rem;
height: 4px;
overflow: hidden;
width: 60px;
}
.usage-fill {
background: linear-gradient(90deg, #28a745, #20c997);
height: 100%;
transition: width 0.3s ease;
}
.filter-section {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
padding: 1.5rem;
}
.filter-row {
align-items: end;
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.filter-group {
flex: 1;
min-width: 200px;
}
.quick-actions {
background: white;
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1.5rem;
padding: 1.5rem;
}
.action-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.action-card {
border: 2px dashed #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
transition: all 0.3s ease;
}
.action-card:hover {
border-color: #007bff;
border-style: solid;
transform: translateY(-2px);
}
.action-icon {
color: #007bff;
font-size: 2rem;
margin-bottom: 1rem;
}
.action-title {
color: #495057;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.action-description {
color: #6c757d;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.template-header {
padding: 1.5rem;
text-align: center;
}
.template-stats {
grid-template-columns: repeat(2, 1fr);
}
.table-header {
flex-direction: column;
gap: 1rem;
}
.filter-row {
flex-direction: column;
}
.template-meta {
flex-direction: column;
gap: 0.5rem;
}
.template-actions {
flex-direction: column;
}
.action-grid {
grid-template-columns: 1fr;
}
}
.fade-in {
animation: fadeIn 0.6s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.template-preview {
background: #f8f9fa;
border-radius: 0.375rem;
margin-top: 1rem;
max-height: 0;
overflow: hidden;
padding: 0 1rem;
transition: all 0.3s ease;
}
.template-preview.show {
max-height: 200px;
padding: 1rem;
}
.preview-content {
color: #6c757d;
font-family: 'Courier New', monospace;
font-size: 0.8rem;
line-height: 1.4;
}
</style>
{% endblock %}
{% block content %}
<div id="content" class="app-content">
<div class="container-fluid">
<!-- Page Header -->
<div class="template-header fade-in">
<div class="row align-items-center">
<div class="col-md-8">
<h1 class="mb-2">
<i class="fas fa-file-medical me-3"></i>
Surgical Note Templates
</h1>
<p class="mb-0 opacity-75">
Manage and organize surgical note templates for consistent documentation across all procedures
</p>
</div>
<div class="col-md-4 text-md-end">
<a href="{% url 'operating_theatre:surgical_note_template_create' %}" class="btn btn-light btn-lg">
<i class="fas fa-plus me-2"></i>Create Template
</a>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="template-stats fade-in">
<div class="stat-card">
<div class="stat-icon primary">
<i class="fas fa-file-medical"></i>
</div>
<div class="stat-number">{{ total_templates|default:0 }}</div>
<div class="stat-label">Total Templates</div>
</div>
<div class="stat-card">
<div class="stat-icon success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-number">{{ active_templates|default:0 }}</div>
<div class="stat-label">Active Templates</div>
</div>
<div class="stat-card">
<div class="stat-icon warning">
<i class="fas fa-star"></i>
</div>
<div class="stat-number">{{ popular_templates|default:0 }}</div>
<div class="stat-label">Most Used</div>
</div>
<div class="stat-card">
<div class="stat-icon info">
<i class="fas fa-clock"></i>
</div>
<div class="stat-number">{{ recent_templates|default:0 }}</div>
<div class="stat-label">Recent Updates</div>
</div>
</div>
<!-- Quick Actions -->
<div class="quick-actions fade-in">
<h5 class="mb-3">
<i class="fas fa-bolt text-warning me-2"></i>
Quick Actions
</h5>
<div class="action-grid">
<div class="action-card" onclick="location.href='{% url 'operating_theatre:surgical_note_template_create' %}'">
<div class="action-icon">
<i class="fas fa-plus-circle"></i>
</div>
<div class="action-title">Create New Template</div>
<div class="action-description">Build a new surgical note template from scratch</div>
</div>
<div class="action-card" onclick="importTemplate()">
<div class="action-icon">
<i class="fas fa-file-import"></i>
</div>
<div class="action-title">Import Template</div>
<div class="action-description">Import templates from external sources or files</div>
</div>
<div class="action-card" onclick="exportTemplates()">
<div class="action-icon">
<i class="fas fa-file-export"></i>
</div>
<div class="action-title">Export Templates</div>
<div class="action-description">Export selected templates for backup or sharing</div>
</div>
<div class="action-card" onclick="showTemplateAnalytics()">
<div class="action-icon">
<i class="fas fa-chart-bar"></i>
</div>
<div class="action-title">Usage Analytics</div>
<div class="action-description">View detailed usage statistics and insights</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="filter-section fade-in">
<h5 class="mb-3">
<i class="fas fa-filter text-primary me-2"></i>
Filter Templates
</h5>
<div class="filter-row">
<div class="filter-group">
<label class="form-label">Search Templates</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by name, specialty, or description...">
</div>
</div>
<div class="filter-group">
<label class="form-label">Specialty</label>
<select class="form-select" id="specialtyFilter">
<option value="">All Specialties</option>
<option value="general">General Surgery</option>
<option value="cardiac">Cardiac Surgery</option>
<option value="orthopedic">Orthopedic Surgery</option>
<option value="neurosurgery">Neurosurgery</option>
<option value="plastic">Plastic Surgery</option>
<option value="vascular">Vascular Surgery</option>
</select>
</div>
<div class="filter-group">
<label class="form-label">Status</label>
<select class="form-select" id="statusFilter">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="draft">Draft</option>
<option value="archived">Archived</option>
</select>
</div>
<div class="filter-group">
<label class="form-label">Sort By</label>
<select class="form-select" id="sortFilter">
<option value="name">Name</option>
<option value="created">Date Created</option>
<option value="updated">Last Updated</option>
<option value="usage">Usage Count</option>
</select>
</div>
<div class="filter-group">
<button type="button" class="btn btn-outline-secondary" onclick="clearFilters()">
<i class="fas fa-times me-1"></i>Clear
</button>
</div>
</div>
</div>
<!-- Templates List -->
<div class="templates-table-container fade-in">
<div class="table-header">
<h4 class="table-title">
<i class="fas fa-list me-2"></i>
Template Library
</h4>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary btn-sm" onclick="toggleView('grid')" id="gridViewBtn">
<i class="fas fa-th me-1"></i>Grid
</button>
<button class="btn btn-primary btn-sm" onclick="toggleView('list')" id="listViewBtn">
<i class="fas fa-list me-1"></i>List
</button>
</div>
</div>
<!-- Grid View -->
<div id="gridView" class="row" style="display: none;">
{% for template in templates %}
<div class="col-lg-6 col-xl-4 mb-4 template-item"
data-specialty="{{ template.specialty|lower }}"
data-status="{{ template.status|lower }}"
data-name="{{ template.name|lower }}">
<div class="template-card">
<div class="template-card-header">
<h5 class="template-title">{{ template.name }}</h5>
<div class="template-specialty">
<i class="fas fa-stethoscope me-1"></i>
{{ template.get_specialty_display }}
</div>
</div>
<div class="template-card-body">
<p class="template-description">{{ template.description|truncatewords:20 }}</p>
<div class="template-meta">
<div class="meta-item">
<i class="fas fa-calendar"></i>
{{ template.created_at|date:"M d, Y" }}
</div>
<div class="meta-item">
<i class="fas fa-user"></i>
{{ template.created_by.get_full_name }}
</div>
<div class="meta-item">
<i class="fas fa-chart-line"></i>
<div class="usage-indicator">
<span>{{ template.usage_count|default:0 }}</span>
<div class="usage-bar">
<div class="usage-fill" style="width: {{ template.usage_percentage|default:0 }}%"></div>
</div>
</div>
</div>
</div>
<div class="template-tags">
{% for tag in template.tags.all %}
<span class="template-tag">{{ tag.name }}</span>
{% empty %}
<span class="template-tag">General</span>
{% endfor %}
</div>
<div class="template-actions">
<button class="btn btn-outline-info btn-template btn-sm"
onclick="previewTemplate({{ template.id }})">
<i class="fas fa-eye me-1"></i>Preview
</button>
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}"
class="btn btn-outline-primary btn-template btn-sm">
<i class="fas fa-info-circle me-1"></i>Details
</a>
<a href="{% url 'operating_theatre:surgical_note_template_edit' template.pk %}"
class="btn btn-primary btn-template btn-sm">
<i class="fas fa-edit me-1"></i>Edit
</a>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="text-center py-5">
<i class="fas fa-file-medical text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">No Templates Found</h4>
<p class="text-muted">Create your first surgical note template to get started.</p>
<a href="{% url 'operating_theatre:surgical_note_template_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Create Template
</a>
</div>
</div>
{% endfor %}
</div>
<!-- List View -->
<div id="listView">
<div class="table-responsive">
<table class="table table-hover" id="templatesTable">
<thead>
<tr>
<th>Template Name</th>
<th>Specialty</th>
<th>Status</th>
<th>Usage</th>
<th>Created</th>
<th>Last Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for template in templates %}
<tr class="template-item"
data-specialty="{{ template.specialty|lower }}"
data-status="{{ template.status|lower }}"
data-name="{{ template.name|lower }}">
<td>
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-file-medical text-primary"></i>
</div>
<div>
<div class="fw-bold">{{ template.name }}</div>
<small class="text-muted">{{ template.description|truncatewords:10 }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-light text-dark">
{{ template.get_specialty_display }}
</span>
</td>
<td>
{% if template.is_active %}
<span class="badge bg-success">Active</span>
{% elif template.is_draft %}
<span class="badge bg-warning">Draft</span>
{% else %}
<span class="badge bg-secondary">Archived</span>
{% endif %}
</td>
<td>
<div class="d-flex align-items-center">
<span class="me-2">{{ template.usage_count|default:0 }}</span>
<div class="usage-bar">
<div class="usage-fill" style="width: {{ template.usage_percentage|default:0 }}%"></div>
</div>
</div>
</td>
<td>
<div>{{ template.created_at|date:"M d, Y" }}</div>
<small class="text-muted">{{ template.created_by.get_full_name }}</small>
</td>
<td>{{ template.updated_at|date:"M d, Y g:i A" }}</td>
<td>
<div class="btn-group" role="group">
<button class="btn btn-outline-info btn-sm"
onclick="previewTemplate({{ template.id }})"
title="Preview">
<i class="fas fa-eye"></i>
</button>
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}"
class="btn btn-outline-primary btn-sm" title="View Details">
<i class="fas fa-info-circle"></i>
</a>
<a href="{% url 'operating_theatre:surgical_note_template_edit' template.pk %}"
class="btn btn-primary btn-sm" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
data-bs-toggle="dropdown" title="More Actions">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="duplicateTemplate({{ template.id }})">
<i class="fas fa-copy me-2"></i>Duplicate
</a></li>
<li><a class="dropdown-item" href="#" onclick="exportTemplate({{ template.id }})">
<i class="fas fa-download me-2"></i>Export
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger"
href="{% url 'operating_theatre:surgical_note_template_delete' template.pk %}">
<i class="fas fa-trash me-2"></i>Delete
</a></li>
</ul>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-5">
<i class="fas fa-file-medical text-muted" style="font-size: 3rem;"></i>
<h5 class="mt-3 text-muted">No Templates Found</h5>
<p class="text-muted">Create your first surgical note template to get started.</p>
<a href="{% url 'operating_theatre:surgical_note_template_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-2"></i>Create Template
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Template Preview Modal -->
<div class="modal fade" id="templatePreviewModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-eye me-2"></i>Template Preview
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="templatePreviewContent">
<!-- Preview content will be loaded here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="useTemplateBtn">
<i class="fas fa-plus me-1"></i>Use This Template
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
$('#templatesTable').DataTable({
responsive: true,
pageLength: 25,
order: [[0, 'asc']],
columnDefs: [
{ orderable: false, targets: [6] }
]
});
// Initialize view (default to list view)
toggleView('list');
// Search functionality
$('#searchInput').on('input', function() {
filterTemplates();
});
// Filter functionality
$('#specialtyFilter, #statusFilter, #sortFilter').on('change', function() {
filterTemplates();
});
function toggleView(viewType) {
if (viewType === 'grid') {
$('#listView').hide();
$('#gridView').show();
$('#gridViewBtn').removeClass('btn-outline-primary').addClass('btn-primary');
$('#listViewBtn').removeClass('btn-primary').addClass('btn-outline-primary');
} else {
$('#gridView').hide();
$('#listView').show();
$('#listViewBtn').removeClass('btn-outline-primary').addClass('btn-primary');
$('#gridViewBtn').removeClass('btn-primary').addClass('btn-outline-primary');
}
}
function filterTemplates() {
const searchTerm = $('#searchInput').val().toLowerCase();
const specialtyFilter = $('#specialtyFilter').val().toLowerCase();
const statusFilter = $('#statusFilter').val().toLowerCase();
$('.template-item').each(function() {
const $item = $(this);
const name = $item.data('name');
const specialty = $item.data('specialty');
const status = $item.data('status');
let show = true;
if (searchTerm && !name.includes(searchTerm)) {
show = false;
}
if (specialtyFilter && specialty !== specialtyFilter) {
show = false;
}
if (statusFilter && status !== statusFilter) {
show = false;
}
if (show) {
$item.show();
} else {
$item.hide();
}
});
}
function clearFilters() {
$('#searchInput').val('');
$('#specialtyFilter').val('');
$('#statusFilter').val('');
$('#sortFilter').val('name');
filterTemplates();
}
// Make functions globally available
window.toggleView = toggleView;
window.clearFilters = clearFilters;
});
function previewTemplate(templateId) {
// Show loading state
$('#templatePreviewContent').html(`
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading template preview...</p>
</div>
`);
$('#templatePreviewModal').modal('show');
// Load template preview via AJAX
$.ajax({
url: `/operating-theatre/templates/${templateId}/preview/`,
method: 'GET',
success: function(response) {
$('#templatePreviewContent').html(response);
$('#useTemplateBtn').data('template-id', templateId);
},
error: function() {
$('#templatePreviewContent').html(`
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Error loading template preview. Please try again.
</div>
`);
}
});
}
function duplicateTemplate(templateId) {
if (confirm('Are you sure you want to duplicate this template?')) {
$.ajax({
url: `/operating-theatre/templates/${templateId}/duplicate/`,
method: 'POST',
headers: {
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
},
success: function(response) {
showAlert('Template duplicated successfully!', 'success');
setTimeout(() => location.reload(), 1500);
},
error: function() {
showAlert('Error duplicating template.', 'error');
}
});
}
}
function exportTemplate(templateId) {
window.location.href = `/operating-theatre/templates/${templateId}/export/`;
}
function importTemplate() {
// Create file input dynamically
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json,.xml,.txt';
input.onchange = function(e) {
const file = e.target.files[0];
if (file) {
const formData = new FormData();
formData.append('template_file', file);
$.ajax({
url: '/operating-theatre/templates/import/',
method: 'POST',
data: formData,
processData: false,
contentType: false,
headers: {
'X-CSRFToken': $('[name=csrfmiddlewaretoken]').val()
},
success: function(response) {
showAlert('Template imported successfully!', 'success');
setTimeout(() => location.reload(), 1500);
},
error: function() {
showAlert('Error importing template.', 'error');
}
});
}
};
input.click();
}
function exportTemplates() {
const selectedTemplates = [];
$('.template-item:visible').each(function() {
selectedTemplates.push($(this).data('id'));
});
if (selectedTemplates.length === 0) {
showAlert('No templates to export.', 'warning');
return;
}
window.location.href = `/operating-theatre/templates/export/?ids=${selectedTemplates.join(',')}`;
}
function showTemplateAnalytics() {
window.location.href = '/operating-theatre/templates/analytics/';
}
function showAlert(message, type) {
const alertClass = type === 'success' ? 'alert-success' :
type === 'warning' ? 'alert-warning' : '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>
`;
$('.container-fluid').prepend(alertHtml);
setTimeout(() => $('.alert').fadeOut(), 5000);
}
// Use template button functionality
$('#useTemplateBtn').on('click', function() {
const templateId = $(this).data('template-id');
window.location.href = `/operating-theatre/surgical-notes/create/?template=${templateId}`;
});
</script>
{% endblock %}

BIN
patients/.DS_Store vendored

Binary file not shown.

View File

@ -0,0 +1,488 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Patient Management Dashboard{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/jvectormap-next/jquery-jvectormap.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.metric-card {
transition: transform 0.2s;
}
.metric-card:hover {
transform: translateY(-2px);
}
.patient-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
}
.priority-high { border-left: 4px solid #dc3545; }
.priority-medium { border-left: 4px solid #ffc107; }
.priority-low { border-left: 4px solid #28a745; }
.chart-container {
position: relative;
height: 300px;
}
</style>
{% endblock %}
{% block content %}
<!-- BEGIN breadcrumb -->
<ol class="breadcrumb float-xl-end">
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
<li class="breadcrumb-item active">Patient Management</li>
</ol>
<!-- END breadcrumb -->
<!-- BEGIN page-header -->
<h1 class="page-header">
<i class="fas fa-users text-primary me-2"></i>
Patient Management Dashboard
<small class="text-muted ms-2">Comprehensive overview of patient care and management</small>
</h1>
<!-- END page-header -->
<!-- BEGIN statistics cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6">
<div class="card bg-primary text-white metric-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h6 class="card-title mb-1">Total Patients</h6>
<h3 class="mb-0" id="total-patients">{{ stats.total_patients|default:0 }}</h3>
<small class="opacity-75">Active registrations</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-users fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-success text-white metric-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h6 class="card-title mb-1">Active Inpatients</h6>
<h3 class="mb-0" id="active-inpatients">{{ stats.active_inpatients|default:0 }}</h3>
<small class="opacity-75">Currently admitted</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-bed fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-warning text-white metric-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h6 class="card-title mb-1">Today's Visits</h6>
<h3 class="mb-0" id="todays-visits">{{ stats.todays_visits|default:0 }}</h3>
<small class="opacity-75">Outpatient visits</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-calendar-check fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card bg-info text-white metric-card">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<h6 class="card-title mb-1">Critical Alerts</h6>
<h3 class="mb-0" id="critical-alerts">{{ stats.critical_alerts|default:0 }}</h3>
<small class="opacity-75">Require attention</small>
</div>
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle fa-2x opacity-75"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END statistics cards -->
<!-- BEGIN quick actions -->
<div class="row mb-4">
<div class="col-md-12">
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-bolt me-2"></i>Quick Actions
</h4>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-2 col-sm-4 col-6 mb-3">
<a href="{% url 'patients:patient_create' %}" class="btn btn-outline-primary w-100 py-3">
<i class="fas fa-user-plus fa-2x mb-2"></i>
<div>Register Patient</div>
</a>
</div>
<div class="col-md-2 col-sm-4 col-6 mb-3">
<a href="{% url 'patients:patient_search' %}" class="btn btn-outline-info w-100 py-3">
<i class="fas fa-search fa-2x mb-2"></i>
<div>Search Patients</div>
</a>
</div>
<div class="col-md-2 col-sm-4 col-6 mb-3">
<a href="{% url 'appointments:appointment_create' %}" class="btn btn-outline-success w-100 py-3">
<i class="fas fa-calendar-plus fa-2x mb-2"></i>
<div>Schedule Visit</div>
</a>
</div>
<div class="col-md-2 col-sm-4 col-6 mb-3">
<a href="{% url 'inpatients:admission_create' %}" class="btn btn-outline-warning w-100 py-3">
<i class="fas fa-hospital fa-2x mb-2"></i>
<div>Admit Patient</div>
</a>
</div>
<div class="col-md-2 col-sm-4 col-6 mb-3">
<a href="{% url 'patients:emergency_registration' %}" class="btn btn-outline-danger w-100 py-3">
<i class="fas fa-ambulance fa-2x mb-2"></i>
<div>Emergency Reg.</div>
</a>
</div>
<div class="col-md-2 col-sm-4 col-6 mb-3">
<a href="{% url 'patients:patient_reports' %}" class="btn btn-outline-secondary w-100 py-3">
<i class="fas fa-chart-bar fa-2x mb-2"></i>
<div>Reports</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- END quick actions -->
<!-- BEGIN main content -->
<div class="row">
<!-- LEFT COLUMN -->
<div class="col-xl-8">
<!-- Recent Admissions -->
<div class="panel panel-inverse mb-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-hospital-user me-2"></i>Recent Admissions
</h4>
<div class="panel-heading-btn">
<a href="{% url 'inpatients:admission_list' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-eye me-1"></i>View All
</a>
</div>
</div>
<div class="panel-body">
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Patient</th>
<th>Room</th>
<th>Admission Date</th>
<th>Attending Physician</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for admission in recent_admissions %}
<tr class="{% if admission.priority == 'HIGH' %}priority-high{% elif admission.priority == 'MEDIUM' %}priority-medium{% else %}priority-low{% endif %}">
<td>
<div class="d-flex align-items-center">
<div class="patient-avatar bg-primary me-2">
{{ admission.patient.first_name|first }}{{ admission.patient.last_name|first }}
</div>
<div>
<div class="fw-bold">{{ admission.patient.get_full_name }}</div>
<small class="text-muted">MRN: {{ admission.patient.mrn }}</small>
</div>
</div>
</td>
<td>
<span class="badge bg-info">{{ admission.room.number }}</span>
<small class="d-block text-muted">{{ admission.room.ward.name }}</small>
</td>
<td>{{ admission.admission_date|date:"M d, Y H:i" }}</td>
<td>{{ admission.attending_physician.get_full_name }}</td>
<td>
{% if admission.status == 'ACTIVE' %}
<span class="badge bg-success">Active</span>
{% elif admission.status == 'PENDING' %}
<span class="badge bg-warning">Pending</span>
{% else %}
<span class="badge bg-secondary">{{ admission.get_status_display }}</span>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<a href="{% url 'patients:patient_detail' admission.patient.pk %}"
class="btn btn-outline-primary btn-sm" title="View Patient">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'inpatients:admission_detail' admission.pk %}"
class="btn btn-outline-info btn-sm" title="View Admission">
<i class="fas fa-hospital"></i>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<i class="fas fa-inbox fa-2x mb-2"></i>
<p class="mb-0">No recent admissions</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Patient Activity Chart -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-chart-line me-2"></i>Patient Activity Trends
</h4>
<div class="panel-heading-btn">
<select class="form-select form-select-sm" id="chart-period">
<option value="7">Last 7 days</option>
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
</select>
</div>
</div>
<div class="panel-body">
<div class="chart-container">
<canvas id="patientActivityChart"></canvas>
</div>
</div>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="col-xl-4">
<!-- Critical Alerts -->
<div class="panel panel-inverse mb-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-exclamation-triangle me-2"></i>Critical Alerts
</h4>
<div class="panel-heading-btn">
<button class="btn btn-outline-secondary btn-sm" onclick="refreshAlerts()">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="panel-body" style="max-height: 300px; overflow-y: auto;">
{% for alert in critical_alerts %}
<div class="alert alert-{% if alert.severity == 'CRITICAL' %}danger{% elif alert.severity == 'HIGH' %}warning{% else %}info{% endif %} alert-dismissible fade show mb-2">
<div class="d-flex align-items-start">
<div class="flex-grow-1">
<h6 class="alert-heading mb-1">{{ alert.title }}</h6>
<p class="mb-1">{{ alert.message }}</p>
<small class="text-muted">
<i class="fas fa-clock me-1"></i>{{ alert.created_at|timesince }} ago
</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
</div>
{% empty %}
<div class="text-center text-muted py-4">
<i class="fas fa-check-circle fa-2x mb-2"></i>
<p class="mb-0">No critical alerts</p>
</div>
{% endfor %}
</div>
</div>
<!-- Today's Schedule -->
<div class="panel panel-inverse mb-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-calendar-day me-2"></i>Today's Schedule
</h4>
<div class="panel-heading-btn">
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-outline-primary btn-sm">
<i class="fas fa-calendar me-1"></i>Full Calendar
</a>
</div>
</div>
<div class="panel-body" style="max-height: 300px; overflow-y: auto;">
{% for appointment in todays_appointments %}
<div class="d-flex align-items-center mb-3 pb-3 border-bottom">
<div class="patient-avatar bg-primary me-3">
{{ appointment.patient.first_name|first }}{{ appointment.patient.last_name|first }}
</div>
<div class="flex-grow-1">
<div class="fw-bold">{{ appointment.patient.get_full_name }}</div>
<small class="text-muted">{{ appointment.get_appointment_type_display }}</small>
<div class="text-primary">
<i class="fas fa-clock me-1"></i>{{ appointment.appointment_time|time:"g:i A" }}
</div>
</div>
<div class="text-end">
<span class="badge bg-{% if appointment.status == 'CONFIRMED' %}success{% elif appointment.status == 'PENDING' %}warning{% else %}secondary{% endif %}">
{{ appointment.get_status_display }}
</span>
</div>
</div>
{% empty %}
<div class="text-center text-muted py-4">
<i class="fas fa-calendar-times fa-2x mb-2"></i>
<p class="mb-0">No appointments scheduled for today</p>
</div>
{% endfor %}
</div>
</div>
<!-- Department Occupancy -->
<div class="panel panel-inverse">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-hospital me-2"></i>Department Occupancy
</h4>
</div>
<div class="panel-body">
{% for dept in department_occupancy %}
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<span class="fw-bold">{{ dept.name }}</span>
<span class="text-muted">{{ dept.occupied }}/{{ dept.capacity }}</span>
</div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-{% if dept.occupancy_rate >= 90 %}danger{% elif dept.occupancy_rate >= 75 %}warning{% else %}success{% endif %}"
style="width: {{ dept.occupancy_rate }}%"></div>
</div>
<small class="text-muted">{{ dept.occupancy_rate }}% occupancy</small>
</div>
{% empty %}
<div class="text-center text-muted py-4">
<i class="fas fa-building fa-2x mb-2"></i>
<p class="mb-0">No department data available</p>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<!-- END main content -->
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/chart.js/dist/chart.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize Patient Activity Chart
const ctx = document.getElementById('patientActivityChart').getContext('2d');
const patientActivityChart = new Chart(ctx, {
type: 'line',
data: {
labels: {{ chart_labels|safe }},
datasets: [{
label: 'New Registrations',
data: {{ new_registrations_data|safe }},
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.1)',
tension: 0.1
}, {
label: 'Admissions',
data: {{ admissions_data|safe }},
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.1
}, {
label: 'Discharges',
data: {{ discharges_data|safe }},
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Patient Activity Over Time'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
// Chart period change handler
$('#chart-period').change(function() {
const period = $(this).val();
// AJAX call to update chart data
$.get('{% url "patients:dashboard_chart_data" %}', {period: period}, function(data) {
patientActivityChart.data.labels = data.labels;
patientActivityChart.data.datasets[0].data = data.new_registrations;
patientActivityChart.data.datasets[1].data = data.admissions;
patientActivityChart.data.datasets[2].data = data.discharges;
patientActivityChart.update();
});
});
// Auto-refresh stats every 30 seconds
setInterval(function() {
refreshStats();
}, 30000);
// Auto-refresh alerts every 60 seconds
setInterval(function() {
refreshAlerts();
}, 60000);
});
function refreshStats() {
$.get('{% url "patients:dashboard_stats" %}', function(data) {
$('#total-patients').text(data.total_patients);
$('#active-inpatients').text(data.active_inpatients);
$('#todays-visits').text(data.todays_visits);
$('#critical-alerts').text(data.critical_alerts);
});
}
function refreshAlerts() {
// Refresh critical alerts section
$.get('{% url "patients:dashboard_alerts" %}', function(data) {
// Update alerts section with new data
// Implementation would depend on your specific alert system
});
}
</script>
{% endblock %}