update
This commit is contained in:
parent
a710d1c4d8
commit
beba30e532
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,10 +5,7 @@ Admin configuration for appointments app.
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from django.utils import timezone
|
||||
from .models import (
|
||||
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
|
||||
TelemedicineSession, AppointmentTemplate
|
||||
)
|
||||
from .models import *
|
||||
|
||||
|
||||
class QueueEntryInline(admin.TabularInline):
|
||||
@ -459,3 +456,151 @@ class AppointmentTemplateAdmin(admin.ModelAdmin):
|
||||
'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'
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -5,15 +5,11 @@ Forms for Appointments app CRUD operations.
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from datetime import datetime, time, timedelta
|
||||
from .models import (
|
||||
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
|
||||
TelemedicineSession, AppointmentTemplate
|
||||
)
|
||||
from datetime import datetime, time, timedelta, date
|
||||
from .models import *
|
||||
from patients.models import PatientProfile
|
||||
from accounts.models import User
|
||||
from hr.models import Employee
|
||||
|
||||
from hr.models import Employee, Department
|
||||
|
||||
class AppointmentRequestForm(forms.ModelForm):
|
||||
"""
|
||||
@ -361,8 +357,6 @@ class AppointmentSearchForm(forms.Form):
|
||||
).order_by('last_name', 'first_name')
|
||||
|
||||
|
||||
|
||||
|
||||
class QueueSearchForm(forms.Form):
|
||||
"""
|
||||
Form for searching queues.
|
||||
@ -441,6 +435,437 @@ class SlotSearchForm(forms.Form):
|
||||
).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.core.exceptions import ValidationError
|
||||
# from django.utils import timezone
|
||||
|
||||
@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -1176,3 +1176,640 @@ class AppointmentTemplate(models.Model):
|
||||
def __str__(self):
|
||||
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})"
|
||||
|
||||
|
||||
BIN
appointments/templates/.DS_Store
vendored
BIN
appointments/templates/.DS_Store
vendored
Binary file not shown.
BIN
appointments/templates/appointments/.DS_Store
vendored
BIN
appointments/templates/appointments/.DS_Store
vendored
Binary file not shown.
@ -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 %}
|
||||
|
||||
@ -151,22 +151,15 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h4 class="section-title">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
|
||||
</h4>
|
||||
<small class="text-light">Configure the basic details of the waiting queue</small>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
<p class="section-description">Configure the basic details of the waiting queue</p>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
@ -207,6 +200,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">Description</label>
|
||||
@ -221,8 +215,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Queue Configuration -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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"> </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 %}
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
{% block title %}Waitlist Management{% endblock %}
|
||||
|
||||
{% block 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 '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" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -163,15 +163,6 @@
|
||||
<h4 class="card-title">Current Waitlist</h4>
|
||||
</div>
|
||||
<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">
|
||||
<table id="waitlistTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
@ -405,10 +396,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block 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 src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
@ -65,6 +65,15 @@ urlpatterns = [
|
||||
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')
|
||||
|
||||
# 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
|
||||
# path('api/', include('appointments.api.urls')),
|
||||
|
||||
@ -18,15 +18,8 @@ from django.core.paginator import Paginator
|
||||
from datetime import timedelta, datetime, time, date
|
||||
|
||||
from hr.models import Schedule, Employee
|
||||
from .models import (
|
||||
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
|
||||
TelemedicineSession, AppointmentTemplate
|
||||
)
|
||||
from .forms import (
|
||||
AppointmentRequestForm, SlotAvailabilityForm, WaitingQueueForm,
|
||||
QueueEntryForm, TelemedicineSessionForm, AppointmentTemplateForm,
|
||||
AppointmentSearchForm, QueueSearchForm, SlotSearchForm
|
||||
)
|
||||
from .models import *
|
||||
from .forms import *
|
||||
from patients.models import PatientProfile
|
||||
from accounts.models import User
|
||||
from core.utils import AuditLogger
|
||||
@ -1216,7 +1209,349 @@ class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
|
||||
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
|
||||
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@ -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,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: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
|
||||
|
||||
BIN
operating_theatre/.DS_Store
vendored
BIN
operating_theatre/.DS_Store
vendored
Binary file not shown.
BIN
operating_theatre/templates/.DS_Store
vendored
Normal file
BIN
operating_theatre/templates/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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 %}
|
||||
|
||||
@ -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
BIN
patients/.DS_Store
vendored
Binary file not shown.
488
patients/templates/patients/dashboard-last one.html
Normal file
488
patients/templates/patients/dashboard-last one.html
Normal 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 %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user