update
This commit is contained in:
parent
d1cee9194e
commit
0422966e14
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -17,7 +17,37 @@ class OperatingRoom(models.Model):
|
||||
"""
|
||||
Operating room model for OR configuration and management.
|
||||
"""
|
||||
|
||||
ROOM_TYPE_CHOICES = [
|
||||
('GENERAL', 'General Surgery'),
|
||||
('CARDIAC', 'Cardiac Surgery'),
|
||||
('NEURO', 'Neurosurgery'),
|
||||
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
||||
('TRAUMA', 'Trauma Surgery'),
|
||||
('PEDIATRIC', 'Pediatric Surgery'),
|
||||
('OBSTETRIC', 'Obstetric Surgery'),
|
||||
('OPHTHALMOLOGY', 'Ophthalmology'),
|
||||
('ENT', 'ENT Surgery'),
|
||||
('UROLOGY', 'Urology'),
|
||||
('PLASTIC', 'Plastic Surgery'),
|
||||
('VASCULAR', 'Vascular Surgery'),
|
||||
('THORACIC', 'Thoracic Surgery'),
|
||||
('TRANSPLANT', 'Transplant Surgery'),
|
||||
('ROBOTIC', 'Robotic Surgery'),
|
||||
('HYBRID', 'Hybrid OR'),
|
||||
('AMBULATORY', 'Ambulatory Surgery'),
|
||||
('EMERGENCY', 'Emergency Surgery'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('AVAILABLE', 'Available'),
|
||||
('OCCUPIED', 'Occupied'),
|
||||
('CLEANING', 'Cleaning'),
|
||||
('MAINTENANCE', 'Maintenance'),
|
||||
('SETUP', 'Setup'),
|
||||
('TURNOVER', 'Turnover'),
|
||||
('OUT_OF_ORDER', 'Out of Order'),
|
||||
('CLOSED', 'Closed'),
|
||||
]
|
||||
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -45,42 +75,14 @@ class OperatingRoom(models.Model):
|
||||
# Room Type and Capabilities
|
||||
room_type = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
('GENERAL', 'General Surgery'),
|
||||
('CARDIAC', 'Cardiac Surgery'),
|
||||
('NEURO', 'Neurosurgery'),
|
||||
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
||||
('TRAUMA', 'Trauma Surgery'),
|
||||
('PEDIATRIC', 'Pediatric Surgery'),
|
||||
('OBSTETRIC', 'Obstetric Surgery'),
|
||||
('OPHTHALMOLOGY', 'Ophthalmology'),
|
||||
('ENT', 'ENT Surgery'),
|
||||
('UROLOGY', 'Urology'),
|
||||
('PLASTIC', 'Plastic Surgery'),
|
||||
('VASCULAR', 'Vascular Surgery'),
|
||||
('THORACIC', 'Thoracic Surgery'),
|
||||
('TRANSPLANT', 'Transplant Surgery'),
|
||||
('ROBOTIC', 'Robotic Surgery'),
|
||||
('HYBRID', 'Hybrid OR'),
|
||||
('AMBULATORY', 'Ambulatory Surgery'),
|
||||
('EMERGENCY', 'Emergency Surgery'),
|
||||
],
|
||||
choices=ROOM_TYPE_CHOICES,
|
||||
help_text='Operating room type'
|
||||
)
|
||||
|
||||
# Room Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('AVAILABLE', 'Available'),
|
||||
('OCCUPIED', 'Occupied'),
|
||||
('CLEANING', 'Cleaning'),
|
||||
('MAINTENANCE', 'Maintenance'),
|
||||
('SETUP', 'Setup'),
|
||||
('TURNOVER', 'Turnover'),
|
||||
('OUT_OF_ORDER', 'Out of Order'),
|
||||
('CLOSED', 'Closed'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='AVAILABLE',
|
||||
help_text='Current room status'
|
||||
)
|
||||
@ -275,7 +277,38 @@ class ORBlock(models.Model):
|
||||
"""
|
||||
OR block model for surgical scheduling and time management.
|
||||
"""
|
||||
|
||||
BLOCK_TYPE_CHOICES = [
|
||||
('SCHEDULED', 'Scheduled Block'),
|
||||
('EMERGENCY', 'Emergency Block'),
|
||||
('MAINTENANCE', 'Maintenance Block'),
|
||||
('CLEANING', 'Deep Cleaning'),
|
||||
('RESERVED', 'Reserved'),
|
||||
('BLOCKED', 'Blocked'),
|
||||
]
|
||||
SERVICE_CHOICES = [
|
||||
('GENERAL', 'General Surgery'),
|
||||
('CARDIAC', 'Cardiac Surgery'),
|
||||
('NEURO', 'Neurosurgery'),
|
||||
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
||||
('TRAUMA', 'Trauma Surgery'),
|
||||
('PEDIATRIC', 'Pediatric Surgery'),
|
||||
('OBSTETRIC', 'Obstetric Surgery'),
|
||||
('OPHTHALMOLOGY', 'Ophthalmology'),
|
||||
('ENT', 'ENT Surgery'),
|
||||
('UROLOGY', 'Urology'),
|
||||
('PLASTIC', 'Plastic Surgery'),
|
||||
('VASCULAR', 'Vascular Surgery'),
|
||||
('THORACIC', 'Thoracic Surgery'),
|
||||
('TRANSPLANT', 'Transplant Surgery'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('ACTIVE', 'Active'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('DELAYED', 'Delayed'),
|
||||
]
|
||||
|
||||
# Operating Room relationship
|
||||
operating_room = models.ForeignKey(
|
||||
OperatingRoom,
|
||||
@ -306,14 +339,7 @@ class ORBlock(models.Model):
|
||||
# Block Type
|
||||
block_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('SCHEDULED', 'Scheduled Block'),
|
||||
('EMERGENCY', 'Emergency Block'),
|
||||
('MAINTENANCE', 'Maintenance Block'),
|
||||
('CLEANING', 'Deep Cleaning'),
|
||||
('RESERVED', 'Reserved'),
|
||||
('BLOCKED', 'Blocked'),
|
||||
],
|
||||
choices=BLOCK_TYPE_CHOICES,
|
||||
default='SCHEDULED',
|
||||
help_text='Block type'
|
||||
)
|
||||
@ -335,35 +361,14 @@ class ORBlock(models.Model):
|
||||
# Service Assignment
|
||||
service = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
('GENERAL', 'General Surgery'),
|
||||
('CARDIAC', 'Cardiac Surgery'),
|
||||
('NEURO', 'Neurosurgery'),
|
||||
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
||||
('TRAUMA', 'Trauma Surgery'),
|
||||
('PEDIATRIC', 'Pediatric Surgery'),
|
||||
('OBSTETRIC', 'Obstetric Surgery'),
|
||||
('OPHTHALMOLOGY', 'Ophthalmology'),
|
||||
('ENT', 'ENT Surgery'),
|
||||
('UROLOGY', 'Urology'),
|
||||
('PLASTIC', 'Plastic Surgery'),
|
||||
('VASCULAR', 'Vascular Surgery'),
|
||||
('THORACIC', 'Thoracic Surgery'),
|
||||
('TRANSPLANT', 'Transplant Surgery'),
|
||||
],
|
||||
choices=SERVICE_CHOICES,
|
||||
help_text='Surgical service'
|
||||
)
|
||||
|
||||
# Block Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('ACTIVE', 'Active'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('DELAYED', 'Delayed'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='SCHEDULED',
|
||||
help_text='Block status'
|
||||
)
|
||||
@ -457,7 +462,49 @@ class SurgicalCase(models.Model):
|
||||
"""
|
||||
Surgical case model for individual surgical procedures.
|
||||
"""
|
||||
|
||||
CASE_TYPE_CHOICES = [
|
||||
('ELECTIVE', 'Elective'),
|
||||
('URGENT', 'Urgent'),
|
||||
('EMERGENCY', 'Emergency'),
|
||||
('TRAUMA', 'Trauma'),
|
||||
('TRANSPLANT', 'Transplant'),
|
||||
]
|
||||
APPROACH_CHOICES = [
|
||||
('OPEN', 'Open'),
|
||||
('LAPAROSCOPIC', 'Laparoscopic'),
|
||||
('ROBOTIC', 'Robotic'),
|
||||
('ENDOSCOPIC', 'Endoscopic'),
|
||||
('PERCUTANEOUS', 'Percutaneous'),
|
||||
('HYBRID', 'Hybrid'),
|
||||
]
|
||||
ANESTHESIA_TYPE_CHOICES = [
|
||||
('GENERAL', 'General'),
|
||||
('REGIONAL', 'Regional'),
|
||||
('LOCAL', 'Local'),
|
||||
('SEDATION', 'Sedation'),
|
||||
('SPINAL', 'Spinal'),
|
||||
('EPIDURAL', 'Epidural'),
|
||||
('COMBINED', 'Combined'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('DELAYED', 'Delayed'),
|
||||
('IN_PROGRESS', 'In Progress'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('POSTPONED', 'Postponed'),
|
||||
]
|
||||
PATIENT_POSITION_CHOICES = [
|
||||
('SUPINE', 'Supine'),
|
||||
('PRONE', 'Prone'),
|
||||
('LATERAL', 'Lateral'),
|
||||
('LITHOTOMY', 'Lithotomy'),
|
||||
('TRENDELENBURG', 'Trendelenburg'),
|
||||
('REVERSE_TREND', 'Reverse Trendelenburg'),
|
||||
('SITTING', 'Sitting'),
|
||||
('JACKKNIFE', 'Jackknife'),
|
||||
]
|
||||
|
||||
# OR Block relationship
|
||||
or_block = models.ForeignKey(
|
||||
ORBlock,
|
||||
@ -542,13 +589,7 @@ class SurgicalCase(models.Model):
|
||||
# Case Classification
|
||||
case_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('ELECTIVE', 'Elective'),
|
||||
('URGENT', 'Urgent'),
|
||||
('EMERGENCY', 'Emergency'),
|
||||
('TRAUMA', 'Trauma'),
|
||||
('TRANSPLANT', 'Transplant'),
|
||||
],
|
||||
choices=CASE_TYPE_CHOICES,
|
||||
default='ELECTIVE',
|
||||
help_text='Case type'
|
||||
)
|
||||
@ -556,29 +597,14 @@ class SurgicalCase(models.Model):
|
||||
# Surgical Approach
|
||||
approach = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('OPEN', 'Open'),
|
||||
('LAPAROSCOPIC', 'Laparoscopic'),
|
||||
('ROBOTIC', 'Robotic'),
|
||||
('ENDOSCOPIC', 'Endoscopic'),
|
||||
('PERCUTANEOUS', 'Percutaneous'),
|
||||
('HYBRID', 'Hybrid'),
|
||||
],
|
||||
choices=APPROACH_CHOICES,
|
||||
help_text='Surgical approach'
|
||||
)
|
||||
|
||||
# Anesthesia
|
||||
anesthesia_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('GENERAL', 'General'),
|
||||
('REGIONAL', 'Regional'),
|
||||
('LOCAL', 'Local'),
|
||||
('SEDATION', 'Sedation'),
|
||||
('SPINAL', 'Spinal'),
|
||||
('EPIDURAL', 'Epidural'),
|
||||
('COMBINED', 'Combined'),
|
||||
],
|
||||
choices=ANESTHESIA_TYPE_CHOICES,
|
||||
help_text='Anesthesia type'
|
||||
)
|
||||
|
||||
@ -603,14 +629,7 @@ class SurgicalCase(models.Model):
|
||||
# Case Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('DELAYED', 'Delayed'),
|
||||
('IN_PROGRESS', 'In Progress'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('POSTPONED', 'Postponed'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='SCHEDULED',
|
||||
help_text='Case status'
|
||||
)
|
||||
@ -647,16 +666,7 @@ class SurgicalCase(models.Model):
|
||||
# Patient Positioning
|
||||
patient_position = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('SUPINE', 'Supine'),
|
||||
('PRONE', 'Prone'),
|
||||
('LATERAL', 'Lateral'),
|
||||
('LITHOTOMY', 'Lithotomy'),
|
||||
('TRENDELENBURG', 'Trendelenburg'),
|
||||
('REVERSE_TREND', 'Reverse Trendelenburg'),
|
||||
('SITTING', 'Sitting'),
|
||||
('JACKKNIFE', 'Jackknife'),
|
||||
],
|
||||
choices=PATIENT_POSITION_CHOICES,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text='Patient positioning'
|
||||
@ -777,7 +787,27 @@ class SurgicalNote(models.Model):
|
||||
"""
|
||||
Surgical note model for perioperative documentation.
|
||||
"""
|
||||
|
||||
CONDITION_CHOICES = [
|
||||
('STABLE', 'Stable'),
|
||||
('CRITICAL', 'Critical'),
|
||||
('GUARDED', 'Guarded'),
|
||||
('FAIR', 'Fair'),
|
||||
('GOOD', 'Good'),
|
||||
('EXCELLENT', 'Excellent'),
|
||||
]
|
||||
DISPOSITION_CHOICES = [
|
||||
('RECOVERY', 'Recovery Room'),
|
||||
('ICU', 'Intensive Care Unit'),
|
||||
('WARD', 'Ward'),
|
||||
('DISCHARGE', 'Discharge'),
|
||||
('MORGUE', 'Morgue'),
|
||||
]
|
||||
STATUS_CHOICES = [
|
||||
('DRAFT', 'Draft'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('SIGNED', 'Signed'),
|
||||
('AMENDED', 'Amended'),
|
||||
]
|
||||
# Surgical Case relationship
|
||||
surgical_case = models.OneToOneField(
|
||||
SurgicalCase,
|
||||
@ -833,25 +863,12 @@ class SurgicalNote(models.Model):
|
||||
)
|
||||
condition = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('STABLE', 'Stable'),
|
||||
('CRITICAL', 'Critical'),
|
||||
('GUARDED', 'Guarded'),
|
||||
('FAIR', 'Fair'),
|
||||
('GOOD', 'Good'),
|
||||
('EXCELLENT', 'Excellent'),
|
||||
],
|
||||
choices=CONDITION_CHOICES,
|
||||
help_text='Patient condition post-surgery'
|
||||
)
|
||||
disposition = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
('RECOVERY', 'Recovery Room'),
|
||||
('ICU', 'Intensive Care Unit'),
|
||||
('WARD', 'Ward'),
|
||||
('DISCHARGE', 'Discharge'),
|
||||
('MORGUE', 'Morgue'),
|
||||
],
|
||||
choices=DISPOSITION_CHOICES,
|
||||
help_text='Patient disposition'
|
||||
)
|
||||
|
||||
@ -915,12 +932,7 @@ class SurgicalNote(models.Model):
|
||||
# Note Status
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('DRAFT', 'Draft'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('SIGNED', 'Signed'),
|
||||
('AMENDED', 'Amended'),
|
||||
],
|
||||
choices=STATUS_CHOICES,
|
||||
default='DRAFT',
|
||||
help_text='Note status'
|
||||
)
|
||||
@ -980,7 +992,30 @@ class EquipmentUsage(models.Model):
|
||||
"""
|
||||
Equipment usage model for tracking surgical equipment.
|
||||
"""
|
||||
|
||||
EQUIPMENT_TYPE_CHOICES = [
|
||||
('SURGICAL_INSTRUMENT', 'Surgical Instrument'),
|
||||
('MONITORING_DEVICE', 'Monitoring Device'),
|
||||
('ANESTHESIA_MACHINE', 'Anesthesia Machine'),
|
||||
('VENTILATOR', 'Ventilator'),
|
||||
('ELECTROCAUTERY', 'Electrocautery'),
|
||||
('LASER', 'Laser'),
|
||||
('MICROSCOPE', 'Microscope'),
|
||||
('C_ARM', 'C-Arm'),
|
||||
('ULTRASOUND', 'Ultrasound'),
|
||||
('ROBOT', 'Surgical Robot'),
|
||||
('IMPLANT', 'Implant'),
|
||||
('DISPOSABLE', 'Disposable'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
UNIT_OF_MEASURE_CHOICES = [
|
||||
('EACH', 'Each'),
|
||||
('SET', 'Set'),
|
||||
('PACK', 'Pack'),
|
||||
('BOX', 'Box'),
|
||||
('UNIT', 'Unit'),
|
||||
('PIECE', 'Piece'),
|
||||
]
|
||||
|
||||
# Surgical Case relationship
|
||||
surgical_case = models.ForeignKey(
|
||||
SurgicalCase,
|
||||
@ -1002,21 +1037,7 @@ class EquipmentUsage(models.Model):
|
||||
)
|
||||
equipment_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=[
|
||||
('SURGICAL_INSTRUMENT', 'Surgical Instrument'),
|
||||
('MONITORING_DEVICE', 'Monitoring Device'),
|
||||
('ANESTHESIA_MACHINE', 'Anesthesia Machine'),
|
||||
('VENTILATOR', 'Ventilator'),
|
||||
('ELECTROCAUTERY', 'Electrocautery'),
|
||||
('LASER', 'Laser'),
|
||||
('MICROSCOPE', 'Microscope'),
|
||||
('C_ARM', 'C-Arm'),
|
||||
('ULTRASOUND', 'Ultrasound'),
|
||||
('ROBOT', 'Surgical Robot'),
|
||||
('IMPLANT', 'Implant'),
|
||||
('DISPOSABLE', 'Disposable'),
|
||||
('OTHER', 'Other'),
|
||||
],
|
||||
choices=EQUIPMENT_TYPE_CHOICES,
|
||||
help_text='Equipment type'
|
||||
)
|
||||
|
||||
@ -1047,14 +1068,7 @@ class EquipmentUsage(models.Model):
|
||||
)
|
||||
unit_of_measure = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
('EACH', 'Each'),
|
||||
('SET', 'Set'),
|
||||
('PACK', 'Pack'),
|
||||
('BOX', 'Box'),
|
||||
('UNIT', 'Unit'),
|
||||
('PIECE', 'Piece'),
|
||||
],
|
||||
choices=UNIT_OF_MEASURE_CHOICES,
|
||||
default='EACH',
|
||||
help_text='Unit of measure'
|
||||
)
|
||||
@ -1172,7 +1186,24 @@ class SurgicalNoteTemplate(models.Model):
|
||||
"""
|
||||
Surgical note template model for standardized documentation.
|
||||
"""
|
||||
|
||||
SPECIALTY_CHOICES = [
|
||||
('ALL', 'All Specialties'),
|
||||
('GENERAL', 'General Surgery'),
|
||||
('CARDIAC', 'Cardiac Surgery'),
|
||||
('NEURO', 'Neurosurgery'),
|
||||
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
||||
('TRAUMA', 'Trauma Surgery'),
|
||||
('PEDIATRIC', 'Pediatric Surgery'),
|
||||
('OBSTETRIC', 'Obstetric Surgery'),
|
||||
('OPHTHALMOLOGY', 'Ophthalmology'),
|
||||
('ENT', 'ENT Surgery'),
|
||||
('UROLOGY', 'Urology'),
|
||||
('PLASTIC', 'Plastic Surgery'),
|
||||
('VASCULAR', 'Vascular Surgery'),
|
||||
('THORACIC', 'Thoracic Surgery'),
|
||||
('TRANSPLANT', 'Transplant Surgery'),
|
||||
]
|
||||
|
||||
# Tenant relationship
|
||||
tenant = models.ForeignKey(
|
||||
'core.Tenant',
|
||||
@ -1207,23 +1238,7 @@ class SurgicalNoteTemplate(models.Model):
|
||||
)
|
||||
specialty = models.CharField(
|
||||
max_length=30,
|
||||
choices=[
|
||||
('ALL', 'All Specialties'),
|
||||
('GENERAL', 'General Surgery'),
|
||||
('CARDIAC', 'Cardiac Surgery'),
|
||||
('NEURO', 'Neurosurgery'),
|
||||
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
||||
('TRAUMA', 'Trauma Surgery'),
|
||||
('PEDIATRIC', 'Pediatric Surgery'),
|
||||
('OBSTETRIC', 'Obstetric Surgery'),
|
||||
('OPHTHALMOLOGY', 'Ophthalmology'),
|
||||
('ENT', 'ENT Surgery'),
|
||||
('UROLOGY', 'Urology'),
|
||||
('PLASTIC', 'Plastic Surgery'),
|
||||
('VASCULAR', 'Vascular Surgery'),
|
||||
('THORACIC', 'Thoracic Surgery'),
|
||||
('TRANSPLANT', 'Transplant Surgery'),
|
||||
],
|
||||
choices=SPECIALTY_CHOICES,
|
||||
default='ALL',
|
||||
help_text='Applicable specialty'
|
||||
)
|
||||
|
||||
@ -67,62 +67,62 @@ class OperatingTheatreDashboardView(LoginRequiredMixin, TemplateView):
|
||||
status='MAINTENANCE'
|
||||
).count(),
|
||||
'cases_today': SurgicalCase.objects.filter(
|
||||
tenant=tenant,
|
||||
scheduled_start_time__date=today
|
||||
admission__tenant=tenant,
|
||||
scheduled_start__date=today
|
||||
).count(),
|
||||
'cases_in_progress': SurgicalCase.objects.filter(
|
||||
tenant=tenant,
|
||||
admission__tenant=tenant,
|
||||
status='IN_PROGRESS'
|
||||
).count(),
|
||||
'cases_completed_today': SurgicalCase.objects.filter(
|
||||
tenant=tenant,
|
||||
actual_end_time__date=today,
|
||||
admission__tenant=tenant,
|
||||
actual_end__date=today,
|
||||
status='COMPLETED'
|
||||
).count(),
|
||||
'emergency_cases_today': SurgicalCase.objects.filter(
|
||||
tenant=tenant,
|
||||
scheduled_start_time__date=today,
|
||||
priority='EMERGENCY'
|
||||
admission__tenant=tenant,
|
||||
scheduled_start__date=today,
|
||||
case_type='EMERGENCY'
|
||||
).count(),
|
||||
'blocks_today': ORBlock.objects.filter(
|
||||
tenant=tenant,
|
||||
operating_room__tenant=tenant,
|
||||
date=today
|
||||
).count(),
|
||||
'equipment_in_use': EquipmentUsage.objects.filter(
|
||||
tenant=tenant,
|
||||
status='IN_USE'
|
||||
surgical_case__admission__tenant=tenant,
|
||||
# status='IN_USE'
|
||||
).count(),
|
||||
'notes_pending': SurgicalNote.objects.filter(
|
||||
tenant=tenant,
|
||||
surgical_case__admission__tenant=tenant,
|
||||
status='DRAFT'
|
||||
).count(),
|
||||
})
|
||||
|
||||
# Recent surgical cases
|
||||
context['recent_cases'] = SurgicalCase.objects.filter(
|
||||
tenant=tenant
|
||||
admission__tenant=tenant
|
||||
).select_related(
|
||||
'patient', 'primary_surgeon', 'operating_room'
|
||||
).order_by('-scheduled_start_time')[:10]
|
||||
'patient', 'primary_surgeon', 'or_block__operating_room'
|
||||
).order_by('-scheduled_start')[:10]
|
||||
|
||||
# Today's schedule
|
||||
context['todays_schedule'] = SurgicalCase.objects.filter(
|
||||
tenant=tenant,
|
||||
scheduled_start_time__date=today
|
||||
admission__tenant=tenant,
|
||||
scheduled_start__date=today
|
||||
).select_related(
|
||||
'patient', 'primary_surgeon', 'operating_room'
|
||||
).order_by('scheduled_start_time')
|
||||
'patient', 'primary_surgeon', 'or_block__operating_room'
|
||||
).order_by('scheduled_start')
|
||||
|
||||
# Room utilization
|
||||
context['room_utilization'] = OperatingRoom.objects.filter(
|
||||
tenant=tenant,
|
||||
is_active=True
|
||||
).annotate(
|
||||
cases_today=Count(
|
||||
'surgical_cases',
|
||||
filter=Q(surgical_cases__scheduled_start_time__date=today)
|
||||
)
|
||||
).order_by('room_number')
|
||||
# context['room_utilization'] = OperatingRoom.objects.filter(
|
||||
# tenant=tenant,
|
||||
# is_active=True
|
||||
# ).annotate(
|
||||
# cases_today=Count(
|
||||
# 'or_block__surgical_cases',
|
||||
# filter=Q(surgical_cases__scheduled_start__date=today)
|
||||
# )
|
||||
# ).order_by('room_number')
|
||||
|
||||
return context
|
||||
|
||||
@ -136,7 +136,7 @@ class OperatingRoomListView(LoginRequiredMixin, ListView):
|
||||
List all operating rooms with filtering and search.
|
||||
"""
|
||||
model = OperatingRoom
|
||||
template_name = 'operating_theatre/operating_room_list.html'
|
||||
template_name = 'operating_theatre/rooms/operating_room_list.html'
|
||||
context_object_name = 'operating_rooms'
|
||||
paginate_by = 25
|
||||
|
||||
@ -189,7 +189,7 @@ class OperatingRoomDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about an operating room.
|
||||
"""
|
||||
model = OperatingRoom
|
||||
template_name = 'operating_theatre/operating_room_detail.html'
|
||||
template_name = 'operating_theatre/rooms/operating_room_detail.html'
|
||||
context_object_name = 'operating_room'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -241,7 +241,7 @@ class OperatingRoomCreateView(LoginRequiredMixin, PermissionRequiredMixin, Creat
|
||||
"""
|
||||
model = OperatingRoom
|
||||
form_class = OperatingRoomForm
|
||||
template_name = 'operating_theatre/operating_room_form.html'
|
||||
template_name = 'operating_theatre/rooms/operating_room_form.html'
|
||||
permission_required = 'operating_theatre.add_operatingroom'
|
||||
success_url = reverse_lazy('operating_theatre:operating_room_list')
|
||||
|
||||
@ -272,7 +272,7 @@ class OperatingRoomUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Updat
|
||||
"""
|
||||
model = OperatingRoom
|
||||
form_class = OperatingRoomForm
|
||||
template_name = 'operating_theatre/operating_room_form.html'
|
||||
template_name = 'operating_theatre/rooms/operating_room_form.html'
|
||||
permission_required = 'operating_theatre.change_operatingroom'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -305,7 +305,7 @@ class OperatingRoomDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Delet
|
||||
Delete an operating room (soft delete by deactivating).
|
||||
"""
|
||||
model = OperatingRoom
|
||||
template_name = 'operating_theatre/operating_room_confirm_delete.html'
|
||||
template_name = 'operating_theatre/rooms/operating_room_confirm_delete.html'
|
||||
permission_required = 'operating_theatre.delete_operatingroom'
|
||||
success_url = reverse_lazy('operating_theatre:operating_room_list')
|
||||
|
||||
@ -333,7 +333,7 @@ class OperatingRoomDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Delet
|
||||
self.object.save()
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=request.user,
|
||||
action='OPERATING_ROOM_DEACTIVATED',
|
||||
model='OperatingRoom',
|
||||
@ -431,7 +431,7 @@ class SurgicalNoteTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='SURGICAL_NOTE_TEMPLATE_CREATED',
|
||||
model='SurgicalNoteTemplate',
|
||||
@ -682,7 +682,8 @@ class SurgicalCaseListView(LoginRequiredMixin, ListView):
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = SurgicalCase.objects.filter(tenant=self.request.user.tenant)
|
||||
tenant = self.request.user.tenant
|
||||
queryset = SurgicalCase.objects.filter(admission__tenant=tenant)
|
||||
|
||||
# Search functionality
|
||||
search = self.request.GET.get('search')
|
||||
@ -718,19 +719,19 @@ class SurgicalCaseListView(LoginRequiredMixin, ListView):
|
||||
date_from = self.request.GET.get('date_from')
|
||||
date_to = self.request.GET.get('date_to')
|
||||
if date_from:
|
||||
queryset = queryset.filter(scheduled_start_time__date__gte=date_from)
|
||||
queryset = queryset.filter(scheduled_start__date__gte=date_from)
|
||||
if date_to:
|
||||
queryset = queryset.filter(scheduled_start_time__date__lte=date_to)
|
||||
queryset = queryset.filter(scheduled_start__date__lte=date_to)
|
||||
|
||||
return queryset.select_related(
|
||||
'patient', 'primary_surgeon', 'operating_room'
|
||||
).order_by('-scheduled_start_time')
|
||||
'patient', 'primary_surgeon', 'or_block__operating_room'
|
||||
).order_by('-scheduled_start')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'statuses': SurgicalCase._meta.get_field('status').choices,
|
||||
'priorities': SurgicalCase._meta.get_field('priority').choices,
|
||||
'statuses': SurgicalCase.STATUS_CHOICES,
|
||||
'priorities': SurgicalCase.CASE_TYPE_CHOICES,
|
||||
'operating_rooms': OperatingRoom.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
is_active=True
|
||||
@ -744,7 +745,7 @@ class SurgicalCaseDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a surgical case.
|
||||
"""
|
||||
model = SurgicalCase
|
||||
template_name = 'operating_theatre/surgical_case_detail.html'
|
||||
template_name = 'operating_theatre/cases/surgical_case_detail.html'
|
||||
context_object_name = 'surgical_case'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -776,7 +777,7 @@ class SurgicalCaseCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
|
||||
"""
|
||||
model = SurgicalCase
|
||||
form_class = SurgicalCaseForm
|
||||
template_name = 'operating_theatre/surgical_case_form.html'
|
||||
template_name = 'operating_theatre/cases/surgical_case_form.html'
|
||||
permission_required = 'operating_theatre.add_surgicalcase'
|
||||
success_url = reverse_lazy('operating_theatre:surgical_case_list')
|
||||
|
||||
@ -806,7 +807,7 @@ class SurgicalCaseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
Update surgical case (limited to status and notes after surgery starts).
|
||||
"""
|
||||
model = SurgicalCase
|
||||
template_name = 'operating_theatre/surgical_case_update_form.html'
|
||||
template_name = 'operating_theatre/cases/surgical_case_form.html'
|
||||
permission_required = 'operating_theatre.change_surgicalcase'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -830,7 +831,7 @@ class SurgicalCaseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='SURGICAL_CASE_UPDATED',
|
||||
model='SurgicalCase',
|
||||
|
||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
BIN
templates/operating_theatre/.DS_Store
vendored
Normal file
BIN
templates/operating_theatre/.DS_Store
vendored
Normal file
Binary file not shown.
523
templates/operating_theatre/blocks/block_confirm_delete.html
Normal file
523
templates/operating_theatre/blocks/block_confirm_delete.html
Normal file
@ -0,0 +1,523 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete OR Block - {{ block.operating_room.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-header {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-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;
|
||||
}
|
||||
|
||||
.block-info {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.affected-cases {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.case-item {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.case-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.case-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.case-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.case-details {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.case-status {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.alternative-actions {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #fff5f5;
|
||||
border: 2px solid #fed7d7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.type-to-confirm {
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.case-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.case-status {
|
||||
margin-left: 0;
|
||||
margin-top: 0.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"><a href="{% url 'operating_theatre:block_schedule_list' %}">Block Schedule</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:block_detail' block.pk %}">{{ block.operating_room.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete Block</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete OR Block
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:block_detail' block.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Block
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Header -->
|
||||
<div class="delete-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Delete Operating Room Block
|
||||
</h2>
|
||||
<p class="mb-0">
|
||||
You are about to permanently delete this OR block. This action cannot be undone and will affect all scheduled cases within this block.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-2">
|
||||
<i class="fas fa-calendar-times fa-3x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning Section -->
|
||||
<div class="warning-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Critical Warning
|
||||
</h5>
|
||||
<ul class="mb-0">
|
||||
<li>This OR block will be permanently deleted</li>
|
||||
<li>All scheduled cases within this block will be affected</li>
|
||||
<li>Team assignments and resource allocations will be removed</li>
|
||||
<li>Historical data and logs will be preserved for audit purposes</li>
|
||||
<li>Patients and staff will need to be notified of schedule changes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Block Information -->
|
||||
<div class="block-info">
|
||||
<h5 class="mb-3">Block Information</h5>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Operating Room</div>
|
||||
<div class="info-value">{{ block.operating_room.name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Date & Time</div>
|
||||
<div class="info-value">
|
||||
{{ block.date|date:"l, M d, Y" }}<br>
|
||||
{{ block.start_time|time:"g:i A" }} - {{ block.end_time|time:"g:i A" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Block Type</div>
|
||||
<div class="info-value">{{ block.get_block_type_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Primary Surgeon</div>
|
||||
<div class="info-value">
|
||||
{% if block.assigned_surgeon %}
|
||||
{{ block.assigned_surgeon.get_full_name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Anesthesiologist</div>
|
||||
<div class="info-value">
|
||||
{% if block.assigned_anesthesiologist %}
|
||||
{{ block.assigned_anesthesiologist.get_full_name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value">
|
||||
{% if block.status == 'scheduled' %}
|
||||
<span class="badge bg-primary">Scheduled</span>
|
||||
{% elif block.status == 'in_progress' %}
|
||||
<span class="badge bg-success">In Progress</span>
|
||||
{% elif block.status == 'completed' %}
|
||||
<span class="badge bg-secondary">Completed</span>
|
||||
{% elif block.status == 'cancelled' %}
|
||||
<span class="badge bg-danger">Cancelled</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Analysis -->
|
||||
<div class="impact-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>Impact Analysis
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-danger">{{ affected_cases.count }}</div>
|
||||
<div class="text-muted">Scheduled Cases</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-warning">{{ affected_staff.count }}</div>
|
||||
<div class="text-muted">Affected Staff</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-info">{{ affected_patients.count }}</div>
|
||||
<div class="text-muted">Affected Patients</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Affected Cases -->
|
||||
{% if affected_cases %}
|
||||
<div class="impact-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-procedures me-2"></i>Affected Surgical Cases
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="affected-cases">
|
||||
{% for case in affected_cases %}
|
||||
<div class="case-item">
|
||||
<div class="case-info">
|
||||
<div class="case-title">{{ case.procedure_name }}</div>
|
||||
<div class="case-details">
|
||||
Patient: {{ case.patient.get_full_name }} |
|
||||
Surgeon: {{ case.surgeon.get_full_name }} |
|
||||
Time: {{ case.scheduled_start_time|time:"g:i A" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-status">
|
||||
{% if case.status == 'scheduled' %}
|
||||
<span class="badge bg-primary">Scheduled</span>
|
||||
{% elif case.status == 'confirmed' %}
|
||||
<span class="badge bg-success">Confirmed</span>
|
||||
{% elif case.status == 'in_progress' %}
|
||||
<span class="badge bg-info">In Progress</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Alternative Actions -->
|
||||
<div class="alternative-actions">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
Consider These Alternatives
|
||||
</h5>
|
||||
<p class="mb-3">Before deleting this block, consider these alternative actions:</p>
|
||||
|
||||
<button class="btn btn-outline-primary action-button" onclick="rescheduleBlock()">
|
||||
<i class="fas fa-calendar-alt me-1"></i>Reschedule Block
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-warning action-button" onclick="postponeBlock()">
|
||||
<i class="fas fa-pause me-1"></i>Postpone Block
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-info action-button" onclick="transferCases()">
|
||||
<i class="fas fa-exchange-alt me-1"></i>Transfer Cases
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-secondary action-button" onclick="notifyStaff()">
|
||||
<i class="fas fa-bell me-1"></i>Notify Staff First
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Section -->
|
||||
<div class="confirmation-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
Deletion Confirmation Required
|
||||
</h5>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<strong>Type the following text to confirm deletion:</strong>
|
||||
</label>
|
||||
<div class="type-to-confirm">
|
||||
DELETE {{ block.operating_room.name }} {{ block.date|date:"Y-m-d" }}
|
||||
</div>
|
||||
<input type="text" class="form-control" id="confirmationText"
|
||||
placeholder="Type the confirmation text here..." required>
|
||||
<div class="form-text">This confirmation helps prevent accidental deletions.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reason for Deletion</label>
|
||||
<select class="form-select" name="deletion_reason" required>
|
||||
<option value="">Select a reason...</option>
|
||||
<option value="schedule_conflict">Schedule Conflict</option>
|
||||
<option value="surgeon_unavailable">Surgeon Unavailable</option>
|
||||
<option value="equipment_failure">Equipment Failure</option>
|
||||
<option value="emergency_case">Emergency Case Priority</option>
|
||||
<option value="patient_cancellation">Patient Cancellation</option>
|
||||
<option value="administrative">Administrative Decision</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Additional Notes</label>
|
||||
<textarea class="form-control" name="deletion_notes" rows="3"
|
||||
placeholder="Provide additional context for this deletion..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notifyStaffCheck" name="notify_staff" checked>
|
||||
<label class="form-check-label" for="notifyStaffCheck">
|
||||
Automatically notify affected staff members
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notifyPatientsCheck" name="notify_patients" checked>
|
||||
<label class="form-check-label" for="notifyPatientsCheck">
|
||||
Automatically notify affected patients
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:block_detail' block.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-danger" id="deleteButton" disabled>
|
||||
<i class="fas fa-trash-alt me-1"></i>
|
||||
Permanently Delete Block
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const expectedText = 'DELETE {{ block.operating_room.name }} {{ block.date|date:"Y-m-d" }}';
|
||||
|
||||
$('#confirmationText').on('input', function() {
|
||||
const enteredText = $(this).val().trim();
|
||||
const deleteButton = $('#deleteButton');
|
||||
|
||||
if (enteredText === expectedText) {
|
||||
deleteButton.prop('disabled', false);
|
||||
$(this).removeClass('is-invalid').addClass('is-valid');
|
||||
} else {
|
||||
deleteButton.prop('disabled', true);
|
||||
$(this).removeClass('is-valid');
|
||||
if (enteredText.length > 0) {
|
||||
$(this).addClass('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function rescheduleBlock() {
|
||||
window.location.href = '{% url "operating_theatre:block_reschedule" block.pk %}';
|
||||
}
|
||||
|
||||
function postponeBlock() {
|
||||
if (confirm('Postpone this block to a later date?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:block_postpone" block.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Block has been postponed successfully.');
|
||||
window.location.href = '{% url "operating_theatre:block_schedule_list" %}';
|
||||
} else {
|
||||
alert('Error postponing block: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error postponing block');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function transferCases() {
|
||||
window.location.href = '{% url "operating_theatre:block_transfer_cases" block.pk %}';
|
||||
}
|
||||
|
||||
function notifyStaff() {
|
||||
window.location.href = '{% url "operating_theatre:block_notify_staff" block.pk %}';
|
||||
}
|
||||
|
||||
// Form submission with additional confirmation
|
||||
$('#deleteForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const affectedCases = {{ affected_cases.count }};
|
||||
const affectedStaff = {{ affected_staff.count }};
|
||||
|
||||
let confirmMessage = 'Are you absolutely sure you want to delete this OR block?\n\n';
|
||||
confirmMessage += `This will affect ${affectedCases} scheduled case(s) and ${affectedStaff} staff member(s).\n\n`;
|
||||
confirmMessage += 'This action cannot be undone.';
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
// Show loading state
|
||||
$('#deleteButton').html('<i class="fas fa-spinner fa-spin me-1"></i>Deleting...').prop('disabled', true);
|
||||
|
||||
// Submit the form
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
534
templates/operating_theatre/blocks/block_detail.html
Normal file
534
templates/operating_theatre/blocks/block_detail.html
Normal file
@ -0,0 +1,534 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}OR Block - {{ block.operating_room.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.block-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #495057;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.utilization-chart {
|
||||
height: 200px;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.case-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.case-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.case-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-available { background-color: #28a745; }
|
||||
.status-booked { background-color: #007bff; }
|
||||
.status-in-progress { background-color: #ffc107; }
|
||||
.status-completed { background-color: #6c757d; }
|
||||
.status-cancelled { background-color: #dc3545; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.block-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:block_schedule_list' %}">Block Schedule</a></li>
|
||||
<li class="breadcrumb-item active">{{ block.operating_room.name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-calendar-alt me-2"></i>OR Block Details
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:block_schedule_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back
|
||||
</a>
|
||||
{% if block.status == 'available' %}
|
||||
<a href="{% url 'operating_theatre:block_edit' block.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-secondary" onclick="window.print()">
|
||||
<i class="fas fa-print me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Header -->
|
||||
<div class="block-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{{ block.operating_room.name }}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Date:</strong> {{ block.date|date:"l, F d, Y" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Time:</strong> {{ block.start_time|date:"H:i" }} - {{ block.end_time|date:"H:i" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Duration:</strong> {{ block.duration_hours }} hours
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Block Type:</strong> {{ block.get_block_type_display }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Assigned Surgeon:</strong>
|
||||
{% if block.assigned_surgeon %}
|
||||
{{ block.assigned_surgeon.get_full_name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Unassigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Created:</strong> {{ block.created_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-3">
|
||||
<span class="status-indicator status-{{ block.status }}"></span>
|
||||
{% if block.status == 'available' %}
|
||||
<span class="badge bg-success fs-6">Available</span>
|
||||
{% elif block.status == 'booked' %}
|
||||
<span class="badge bg-primary fs-6">Booked</span>
|
||||
{% elif block.status == 'in_progress' %}
|
||||
<span class="badge bg-warning fs-6">In Progress</span>
|
||||
{% elif block.status == 'completed' %}
|
||||
<span class="badge bg-secondary fs-6">Completed</span>
|
||||
{% elif block.status == 'cancelled' %}
|
||||
<span class="badge bg-danger fs-6">Cancelled</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="fs-1 fw-bold">{{ block.utilization_percentage }}%</div>
|
||||
<div>Utilization</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Block Information -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Utilization Chart -->
|
||||
<div class="info-card">
|
||||
<div class="info-header">
|
||||
<i class="fas fa-chart-bar me-2"></i>Block Utilization
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="utilization-chart">
|
||||
<canvas id="utilizationChart"></canvas>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<div class="fw-bold text-primary">{{ block.total_minutes }}</div>
|
||||
<small class="text-muted">Total Minutes</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="fw-bold text-success">{{ block.scheduled_minutes }}</div>
|
||||
<small class="text-muted">Scheduled</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="fw-bold text-warning">{{ block.available_minutes }}</div>
|
||||
<small class="text-muted">Available</small>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="fw-bold text-info">{{ block.turnover_minutes }}</div>
|
||||
<small class="text-muted">Turnover</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scheduled Cases -->
|
||||
<div class="info-card">
|
||||
<div class="info-header">
|
||||
<i class="fas fa-procedures me-2"></i>Scheduled Cases ({{ block.scheduled_cases.count }})
|
||||
{% if block.status == 'available' %}
|
||||
<div class="ms-auto">
|
||||
<button class="btn btn-sm btn-primary" onclick="addCase()">
|
||||
<i class="fas fa-plus me-1"></i>Add Case
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="info-content">
|
||||
{% for case in block.scheduled_cases.all %}
|
||||
<div class="case-card">
|
||||
<div class="case-header">
|
||||
<div>
|
||||
<h6 class="mb-1">{{ case.patient.get_full_name }}</h6>
|
||||
<small class="text-muted">{{ case.scheduled_start_time|date:"H:i" }} - {{ case.estimated_end_time|date:"H:i" }}</small>
|
||||
</div>
|
||||
<div>
|
||||
{% if case.status == 'scheduled' %}
|
||||
<span class="badge bg-primary">Scheduled</span>
|
||||
{% elif case.status == 'in_progress' %}
|
||||
<span class="badge bg-warning">In Progress</span>
|
||||
{% elif case.status == 'completed' %}
|
||||
<span class="badge bg-success">Completed</span>
|
||||
{% elif case.status == 'cancelled' %}
|
||||
<span class="badge bg-danger">Cancelled</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="case-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Procedure</div>
|
||||
<div class="info-value">{{ case.procedure_name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Surgeon</div>
|
||||
<div class="info-value">{{ case.primary_surgeon.get_full_name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Estimated Duration</div>
|
||||
<div class="info-value">{{ case.estimated_duration }} minutes</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Priority</div>
|
||||
<div class="info-value">
|
||||
{% if case.priority == 'emergency' %}
|
||||
<span class="badge bg-danger">Emergency</span>
|
||||
{% elif case.priority == 'urgent' %}
|
||||
<span class="badge bg-warning">Urgent</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">Routine</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-3">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:case_detail' case.pk %}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
{% if case.status == 'scheduled' %}
|
||||
<a href="{% url 'operating_theatre:case_edit' case.pk %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
{% if case.status == 'scheduled' %}
|
||||
<button class="btn btn-success btn-sm" onclick="startCase('{{ case.pk }}')">
|
||||
<i class="fas fa-play me-1"></i>Start
|
||||
</button>
|
||||
{% elif case.status == 'in_progress' %}
|
||||
<button class="btn btn-warning btn-sm" onclick="completeCase('{{ case.pk }}')">
|
||||
<i class="fas fa-check me-1"></i>Complete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-calendar-plus fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted mb-0">No cases scheduled for this block</p>
|
||||
{% if block.status == 'available' %}
|
||||
<button class="btn btn-primary mt-2" onclick="addCase()">
|
||||
<i class="fas fa-plus me-1"></i>Add First Case
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Block Details -->
|
||||
<div class="info-card">
|
||||
<div class="info-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Block Details
|
||||
</div>
|
||||
<div class="info-content">
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Operating Room</div>
|
||||
<div class="info-value">{{ block.operating_room.name }}</div>
|
||||
</div>
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Room Type</div>
|
||||
<div class="info-value">{{ block.operating_room.get_room_type_display }}</div>
|
||||
</div>
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Capacity</div>
|
||||
<div class="info-value">{{ block.operating_room.capacity }} people</div>
|
||||
</div>
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Equipment Level</div>
|
||||
<div class="info-value">{{ block.operating_room.get_equipment_level_display }}</div>
|
||||
</div>
|
||||
{% if block.notes %}
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Notes</div>
|
||||
<div class="info-value">{{ block.notes }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Information -->
|
||||
{% if block.assigned_surgeon or block.assigned_anesthesiologist or block.assigned_nurses.exists %}
|
||||
<div class="info-card">
|
||||
<div class="info-header">
|
||||
<i class="fas fa-users me-2"></i>Assigned Team
|
||||
</div>
|
||||
<div class="info-content">
|
||||
{% if block.assigned_surgeon %}
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Primary Surgeon</div>
|
||||
<div class="info-value">{{ block.assigned_surgeon.get_full_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if block.assigned_anesthesiologist %}
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Anesthesiologist</div>
|
||||
<div class="info-value">{{ block.assigned_anesthesiologist.get_full_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if block.assigned_nurses.exists %}
|
||||
<div class="info-item mb-3">
|
||||
<div class="info-label">Nursing Team</div>
|
||||
<div class="info-value">
|
||||
{% for nurse in block.assigned_nurses.all %}
|
||||
<div>{{ nurse.get_full_name }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Activity Timeline -->
|
||||
<div class="info-card">
|
||||
<div class="info-header">
|
||||
<i class="fas fa-history me-2"></i>Activity Timeline
|
||||
</div>
|
||||
<div class="info-content">
|
||||
{% for activity in block.activities.all %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon bg-primary text-white">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="fw-bold">{{ activity.action }}</div>
|
||||
<div class="text-muted small">{{ activity.created_at|date:"M d, Y H:i" }}</div>
|
||||
{% if activity.notes %}
|
||||
<div class="mt-1">{{ activity.notes }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-history fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted mb-0">No activity recorded</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
// Utilization Chart
|
||||
const ctx = document.getElementById('utilizationChart').getContext('2d');
|
||||
const utilizationChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Scheduled', 'Available', 'Turnover'],
|
||||
datasets: [{
|
||||
data: [{{ block.scheduled_minutes }}, {{ block.available_minutes }}, {{ block.turnover_minutes }}],
|
||||
backgroundColor: ['#28a745', '#ffc107', '#17a2b8'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function addCase() {
|
||||
window.location.href = '{% url "operating_theatre:case_create" %}?block={{ block.pk }}';
|
||||
}
|
||||
|
||||
function startCase(caseId) {
|
||||
if (confirm('Start this surgical case?')) {
|
||||
$.ajax({
|
||||
url: `/operating-theatre/cases/${caseId}/start/`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error starting case: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error starting case');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function completeCase(caseId) {
|
||||
if (confirm('Mark this surgical case as completed?')) {
|
||||
$.ajax({
|
||||
url: `/operating-theatre/cases/${caseId}/complete/`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error completing case: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error completing case');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
630
templates/operating_theatre/blocks/block_form.html
Normal file
630
templates/operating_theatre/blocks/block_form.html
Normal file
@ -0,0 +1,630 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if form.instance.pk %}Edit{% else %}Create{% endif %} OR Block{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-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;
|
||||
}
|
||||
|
||||
.time-picker-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.duration-display {
|
||||
background: #e9ecef;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.team-selection {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.selected-team-member {
|
||||
display: inline-block;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
margin: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.conflict-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.availability-indicator {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.available { background-color: #28a745; }
|
||||
.busy { background-color: #dc3545; }
|
||||
.partial { background-color: #ffc107; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.time-picker-group {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:block_schedule_list' %}">Block Schedule</a></li>
|
||||
<li class="breadcrumb-item active">{% if form.instance.pk %}Edit{% else %}Create{% endif %} Block</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-calendar-plus me-2"></i>{% if form.instance.pk %}Edit{% else %}Create{% endif %} OR Block
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:block_schedule_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Schedule
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Header -->
|
||||
<div class="form-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{% if form.instance.pk %}Edit OR Block{% else %}Create New OR Block{% endif %}</h2>
|
||||
<p class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
Modify the details of this operating room block. Changes will affect all scheduled cases.
|
||||
{% else %}
|
||||
Create a new operating room block to schedule surgical cases and allocate resources.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-2">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span>Block scheduling helps optimize OR utilization</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="blockForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Operating Room</label>
|
||||
{{ form.operating_room }}
|
||||
{% if form.operating_room.errors %}
|
||||
<div class="text-danger small">{{ form.operating_room.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Select the operating room for this block</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Block Type</label>
|
||||
{{ form.block_type }}
|
||||
{% if form.block_type.errors %}
|
||||
<div class="text-danger small">{{ form.block_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Type of surgical block</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Date</label>
|
||||
{{ form.date }}
|
||||
{% if form.date.errors %}
|
||||
<div class="text-danger small">{{ form.date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Time Schedule</label>
|
||||
<div class="time-picker-group">
|
||||
<div class="flex-fill">
|
||||
<label class="form-label small">Start Time</label>
|
||||
{{ form.start_time }}
|
||||
{% if form.start_time.errors %}
|
||||
<div class="text-danger small">{{ form.start_time.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="align-self-end pb-2">
|
||||
<i class="fas fa-arrow-right text-muted"></i>
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<label class="form-label small">End Time</label>
|
||||
{{ form.end_time }}
|
||||
{% if form.end_time.errors %}
|
||||
<div class="text-danger small">{{ form.end_time.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="align-self-end pb-2">
|
||||
<div class="duration-display" id="durationDisplay">
|
||||
<i class="fas fa-clock me-1"></i>
|
||||
<span id="durationText">0 hours</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Team Assignment -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-users me-2"></i>Team Assignment
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Primary Surgeon</label>
|
||||
{{ form.assigned_surgeon }}
|
||||
{% if form.assigned_surgeon.errors %}
|
||||
<div class="text-danger small">{{ form.assigned_surgeon.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Lead surgeon for this block</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Anesthesiologist</label>
|
||||
{{ form.assigned_anesthesiologist }}
|
||||
{% if form.assigned_anesthesiologist.errors %}
|
||||
<div class="text-danger small">{{ form.assigned_anesthesiologist.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Assigned anesthesiologist</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Nursing Team</label>
|
||||
{{ form.assigned_nurses }}
|
||||
{% if form.assigned_nurses.errors %}
|
||||
<div class="text-danger small">{{ form.assigned_nurses.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Select nursing staff for this block</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Configuration -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-cogs me-2"></i>Block Configuration
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Priority Level</label>
|
||||
{{ form.priority }}
|
||||
{% if form.priority.errors %}
|
||||
<div class="text-danger small">{{ form.priority.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Block priority for scheduling conflicts</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Maximum Cases</label>
|
||||
{{ form.max_cases }}
|
||||
{% if form.max_cases.errors %}
|
||||
<div class="text-danger small">{{ form.max_cases.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Maximum number of cases for this block</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Turnover Time (minutes)</label>
|
||||
{{ form.turnover_time }}
|
||||
{% if form.turnover_time.errors %}
|
||||
<div class="text-danger small">{{ form.turnover_time.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Time between cases for room preparation</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-check mt-4">
|
||||
{{ form.allow_emergency_cases }}
|
||||
<label class="form-check-label" for="{{ form.allow_emergency_cases.id_for_label }}">
|
||||
Allow Emergency Cases
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-text">Allow emergency cases to be scheduled in this block</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes and Instructions -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-sticky-note me-2"></i>Notes and Instructions
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Block Notes</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Special instructions or notes for this block</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Setup Requirements</label>
|
||||
{{ form.setup_requirements }}
|
||||
{% if form.setup_requirements.errors %}
|
||||
<div class="text-danger small">{{ form.setup_requirements.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Special equipment or setup requirements</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Availability Check -->
|
||||
<div id="availabilityCheck" class="form-section" style="display: none;">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-calendar-check me-2"></i>Availability Check
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div id="availabilityResults">
|
||||
<!-- Availability results will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Conflicts Warning -->
|
||||
<div id="conflictsWarning" class="conflict-warning" style="display: none;">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Scheduling Conflicts Detected
|
||||
</h6>
|
||||
<div id="conflictsList">
|
||||
<!-- Conflicts will be listed here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Preview -->
|
||||
<div class="preview-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-eye me-2"></i>Block Preview
|
||||
</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Operating Room:</strong> <span id="previewRoom">Not selected</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Date & Time:</strong> <span id="previewDateTime">Not set</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Duration:</strong> <span id="previewDuration">0 hours</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Block Type:</strong> <span id="previewType">Not selected</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Primary Surgeon:</strong> <span id="previewSurgeon">Not assigned</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Max Cases:</strong> <span id="previewMaxCases">Not set</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-between mt-4">
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:block_schedule_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-primary me-2" onclick="checkAvailability()">
|
||||
<i class="fas fa-search me-1"></i>Check Availability
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% if form.instance.pk %}Update Block{% else %}Create Block{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize form
|
||||
updatePreview();
|
||||
calculateDuration();
|
||||
|
||||
// Form change handlers
|
||||
$('#blockForm input, #blockForm select, #blockForm textarea').on('change', function() {
|
||||
updatePreview();
|
||||
if ($(this).attr('name') === 'start_time' || $(this).attr('name') === 'end_time') {
|
||||
calculateDuration();
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time availability checking
|
||||
$('#id_operating_room, #id_date, #id_start_time, #id_end_time').on('change', function() {
|
||||
if (allFieldsFilled()) {
|
||||
setTimeout(checkAvailability, 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function calculateDuration() {
|
||||
const startTime = $('#id_start_time').val();
|
||||
const endTime = $('#id_end_time').val();
|
||||
|
||||
if (startTime && endTime) {
|
||||
const start = new Date('2000-01-01 ' + startTime);
|
||||
const end = new Date('2000-01-01 ' + endTime);
|
||||
|
||||
if (end > start) {
|
||||
const diffMs = end - start;
|
||||
const diffHours = diffMs / (1000 * 60 * 60);
|
||||
const hours = Math.floor(diffHours);
|
||||
const minutes = Math.round((diffHours - hours) * 60);
|
||||
|
||||
let durationText = '';
|
||||
if (hours > 0) {
|
||||
durationText += hours + ' hour' + (hours !== 1 ? 's' : '');
|
||||
}
|
||||
if (minutes > 0) {
|
||||
if (hours > 0) durationText += ' ';
|
||||
durationText += minutes + ' min';
|
||||
}
|
||||
|
||||
$('#durationText').text(durationText || '0 hours');
|
||||
$('#previewDuration').text(durationText || '0 hours');
|
||||
} else {
|
||||
$('#durationText').text('Invalid time range');
|
||||
$('#previewDuration').text('Invalid time range');
|
||||
}
|
||||
} else {
|
||||
$('#durationText').text('0 hours');
|
||||
$('#previewDuration').text('0 hours');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
// Update room
|
||||
const roomSelect = $('#id_operating_room');
|
||||
const roomText = roomSelect.find('option:selected').text();
|
||||
$('#previewRoom').text(roomText !== '---------' ? roomText : 'Not selected');
|
||||
|
||||
// Update date and time
|
||||
const date = $('#id_date').val();
|
||||
const startTime = $('#id_start_time').val();
|
||||
const endTime = $('#id_end_time').val();
|
||||
|
||||
if (date && startTime && endTime) {
|
||||
const dateObj = new Date(date);
|
||||
const dateStr = dateObj.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
$('#previewDateTime').text(`${dateStr}, ${startTime} - ${endTime}`);
|
||||
} else {
|
||||
$('#previewDateTime').text('Not set');
|
||||
}
|
||||
|
||||
// Update block type
|
||||
const typeSelect = $('#id_block_type');
|
||||
const typeText = typeSelect.find('option:selected').text();
|
||||
$('#previewType').text(typeText !== '---------' ? typeText : 'Not selected');
|
||||
|
||||
// Update surgeon
|
||||
const surgeonSelect = $('#id_assigned_surgeon');
|
||||
const surgeonText = surgeonSelect.find('option:selected').text();
|
||||
$('#previewSurgeon').text(surgeonText !== '---------' ? surgeonText : 'Not assigned');
|
||||
|
||||
// Update max cases
|
||||
const maxCases = $('#id_max_cases').val();
|
||||
$('#previewMaxCases').text(maxCases || 'Not set');
|
||||
}
|
||||
|
||||
function allFieldsFilled() {
|
||||
return $('#id_operating_room').val() &&
|
||||
$('#id_date').val() &&
|
||||
$('#id_start_time').val() &&
|
||||
$('#id_end_time').val();
|
||||
}
|
||||
|
||||
function checkAvailability() {
|
||||
if (!allFieldsFilled()) {
|
||||
alert('Please fill in operating room, date, start time, and end time first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
'operating_room': $('#id_operating_room').val(),
|
||||
'date': $('#id_date').val(),
|
||||
'start_time': $('#id_start_time').val(),
|
||||
'end_time': $('#id_end_time').val(),
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:check_availability" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
success: function(response) {
|
||||
displayAvailabilityResults(response);
|
||||
},
|
||||
error: function() {
|
||||
alert('Error checking availability');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function displayAvailabilityResults(data) {
|
||||
const availabilitySection = $('#availabilityCheck');
|
||||
const resultsDiv = $('#availabilityResults');
|
||||
const conflictsWarning = $('#conflictsWarning');
|
||||
const conflictsList = $('#conflictsList');
|
||||
|
||||
// Show availability section
|
||||
availabilitySection.show();
|
||||
|
||||
// Clear previous results
|
||||
resultsDiv.empty();
|
||||
conflictsList.empty();
|
||||
|
||||
// Room availability
|
||||
const roomStatus = data.room_available ? 'available' : 'busy';
|
||||
const roomIndicator = `<span class="availability-indicator ${roomStatus}"></span>`;
|
||||
resultsDiv.append(`
|
||||
<div class="mb-3">
|
||||
<h6>Operating Room Availability</h6>
|
||||
<div>${roomIndicator} ${data.room_available ? 'Available' : 'Not Available'}</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Team availability
|
||||
if (data.team_availability) {
|
||||
resultsDiv.append('<div class="mb-3"><h6>Team Availability</h6></div>');
|
||||
|
||||
Object.keys(data.team_availability).forEach(function(member) {
|
||||
const availability = data.team_availability[member];
|
||||
const status = availability.available ? 'available' : 'busy';
|
||||
const indicator = `<span class="availability-indicator ${status}"></span>`;
|
||||
resultsDiv.append(`
|
||||
<div class="mb-1">
|
||||
${indicator} ${member}: ${availability.available ? 'Available' : availability.reason}
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
// Show conflicts if any
|
||||
if (data.conflicts && data.conflicts.length > 0) {
|
||||
conflictsWarning.show();
|
||||
data.conflicts.forEach(function(conflict) {
|
||||
conflictsList.append(`
|
||||
<div class="mb-1">
|
||||
<i class="fas fa-exclamation-circle me-1"></i>
|
||||
${conflict.message}
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
} else {
|
||||
conflictsWarning.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Form validation
|
||||
$('#blockForm').on('submit', function(e) {
|
||||
const startTime = $('#id_start_time').val();
|
||||
const endTime = $('#id_end_time').val();
|
||||
|
||||
if (startTime && endTime) {
|
||||
const start = new Date('2000-01-01 ' + startTime);
|
||||
const end = new Date('2000-01-01 ' + endTime);
|
||||
|
||||
if (end <= start) {
|
||||
e.preventDefault();
|
||||
alert('End time must be after start time.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
589
templates/operating_theatre/blocks/block_schedule_list.html
Normal file
589
templates/operating_theatre/blocks/block_schedule_list.html
Normal file
@ -0,0 +1,589 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}OR Block Schedule{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.schedule-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.block-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.block-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.block-header {
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.block-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.block-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.utilization-bar {
|
||||
height: 20px;
|
||||
background: #e9ecef;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.utilization-fill {
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.utilization-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.case-list {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.case-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
border-left: 4px solid #007bff;
|
||||
}
|
||||
|
||||
.case-time {
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin-right: 1rem;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.case-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.case-patient {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.case-procedure {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.case-status {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.date-picker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.schedule-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.block-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.case-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.case-time {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</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">Block Schedule</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-calendar-alt me-2"></i>OR Block Schedule
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:block_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>New Block
|
||||
</a>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="exportSchedule('pdf')">
|
||||
<i class="fas fa-file-pdf me-2"></i>PDF
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportSchedule('excel')">
|
||||
<i class="fas fa-file-excel me-2"></i>Excel
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Header -->
|
||||
<div class="schedule-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{{ current_date|date:"l, F d, Y" }}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<strong>Total Blocks:</strong> {{ stats.total_blocks }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Active Blocks:</strong> {{ stats.active_blocks }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<strong>Scheduled Cases:</strong> {{ stats.scheduled_cases }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Available Slots:</strong> {{ stats.available_slots }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<strong>Overall Utilization:</strong> {{ stats.utilization_percentage }}%
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Emergency Slots:</strong> {{ stats.emergency_slots }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="calendar-nav">
|
||||
<a href="?date={{ previous_date|date:'Y-m-d' }}" class="btn btn-outline-light">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
<div class="date-picker">
|
||||
<input type="date" class="form-control" id="datePicker"
|
||||
value="{{ current_date|date:'Y-m-d' }}" onchange="changeDate()">
|
||||
</div>
|
||||
<a href="?date={{ next_date|date:'Y-m-d' }}" class="btn btn-outline-light">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<form method="get" id="filterForm">
|
||||
<input type="hidden" name="date" value="{{ current_date|date:'Y-m-d' }}">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Operating Room</label>
|
||||
<select class="form-select" name="room">
|
||||
<option value="">All Rooms</option>
|
||||
{% for room in operating_rooms %}
|
||||
<option value="{{ room.id }}" {% if request.GET.room == room.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ room.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Surgeon</label>
|
||||
<select class="form-select" name="surgeon">
|
||||
<option value="">All Surgeons</option>
|
||||
{% for surgeon in surgeons %}
|
||||
<option value="{{ surgeon.id }}" {% if request.GET.surgeon == surgeon.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ surgeon.get_full_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Block Type</label>
|
||||
<select class="form-select" name="block_type">
|
||||
<option value="">All Types</option>
|
||||
<option value="regular" {% if request.GET.block_type == 'regular' %}selected{% endif %}>Regular</option>
|
||||
<option value="emergency" {% if request.GET.block_type == 'emergency' %}selected{% endif %}>Emergency</option>
|
||||
<option value="maintenance" {% if request.GET.block_type == 'maintenance' %}selected{% endif %}>Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Status</option>
|
||||
<option value="available" {% if request.GET.status == 'available' %}selected{% endif %}>Available</option>
|
||||
<option value="booked" {% if request.GET.status == 'booked' %}selected{% endif %}>Booked</option>
|
||||
<option value="in_progress" {% if request.GET.status == 'in_progress' %}selected{% endif %}>In Progress</option>
|
||||
<option value="completed" {% if request.GET.status == 'completed' %}selected{% endif %}>Completed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary me-2">
|
||||
<i class="fas fa-filter me-1"></i>Apply Filters
|
||||
</button>
|
||||
<a href="?date={{ current_date|date:'Y-m-d' }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Block Schedule -->
|
||||
<div class="row">
|
||||
{% for block in blocks %}
|
||||
<div class="col-lg-6 col-xl-4">
|
||||
<div class="block-card">
|
||||
<div class="block-header">
|
||||
<div>
|
||||
<h5 class="mb-1">{{ block.operating_room.name }}</h5>
|
||||
<small class="text-muted">{{ block.start_time|date:"H:i" }} - {{ block.end_time|date:"H:i" }}</small>
|
||||
</div>
|
||||
<div>
|
||||
{% if block.status == 'available' %}
|
||||
<span class="badge bg-success">Available</span>
|
||||
{% elif block.status == 'booked' %}
|
||||
<span class="badge bg-primary">Booked</span>
|
||||
{% elif block.status == 'in_progress' %}
|
||||
<span class="badge bg-warning">In Progress</span>
|
||||
{% elif block.status == 'completed' %}
|
||||
<span class="badge bg-secondary">Completed</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block-content">
|
||||
<div class="block-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Assigned Surgeon</div>
|
||||
<div class="info-value">
|
||||
{% if block.assigned_surgeon %}
|
||||
{{ block.assigned_surgeon.get_full_name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Unassigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Block Type</div>
|
||||
<div class="info-value">{{ block.get_block_type_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Duration</div>
|
||||
<div class="info-value">{{ block.duration_hours }} hours</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Utilization</div>
|
||||
<div class="info-value">
|
||||
<div class="utilization-bar">
|
||||
<div class="utilization-fill bg-{% if block.utilization_percentage >= 80 %}success{% elif block.utilization_percentage >= 60 %}warning{% else %}danger{% endif %}"
|
||||
style="width: {{ block.utilization_percentage }}%"></div>
|
||||
<div class="utilization-text">{{ block.utilization_percentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if block.scheduled_cases.exists %}
|
||||
<div class="case-list">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-procedures me-1"></i>Scheduled Cases ({{ block.scheduled_cases.count }})
|
||||
</h6>
|
||||
{% for case in block.scheduled_cases.all %}
|
||||
<div class="case-item">
|
||||
<div class="case-time">{{ case.scheduled_start_time|date:"H:i" }}</div>
|
||||
<div class="case-details">
|
||||
<div class="case-patient">{{ case.patient.get_full_name }}</div>
|
||||
<div class="case-procedure">{{ case.procedure_name }}</div>
|
||||
</div>
|
||||
<div class="case-status">
|
||||
{% if case.status == 'scheduled' %}
|
||||
<span class="badge bg-primary">Scheduled</span>
|
||||
{% elif case.status == 'in_progress' %}
|
||||
<span class="badge bg-warning">In Progress</span>
|
||||
{% elif case.status == 'completed' %}
|
||||
<span class="badge bg-success">Completed</span>
|
||||
{% elif case.status == 'cancelled' %}
|
||||
<span class="badge bg-danger">Cancelled</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-calendar-plus fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted mb-0">No cases scheduled</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex justify-content-between mt-3">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:block_detail' block.pk %}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
{% if block.status == 'available' %}
|
||||
<a href="{% url 'operating_theatre:block_edit' block.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
{% if block.status == 'available' %}
|
||||
<button class="btn btn-success btn-sm" onclick="addCase('{{ block.pk }}')">
|
||||
<i class="fas fa-plus me-1"></i>Add Case
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="duplicateBlock('{{ block.pk }}')">
|
||||
<i class="fas fa-copy me-2"></i>Duplicate
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="printBlock('{{ block.pk }}')">
|
||||
<i class="fas fa-print me-2"></i>Print Schedule
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="#" onclick="cancelBlock('{{ block.pk }}')">
|
||||
<i class="fas fa-times me-2"></i>Cancel Block
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No blocks scheduled</h5>
|
||||
<p class="text-muted">No OR blocks are scheduled for the selected date and filters.</p>
|
||||
<a href="{% url 'operating_theatre:block_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create First Block
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function changeDate() {
|
||||
const selectedDate = document.getElementById('datePicker').value;
|
||||
window.location.href = `?date=${selectedDate}`;
|
||||
}
|
||||
|
||||
function addCase(blockId) {
|
||||
window.location.href = `/operating-theatre/cases/create/?block=${blockId}`;
|
||||
}
|
||||
|
||||
function duplicateBlock(blockId) {
|
||||
if (confirm('Duplicate this block for another date?')) {
|
||||
window.location.href = `/operating-theatre/blocks/${blockId}/duplicate/`;
|
||||
}
|
||||
}
|
||||
|
||||
function printBlock(blockId) {
|
||||
window.open(`/operating-theatre/blocks/${blockId}/print/`, '_blank');
|
||||
}
|
||||
|
||||
function cancelBlock(blockId) {
|
||||
if (confirm('Cancel this OR block? All scheduled cases will need to be rescheduled.')) {
|
||||
$.ajax({
|
||||
url: `/operating-theatre/blocks/${blockId}/cancel/`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error cancelling block: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error cancelling block');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exportSchedule(format) {
|
||||
const currentDate = '{{ current_date|date:"Y-m-d" }}';
|
||||
window.location.href = `/operating-theatre/blocks/export/?date=${currentDate}&format=${format}`;
|
||||
}
|
||||
|
||||
// Auto-submit form on filter change
|
||||
$('#filterForm select').on('change', function() {
|
||||
$('#filterForm').submit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
{% block title %}{% if object %}Edit{% else %}Create{% endif %} Surgical Case - Operating Theatre{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/bootstrap-timepicker/css/bootstrap-timepicker.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/bootstrap-timepicker/css/bootstrap-timepicker.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -404,9 +404,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/bootstrap-timepicker/js/bootstrap-timepicker.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/bootstrap-timepicker/js/bootstrap-timepicker.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
@ -447,7 +447,7 @@ $(document).ready(function() {
|
||||
|
||||
function loadPatientInfo(patientId) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:patient_info" 0 %}'.replace('0', patientId),
|
||||
url: '{% url "patients:get_patient_info" 0 %}'.replace('0', patientId),
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
@ -467,62 +467,62 @@ function loadPatientInfo(patientId) {
|
||||
});
|
||||
}
|
||||
|
||||
function checkRoomAvailability() {
|
||||
var roomId = $('#id_operating_room').val();
|
||||
var date = $('#id_scheduled_date').val();
|
||||
var startTime = $('#id_scheduled_start_time').val();
|
||||
var duration = $('#id_estimated_duration').val();
|
||||
|
||||
if (roomId && date && startTime && duration) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:check_room_availability" %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}',
|
||||
'room_id': roomId,
|
||||
'date': date,
|
||||
'start_time': startTime,
|
||||
'duration': duration,
|
||||
'case_id': '{{ object.id|default:"" }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.available) {
|
||||
$('#availability-message').html('Room is available for the selected time slot');
|
||||
$('#room-availability').removeClass('alert-warning alert-danger').addClass('alert-success').show();
|
||||
} else {
|
||||
$('#availability-message').html('Room conflict: ' + response.conflict_reason);
|
||||
$('#room-availability').removeClass('alert-success alert-info').addClass('alert-danger').show();
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$('#availability-message').html('Unable to check room availability');
|
||||
$('#room-availability').removeClass('alert-success alert-info').addClass('alert-warning').show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function checkRoomAvailability() {#}
|
||||
{# var roomId = $('#id_operating_room').val();#}
|
||||
{# var date = $('#id_scheduled_date').val();#}
|
||||
{# var startTime = $('#id_scheduled_start_time').val();#}
|
||||
{# var duration = $('#id_estimated_duration').val();#}
|
||||
{# #}
|
||||
{# if (roomId && date && startTime && duration) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "operating_theatre:check_room_availability" %}',#}
|
||||
{# method: 'POST',#}
|
||||
{# data: {#}
|
||||
{# 'csrfmiddlewaretoken': '{{ csrf_token }}',#}
|
||||
{# 'room_id': roomId,#}
|
||||
{# 'date': date,#}
|
||||
{# 'start_time': startTime,#}
|
||||
{# 'duration': duration,#}
|
||||
{# 'case_id': '{{ object.id|default:"" }}'#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# if (response.available) {#}
|
||||
{# $('#availability-message').html('Room is available for the selected time slot');#}
|
||||
{# $('#room-availability').removeClass('alert-warning alert-danger').addClass('alert-success').show();#}
|
||||
{# } else {#}
|
||||
{# $('#availability-message').html('Room conflict: ' + response.conflict_reason);#}
|
||||
{# $('#room-availability').removeClass('alert-success alert-info').addClass('alert-danger').show();#}
|
||||
{# }#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# $('#availability-message').html('Unable to check room availability');#}
|
||||
{# $('#room-availability').removeClass('alert-success alert-info').addClass('alert-warning').show();#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function loadRoomSchedule() {
|
||||
var roomId = $('#id_operating_room').val();
|
||||
var date = $('#id_scheduled_date').val();
|
||||
|
||||
if (roomId && date) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:room_schedule" %}',
|
||||
method: 'GET',
|
||||
data: {
|
||||
'room_id': roomId,
|
||||
'date': date
|
||||
},
|
||||
success: function(response) {
|
||||
$('#room-schedule').html(response.html);
|
||||
},
|
||||
error: function() {
|
||||
$('#room-schedule').html('<p class="text-danger">Unable to load room schedule</p>');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{#function loadRoomSchedule() {#}
|
||||
{# var roomId = $('#id_operating_room').val();#}
|
||||
{# var date = $('#id_scheduled_date').val();#}
|
||||
{# #}
|
||||
{# if (roomId && date) {#}
|
||||
{# $.ajax({#}
|
||||
{# url: '{% url "operating_theatre:room_schedule" %}',#}
|
||||
{# method: 'GET',#}
|
||||
{# data: {#}
|
||||
{# 'room_id': roomId,#}
|
||||
{# 'date': date#}
|
||||
{# },#}
|
||||
{# success: function(response) {#}
|
||||
{# $('#room-schedule').html(response.html);#}
|
||||
{# },#}
|
||||
{# error: function() {#}
|
||||
{# $('#room-schedule').html('<p class="text-danger">Unable to load room schedule</p>');#}
|
||||
{# }#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
|
||||
function addTeamMember() {
|
||||
var html = `
|
||||
|
||||
@ -0,0 +1,537 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Equipment - {{ equipment.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-header {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.equipment-info {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.impact-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;
|
||||
}
|
||||
|
||||
.alternative-actions {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #fff5f5;
|
||||
border: 2px solid #fed7d7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.type-to-confirm {
|
||||
background: #f7fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.equipment-image {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
border-radius: 0.375rem;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:equipment_list' %}">Equipment</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:equipment_detail' equipment.pk %}">{{ equipment.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete Equipment</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Equipment
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:equipment_detail' equipment.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Equipment
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Header -->
|
||||
<div class="delete-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Delete Equipment
|
||||
</h2>
|
||||
<p class="mb-0">
|
||||
You are about to permanently delete this equipment from the system. This action cannot be undone and will affect all related records and maintenance history.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-2">
|
||||
<i class="fas fa-tools fa-3x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Warning Section -->
|
||||
<div class="warning-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Critical Warning
|
||||
</h5>
|
||||
<ul class="mb-0">
|
||||
<li>This equipment record will be permanently deleted</li>
|
||||
<li>All maintenance history and logs will be removed</li>
|
||||
<li>Any active reservations will be cancelled</li>
|
||||
<li>Financial records and depreciation data will be lost</li>
|
||||
<li>QR codes and asset tags will become invalid</li>
|
||||
<li>Related documents and images will be deleted</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Information -->
|
||||
<div class="equipment-info">
|
||||
<h5 class="mb-3">Equipment Information</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Equipment Name</div>
|
||||
<div class="info-value">{{ equipment.name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Equipment ID</div>
|
||||
<div class="info-value">{{ equipment.equipment_id|default:"Not assigned" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Serial Number</div>
|
||||
<div class="info-value">{{ equipment.serial_number }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Manufacturer</div>
|
||||
<div class="info-value">{{ equipment.manufacturer }} {{ equipment.model }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Category</div>
|
||||
<div class="info-value">{{ equipment.get_category_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Current Location</div>
|
||||
<div class="info-value">{{ equipment.current_location|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value">
|
||||
{% if equipment.status == 'available' %}
|
||||
<span class="badge bg-success">Available</span>
|
||||
{% elif equipment.status == 'in_use' %}
|
||||
<span class="badge bg-primary">In Use</span>
|
||||
{% elif equipment.status == 'maintenance' %}
|
||||
<span class="badge bg-warning">Maintenance</span>
|
||||
{% elif equipment.status == 'out_of_order' %}
|
||||
<span class="badge bg-danger">Out of Order</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Purchase Cost</div>
|
||||
<div class="info-value">
|
||||
{% if equipment.purchase_cost %}
|
||||
${{ equipment.purchase_cost|floatformat:2 }}
|
||||
{% else %}
|
||||
Not specified
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 text-center">
|
||||
{% if equipment.image %}
|
||||
<img src="{{ equipment.image.url }}" alt="{{ equipment.name }}" class="equipment-image">
|
||||
{% else %}
|
||||
<div class="equipment-image d-flex align-items-center justify-content-center bg-light">
|
||||
<i class="fas fa-tools fa-3x text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Impact Analysis -->
|
||||
<div class="impact-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>Impact Analysis
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-danger">{{ maintenance_records_count }}</div>
|
||||
<div class="text-muted">Maintenance Records</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-warning">{{ active_reservations_count }}</div>
|
||||
<div class="text-muted">Active Reservations</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-info">{{ usage_logs_count }}</div>
|
||||
<div class="text-muted">Usage Logs</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center mb-3">
|
||||
<div class="h3 text-secondary">{{ documents_count }}</div>
|
||||
<div class="text-muted">Related Documents</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocking Conditions -->
|
||||
{% if blocking_conditions %}
|
||||
<div class="impact-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-ban me-2"></i>Deletion Blocked
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="alert alert-danger">
|
||||
<h6 class="alert-heading">Cannot Delete Equipment</h6>
|
||||
<p class="mb-0">This equipment cannot be deleted due to the following conditions:</p>
|
||||
<ul class="mt-2 mb-0">
|
||||
{% for condition in blocking_conditions %}
|
||||
<li>{{ condition }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<p class="mb-0">Please resolve these issues before attempting to delete this equipment.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<!-- Alternative Actions -->
|
||||
<div class="alternative-actions">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
Consider These Alternatives
|
||||
</h5>
|
||||
<p class="mb-3">Before deleting this equipment, consider these alternative actions:</p>
|
||||
|
||||
<button class="btn btn-outline-primary action-button" onclick="deactivateEquipment()">
|
||||
<i class="fas fa-power-off me-1"></i>Deactivate Equipment
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-warning action-button" onclick="markOutOfOrder()">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>Mark Out of Order
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-info action-button" onclick="transferEquipment()">
|
||||
<i class="fas fa-exchange-alt me-1"></i>Transfer to Storage
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-secondary action-button" onclick="archiveEquipment()">
|
||||
<i class="fas fa-archive me-1"></i>Archive Equipment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Section -->
|
||||
<div class="confirmation-section">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
Deletion Confirmation Required
|
||||
</h5>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
<strong>Type the following text to confirm deletion:</strong>
|
||||
</label>
|
||||
<div class="type-to-confirm">
|
||||
DELETE {{ equipment.name }} {{ equipment.serial_number }}
|
||||
</div>
|
||||
<input type="text" class="form-control" id="confirmationText"
|
||||
placeholder="Type the confirmation text here..." required>
|
||||
<div class="form-text">This confirmation helps prevent accidental deletions.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reason for Deletion</label>
|
||||
<select class="form-select" name="deletion_reason" required>
|
||||
<option value="">Select a reason...</option>
|
||||
<option value="equipment_failure">Equipment Failure</option>
|
||||
<option value="obsolete">Equipment Obsolete</option>
|
||||
<option value="replaced">Replaced by New Equipment</option>
|
||||
<option value="sold">Equipment Sold</option>
|
||||
<option value="donated">Equipment Donated</option>
|
||||
<option value="scrapped">Equipment Scrapped</option>
|
||||
<option value="lost_stolen">Lost or Stolen</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Additional Notes</label>
|
||||
<textarea class="form-control" name="deletion_notes" rows="3"
|
||||
placeholder="Provide additional context for this deletion..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="cancelReservationsCheck" name="cancel_reservations" checked>
|
||||
<label class="form-check-label" for="cancelReservationsCheck">
|
||||
Automatically cancel all active reservations
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="notifyUsersCheck" name="notify_users" checked>
|
||||
<label class="form-check-label" for="notifyUsersCheck">
|
||||
Notify users with active reservations
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="archiveDataCheck" name="archive_data">
|
||||
<label class="form-check-label" for="archiveDataCheck">
|
||||
Archive maintenance and usage data before deletion
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:equipment_detail' equipment.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-danger" id="deleteButton" disabled>
|
||||
<i class="fas fa-trash-alt me-1"></i>
|
||||
Permanently Delete Equipment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const expectedText = 'DELETE {{ equipment.name }} {{ equipment.serial_number }}';
|
||||
|
||||
$('#confirmationText').on('input', function() {
|
||||
const enteredText = $(this).val().trim();
|
||||
const deleteButton = $('#deleteButton');
|
||||
|
||||
if (enteredText === expectedText) {
|
||||
deleteButton.prop('disabled', false);
|
||||
$(this).removeClass('is-invalid').addClass('is-valid');
|
||||
} else {
|
||||
deleteButton.prop('disabled', true);
|
||||
$(this).removeClass('is-valid');
|
||||
if (enteredText.length > 0) {
|
||||
$(this).addClass('is-invalid');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function deactivateEquipment() {
|
||||
if (confirm('Deactivate this equipment instead of deleting it?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:equipment_deactivate" equipment.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Equipment has been deactivated successfully.');
|
||||
window.location.href = '{% url "operating_theatre:equipment_list" %}';
|
||||
} else {
|
||||
alert('Error deactivating equipment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deactivating equipment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function markOutOfOrder() {
|
||||
if (confirm('Mark this equipment as out of order?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:equipment_out_of_order" equipment.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Equipment has been marked as out of order.');
|
||||
window.location.href = '{% url "operating_theatre:equipment_detail" equipment.pk %}';
|
||||
} else {
|
||||
alert('Error updating equipment status: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error updating equipment status');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function transferEquipment() {
|
||||
window.location.href = '{% url "operating_theatre:equipment_transfer" equipment.pk %}';
|
||||
}
|
||||
|
||||
function archiveEquipment() {
|
||||
if (confirm('Archive this equipment instead of deleting it?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:equipment_archive" equipment.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Equipment has been archived successfully.');
|
||||
window.location.href = '{% url "operating_theatre:equipment_list" %}';
|
||||
} else {
|
||||
alert('Error archiving equipment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error archiving equipment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Form submission with additional confirmation
|
||||
$('#deleteForm').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const maintenanceRecords = {{ maintenance_records_count }};
|
||||
const activeReservations = {{ active_reservations_count }};
|
||||
|
||||
let confirmMessage = 'Are you absolutely sure you want to delete this equipment?\n\n';
|
||||
confirmMessage += `This will remove ${maintenanceRecords} maintenance record(s) and cancel ${activeReservations} active reservation(s).\n\n`;
|
||||
confirmMessage += 'This action cannot be undone.';
|
||||
|
||||
if (confirm(confirmMessage)) {
|
||||
// Show loading state
|
||||
$('#deleteButton').html('<i class="fas fa-spinner fa-spin me-1"></i>Deleting...').prop('disabled', true);
|
||||
|
||||
// Submit the form
|
||||
this.submit();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
691
templates/operating_theatre/equipment/equipment_detail.html
Normal file
691
templates/operating_theatre/equipment/equipment_detail.html
Normal file
@ -0,0 +1,691 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ equipment.name }} - Equipment Details{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.equipment-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.equipment-image {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 300px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.375rem;
|
||||
border: 3px solid white;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.info-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: 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;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-available { background-color: #28a745; }
|
||||
.status-in-use { background-color: #007bff; }
|
||||
.status-maintenance { background-color: #ffc107; }
|
||||
.status-out-of-order { background-color: #dc3545; }
|
||||
.status-reserved { background-color: #6f42c1; }
|
||||
|
||||
.maintenance-timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 1.5rem;
|
||||
border-left: 2px solid #dee2e6;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.timeline-item:last-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px #007bff;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.timeline-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.timeline-description {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.usage-chart {
|
||||
height: 300px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.qr-code-section {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 0 auto 1rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.alert-maintenance {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.specifications-table {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.specifications-table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.specifications-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.equipment-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:equipment_list' %}">Equipment</a></li>
|
||||
<li class="breadcrumb-item active">{{ equipment.name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-tools me-2"></i>{{ equipment.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:equipment_edit' equipment.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="scheduleMaintenance()">
|
||||
<i class="fas fa-wrench me-2"></i>Schedule Maintenance
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="generateQR()">
|
||||
<i class="fas fa-qrcode me-2"></i>Generate QR Code
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="printDetails()">
|
||||
<i class="fas fa-print me-2"></i>Print Details
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="#" onclick="reportIssue()">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Report Issue
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="{% url 'operating_theatre:equipment_list' %}" class="btn btn-outline-secondary ms-2">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Header -->
|
||||
<div class="equipment-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<span class="status-indicator status-{{ equipment.status }}"></span>
|
||||
{% if equipment.status == 'available' %}
|
||||
<span class="badge bg-success me-2">Available</span>
|
||||
{% elif equipment.status == 'in_use' %}
|
||||
<span class="badge bg-primary me-2">In Use</span>
|
||||
{% elif equipment.status == 'maintenance' %}
|
||||
<span class="badge bg-warning me-2">Maintenance</span>
|
||||
{% elif equipment.status == 'out_of_order' %}
|
||||
<span class="badge bg-danger me-2">Out of Order</span>
|
||||
{% elif equipment.status == 'reserved' %}
|
||||
<span class="badge bg-info me-2">Reserved</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-light text-dark">{{ equipment.get_category_display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="mb-2">{{ equipment.name }}</h2>
|
||||
<p class="mb-3">{{ equipment.description|default:"No description available" }}</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Serial Number:</strong> {{ equipment.serial_number }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Manufacturer:</strong> {{ equipment.manufacturer }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Model:</strong> {{ equipment.model }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Location:</strong>
|
||||
{% if equipment.current_location %}
|
||||
{{ equipment.current_location.name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Purchase Date:</strong>
|
||||
{% if equipment.purchase_date %}
|
||||
{{ equipment.purchase_date|date:"M d, Y" }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not recorded</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Warranty Expires:</strong>
|
||||
{% if equipment.warranty_expiry %}
|
||||
{{ equipment.warranty_expiry|date:"M d, Y" }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not recorded</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
{% if equipment.image %}
|
||||
<img src="{{ equipment.image.url }}" alt="{{ equipment.name }}" class="equipment-image">
|
||||
{% else %}
|
||||
<div class="equipment-image bg-light d-flex align-items-center justify-content-center">
|
||||
<i class="fas fa-tools fa-4x text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Alert -->
|
||||
{% if equipment.maintenance_due %}
|
||||
<div class="alert-maintenance">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Maintenance Due
|
||||
</h6>
|
||||
<p class="mb-0">
|
||||
This equipment is due for maintenance. Last maintenance was on
|
||||
{% if equipment.last_maintenance_date %}
|
||||
{{ equipment.last_maintenance_date|date:"M d, Y" }}
|
||||
{% else %}
|
||||
never
|
||||
{% endif %}.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<!-- Equipment Information -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Basic Information -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Equipment Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Equipment ID</div>
|
||||
<div class="info-value">{{ equipment.equipment_id|default:"Not assigned" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Asset Tag</div>
|
||||
<div class="info-value">{{ equipment.asset_tag|default:"Not assigned" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Category</div>
|
||||
<div class="info-value">{{ equipment.get_category_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Current Status</div>
|
||||
<div class="info-value">
|
||||
<span class="status-indicator status-{{ equipment.status }}"></span>
|
||||
{{ equipment.get_status_display }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Purchase Cost</div>
|
||||
<div class="info-value">
|
||||
{% if equipment.purchase_cost %}
|
||||
${{ equipment.purchase_cost|floatformat:2 }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not recorded</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Depreciation</div>
|
||||
<div class="info-value">
|
||||
{% if equipment.depreciation_rate %}
|
||||
{{ equipment.depreciation_rate }}% per year
|
||||
{% else %}
|
||||
<span class="text-muted">Not set</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Specifications -->
|
||||
{% if equipment.specifications %}
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-cogs me-2"></i>Technical Specifications
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<table class="specifications-table table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Specification</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for spec, value in equipment.specifications.items %}
|
||||
<tr>
|
||||
<td>{{ spec|title }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Maintenance History -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-history me-2"></i>Maintenance History
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% if equipment.maintenance_records.all %}
|
||||
<div class="maintenance-timeline">
|
||||
{% for record in equipment.maintenance_records.all %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-date">{{ record.date|date:"M d, Y" }}</div>
|
||||
<div class="timeline-title">{{ record.maintenance_type|title }}</div>
|
||||
<div class="timeline-description">
|
||||
{{ record.description|default:"No description provided" }}
|
||||
{% if record.performed_by %}
|
||||
<br><small class="text-muted">Performed by: {{ record.performed_by.get_full_name }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-wrench fa-2x text-muted mb-3"></i>
|
||||
<p class="text-muted">No maintenance records found</p>
|
||||
<button class="btn btn-primary" onclick="scheduleMaintenance()">
|
||||
<i class="fas fa-plus me-1"></i>Schedule First Maintenance
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-chart-line me-2"></i>Usage Statistics
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="usage-chart">
|
||||
<canvas id="usageChart"></canvas>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<strong class="text-primary">{{ equipment.total_usage_hours|default:0 }}</strong>
|
||||
</div>
|
||||
<div class="text-muted small">Total Hours</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<strong class="text-success">{{ equipment.monthly_usage_hours|default:0 }}</strong>
|
||||
</div>
|
||||
<div class="text-muted small">This Month</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<strong class="text-warning">{{ equipment.utilization_rate|default:0 }}%</strong>
|
||||
</div>
|
||||
<div class="text-muted small">Utilization</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-2">
|
||||
<strong class="text-info">{{ equipment.efficiency_score|default:0 }}%</strong>
|
||||
</div>
|
||||
<div class="text-muted small">Efficiency</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- QR Code -->
|
||||
<div class="qr-code-section">
|
||||
<h6 class="mb-3">Equipment QR Code</h6>
|
||||
<div class="qr-code">
|
||||
{% if equipment.qr_code %}
|
||||
<img src="{{ equipment.qr_code.url }}" alt="QR Code" style="width: 100%; height: 100%;">
|
||||
{% else %}
|
||||
<i class="fas fa-qrcode fa-3x text-muted"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="generateQR()">
|
||||
<i class="fas fa-download me-1"></i>Download QR Code
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="d-grid gap-2">
|
||||
{% if equipment.status == 'available' %}
|
||||
<button class="btn btn-success" onclick="reserveEquipment()">
|
||||
<i class="fas fa-lock me-1"></i>Reserve Equipment
|
||||
</button>
|
||||
{% elif equipment.status == 'reserved' %}
|
||||
<button class="btn btn-warning" onclick="releaseEquipment()">
|
||||
<i class="fas fa-unlock me-1"></i>Release Reservation
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button class="btn btn-primary" onclick="scheduleMaintenance()">
|
||||
<i class="fas fa-wrench me-1"></i>Schedule Maintenance
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-secondary" onclick="viewHistory()">
|
||||
<i class="fas fa-history me-1"></i>View Full History
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-danger" onclick="reportIssue()">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>Report Issue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Assignment -->
|
||||
{% if equipment.current_assignment %}
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-user-md me-2"></i>Current Assignment
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="mb-2">
|
||||
<strong>Assigned to:</strong> {{ equipment.current_assignment.assigned_to.get_full_name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Since:</strong> {{ equipment.current_assignment.assigned_date|date:"M d, Y" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Purpose:</strong> {{ equipment.current_assignment.purpose|default:"General use" }}
|
||||
</div>
|
||||
{% if equipment.current_assignment.expected_return %}
|
||||
<div class="mb-2">
|
||||
<strong>Expected Return:</strong> {{ equipment.current_assignment.expected_return|date:"M d, Y" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Related Documents -->
|
||||
{% if equipment.documents.all %}
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-file-alt me-2"></i>Related Documents
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% for document in equipment.documents.all %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<div class="fw-bold">{{ document.name }}</div>
|
||||
<small class="text-muted">{{ document.document_type|title }}</small>
|
||||
</div>
|
||||
<a href="{{ document.file.url }}" class="btn btn-outline-primary btn-sm" target="_blank">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
// Usage Chart
|
||||
const ctx = document.getElementById('usageChart').getContext('2d');
|
||||
const usageChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: {{ equipment.usage_chart_labels|safe }},
|
||||
datasets: [{
|
||||
label: 'Usage Hours',
|
||||
data: {{ equipment.usage_chart_data|safe }},
|
||||
borderColor: '#007bff',
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
||||
tension: 0.4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function reserveEquipment() {
|
||||
if (confirm('Reserve this equipment?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:equipment_reserve" equipment.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error reserving equipment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error reserving equipment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function releaseEquipment() {
|
||||
if (confirm('Release this equipment reservation?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:equipment_release" equipment.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error releasing equipment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error releasing equipment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleMaintenance() {
|
||||
window.location.href = '{% url "operating_theatre:equipment_maintenance_schedule" equipment.pk %}';
|
||||
}
|
||||
|
||||
function generateQR() {
|
||||
window.open('{% url "operating_theatre:equipment_qr_code" equipment.pk %}', '_blank');
|
||||
}
|
||||
|
||||
function viewHistory() {
|
||||
window.location.href = '{% url "operating_theatre:equipment_history" equipment.pk %}';
|
||||
}
|
||||
|
||||
function reportIssue() {
|
||||
window.location.href = '{% url "operating_theatre:equipment_report_issue" equipment.pk %}';
|
||||
}
|
||||
|
||||
function printDetails() {
|
||||
window.print();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
743
templates/operating_theatre/equipment/equipment_form.html
Normal file
743
templates/operating_theatre/equipment/equipment_form.html
Normal file
@ -0,0 +1,743 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% if form.instance.pk %}Edit{% else %}Add{% endif %} Equipment{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.form-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;
|
||||
}
|
||||
|
||||
.image-upload-area {
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
background: #f8f9fa;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-upload-area:hover {
|
||||
border-color: #007bff;
|
||||
background: #e7f3ff;
|
||||
}
|
||||
|
||||
.image-upload-area.dragover {
|
||||
border-color: #007bff;
|
||||
background: #e7f3ff;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 0.375rem;
|
||||
border: 2px solid #dee2e6;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.specifications-builder {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spec-item input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.maintenance-schedule {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.cost-calculator {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.validation-feedback {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.validation-feedback.valid {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.validation-feedback.invalid {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.auto-generate-btn {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: none;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.input-group-auto {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.spec-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.spec-item input {
|
||||
margin-bottom: 0.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"><a href="{% url 'operating_theatre:equipment_list' %}">Equipment</a></li>
|
||||
<li class="breadcrumb-item active">{% if form.instance.pk %}Edit{% else %}Add{% endif %} Equipment</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-tools me-2"></i>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Equipment
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:equipment_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Header -->
|
||||
<div class="form-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{% if form.instance.pk %}Edit Equipment{% else %}Add New Equipment{% endif %}</h2>
|
||||
<p class="mb-0">
|
||||
{% if form.instance.pk %}
|
||||
Update the equipment information. Changes will be logged for audit purposes.
|
||||
{% else %}
|
||||
Add new equipment to the operating theatre inventory. All fields marked with * are required.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-2">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<span>Equipment tracking helps optimize OR efficiency</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" id="equipmentForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Equipment Name</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Descriptive name for the equipment</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Category</label>
|
||||
{{ form.category }}
|
||||
{% if form.category.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.category.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Equipment category for organization</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
{{ form.description }}
|
||||
{% if form.description.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.description.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Detailed description of the equipment</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Equipment ID</label>
|
||||
<div class="input-group-auto">
|
||||
{{ form.equipment_id }}
|
||||
<button type="button" class="auto-generate-btn" onclick="generateEquipmentId()">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% if form.equipment_id.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.equipment_id.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Unique identifier for the equipment</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Serial Number</label>
|
||||
{{ form.serial_number }}
|
||||
{% if form.serial_number.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.serial_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Manufacturer's serial number</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Equipment Image</label>
|
||||
<div class="image-upload-area" onclick="$('#id_image').click()">
|
||||
{% if form.instance.image %}
|
||||
<img src="{{ form.instance.image.url }}" alt="Equipment Image" class="image-preview" id="imagePreview">
|
||||
{% else %}
|
||||
<div id="uploadPlaceholder">
|
||||
<i class="fas fa-camera fa-3x text-muted mb-2"></i>
|
||||
<div class="text-muted">Click to upload image</div>
|
||||
<small class="text-muted">JPG, PNG up to 5MB</small>
|
||||
</div>
|
||||
<img id="imagePreview" class="image-preview" style="display: none;">
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ form.image }}
|
||||
{% if form.image.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.image.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manufacturer Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-industry me-2"></i>Manufacturer Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Manufacturer</label>
|
||||
{{ form.manufacturer }}
|
||||
{% if form.manufacturer.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.manufacturer.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Model</label>
|
||||
{{ form.model }}
|
||||
{% if form.model.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.model.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Model Year</label>
|
||||
{{ form.model_year }}
|
||||
{% if form.model_year.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.model_year.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location and Status -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>Location and Status
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Current Location</label>
|
||||
{{ form.current_location }}
|
||||
{% if form.current_location.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.current_location.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Operating room or storage location</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label required">Status</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.status.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Current operational status</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Asset Tag</label>
|
||||
<div class="input-group-auto">
|
||||
{{ form.asset_tag }}
|
||||
<button type="button" class="auto-generate-btn" onclick="generateAssetTag()">
|
||||
<i class="fas fa-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% if form.asset_tag.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.asset_tag.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Hospital asset tracking number</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Financial Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-dollar-sign me-2"></i>Financial Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Purchase Date</label>
|
||||
{{ form.purchase_date }}
|
||||
{% if form.purchase_date.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.purchase_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Purchase Cost</label>
|
||||
{{ form.purchase_cost }}
|
||||
{% if form.purchase_cost.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.purchase_cost.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Warranty Expiry</label>
|
||||
{{ form.warranty_expiry }}
|
||||
{% if form.warranty_expiry.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.warranty_expiry.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Depreciation Rate (%)</label>
|
||||
{{ form.depreciation_rate }}
|
||||
{% if form.depreciation_rate.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.depreciation_rate.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cost-calculator">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-calculator me-2"></i>Cost Analysis
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="h5 text-primary" id="currentValue">$0.00</div>
|
||||
<small class="text-muted">Current Value</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="h5 text-warning" id="annualDepreciation">$0.00</div>
|
||||
<small class="text-muted">Annual Depreciation</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-center">
|
||||
<div class="h5 text-info" id="equipmentAge">0 years</div>
|
||||
<small class="text-muted">Equipment Age</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Technical Specifications -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-cogs me-2"></i>Technical Specifications
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="specifications-builder">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0">Equipment Specifications</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addSpecification()">
|
||||
<i class="fas fa-plus me-1"></i>Add Specification
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="specificationsContainer">
|
||||
{% if form.instance.specifications %}
|
||||
{% for spec, value in form.instance.specifications.items %}
|
||||
<div class="spec-item">
|
||||
<input type="text" class="form-control" placeholder="Specification name" value="{{ spec }}" name="spec_name[]">
|
||||
<input type="text" class="form-control" placeholder="Value" value="{{ value }}" name="spec_value[]">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeSpecification(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-text mt-2">
|
||||
Add technical specifications like power requirements, dimensions, weight, etc.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-wrench me-2"></i>Maintenance Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Last Maintenance Date</label>
|
||||
{{ form.last_maintenance_date }}
|
||||
{% if form.last_maintenance_date.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.last_maintenance_date.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Maintenance Interval (days)</label>
|
||||
{{ form.maintenance_interval }}
|
||||
{% if form.maintenance_interval.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.maintenance_interval.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maintenance-schedule">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Maintenance Schedule
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Next Maintenance Due:</strong> <span id="nextMaintenanceDate">Not calculated</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Days Until Due:</strong> <span id="daysUntilMaintenance">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-sticky-note me-2"></i>Additional Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="form-group mb-3">
|
||||
<label class="form-label">Notes</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="validation-feedback invalid">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Additional notes, special instructions, or important information</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:equipment_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-info me-2" onclick="previewEquipment()">
|
||||
<i class="fas fa-eye me-1"></i>Preview
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>
|
||||
{% if form.instance.pk %}Update Equipment{% else %}Add Equipment{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize calculations
|
||||
calculateCosts();
|
||||
calculateMaintenanceSchedule();
|
||||
|
||||
// Form change handlers
|
||||
$('#id_purchase_cost, #id_purchase_date, #id_depreciation_rate').on('change', calculateCosts);
|
||||
$('#id_last_maintenance_date, #id_maintenance_interval').on('change', calculateMaintenanceSchedule);
|
||||
|
||||
// Image upload handler
|
||||
$('#id_image').on('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
$('#imagePreview').attr('src', e.target.result).show();
|
||||
$('#uploadPlaceholder').hide();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag and drop for image upload
|
||||
$('.image-upload-area').on('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).addClass('dragover');
|
||||
});
|
||||
|
||||
$('.image-upload-area').on('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).removeClass('dragover');
|
||||
});
|
||||
|
||||
$('.image-upload-area').on('drop', function(e) {
|
||||
e.preventDefault();
|
||||
$(this).removeClass('dragover');
|
||||
|
||||
const files = e.originalEvent.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
$('#id_image')[0].files = files;
|
||||
$('#id_image').trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function generateEquipmentId() {
|
||||
const category = $('#id_category').val();
|
||||
const timestamp = Date.now().toString().slice(-6);
|
||||
const equipmentId = `EQ-${category}-${timestamp}`;
|
||||
$('#id_equipment_id').val(equipmentId);
|
||||
}
|
||||
|
||||
function generateAssetTag() {
|
||||
const timestamp = Date.now().toString().slice(-8);
|
||||
const assetTag = `AT-${timestamp}`;
|
||||
$('#id_asset_tag').val(assetTag);
|
||||
}
|
||||
|
||||
function addSpecification() {
|
||||
const container = $('#specificationsContainer');
|
||||
const specItem = $(`
|
||||
<div class="spec-item">
|
||||
<input type="text" class="form-control" placeholder="Specification name" name="spec_name[]">
|
||||
<input type="text" class="form-control" placeholder="Value" name="spec_value[]">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeSpecification(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
container.append(specItem);
|
||||
}
|
||||
|
||||
function removeSpecification(button) {
|
||||
$(button).closest('.spec-item').remove();
|
||||
}
|
||||
|
||||
function calculateCosts() {
|
||||
const purchaseCost = parseFloat($('#id_purchase_cost').val()) || 0;
|
||||
const purchaseDate = $('#id_purchase_date').val();
|
||||
const depreciationRate = parseFloat($('#id_depreciation_rate').val()) || 0;
|
||||
|
||||
if (purchaseCost > 0 && purchaseDate && depreciationRate > 0) {
|
||||
const purchaseDateObj = new Date(purchaseDate);
|
||||
const currentDate = new Date();
|
||||
const yearsOld = (currentDate - purchaseDateObj) / (1000 * 60 * 60 * 24 * 365.25);
|
||||
|
||||
const annualDepreciation = purchaseCost * (depreciationRate / 100);
|
||||
const totalDepreciation = annualDepreciation * yearsOld;
|
||||
const currentValue = Math.max(0, purchaseCost - totalDepreciation);
|
||||
|
||||
$('#currentValue').text('$' + currentValue.toFixed(2));
|
||||
$('#annualDepreciation').text('$' + annualDepreciation.toFixed(2));
|
||||
$('#equipmentAge').text(yearsOld.toFixed(1) + ' years');
|
||||
} else {
|
||||
$('#currentValue').text('$0.00');
|
||||
$('#annualDepreciation').text('$0.00');
|
||||
$('#equipmentAge').text('0 years');
|
||||
}
|
||||
}
|
||||
|
||||
function calculateMaintenanceSchedule() {
|
||||
const lastMaintenanceDate = $('#id_last_maintenance_date').val();
|
||||
const maintenanceInterval = parseInt($('#id_maintenance_interval').val()) || 0;
|
||||
|
||||
if (lastMaintenanceDate && maintenanceInterval > 0) {
|
||||
const lastDate = new Date(lastMaintenanceDate);
|
||||
const nextDate = new Date(lastDate.getTime() + (maintenanceInterval * 24 * 60 * 60 * 1000));
|
||||
const currentDate = new Date();
|
||||
const daysUntil = Math.ceil((nextDate - currentDate) / (1000 * 60 * 60 * 24));
|
||||
|
||||
$('#nextMaintenanceDate').text(nextDate.toLocaleDateString());
|
||||
$('#daysUntilMaintenance').text(daysUntil + ' days');
|
||||
|
||||
if (daysUntil < 0) {
|
||||
$('#daysUntilMaintenance').addClass('text-danger').text(Math.abs(daysUntil) + ' days overdue');
|
||||
} else if (daysUntil < 30) {
|
||||
$('#daysUntilMaintenance').addClass('text-warning');
|
||||
} else {
|
||||
$('#daysUntilMaintenance').addClass('text-success');
|
||||
}
|
||||
} else {
|
||||
$('#nextMaintenanceDate').text('Not calculated');
|
||||
$('#daysUntilMaintenance').text('-');
|
||||
}
|
||||
}
|
||||
|
||||
function previewEquipment() {
|
||||
// Collect form data for preview
|
||||
const formData = new FormData($('#equipmentForm')[0]);
|
||||
|
||||
// Open preview in new window/modal
|
||||
alert('Preview functionality would show equipment details preview');
|
||||
}
|
||||
|
||||
// Form validation
|
||||
$('#equipmentForm').on('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
// Check required fields
|
||||
$(this).find('input[required], select[required]').each(function() {
|
||||
if (!$(this).val()) {
|
||||
isValid = false;
|
||||
$(this).addClass('is-invalid');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert('Please fill in all required fields.');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Collect specifications
|
||||
const specNames = $('input[name="spec_name[]"]').map(function() { return $(this).val(); }).get();
|
||||
const specValues = $('input[name="spec_value[]"]').map(function() { return $(this).val(); }).get();
|
||||
|
||||
const specifications = {};
|
||||
for (let i = 0; i < specNames.length; i++) {
|
||||
if (specNames[i] && specValues[i]) {
|
||||
specifications[specNames[i]] = specValues[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Add specifications as hidden field
|
||||
$('<input>').attr({
|
||||
type: 'hidden',
|
||||
name: 'specifications',
|
||||
value: JSON.stringify(specifications)
|
||||
}).appendTo(this);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
583
templates/operating_theatre/equipment/equipment_list.html
Normal file
583
templates/operating_theatre/equipment/equipment_list.html
Normal file
@ -0,0 +1,583 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}OR Equipment Management{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.equipment-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.equipment-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.equipment-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.equipment-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
border-radius: 0.375rem 0.375rem 0 0;
|
||||
}
|
||||
|
||||
.equipment-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.equipment-info {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-available { background-color: #28a745; }
|
||||
.status-in-use { background-color: #007bff; }
|
||||
.status-maintenance { background-color: #ffc107; }
|
||||
.status-out-of-order { background-color: #dc3545; }
|
||||
.status-reserved { background-color: #6f42c1; }
|
||||
|
||||
.maintenance-alert {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
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-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #007bff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.equipment-actions {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.equipment-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.equipment-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.equipment-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</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">Equipment</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-tools me-2"></i>OR Equipment Management
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:equipment_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add Equipment
|
||||
</a>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="exportEquipment('pdf')">
|
||||
<i class="fas fa-file-pdf me-2"></i>PDF
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportEquipment('excel')">
|
||||
<i class="fas fa-file-excel me-2"></i>Excel
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Header -->
|
||||
<div class="equipment-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">Equipment Inventory</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<strong>Total Equipment:</strong> {{ stats.total_equipment }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Available:</strong> {{ stats.available_equipment }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<strong>In Use:</strong> {{ stats.in_use_equipment }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Maintenance:</strong> {{ stats.maintenance_equipment }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-2">
|
||||
<strong>Out of Order:</strong> {{ stats.out_of_order_equipment }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Utilization:</strong> {{ stats.utilization_percentage }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-3">
|
||||
<button class="btn btn-outline-light" onclick="scheduleMaintenanceCheck()">
|
||||
<i class="fas fa-wrench me-1"></i>Schedule Maintenance
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<button class="btn btn-outline-light" onclick="generateQRCodes()">
|
||||
<i class="fas fa-qrcode me-1"></i>Generate QR Codes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Alerts -->
|
||||
{% if maintenance_alerts %}
|
||||
<div class="maintenance-alert">
|
||||
<h6 class="mb-2">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Maintenance Alerts
|
||||
</h6>
|
||||
<ul class="mb-0">
|
||||
{% for alert in maintenance_alerts %}
|
||||
<li>{{ alert.equipment_name }} - {{ alert.message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number text-success">{{ stats.available_equipment }}</div>
|
||||
<div class="stat-label">Available</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number text-primary">{{ stats.in_use_equipment }}</div>
|
||||
<div class="stat-label">In Use</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number text-warning">{{ stats.maintenance_equipment }}</div>
|
||||
<div class="stat-label">Maintenance</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number text-danger">{{ stats.out_of_order_equipment }}</div>
|
||||
<div class="stat-label">Out of Order</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<form method="get" id="filterForm">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="category">
|
||||
<option value="">All Categories</option>
|
||||
{% for category in equipment_categories %}
|
||||
<option value="{{ category.0 }}" {% if request.GET.category == category.0 %}selected{% endif %}>
|
||||
{{ category.1 }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Status</option>
|
||||
<option value="available" {% if request.GET.status == 'available' %}selected{% endif %}>Available</option>
|
||||
<option value="in_use" {% if request.GET.status == 'in_use' %}selected{% endif %}>In Use</option>
|
||||
<option value="maintenance" {% if request.GET.status == 'maintenance' %}selected{% endif %}>Maintenance</option>
|
||||
<option value="out_of_order" {% if request.GET.status == 'out_of_order' %}selected{% endif %}>Out of Order</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Location</label>
|
||||
<select class="form-select" name="location">
|
||||
<option value="">All Locations</option>
|
||||
{% for room in operating_rooms %}
|
||||
<option value="{{ room.id }}" {% if request.GET.location == room.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ room.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Search</label>
|
||||
<input type="text" class="form-control" name="search"
|
||||
value="{{ request.GET.search }}" placeholder="Search equipment...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary me-2">
|
||||
<i class="fas fa-filter me-1"></i>Apply Filters
|
||||
</button>
|
||||
<a href="{% url 'operating_theatre:equipment_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Equipment Grid -->
|
||||
<div class="row">
|
||||
{% for equipment in equipment_list %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="equipment-card">
|
||||
{% if equipment.image %}
|
||||
<img src="{{ equipment.image.url }}" alt="{{ equipment.name }}" class="equipment-image">
|
||||
{% else %}
|
||||
<div class="equipment-image bg-light d-flex align-items-center justify-content-center">
|
||||
<i class="fas fa-tools fa-3x text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="equipment-content">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h5 class="mb-1">{{ equipment.name }}</h5>
|
||||
<div>
|
||||
<span class="status-indicator status-{{ equipment.status }}"></span>
|
||||
{% if equipment.status == 'available' %}
|
||||
<span class="badge bg-success">Available</span>
|
||||
{% elif equipment.status == 'in_use' %}
|
||||
<span class="badge bg-primary">In Use</span>
|
||||
{% elif equipment.status == 'maintenance' %}
|
||||
<span class="badge bg-warning">Maintenance</span>
|
||||
{% elif equipment.status == 'out_of_order' %}
|
||||
<span class="badge bg-danger">Out of Order</span>
|
||||
{% elif equipment.status == 'reserved' %}
|
||||
<span class="badge bg-info">Reserved</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="equipment-info">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Serial Number</div>
|
||||
<div class="info-value">{{ equipment.serial_number }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Category</div>
|
||||
<div class="info-value">{{ equipment.get_category_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Manufacturer</div>
|
||||
<div class="info-value">{{ equipment.manufacturer }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Model</div>
|
||||
<div class="info-value">{{ equipment.model }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Location</div>
|
||||
<div class="info-value">
|
||||
{% if equipment.current_location %}
|
||||
{{ equipment.current_location.name }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not assigned</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Last Maintenance</div>
|
||||
<div class="info-value">
|
||||
{% if equipment.last_maintenance_date %}
|
||||
{{ equipment.last_maintenance_date|date:"M d, Y" }}
|
||||
{% else %}
|
||||
<span class="text-muted">Never</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if equipment.next_maintenance_due %}
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-calendar-alt me-1"></i>
|
||||
Next maintenance: {{ equipment.next_maintenance_due|date:"M d, Y" }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="equipment-actions">
|
||||
<div class="btn-group flex-fill">
|
||||
<a href="{% url 'operating_theatre:equipment_detail' equipment.pk %}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:equipment_edit' equipment.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
{% if equipment.status == 'available' %}
|
||||
<button class="btn btn-success btn-sm" onclick="reserveEquipment('{{ equipment.pk }}')">
|
||||
<i class="fas fa-lock me-1"></i>Reserve
|
||||
</button>
|
||||
{% elif equipment.status == 'reserved' %}
|
||||
<button class="btn btn-warning btn-sm" onclick="releaseEquipment('{{ equipment.pk }}')">
|
||||
<i class="fas fa-unlock me-1"></i>Release
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="scheduleMaintenance('{{ equipment.pk }}')">
|
||||
<i class="fas fa-wrench me-2"></i>Schedule Maintenance
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="generateQR('{{ equipment.pk }}')">
|
||||
<i class="fas fa-qrcode me-2"></i>Generate QR Code
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="viewHistory('{{ equipment.pk }}')">
|
||||
<i class="fas fa-history me-2"></i>View History
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="#" onclick="reportIssue('{{ equipment.pk }}')">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Report Issue
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-tools fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No equipment found</h5>
|
||||
<p class="text-muted">No equipment matches the selected filters.</p>
|
||||
<a href="{% url 'operating_theatre:equipment_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add First Equipment
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function reserveEquipment(equipmentId) {
|
||||
if (confirm('Reserve this equipment?')) {
|
||||
$.ajax({
|
||||
url: `/operating-theatre/equipment/${equipmentId}/reserve/`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error reserving equipment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error reserving equipment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function releaseEquipment(equipmentId) {
|
||||
if (confirm('Release this equipment reservation?')) {
|
||||
$.ajax({
|
||||
url: `/operating-theatre/equipment/${equipmentId}/release/`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error releasing equipment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error releasing equipment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleMaintenance(equipmentId) {
|
||||
window.location.href = `/operating-theatre/equipment/${equipmentId}/maintenance/schedule/`;
|
||||
}
|
||||
|
||||
function generateQR(equipmentId) {
|
||||
window.open(`/operating-theatre/equipment/${equipmentId}/qr-code/`, '_blank');
|
||||
}
|
||||
|
||||
function viewHistory(equipmentId) {
|
||||
window.location.href = `/operating-theatre/equipment/${equipmentId}/history/`;
|
||||
}
|
||||
|
||||
function reportIssue(equipmentId) {
|
||||
window.location.href = `/operating-theatre/equipment/${equipmentId}/report-issue/`;
|
||||
}
|
||||
|
||||
function scheduleMaintenanceCheck() {
|
||||
window.location.href = '/operating-theatre/maintenance/schedule/';
|
||||
}
|
||||
|
||||
function generateQRCodes() {
|
||||
window.open('/operating-theatre/equipment/qr-codes/bulk/', '_blank');
|
||||
}
|
||||
|
||||
function exportEquipment(format) {
|
||||
window.location.href = `/operating-theatre/equipment/export/?format=${format}`;
|
||||
}
|
||||
|
||||
// Auto-submit form on filter change
|
||||
$('#filterForm select').on('change', function() {
|
||||
$('#filterForm').submit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
608
templates/operating_theatre/notes/operative_note_detail.html
Normal file
608
templates/operating_theatre/notes/operative_note_detail.html
Normal file
@ -0,0 +1,608 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Operative 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;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1.5rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.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(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 600;
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #495057;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.signature-section {
|
||||
background: #e8f5e8;
|
||||
border: 2px solid #28a745;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.signature-pending {
|
||||
background: #fff3cd;
|
||||
border: 2px solid #ffc107;
|
||||
}
|
||||
|
||||
.signature-draft {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #6c757d;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.timeline-time {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.procedure-details {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.complication-alert {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.print-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.print-section {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.note-header {
|
||||
background: #f8f9fa !important;
|
||||
color: #000 !important;
|
||||
border: 2px solid #000;
|
||||
}
|
||||
|
||||
.section-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.note-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</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:operative_note_list' %}">Operative 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>Operative Note
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-secondary" onclick="window.print()">
|
||||
<i class="fas fa-print me-1"></i>Print
|
||||
</button>
|
||||
<a href="{% url 'operating_theatre:operative_note_pdf' note.pk %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-file-pdf me-1"></i>PDF
|
||||
</a>
|
||||
{% if note.status == 'draft' or note.status == 'pending' %}
|
||||
<a href="{% url 'operating_theatre:operative_note_edit' note.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Print Header -->
|
||||
<div class="print-section">
|
||||
<div class="text-center mb-4">
|
||||
<h2>{{ hospital.name }}</h2>
|
||||
<p>{{ hospital.address }}</p>
|
||||
<h3>OPERATIVE NOTE</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note Header -->
|
||||
<div class="note-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{{ note.patient.get_full_name }}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Patient ID:</strong> {{ note.patient.patient_id }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Date of Birth:</strong> {{ note.patient.date_of_birth|date:"M d, Y" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Gender:</strong> {{ note.patient.get_gender_display }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-2">
|
||||
<strong>Surgery Date:</strong> {{ note.surgery_date|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Operating Room:</strong> {{ note.operating_room.name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Case Number:</strong> {{ note.case_number }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-3">
|
||||
{% if note.status == 'draft' %}
|
||||
<span class="badge bg-secondary fs-6">Draft</span>
|
||||
{% elif note.status == 'pending' %}
|
||||
<span class="badge bg-warning fs-6">Pending Signature</span>
|
||||
{% elif note.status == 'signed' %}
|
||||
<span class="badge bg-success fs-6">Signed</span>
|
||||
{% elif note.status == 'amended' %}
|
||||
<span class="badge bg-info fs-6">Amended</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if note.urgency == 'emergency' %}
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-danger fs-6">Emergency</span>
|
||||
</div>
|
||||
{% elif note.urgency == 'urgent' %}
|
||||
<div class="mb-2">
|
||||
<span class="badge bg-warning fs-6">Urgent</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Procedure Information -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-procedures me-2"></i>Procedure Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Primary Procedure</div>
|
||||
<div class="info-value">{{ note.procedure_name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Procedure Code</div>
|
||||
<div class="info-value">{{ note.procedure_code }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Laterality</div>
|
||||
<div class="info-value">{{ note.get_laterality_display|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Approach</div>
|
||||
<div class="info-value">{{ note.get_approach_display|default:"Not specified" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note.secondary_procedures %}
|
||||
<div class="procedure-details">
|
||||
<h6>Secondary Procedures:</h6>
|
||||
<p>{{ note.secondary_procedures }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Surgical Team -->
|
||||
<div class="section-card">
|
||||
<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.primary_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>
|
||||
|
||||
{% if note.other_team_members %}
|
||||
<div class="mt-3">
|
||||
<h6>Other Team Members:</h6>
|
||||
<p>{{ note.other_team_members }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timing Information -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-clock me-2"></i>Timing Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Surgery Start Time</div>
|
||||
<div class="info-value">{{ note.surgery_start_time|date:"H:i" }}</div>
|
||||
</div>
|
||||
{% if note.surgery_end_time %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">Surgery End Time</div>
|
||||
<div class="info-value">{{ note.surgery_end_time|date:"H:i" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Total Duration</div>
|
||||
<div class="info-value">{{ note.surgery_duration }} minutes</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if note.anesthesia_start_time %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">Anesthesia Start</div>
|
||||
<div class="info-value">{{ note.anesthesia_start_time|date:"H:i" }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Clinical Information -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-stethoscope me-2"></i>Clinical Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% if note.preoperative_diagnosis %}
|
||||
<div class="mb-3">
|
||||
<h6>Preoperative Diagnosis:</h6>
|
||||
<p>{{ note.preoperative_diagnosis }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.postoperative_diagnosis %}
|
||||
<div class="mb-3">
|
||||
<h6>Postoperative Diagnosis:</h6>
|
||||
<p>{{ note.postoperative_diagnosis }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.indications %}
|
||||
<div class="mb-3">
|
||||
<h6>Indications for Surgery:</h6>
|
||||
<p>{{ note.indications }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anesthesia Information -->
|
||||
{% if note.anesthesia_type %}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-syringe me-2"></i>Anesthesia Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Anesthesia Type</div>
|
||||
<div class="info-value">{{ note.get_anesthesia_type_display }}</div>
|
||||
</div>
|
||||
{% if note.asa_classification %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">ASA Classification</div>
|
||||
<div class="info-value">{{ note.asa_classification }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if note.anesthesia_notes %}
|
||||
<div class="mt-3">
|
||||
<h6>Anesthesia Notes:</h6>
|
||||
<p>{{ note.anesthesia_notes }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Operative Technique -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-cut me-2"></i>Operative Technique
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% if note.operative_technique %}
|
||||
<div class="mb-3">
|
||||
<h6>Technique Description:</h6>
|
||||
<div style="white-space: pre-wrap;">{{ note.operative_technique }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.findings %}
|
||||
<div class="mb-3">
|
||||
<h6>Operative Findings:</h6>
|
||||
<div style="white-space: pre-wrap;">{{ note.findings }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.specimens %}
|
||||
<div class="mb-3">
|
||||
<h6>Specimens Sent:</h6>
|
||||
<p>{{ note.specimens }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Complications -->
|
||||
{% if note.complications %}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Complications
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="complication-alert">
|
||||
<h6 class="text-danger">Complications Noted:</h6>
|
||||
<p class="mb-0">{{ note.complications }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Postoperative Care -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-bed me-2"></i>Postoperative Care
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% if note.postoperative_instructions %}
|
||||
<div class="mb-3">
|
||||
<h6>Postoperative Instructions:</h6>
|
||||
<div style="white-space: pre-wrap;">{{ note.postoperative_instructions }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.estimated_blood_loss %}
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Estimated Blood Loss</div>
|
||||
<div class="info-value">{{ note.estimated_blood_loss }} mL</div>
|
||||
</div>
|
||||
{% if note.fluid_replacement %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">Fluid Replacement</div>
|
||||
<div class="info-value">{{ note.fluid_replacement }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.disposition %}
|
||||
<div class="mt-3">
|
||||
<h6>Patient Disposition:</h6>
|
||||
<p>{{ note.disposition }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Section -->
|
||||
<div class="signature-section {% if note.status == 'pending' %}signature-pending{% elif note.status == 'draft' %}signature-draft{% endif %}">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-signature me-2"></i>Electronic Signature
|
||||
</h5>
|
||||
{% if note.status == 'signed' %}
|
||||
<div class="mb-2">
|
||||
<strong>Signed by:</strong> {{ note.primary_surgeon.get_full_name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Date & Time:</strong> {{ note.signed_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Digital Signature:</strong> {{ note.signature_hash }}
|
||||
</div>
|
||||
{% elif note.status == 'pending' %}
|
||||
<p class="mb-2">This operative note is pending electronic signature by the primary surgeon.</p>
|
||||
{% if note.primary_surgeon == request.user %}
|
||||
<button class="btn btn-success" onclick="signNote()">
|
||||
<i class="fas fa-signature me-1"></i>Sign Note
|
||||
</button>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="mb-2">This operative note is in draft status and has not been submitted for signature.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
{% if note.status == 'signed' %}
|
||||
<div class="text-success">
|
||||
<i class="fas fa-check-circle fa-3x"></i>
|
||||
<div class="mt-2">Electronically Signed</div>
|
||||
</div>
|
||||
{% elif note.status == 'pending' %}
|
||||
<div class="text-warning">
|
||||
<i class="fas fa-clock fa-3x"></i>
|
||||
<div class="mt-2">Pending Signature</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-secondary">
|
||||
<i class="fas fa-edit fa-3x"></i>
|
||||
<div class="mt-2">Draft</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amendment History -->
|
||||
{% if note.amendments.exists %}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-history me-2"></i>Amendment History
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% for amendment in note.amendments.all %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon bg-info text-white">
|
||||
<i class="fas fa-edit"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="fw-bold">Amendment {{ forloop.counter }}</div>
|
||||
<div class="timeline-time">{{ amendment.created_at|date:"M d, Y H:i" }} by {{ amendment.created_by.get_full_name }}</div>
|
||||
<div class="mt-2">
|
||||
<strong>Reason:</strong> {{ amendment.reason }}
|
||||
</div>
|
||||
{% if amendment.changes %}
|
||||
<div class="mt-1">
|
||||
<strong>Changes:</strong> {{ amendment.changes }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function signNote() {
|
||||
if (confirm('Sign this operative note? Once signed, the note cannot be edited without creating an amendment.')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:operative_note_sign" note.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error signing note: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error signing note');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
465
templates/operating_theatre/notes/operative_note_list.html
Normal file
465
templates/operating_theatre/notes/operative_note_list.html
Normal file
@ -0,0 +1,465 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Operative Notes{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.stats-card .stats-icon {
|
||||
font-size: 2.5rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.stats-card .stats-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.note-card {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.note-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.note-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.procedure-badge {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.urgency-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-draft { background-color: #6c757d; }
|
||||
.status-pending { background-color: #ffc107; }
|
||||
.status-signed { background-color: #28a745; }
|
||||
.status-amended { background-color: #17a2b8; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.note-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.note-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</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">Operative Notes</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-file-medical me-2"></i>Operative Notes
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:operative_note_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>New Note
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stats-number">{{ stats.total_notes }}</div>
|
||||
<div>Total Notes</div>
|
||||
</div>
|
||||
<div class="stats-icon">
|
||||
<i class="fas fa-file-medical"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stats-number">{{ stats.pending_signature }}</div>
|
||||
<div>Pending Signature</div>
|
||||
</div>
|
||||
<div class="stats-icon">
|
||||
<i class="fas fa-signature"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stats-number">{{ stats.today_notes }}</div>
|
||||
<div>Today's Notes</div>
|
||||
</div>
|
||||
<div class="stats-icon">
|
||||
<i class="fas fa-calendar-day"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="stats-card">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="stats-number">{{ stats.overdue_notes }}</div>
|
||||
<div>Overdue</div>
|
||||
</div>
|
||||
<div class="stats-icon">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<form method="get" id="filterForm">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Date Range</label>
|
||||
<select class="form-select" name="date_range">
|
||||
<option value="">All Dates</option>
|
||||
<option value="today" {% if request.GET.date_range == 'today' %}selected{% endif %}>Today</option>
|
||||
<option value="week" {% if request.GET.date_range == 'week' %}selected{% endif %}>This Week</option>
|
||||
<option value="month" {% if request.GET.date_range == 'month' %}selected{% endif %}>This Month</option>
|
||||
<option value="custom" {% if request.GET.date_range == 'custom' %}selected{% endif %}>Custom Range</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" name="status">
|
||||
<option value="">All Status</option>
|
||||
<option value="draft" {% if request.GET.status == 'draft' %}selected{% endif %}>Draft</option>
|
||||
<option value="pending" {% if request.GET.status == 'pending' %}selected{% endif %}>Pending Signature</option>
|
||||
<option value="signed" {% if request.GET.status == 'signed' %}selected{% endif %}>Signed</option>
|
||||
<option value="amended" {% if request.GET.status == 'amended' %}selected{% endif %}>Amended</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Surgeon</label>
|
||||
<select class="form-select" name="surgeon">
|
||||
<option value="">All Surgeons</option>
|
||||
{% for surgeon in surgeons %}
|
||||
<option value="{{ surgeon.id }}" {% if request.GET.surgeon == surgeon.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ surgeon.get_full_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Search</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="search"
|
||||
value="{{ request.GET.search }}" placeholder="Patient name, procedure...">
|
||||
<button class="btn btn-outline-secondary" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary me-2">
|
||||
<i class="fas fa-filter me-1"></i>Apply Filters
|
||||
</button>
|
||||
<a href="{% url 'operating_theatre:operative_note_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Notes List -->
|
||||
<div class="row">
|
||||
{% for note in notes %}
|
||||
<div class="col-12">
|
||||
<div class="note-card">
|
||||
<div class="note-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="status-indicator status-{{ note.status }}"></span>
|
||||
<h5 class="mb-0 me-3">{{ note.patient.get_full_name }}</h5>
|
||||
<span class="procedure-badge badge bg-primary">{{ note.procedure_name }}</span>
|
||||
{% if note.urgency == 'emergency' %}
|
||||
<span class="urgency-badge badge bg-danger ms-2">Emergency</span>
|
||||
{% elif note.urgency == 'urgent' %}
|
||||
<span class="urgency-badge badge bg-warning ms-2">Urgent</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
<small>{{ note.surgery_date|date:"M d, Y H:i" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note-content">
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<strong>Patient ID:</strong> {{ note.patient.patient_id }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Surgeon:</strong> {{ note.primary_surgeon.get_full_name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Operating Room:</strong> {{ note.operating_room.name }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Duration:</strong>
|
||||
{% if note.surgery_end_time %}
|
||||
{{ note.surgery_duration }} minutes
|
||||
{% else %}
|
||||
In Progress
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
{% if note.status == 'draft' %}
|
||||
<span class="badge bg-secondary">Draft</span>
|
||||
{% elif note.status == 'pending' %}
|
||||
<span class="badge bg-warning">Pending Signature</span>
|
||||
{% elif note.status == 'signed' %}
|
||||
<span class="badge bg-success">Signed</span>
|
||||
{% elif note.status == 'amended' %}
|
||||
<span class="badge bg-info">Amended</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Created:</strong> {{ note.created_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>Last Updated:</strong> {{ note.updated_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
{% if note.signed_at %}
|
||||
<div class="mb-2">
|
||||
<strong>Signed:</strong> {{ note.signed_at|date:"M d, Y H:i" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="note-actions">
|
||||
<a href="{% url 'operating_theatre:operative_note_detail' note.pk %}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
{% if note.status == 'draft' or note.status == 'pending' %}
|
||||
<a href="{% url 'operating_theatre:operative_note_edit' note.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if note.status == 'pending' and note.primary_surgeon == request.user %}
|
||||
<button class="btn btn-success btn-sm" onclick="signNote('{{ note.pk }}')">
|
||||
<i class="fas fa-signature me-1"></i>Sign
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
type="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'operating_theatre:operative_note_print' note.pk %}">
|
||||
<i class="fas fa-print me-2"></i>Print
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'operating_theatre:operative_note_pdf' note.pk %}">
|
||||
<i class="fas fa-file-pdf me-2"></i>Download PDF
|
||||
</a>
|
||||
</li>
|
||||
{% if note.status == 'signed' %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="amendNote('{{ note.pk }}')">
|
||||
<i class="fas fa-edit me-2"></i>Amend Note
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="#" onclick="deleteNote('{{ note.pk }}')">
|
||||
<i class="fas fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-file-medical fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No operative notes found</h5>
|
||||
<p class="text-muted">No notes match your current filters.</p>
|
||||
<a href="{% url 'operating_theatre:operative_note_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create First Note
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-center mt-4">
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if request.GET.urlencode %}&{{ request.GET.urlencode }}{% endif %}">Last</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function signNote(noteId) {
|
||||
if (confirm('Sign this operative note? Once signed, the note cannot be edited without creating an amendment.')) {
|
||||
$.ajax({
|
||||
url: `/operating-theatre/notes/${noteId}/sign/`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error signing note: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error signing note');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function amendNote(noteId) {
|
||||
const reason = prompt('Enter reason for amendment:');
|
||||
if (reason) {
|
||||
window.location.href = `/operating-theatre/notes/${noteId}/amend/?reason=${encodeURIComponent(reason)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteNote(noteId) {
|
||||
if (confirm('Delete this operative note? This action cannot be undone.')) {
|
||||
window.location.href = `/operating-theatre/notes/${noteId}/delete/`;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-submit form on filter change
|
||||
$('#filterForm select').on('change', function() {
|
||||
$('#filterForm').submit();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,596 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Surgical Note - {{ note.patient.get_full_name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-header {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.note-details {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.detail-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; }
|
||||
|
||||
.consequences-list {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.consequences-list h6 {
|
||||
color: #721c24;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.consequences-list ul {
|
||||
color: #721c24;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #fff;
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 0.375rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.confirmation-checkbox {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-checkbox .form-check-input:checked {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.delete-reason {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #c82333;
|
||||
border-color: #bd2130;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:disabled {
|
||||
background: #6c757d;
|
||||
border-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.action-buttons, .confirmation-section {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:surgical_note_list' %}">Surgical Notes</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}">{{ note.patient.get_full_name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Surgical Note
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Note
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Warning Header -->
|
||||
<div class="delete-header text-center">
|
||||
<div class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle fa-4x"></i>
|
||||
</div>
|
||||
<h2 class="mb-2">Confirm Deletion</h2>
|
||||
<p class="mb-0 fs-5">You are about to permanently delete this surgical note</p>
|
||||
</div>
|
||||
|
||||
<!-- Warning Section -->
|
||||
<div class="warning-section">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning me-3"></i>
|
||||
<div>
|
||||
<h5 class="mb-1 text-warning">Warning: This action cannot be undone</h5>
|
||||
<p class="mb-0">Deleting this surgical note will permanently remove all associated data from the system.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note Details -->
|
||||
<div class="note-details">
|
||||
<div class="detail-header">
|
||||
<i class="fas fa-file-medical me-2"></i>Surgical Note Details
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Patient</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ note.patient.get_full_name }}</strong><br>
|
||||
<small class="text-muted">ID: {{ note.patient.patient_id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Procedure</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ note.procedure_name }}</strong><br>
|
||||
{% if note.procedure_code %}
|
||||
<small class="text-muted">Code: {{ note.procedure_code }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Surgeon</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ note.surgeon.get_full_name }}</strong><br>
|
||||
{% if note.surgeon.specialization %}
|
||||
<small class="text-muted">{{ note.surgeon.specialization }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Surgery Date</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ note.surgery_date|date:"F d, Y" }}</strong><br>
|
||||
<small class="text-muted">{{ note.surgery_date|time:"g:i A" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value">
|
||||
<span class="note-status status-{{ note.status }}">
|
||||
{{ note.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Priority</div>
|
||||
<div class="info-value">
|
||||
<span class="priority-badge priority-{{ note.priority }}">
|
||||
{{ note.get_priority_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Created</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ note.created_at|date:"M d, Y" }}</strong><br>
|
||||
<small class="text-muted">{{ note.created_at|time:"g:i A" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Last Modified</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ note.updated_at|date:"M d, Y" }}</strong><br>
|
||||
<small class="text-muted">{{ note.updated_at|time:"g:i A" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note.preoperative_diagnosis %}
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Pre-operative Diagnosis</div>
|
||||
<div class="info-value">{{ note.preoperative_diagnosis|truncatewords:10 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if note.postoperative_diagnosis %}
|
||||
<div class="col-md-6">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Post-operative Diagnosis</div>
|
||||
<div class="info-value">{{ note.postoperative_diagnosis|truncatewords:10 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consequences -->
|
||||
<div class="consequences-list">
|
||||
<h6><i class="fas fa-exclamation-circle me-2"></i>Consequences of Deletion</h6>
|
||||
<ul>
|
||||
<li><strong>Permanent Data Loss:</strong> All surgical note content will be permanently deleted</li>
|
||||
<li><strong>Audit Trail:</strong> The deletion will be logged for compliance purposes</li>
|
||||
<li><strong>Patient Records:</strong> This note will no longer appear in the patient's medical history</li>
|
||||
<li><strong>Reporting:</strong> This note will be excluded from all future reports and statistics</li>
|
||||
{% if note.status == 'signed' %}
|
||||
<li><strong>Legal Implications:</strong> Deleting a signed surgical note may have legal consequences</li>
|
||||
{% endif %}
|
||||
{% if note.is_emergency %}
|
||||
<li><strong>Emergency Record:</strong> This emergency surgical record will be permanently lost</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Special Warnings -->
|
||||
{% if note.status == 'signed' %}
|
||||
<div class="alert alert-danger">
|
||||
<h6><i class="fas fa-signature me-2"></i>Signed Document Warning</h6>
|
||||
<p class="mb-0">This surgical note has been electronically signed. Deleting signed medical documents may violate regulatory requirements and institutional policies. Please consult with your compliance officer before proceeding.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.is_emergency %}
|
||||
<div class="alert alert-warning">
|
||||
<h6><i class="fas fa-ambulance me-2"></i>Emergency Surgery Record</h6>
|
||||
<p class="mb-0">This is an emergency surgical note. Emergency records are often subject to additional regulatory scrutiny and may be required for quality assurance reviews.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Confirmation Form -->
|
||||
<form method="post" id="delete-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="confirmation-section">
|
||||
<h5 class="mb-3"><i class="fas fa-check-square me-2"></i>Confirmation Required</h5>
|
||||
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-understanding" required>
|
||||
<label class="form-check-label" for="confirm-understanding">
|
||||
I understand that this action will permanently delete the surgical note and cannot be undone
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-authority" required>
|
||||
<label class="form-check-label" for="confirm-authority">
|
||||
I have the authority to delete this surgical note
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note.status == 'signed' %}
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-signed" required>
|
||||
<label class="form-check-label" for="confirm-signed">
|
||||
I acknowledge the legal implications of deleting a signed medical document
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="delete-reason">
|
||||
<label for="deletion-reason" class="form-label">
|
||||
<i class="fas fa-comment me-1"></i>Reason for Deletion <span class="text-danger">*</span>
|
||||
</label>
|
||||
<textarea class="form-control" id="deletion-reason" name="deletion_reason" rows="3"
|
||||
placeholder="Please provide a detailed reason for deleting this surgical note..." required></textarea>
|
||||
<div class="form-text">This reason will be logged for audit purposes</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmation-text" class="form-label">
|
||||
Type "DELETE" to confirm <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="confirmation-text"
|
||||
placeholder="Type DELETE in capital letters" required>
|
||||
<div class="form-text">This additional confirmation helps prevent accidental deletions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-primary btn-lg ms-2">
|
||||
<i class="fas fa-list me-2"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-info btn-lg me-2" onclick="printRecord()">
|
||||
<i class="fas fa-print me-2"></i>Print Record First
|
||||
</button>
|
||||
<button type="submit" class="btn btn-delete btn-lg" id="delete-button" disabled>
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Surgical Note
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<div class="modal fade" id="finalConfirmationModal" 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 fa-4x text-danger"></i>
|
||||
</div>
|
||||
<h5>Are you absolutely sure?</h5>
|
||||
<p class="text-muted">This surgical note will be permanently deleted. This action cannot be undone.</p>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<strong>Patient:</strong> {{ note.patient.get_full_name }}<br>
|
||||
<strong>Procedure:</strong> {{ note.procedure_name }}<br>
|
||||
<strong>Date:</strong> {{ note.surgery_date|date:"F d, Y" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash-alt me-1"></i>Yes, Delete Permanently
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Enable/disable delete button based on confirmations
|
||||
function checkConfirmations() {
|
||||
const checkboxes = document.querySelectorAll('.confirmation-checkbox input[type="checkbox"]');
|
||||
const confirmationText = document.getElementById('confirmation-text').value.trim();
|
||||
const deletionReason = document.getElementById('deletion-reason').value.trim();
|
||||
const deleteButton = document.getElementById('delete-button');
|
||||
|
||||
let allChecked = true;
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (!checkbox.checked) {
|
||||
allChecked = false;
|
||||
}
|
||||
});
|
||||
|
||||
const isValid = allChecked &&
|
||||
confirmationText === 'DELETE' &&
|
||||
deletionReason.length >= 10;
|
||||
|
||||
deleteButton.disabled = !isValid;
|
||||
|
||||
if (isValid) {
|
||||
deleteButton.classList.remove('btn-secondary');
|
||||
deleteButton.classList.add('btn-delete');
|
||||
} else {
|
||||
deleteButton.classList.remove('btn-delete');
|
||||
deleteButton.classList.add('btn-secondary');
|
||||
}
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
document.querySelectorAll('.confirmation-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', checkConfirmations);
|
||||
});
|
||||
|
||||
document.getElementById('confirmation-text').addEventListener('input', checkConfirmations);
|
||||
document.getElementById('deletion-reason').addEventListener('input', checkConfirmations);
|
||||
|
||||
// Form submission
|
||||
document.getElementById('delete-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show final confirmation modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('finalConfirmationModal'));
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Initial check
|
||||
checkConfirmations();
|
||||
});
|
||||
|
||||
function confirmDelete() {
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('finalConfirmationModal'));
|
||||
modal.hide();
|
||||
|
||||
// Show loading state
|
||||
const deleteButton = document.getElementById('delete-button');
|
||||
const originalText = deleteButton.innerHTML;
|
||||
deleteButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Deleting...';
|
||||
deleteButton.disabled = true;
|
||||
|
||||
// Submit form
|
||||
setTimeout(() => {
|
||||
document.getElementById('delete-form').submit();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function printRecord() {
|
||||
// Open the surgical note detail page in a new window for printing
|
||||
const printUrl = "{% url 'operating_theatre:surgical_note_detail' note.pk %}?print=1";
|
||||
const printWindow = window.open(printUrl, '_blank');
|
||||
|
||||
printWindow.onload = function() {
|
||||
setTimeout(() => {
|
||||
printWindow.print();
|
||||
}, 1000);
|
||||
};
|
||||
}
|
||||
|
||||
// Prevent accidental page navigation
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
const form = document.getElementById('delete-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Check if user has started filling the form
|
||||
if (formData.get('deletion_reason') ||
|
||||
document.getElementById('confirmation-text').value ||
|
||||
document.querySelector('.confirmation-checkbox input:checked')) {
|
||||
|
||||
e.preventDefault();
|
||||
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft reason (for audit purposes)
|
||||
let autoSaveTimer;
|
||||
document.getElementById('deletion-reason').addEventListener('input', function() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(() => {
|
||||
// Save draft reason to session storage
|
||||
sessionStorage.setItem('deletion_reason_draft', this.value);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Restore draft reason on page load
|
||||
window.addEventListener('load', function() {
|
||||
const draftReason = sessionStorage.getItem('deletion_reason_draft');
|
||||
if (draftReason) {
|
||||
document.getElementById('deletion-reason').value = draftReason;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
649
templates/operating_theatre/notes/surgical_note_detail.html
Normal file
649
templates/operating_theatre/notes/surgical_note_detail.html
Normal file
@ -0,0 +1,649 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Surgical Note - {{ note.patient.get_full_name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.note-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.note-section {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.note-status {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-draft { background: #f8f9fa; color: #6c757d; }
|
||||
.status-in-progress { background: #fff3cd; color: #856404; }
|
||||
.status-completed { background: #d4edda; color: #155724; }
|
||||
.status-signed { background: #d1ecf1; color: #0c5460; }
|
||||
.status-amended { background: #f8d7da; color: #721c24; }
|
||||
|
||||
.priority-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.priority-low { background: #d4edda; color: #155724; }
|
||||
.priority-medium { background: #fff3cd; color: #856404; }
|
||||
.priority-high { background: #f8d7da; color: #721c24; }
|
||||
.priority-critical { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
.signature-section {
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.signature-box {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.timeline-item:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -2rem;
|
||||
top: 0;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-item:last-child:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
position: absolute;
|
||||
left: -2.5rem;
|
||||
top: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: 50%;
|
||||
background: #007bff;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px #dee2e6;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.print-section {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.note-header {
|
||||
background: #f8f9fa !important;
|
||||
color: #495057 !important;
|
||||
border: 1px solid #dee2e6 !important;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
background: #f8f9fa !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.note-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.timeline-item:before {
|
||||
left: -1rem;
|
||||
}
|
||||
|
||||
.timeline-marker {
|
||||
left: -1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3 no-print">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_list' %}">Surgical Notes</a></li>
|
||||
<li class="breadcrumb-item active">{{ note.patient.get_full_name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-file-medical me-2"></i>Surgical Note Details
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
{% if note.status != 'signed' %}
|
||||
<a href="{% url 'operating_theatre:surgical_note_edit' note.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-success" onclick="printNote()">
|
||||
<i class="fas fa-print me-1"></i>Print
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-cog me-1"></i>Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="exportNote('pdf')">
|
||||
<i class="fas fa-file-pdf me-2"></i>Export as PDF
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportNote('word')">
|
||||
<i class="fas fa-file-word me-2"></i>Export as Word
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% if note.status == 'completed' and not note.is_signed %}
|
||||
<li><a class="dropdown-item" href="#" onclick="signNote()">
|
||||
<i class="fas fa-signature me-2"></i>Sign Note
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if note.status == 'signed' %}
|
||||
<li><a class="dropdown-item" href="#" onclick="amendNote()">
|
||||
<i class="fas fa-edit me-2"></i>Create Amendment
|
||||
</a></li>
|
||||
{% endif %}
|
||||
<li><a class="dropdown-item" href="#" onclick="duplicateNote()">
|
||||
<i class="fas fa-copy me-2"></i>Duplicate Note
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note Header -->
|
||||
<div class="note-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{{ note.procedure_name }}</h2>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-1"><strong>Patient:</strong> {{ note.patient.get_full_name }}</p>
|
||||
<p class="mb-1"><strong>Patient ID:</strong> {{ note.patient.patient_id }}</p>
|
||||
<p class="mb-1"><strong>Date of Birth:</strong> {{ note.patient.date_of_birth|date:"M d, Y" }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="mb-1"><strong>Surgery Date:</strong> {{ note.surgery_date|date:"M d, Y" }}</p>
|
||||
<p class="mb-1"><strong>Surgeon:</strong> {{ note.surgeon.get_full_name }}</p>
|
||||
<p class="mb-1"><strong>Operating Room:</strong> {{ note.operating_room.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-3">
|
||||
<span class="note-status status-{{ note.status }}">
|
||||
{{ note.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<span class="priority-badge priority-{{ note.priority }}">
|
||||
{{ note.get_priority_display }} Priority
|
||||
</span>
|
||||
</div>
|
||||
<p class="mb-0"><small>Note ID: {{ note.id }}</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pre-operative Information -->
|
||||
<div class="note-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-clipboard-list me-2"></i>Pre-operative Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Pre-operative Diagnosis</div>
|
||||
<div class="info-value">{{ note.preoperative_diagnosis|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Planned Procedure</div>
|
||||
<div class="info-value">{{ note.planned_procedure|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Anesthesia Type</div>
|
||||
<div class="info-value">{{ note.get_anesthesia_type_display|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">ASA Classification</div>
|
||||
<div class="info-value">{{ note.asa_classification|default:"Not specified" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note.preoperative_notes %}
|
||||
<div class="mt-3">
|
||||
<div class="info-label">Pre-operative Notes</div>
|
||||
<div class="info-value">{{ note.preoperative_notes|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operative Procedure -->
|
||||
<div class="note-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-procedures me-2"></i>Operative Procedure
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Actual Procedure</div>
|
||||
<div class="info-value">{{ note.actual_procedure|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Procedure Duration</div>
|
||||
<div class="info-value">{{ note.procedure_duration|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Incision Type</div>
|
||||
<div class="info-value">{{ note.incision_type|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Closure Method</div>
|
||||
<div class="info-value">{{ note.closure_method|default:"Not specified" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note.operative_findings %}
|
||||
<div class="mt-3">
|
||||
<div class="info-label">Operative Findings</div>
|
||||
<div class="info-value">{{ note.operative_findings|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if note.procedure_description %}
|
||||
<div class="mt-3">
|
||||
<div class="info-label">Detailed Procedure Description</div>
|
||||
<div class="info-value">{{ note.procedure_description|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post-operative Information -->
|
||||
<div class="note-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-heartbeat me-2"></i>Post-operative Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Post-operative Diagnosis</div>
|
||||
<div class="info-value">{{ note.postoperative_diagnosis|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Estimated Blood Loss</div>
|
||||
<div class="info-value">{{ note.estimated_blood_loss|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Complications</div>
|
||||
<div class="info-value">{{ note.complications|default:"None reported" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Condition at End</div>
|
||||
<div class="info-value">{{ note.condition_at_end|default:"Not specified" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if note.postoperative_instructions %}
|
||||
<div class="mt-3">
|
||||
<div class="info-label">Post-operative Instructions</div>
|
||||
<div class="info-value">{{ note.postoperative_instructions|linebreaks }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Surgical Team -->
|
||||
<div class="note-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-users me-2"></i>Surgical Team
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Primary Surgeon</div>
|
||||
<div class="info-value">{{ note.surgeon.get_full_name }}</div>
|
||||
</div>
|
||||
{% if note.assistant_surgeon %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">Assistant Surgeon</div>
|
||||
<div class="info-value">{{ note.assistant_surgeon.get_full_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if note.anesthesiologist %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">Anesthesiologist</div>
|
||||
<div class="info-value">{{ note.anesthesiologist.get_full_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if note.scrub_nurse %}
|
||||
<div class="info-item">
|
||||
<div class="info-label">Scrub Nurse</div>
|
||||
<div class="info-value">{{ note.scrub_nurse.get_full_name }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revision History -->
|
||||
{% if note.revisions.exists %}
|
||||
<div class="note-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-history me-2"></i>Revision History
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="timeline">
|
||||
{% for revision in note.revisions.all %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-marker"></div>
|
||||
<div class="timeline-content">
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<h6 class="mb-0">{{ revision.get_action_display }}</h6>
|
||||
<small class="text-muted">{{ revision.created_at|date:"M d, Y g:i A" }}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>By:</strong> {{ revision.created_by.get_full_name }}</p>
|
||||
{% if revision.reason %}
|
||||
<p class="mb-0"><strong>Reason:</strong> {{ revision.reason }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Electronic Signature -->
|
||||
{% if note.status == 'signed' %}
|
||||
<div class="note-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-signature me-2"></i>Electronic Signature
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="signature-box">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-certificate fa-3x text-success mb-3"></i>
|
||||
<h5 class="text-success">Electronically Signed</h5>
|
||||
<p class="mb-1"><strong>Signed by:</strong> {{ note.signed_by.get_full_name }}</p>
|
||||
<p class="mb-1"><strong>Date:</strong> {{ note.signed_at|date:"M d, Y g:i A" }}</p>
|
||||
<p class="mb-0"><strong>IP Address:</strong> {{ note.signature_ip|default:"Not recorded" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% elif note.status == 'completed' %}
|
||||
<div class="signature-section no-print">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-signature me-2"></i>Electronic Signature Required
|
||||
</h5>
|
||||
<p class="text-muted mb-3">This note is complete and ready for electronic signature.</p>
|
||||
<button class="btn btn-primary btn-lg" onclick="signNote()">
|
||||
<i class="fas fa-signature me-2"></i>Sign Note
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Sign Note Modal -->
|
||||
<div class="modal fade" id="signNoteModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-signature me-2"></i>Electronic Signature
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
By signing this note, you confirm that all information is accurate and complete.
|
||||
</div>
|
||||
|
||||
<form id="signatureForm">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password Confirmation</label>
|
||||
<input type="password" class="form-control" name="password" required
|
||||
placeholder="Enter your password to confirm signature">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Signature Comments (Optional)</label>
|
||||
<textarea class="form-control" name="signature_comments" rows="3"
|
||||
placeholder="Any additional comments about this signature..."></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitSignature()">
|
||||
<i class="fas fa-signature me-1"></i>Sign Note
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function printNote() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
function exportNote(format) {
|
||||
const url = `{% url "operating_theatre:surgical_note_export" note.pk %}?format=${format}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
function signNote() {
|
||||
$('#signNoteModal').modal('show');
|
||||
}
|
||||
|
||||
function submitSignature() {
|
||||
const form = document.getElementById('signatureForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:surgical_note_sign" note.pk %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$('#signNoteModal').modal('hide');
|
||||
alert('Note signed successfully!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error signing note: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error signing note');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function amendNote() {
|
||||
const reason = prompt('Please provide a reason for the amendment:');
|
||||
if (reason) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:surgical_note_amend" note.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}',
|
||||
'reason': reason
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Amendment created successfully!');
|
||||
window.location.href = response.amendment_url;
|
||||
} else {
|
||||
alert('Error creating amendment: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error creating amendment');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateNote() {
|
||||
if (confirm('Create a duplicate of this note?')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:surgical_note_duplicate" note.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Note duplicated successfully!');
|
||||
window.location.href = response.duplicate_url;
|
||||
} else {
|
||||
alert('Error duplicating note: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error duplicating note');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
$(document).keydown(function(e) {
|
||||
// Ctrl+P for print
|
||||
if (e.ctrlKey && e.keyCode === 80) {
|
||||
e.preventDefault();
|
||||
printNote();
|
||||
}
|
||||
|
||||
// Ctrl+E for edit (if not signed)
|
||||
{% if note.status != 'signed' %}
|
||||
if (e.ctrlKey && e.keyCode === 69) {
|
||||
e.preventDefault();
|
||||
window.location.href = '{% url "operating_theatre:surgical_note_edit" note.pk %}';
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
// Ctrl+S for sign (if completed)
|
||||
{% if note.status == 'completed' %}
|
||||
if (e.ctrlKey && e.keyCode === 83) {
|
||||
e.preventDefault();
|
||||
signNote();
|
||||
}
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
1009
templates/operating_theatre/notes/surgical_note_form.html
Normal file
1009
templates/operating_theatre/notes/surgical_note_form.html
Normal file
File diff suppressed because it is too large
Load Diff
516
templates/operating_theatre/notes/surgical_note_list.html
Normal file
516
templates/operating_theatre/notes/surgical_note_list.html
Normal file
@ -0,0 +1,516 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Surgical Notes{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<style>
|
||||
.stats-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-icon.primary { background: #007bff; }
|
||||
.stat-icon.success { background: #28a745; }
|
||||
.stat-icon.warning { background: #ffc107; }
|
||||
.stat-icon.info { background: #17a2b8; }
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.note-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-draft { background: #f8f9fa; color: #6c757d; }
|
||||
.status-in-progress { background: #fff3cd; color: #856404; }
|
||||
.status-completed { background: #d4edda; color: #155724; }
|
||||
.status-signed { background: #d1ecf1; color: #0c5460; }
|
||||
.status-amended { background: #f8d7da; color: #721c24; }
|
||||
|
||||
.priority-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.priority-low { background: #d4edda; color: #155724; }
|
||||
.priority-medium { background: #fff3cd; color: #856404; }
|
||||
.priority-high { background: #f8d7da; color: #721c24; }
|
||||
.priority-critical { background: #f5c6cb; color: #721c24; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="content" class="app-content">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:dashboard' %}">Operating Theatre</a></li>
|
||||
<li class="breadcrumb-item active">Surgical Notes</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-file-medical me-2"></i>Surgical Notes Management
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create Note
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-file-medical"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="total-notes">{{ stats.total_notes|default:0 }}</div>
|
||||
<div class="stat-label">Total Notes</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-edit"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="draft-notes">{{ stats.draft_notes|default:0 }}</div>
|
||||
<div class="stat-label">Draft Notes</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="completed-notes">{{ stats.completed_notes|default:0 }}</div>
|
||||
<div class="stat-label">Completed</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon info">
|
||||
<i class="fas fa-signature"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="signed-notes">{{ stats.signed_notes|default:0 }}</div>
|
||||
<div class="stat-label">Signed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="in_progress">In Progress</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="signed">Signed</option>
|
||||
<option value="amended">Amended</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Surgeon</label>
|
||||
<select class="form-select" id="surgeon-filter">
|
||||
<option value="">All Surgeons</option>
|
||||
{% for surgeon in surgeons %}
|
||||
<option value="{{ surgeon.id }}">{{ surgeon.get_full_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Date Range</label>
|
||||
<select class="form-select" id="date-filter">
|
||||
<option value="">All Dates</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="week">This Week</option>
|
||||
<option value="month">This Month</option>
|
||||
<option value="custom">Custom Range</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Priority</label>
|
||||
<select class="form-select" id="priority-filter">
|
||||
<option value="">All Priorities</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3" id="custom-date-range" style="display: none;">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">From Date</label>
|
||||
<input type="date" class="form-control" id="from-date">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">To Date</label>
|
||||
<input type="date" class="form-control" id="to-date">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Search</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="search-input"
|
||||
placeholder="Search by patient name, procedure, or note content...">
|
||||
<button class="btn btn-outline-secondary" type="button" id="search-btn">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<button class="btn btn-outline-primary me-2" onclick="applyFilters()">
|
||||
<i class="fas fa-filter me-1"></i>Apply
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes Table -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>Surgical Notes
|
||||
</h5>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-success btn-sm" onclick="exportNotes('excel')">
|
||||
<i class="fas fa-file-excel me-1"></i>Excel
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="exportNotes('pdf')">
|
||||
<i class="fas fa-file-pdf me-1"></i>PDF
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="printNotes()">
|
||||
<i class="fas fa-print me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="notesTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">
|
||||
<input type="checkbox" id="select-all">
|
||||
</th>
|
||||
<th>Patient</th>
|
||||
<th>Procedure</th>
|
||||
<th>Surgeon</th>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Last Modified</th>
|
||||
<th width="15%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for note in notes %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="note-checkbox" value="{{ note.id }}">
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="avatar avatar-sm me-2">
|
||||
<i class="fas fa-user-circle fa-2x text-muted"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-bold">{{ note.patient.get_full_name }}</div>
|
||||
<small class="text-muted">ID: {{ note.patient.patient_id }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ note.procedure_name }}</div>
|
||||
<small class="text-muted">{{ note.procedure_code|default:"" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ note.surgeon.get_full_name }}</div>
|
||||
<small class="text-muted">{{ note.surgeon.specialization|default:"" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ note.surgery_date|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ note.surgery_date|time:"g:i A" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="note-status status-{{ note.status }}">
|
||||
{{ note.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="priority-badge priority-{{ note.priority }}">
|
||||
{{ note.get_priority_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ note.updated_at|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ note.updated_at|time:"g:i A" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}"
|
||||
class="btn btn-outline-primary" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if note.status != 'signed' %}
|
||||
<a href="{% url 'operating_theatre:surgical_note_edit' note.pk %}"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-success"
|
||||
onclick="printNote('{{ note.pk }}')" title="Print">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
{% if note.status != 'signed' %}
|
||||
<a href="{% url 'operating_theatre:surgical_note_delete' note.pk %}"
|
||||
class="btn btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-4">
|
||||
<i class="fas fa-file-medical fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">No surgical notes found</p>
|
||||
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create First Note
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
const table = $('#notesTable').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[4, 'desc']], // Sort by date descending
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [0, 8] } // Disable sorting for checkbox and actions
|
||||
],
|
||||
language: {
|
||||
search: "",
|
||||
searchPlaceholder: "Search notes...",
|
||||
lengthMenu: "Show _MENU_ notes per page",
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ notes",
|
||||
infoEmpty: "No notes available",
|
||||
infoFiltered: "(filtered from _MAX_ total notes)"
|
||||
}
|
||||
});
|
||||
|
||||
// Custom search
|
||||
$('#search-input').on('keyup', function() {
|
||||
table.search(this.value).draw();
|
||||
});
|
||||
|
||||
// Select all checkbox
|
||||
$('#select-all').on('change', function() {
|
||||
$('.note-checkbox').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
// Date filter change
|
||||
$('#date-filter').on('change', function() {
|
||||
if ($(this).val() === 'custom') {
|
||||
$('#custom-date-range').show();
|
||||
} else {
|
||||
$('#custom-date-range').hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Load statistics
|
||||
loadStats();
|
||||
});
|
||||
|
||||
function loadStats() {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:surgical_note_stats" %}',
|
||||
success: function(data) {
|
||||
$('#total-notes').text(data.total_notes);
|
||||
$('#draft-notes').text(data.draft_notes);
|
||||
$('#completed-notes').text(data.completed_notes);
|
||||
$('#signed-notes').text(data.signed_notes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const filters = {
|
||||
status: $('#status-filter').val(),
|
||||
surgeon: $('#surgeon-filter').val(),
|
||||
date_range: $('#date-filter').val(),
|
||||
priority: $('#priority-filter').val(),
|
||||
from_date: $('#from-date').val(),
|
||||
to_date: $('#to-date').val(),
|
||||
search: $('#search-input').val()
|
||||
};
|
||||
|
||||
// Apply filters via AJAX
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:surgical_note_list" %}',
|
||||
data: filters,
|
||||
success: function(response) {
|
||||
// Reload table data
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#status-filter').val('');
|
||||
$('#surgeon-filter').val('');
|
||||
$('#date-filter').val('');
|
||||
$('#priority-filter').val('');
|
||||
$('#from-date').val('');
|
||||
$('#to-date').val('');
|
||||
$('#search-input').val('');
|
||||
$('#custom-date-range').hide();
|
||||
|
||||
// Clear DataTable search
|
||||
$('#notesTable').DataTable().search('').draw();
|
||||
}
|
||||
|
||||
function exportNotes(format) {
|
||||
const selectedNotes = $('.note-checkbox:checked').map(function() {
|
||||
return this.value;
|
||||
}).get();
|
||||
|
||||
const params = new URLSearchParams({
|
||||
format: format,
|
||||
notes: selectedNotes.join(',')
|
||||
});
|
||||
|
||||
window.open(`{% url "operating_theatre:surgical_note_export" %}?${params}`, '_blank');
|
||||
}
|
||||
|
||||
function printNotes() {
|
||||
const selectedNotes = $('.note-checkbox:checked').map(function() {
|
||||
return this.value;
|
||||
}).get();
|
||||
|
||||
if (selectedNotes.length === 0) {
|
||||
alert('Please select notes to print');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
notes: selectedNotes.join(',')
|
||||
});
|
||||
|
||||
window.open(`{% url "operating_theatre:surgical_note_print" %}?${params}`, '_blank');
|
||||
}
|
||||
|
||||
function printNote(noteId) {
|
||||
window.open(`{% url "operating_theatre:surgical_note_print" %}?notes=${noteId}`, '_blank');
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
$(document).keydown(function(e) {
|
||||
// Ctrl+N for new note
|
||||
if (e.ctrlKey && e.keyCode === 78) {
|
||||
e.preventDefault();
|
||||
window.location.href = '{% url "operating_theatre:surgical_note_create" %}';
|
||||
}
|
||||
|
||||
// Ctrl+F for search
|
||||
if (e.ctrlKey && e.keyCode === 70) {
|
||||
e.preventDefault();
|
||||
$('#search-input').focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,838 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Template - {{ template.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-header {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.template-details {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.detail-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;
|
||||
}
|
||||
|
||||
.template-status {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-active { background: #d4edda; color: #155724; }
|
||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
||||
.status-draft { background: #fff3cd; color: #856404; }
|
||||
|
||||
.template-category {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.usage-impact {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.usage-impact h6 {
|
||||
color: #721c24;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.impact-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.impact-stat {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.impact-number {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #721c24;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.impact-label {
|
||||
color: #721c24;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.consequences-list {
|
||||
color: #721c24;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #fff;
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 0.375rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.confirmation-checkbox {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.confirmation-checkbox .form-check-input:checked {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.delete-reason {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #c82333;
|
||||
border-color: #bd2130;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-delete:disabled {
|
||||
background: #6c757d;
|
||||
border-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dependent-notes {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.note-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.note-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.note-patient {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.note-details {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.impact-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-buttons .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.action-buttons, .confirmation-section {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:surgical_note_template_list' %}">Templates</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}">{{ template.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Template
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Warning Header -->
|
||||
<div class="delete-header text-center">
|
||||
<div class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle fa-4x"></i>
|
||||
</div>
|
||||
<h2 class="mb-2">Confirm Template Deletion</h2>
|
||||
<p class="mb-0 fs-5">You are about to permanently delete this surgical note template</p>
|
||||
</div>
|
||||
|
||||
<!-- Warning Section -->
|
||||
<div class="warning-section">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-exclamation-triangle fa-2x text-warning me-3"></i>
|
||||
<div>
|
||||
<h5 class="mb-1 text-warning">Warning: This action cannot be undone</h5>
|
||||
<p class="mb-0">Deleting this template will permanently remove it from the system and affect all dependent surgical notes.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Details -->
|
||||
<div class="template-details">
|
||||
<div class="detail-header">
|
||||
<i class="fas fa-clipboard-list me-2"></i>Template Details
|
||||
</div>
|
||||
<div class="detail-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Template Name</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ template.name }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Category</div>
|
||||
<div class="info-value">
|
||||
<span class="template-category">
|
||||
{{ template.get_category_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value">
|
||||
<span class="template-status status-{{ template.status }}">
|
||||
{{ template.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Created By</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ template.created_by.get_full_name }}</strong><br>
|
||||
<small class="text-muted">{{ template.created_by.specialization|default:"" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Created Date</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ template.created_at|date:"F d, Y" }}</strong><br>
|
||||
<small class="text-muted">{{ template.created_at|time:"g:i A" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Last Modified</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ template.updated_at|date:"M d, Y" }}</strong><br>
|
||||
<small class="text-muted">{{ template.updated_at|time:"g:i A" }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Version</div>
|
||||
<div class="info-value">
|
||||
<strong>v{{ template.version|default:1 }}</strong><br>
|
||||
{% if template.is_latest_version %}
|
||||
<small class="text-success">Latest Version</small>
|
||||
{% else %}
|
||||
<small class="text-warning">Outdated Version</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<div class="info-label">Content Size</div>
|
||||
<div class="info-value">
|
||||
<strong>{{ template.content|length|filesizeformat }}</strong><br>
|
||||
<small class="text-muted">{{ template.content|length }} characters</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if template.description %}
|
||||
<div class="mb-3">
|
||||
<div class="info-label mb-2">Description</div>
|
||||
<div class="info-value">{{ template.description }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if template.tags.all %}
|
||||
<div class="mb-3">
|
||||
<div class="info-label mb-2">Tags</div>
|
||||
<div>
|
||||
{% for tag in template.tags.all %}
|
||||
<span class="badge bg-light text-dark me-1">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Impact -->
|
||||
<div class="usage-impact">
|
||||
<h6><i class="fas fa-chart-bar me-2"></i>Usage Impact Analysis</h6>
|
||||
|
||||
<div class="impact-stats">
|
||||
<div class="impact-stat">
|
||||
<div class="impact-number">{{ template.usage_count|default:0 }}</div>
|
||||
<div class="impact-label">Total Uses</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-stat">
|
||||
<div class="impact-number">{{ template.active_notes_count|default:0 }}</div>
|
||||
<div class="impact-label">Active Notes</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-stat">
|
||||
<div class="impact-number">{{ template.unique_users|default:0 }}</div>
|
||||
<div class="impact-label">Users Affected</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-stat">
|
||||
<div class="impact-number">{{ template.usage_this_month|default:0 }}</div>
|
||||
<div class="impact-label">Uses This Month</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="consequences-list">
|
||||
<li><strong>Template Removal:</strong> This template will no longer be available for creating new surgical notes</li>
|
||||
<li><strong>Existing Notes:</strong> {{ template.active_notes_count|default:0 }} surgical notes currently use this template</li>
|
||||
<li><strong>User Impact:</strong> {{ template.unique_users|default:0 }} users have used this template</li>
|
||||
<li><strong>Version History:</strong> All version history for this template will be permanently lost</li>
|
||||
{% if template.is_default %}
|
||||
<li><strong>Default Template:</strong> This is the default template for {{ template.get_category_display }} - a new default will need to be set</li>
|
||||
{% endif %}
|
||||
{% if template.is_public %}
|
||||
<li><strong>Public Access:</strong> Other users will lose access to this shared template</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Dependent Notes -->
|
||||
{% if dependent_notes %}
|
||||
<div class="dependent-notes">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-link me-2"></i>Surgical Notes Using This Template ({{ dependent_notes|length }})
|
||||
</h6>
|
||||
<div style="max-height: 300px; overflow-y: auto;">
|
||||
{% for note in dependent_notes %}
|
||||
<div class="note-item">
|
||||
<div class="note-info">
|
||||
<div class="note-patient">{{ note.patient.get_full_name }}</div>
|
||||
<div class="note-details">
|
||||
{{ note.procedure_name }} - {{ note.surgery_date|date:"M d, Y" }}
|
||||
<span class="badge bg-secondary ms-2">{{ note.get_status_display }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'operating_theatre:surgical_note_detail' note.pk %}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="alert alert-warning mt-3 mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Note:</strong> Existing surgical notes will retain their content, but will no longer be linked to this template.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Special Warnings -->
|
||||
{% if template.status == 'active' %}
|
||||
<div class="alert alert-danger">
|
||||
<h6><i class="fas fa-exclamation-triangle me-2"></i>Active Template Warning</h6>
|
||||
<p class="mb-0">This template is currently active and being used by medical staff. Deleting it may disrupt workflow and force users to find alternative templates.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if template.is_default %}
|
||||
<div class="alert alert-warning">
|
||||
<h6><i class="fas fa-star me-2"></i>Default Template Warning</h6>
|
||||
<p class="mb-0">This is the default template for {{ template.get_category_display }}. After deletion, you will need to set a new default template for this category.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if template.usage_count > 50 %}
|
||||
<div class="alert alert-info">
|
||||
<h6><i class="fas fa-chart-line me-2"></i>High Usage Template</h6>
|
||||
<p class="mb-0">This template has been used {{ template.usage_count }} times. Consider archiving instead of deleting to preserve historical data.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Confirmation Form -->
|
||||
<form method="post" id="delete-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="confirmation-section">
|
||||
<h5 class="mb-3"><i class="fas fa-check-square me-2"></i>Confirmation Required</h5>
|
||||
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-understanding" required>
|
||||
<label class="form-check-label" for="confirm-understanding">
|
||||
I understand that this action will permanently delete the template and cannot be undone
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-authority" required>
|
||||
<label class="form-check-label" for="confirm-authority">
|
||||
I have the authority to delete this template
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if template.status == 'active' %}
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-active" required>
|
||||
<label class="form-check-label" for="confirm-active">
|
||||
I acknowledge that deleting an active template may disrupt medical workflows
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if template.is_default %}
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-default" required>
|
||||
<label class="form-check-label" for="confirm-default">
|
||||
I will set a new default template for {{ template.get_category_display }} after deletion
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if dependent_notes %}
|
||||
<div class="confirmation-checkbox">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="confirm-dependent" required>
|
||||
<label class="form-check-label" for="confirm-dependent">
|
||||
I understand that {{ dependent_notes|length }} surgical notes will be affected by this deletion
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="delete-reason">
|
||||
<label for="deletion-reason" class="form-label">
|
||||
<i class="fas fa-comment me-1"></i>Reason for Deletion <span class="text-danger">*</span>
|
||||
</label>
|
||||
<textarea class="form-control" id="deletion-reason" name="deletion_reason" rows="3"
|
||||
placeholder="Please provide a detailed reason for deleting this template..." required></textarea>
|
||||
<div class="form-text">This reason will be logged for audit purposes and compliance</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="replacement-template" class="form-label">
|
||||
Replacement Template (Optional)
|
||||
</label>
|
||||
<select class="form-select" id="replacement-template" name="replacement_template">
|
||||
<option value="">No replacement</option>
|
||||
{% for alt_template in alternative_templates %}
|
||||
<option value="{{ alt_template.id }}">{{ alt_template.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div class="form-text">Suggest an alternative template for users</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirmation-text" class="form-label">
|
||||
Type "DELETE TEMPLATE" to confirm <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="confirmation-text"
|
||||
placeholder="Type DELETE TEMPLATE in capital letters" required>
|
||||
<div class="form-text">This additional confirmation helps prevent accidental deletions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}" class="btn btn-outline-secondary btn-lg">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_list' %}" class="btn btn-outline-primary btn-lg ms-2">
|
||||
<i class="fas fa-list me-2"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-warning btn-lg me-2" onclick="archiveTemplate()">
|
||||
<i class="fas fa-archive me-2"></i>Archive Instead
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info btn-lg me-2" onclick="exportTemplate()">
|
||||
<i class="fas fa-download me-2"></i>Export First
|
||||
</button>
|
||||
<button type="submit" class="btn btn-delete btn-lg" id="delete-button" disabled>
|
||||
<i class="fas fa-trash-alt me-2"></i>Delete Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Final Confirmation Modal -->
|
||||
<div class="modal fade" id="finalConfirmationModal" 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 fa-4x text-danger"></i>
|
||||
</div>
|
||||
<h5>Are you absolutely sure?</h5>
|
||||
<p class="text-muted">This template will be permanently deleted. This action cannot be undone.</p>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<strong>Template:</strong> {{ template.name }}<br>
|
||||
<strong>Category:</strong> {{ template.get_category_display }}<br>
|
||||
<strong>Usage Count:</strong> {{ template.usage_count|default:0 }} times<br>
|
||||
{% if dependent_notes %}
|
||||
<strong>Affected Notes:</strong> {{ dependent_notes|length }} surgical notes
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="confirmDelete()">
|
||||
<i class="fas fa-trash-alt me-1"></i>Yes, Delete Permanently
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Archive Modal -->
|
||||
<div class="modal fade" id="archiveModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-archive me-2"></i>Archive Template
|
||||
</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>
|
||||
Archiving will make the template inactive but preserve all data and history.
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="archive-reason" class="form-label">Archive Reason</label>
|
||||
<textarea class="form-control" id="archive-reason" rows="3"
|
||||
placeholder="Reason for archiving this template..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" onclick="confirmArchive()">
|
||||
<i class="fas fa-archive me-1"></i>Archive Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Enable/disable delete button based on confirmations
|
||||
function checkConfirmations() {
|
||||
const checkboxes = document.querySelectorAll('.confirmation-checkbox input[type="checkbox"]');
|
||||
const confirmationText = document.getElementById('confirmation-text').value.trim();
|
||||
const deletionReason = document.getElementById('deletion-reason').value.trim();
|
||||
const deleteButton = document.getElementById('delete-button');
|
||||
|
||||
let allChecked = true;
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (!checkbox.checked) {
|
||||
allChecked = false;
|
||||
}
|
||||
});
|
||||
|
||||
const isValid = allChecked &&
|
||||
confirmationText === 'DELETE TEMPLATE' &&
|
||||
deletionReason.length >= 10;
|
||||
|
||||
deleteButton.disabled = !isValid;
|
||||
|
||||
if (isValid) {
|
||||
deleteButton.classList.remove('btn-secondary');
|
||||
deleteButton.classList.add('btn-delete');
|
||||
} else {
|
||||
deleteButton.classList.remove('btn-delete');
|
||||
deleteButton.classList.add('btn-secondary');
|
||||
}
|
||||
}
|
||||
|
||||
// Attach event listeners
|
||||
document.querySelectorAll('.confirmation-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', checkConfirmations);
|
||||
});
|
||||
|
||||
document.getElementById('confirmation-text').addEventListener('input', checkConfirmations);
|
||||
document.getElementById('deletion-reason').addEventListener('input', checkConfirmations);
|
||||
|
||||
// Form submission
|
||||
document.getElementById('delete-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show final confirmation modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('finalConfirmationModal'));
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Initial check
|
||||
checkConfirmations();
|
||||
});
|
||||
|
||||
function confirmDelete() {
|
||||
// Close modal
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('finalConfirmationModal'));
|
||||
modal.hide();
|
||||
|
||||
// Show loading state
|
||||
const deleteButton = document.getElementById('delete-button');
|
||||
const originalText = deleteButton.innerHTML;
|
||||
deleteButton.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Deleting...';
|
||||
deleteButton.disabled = true;
|
||||
|
||||
// Submit form
|
||||
setTimeout(() => {
|
||||
document.getElementById('delete-form').submit();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function archiveTemplate() {
|
||||
new bootstrap.Modal(document.getElementById('archiveModal')).show();
|
||||
}
|
||||
|
||||
function confirmArchive() {
|
||||
const reason = document.getElementById('archive-reason').value;
|
||||
|
||||
fetch(`/operating-theatre/templates/{{ template.pk }}/archive/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reason: reason
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('Template archived successfully', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '{% url "operating_theatre:surgical_note_template_list" %}';
|
||||
}, 1500);
|
||||
} else {
|
||||
showAlert('Error archiving template', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error archiving template', 'danger');
|
||||
});
|
||||
|
||||
bootstrap.Modal.getInstance(document.getElementById('archiveModal')).hide();
|
||||
}
|
||||
|
||||
function exportTemplate() {
|
||||
window.open(`/operating-theatre/templates/{{ template.pk }}/export/`, '_blank');
|
||||
}
|
||||
|
||||
// Prevent accidental page navigation
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
const form = document.getElementById('delete-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Check if user has started filling the form
|
||||
if (formData.get('deletion_reason') ||
|
||||
document.getElementById('confirmation-text').value ||
|
||||
document.querySelector('.confirmation-checkbox input:checked')) {
|
||||
|
||||
e.preventDefault();
|
||||
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
|
||||
return e.returnValue;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save draft reason (for audit purposes)
|
||||
let autoSaveTimer;
|
||||
document.getElementById('deletion-reason').addEventListener('input', function() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(() => {
|
||||
// Save draft reason to session storage
|
||||
sessionStorage.setItem('template_deletion_reason_draft', this.value);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Restore draft reason on page load
|
||||
window.addEventListener('load', function() {
|
||||
const draftReason = sessionStorage.getItem('template_deletion_reason_draft');
|
||||
if (draftReason) {
|
||||
document.getElementById('deletion-reason').value = draftReason;
|
||||
}
|
||||
});
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,941 @@
|
||||
{% 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>
|
||||
.templates-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.stat-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.template-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.template-status {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-active { background: #d4edda; color: #155724; }
|
||||
.status-inactive { background: #f8d7da; color: #721c24; }
|
||||
.status-draft { background: #fff3cd; color: #856404; }
|
||||
|
||||
.template-category {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.usage-stats {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.75rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.usage-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.usage-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.usage-bar {
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.usage-fill {
|
||||
height: 100%;
|
||||
background: #007bff;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-template-action {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.templates-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.template-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.filter-section, .quick-actions, .btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</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">Note Templates</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-clipboard-list me-2"></i>Surgical Note Templates
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-info" onclick="importTemplate()">
|
||||
<i class="fas fa-file-import me-1"></i>Import
|
||||
</button>
|
||||
<button class="btn btn-outline-success" onclick="exportTemplates()">
|
||||
<i class="fas fa-file-export me-1"></i>Export
|
||||
</button>
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon primary">
|
||||
<i class="fas fa-clipboard-list"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="total-templates">{{ stats.total_templates|default:0 }}</div>
|
||||
<div class="stat-label">Total Templates</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="active-templates">{{ stats.active_templates|default:0 }}</div>
|
||||
<div class="stat-label">Active Templates</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon warning">
|
||||
<i class="fas fa-star"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="popular-templates">{{ stats.popular_templates|default:0 }}</div>
|
||||
<div class="stat-label">Popular Templates</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon info">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
</div>
|
||||
<div class="stat-number" id="usage-this-month">{{ stats.usage_this_month|default:0 }}</div>
|
||||
<div class="stat-label">Used This Month</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" id="category-filter">
|
||||
<option value="">All Categories</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="emergency">Emergency Surgery</option>
|
||||
<option value="pediatric">Pediatric Surgery</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Created By</label>
|
||||
<select class="form-select" id="author-filter">
|
||||
<option value="">All Authors</option>
|
||||
{% for author in authors %}
|
||||
<option value="{{ author.id }}">{{ author.get_full_name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sort By</label>
|
||||
<select class="form-select" id="sort-filter">
|
||||
<option value="name">Name</option>
|
||||
<option value="created">Date Created</option>
|
||||
<option value="updated">Last Updated</option>
|
||||
<option value="usage">Usage Count</option>
|
||||
<option value="popularity">Popularity</option>
|
||||
</select>
|
||||
</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 templates by name, description, or 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>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="btn-group" role="group">
|
||||
<input type="radio" class="btn-check" name="view-mode" id="card-view" checked>
|
||||
<label class="btn btn-outline-primary" for="card-view">
|
||||
<i class="fas fa-th-large me-1"></i>Cards
|
||||
</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="view-mode" id="table-view">
|
||||
<label class="btn btn-outline-primary" for="table-view">
|
||||
<i class="fas fa-list me-1"></i>Table
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-success btn-sm" onclick="exportTemplates('excel')">
|
||||
<i class="fas fa-file-excel me-1"></i>Excel
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="exportTemplates('pdf')">
|
||||
<i class="fas fa-file-pdf me-1"></i>PDF
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="printTemplates()">
|
||||
<i class="fas fa-print me-1"></i>Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card View -->
|
||||
<div id="card-view-container">
|
||||
<div class="row">
|
||||
{% for template in templates %}
|
||||
<div class="col-lg-6 col-xl-4 mb-3">
|
||||
<div class="template-card">
|
||||
<div class="template-header">
|
||||
<div>
|
||||
<h6 class="mb-1">{{ template.name }}</h6>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="template-status status-{{ template.status }}">
|
||||
{{ template.get_status_display }}
|
||||
</span>
|
||||
<span class="template-category">
|
||||
{{ template.get_category_display }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}">
|
||||
<i class="fas fa-eye me-2"></i>View Details
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'operating_theatre:surgical_note_template_edit' template.pk %}">
|
||||
<i class="fas fa-edit me-2"></i>Edit Template
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="duplicateTemplate('{{ template.pk }}')">
|
||||
<i class="fas fa-copy me-2"></i>Duplicate
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="exportTemplate('{{ template.pk }}')">
|
||||
<i class="fas fa-download me-2"></i>Export
|
||||
</a>
|
||||
</li>
|
||||
{% if template.status != 'active' %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="template-content">
|
||||
<p class="text-muted mb-3">{{ template.description|truncatewords:20 }}</p>
|
||||
|
||||
<div class="template-meta">
|
||||
<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-calendar"></i>
|
||||
{{ template.created_at|date:"M d, Y" }}
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-clock"></i>
|
||||
{{ template.updated_at|timesince }} ago
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
Used {{ template.usage_count|default:0 }} times
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if template.tags.all %}
|
||||
<div class="mb-3">
|
||||
{% for tag in template.tags.all %}
|
||||
<span class="badge bg-light text-dark me-1">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="usage-stats">
|
||||
<div class="usage-item">
|
||||
<span class="small text-muted">This Month</span>
|
||||
<span class="small fw-bold">{{ template.usage_this_month|default:0 }}</span>
|
||||
</div>
|
||||
<div class="usage-bar">
|
||||
<div class="usage-fill" style="width: {{ template.usage_percentage|default:0 }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<a href="{% url 'operating_theatre:surgical_note_create' %}?template={{ template.pk }}"
|
||||
class="btn btn-primary btn-template-action">
|
||||
<i class="fas fa-plus me-1"></i>Use Template
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}"
|
||||
class="btn btn-outline-primary btn-template-action">
|
||||
<i class="fas fa-eye me-1"></i>Preview
|
||||
</a>
|
||||
{% if template.is_favorite %}
|
||||
<button class="btn btn-outline-warning btn-template-action"
|
||||
onclick="toggleFavorite('{{ template.pk }}', false)">
|
||||
<i class="fas fa-star"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
<button class="btn btn-outline-secondary btn-template-action"
|
||||
onclick="toggleFavorite('{{ template.pk }}', true)">
|
||||
<i class="far fa-star"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-clipboard-list fa-4x text-muted mb-3"></i>
|
||||
<h5 class="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-1"></i>Create Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table View -->
|
||||
<div id="table-view-container" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table id="templatesTable" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="5%">
|
||||
<input type="checkbox" id="select-all">
|
||||
</th>
|
||||
<th>Template Name</th>
|
||||
<th>Category</th>
|
||||
<th>Status</th>
|
||||
<th>Created By</th>
|
||||
<th>Usage Count</th>
|
||||
<th>Last Updated</th>
|
||||
<th width="15%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for template in templates %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="template-checkbox" value="{{ template.id }}">
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong>{{ template.name }}</strong><br>
|
||||
<small class="text-muted">{{ template.description|truncatewords:10 }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="template-category">
|
||||
{{ template.get_category_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="template-status status-{{ template.status }}">
|
||||
{{ template.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ template.created_by.get_full_name }}</td>
|
||||
<td>
|
||||
<div class="text-center">
|
||||
<strong>{{ template.usage_count|default:0 }}</strong><br>
|
||||
<small class="text-muted">{{ template.usage_this_month|default:0 }} this month</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ template.updated_at|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ template.updated_at|time:"g:i A" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_detail' template.pk %}"
|
||||
class="btn btn-outline-primary" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_edit' template.pk %}"
|
||||
class="btn btn-outline-warning" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:surgical_note_create' %}?template={{ template.pk }}"
|
||||
class="btn btn-outline-success" title="Use Template">
|
||||
<i class="fas fa-plus"></i>
|
||||
</a>
|
||||
{% if template.status != 'active' %}
|
||||
<a href="{% url 'operating_theatre:surgical_note_template_delete' template.pk %}"
|
||||
class="btn btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import Template Modal -->
|
||||
<div class="modal fade" id="importModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="fas fa-file-import me-2"></i>Import Template
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="import-form" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
<div class="mb-3">
|
||||
<label for="template-file" class="form-label">Template File</label>
|
||||
<input type="file" class="form-control" id="template-file"
|
||||
accept=".json,.xml,.txt" required>
|
||||
<div class="form-text">Supported formats: JSON, XML, TXT</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="overwrite-existing">
|
||||
<label class="form-check-label" for="overwrite-existing">
|
||||
Overwrite existing templates with same name
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" onclick="processImport()">
|
||||
<i class="fas fa-upload me-1"></i>Import 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
|
||||
const table = $('#templatesTable').DataTable({
|
||||
responsive: true,
|
||||
pageLength: 25,
|
||||
order: [[6, 'desc']], // Sort by last updated descending
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: [0, 7] } // Disable sorting for checkbox and actions
|
||||
],
|
||||
language: {
|
||||
search: "",
|
||||
searchPlaceholder: "Search templates...",
|
||||
lengthMenu: "Show _MENU_ templates per page",
|
||||
info: "Showing _START_ to _END_ of _TOTAL_ templates",
|
||||
infoEmpty: "No templates available",
|
||||
infoFiltered: "(filtered from _MAX_ total templates)"
|
||||
}
|
||||
});
|
||||
|
||||
// View mode toggle
|
||||
$('input[name="view-mode"]').on('change', function() {
|
||||
if (this.id === 'card-view') {
|
||||
$('#card-view-container').show();
|
||||
$('#table-view-container').hide();
|
||||
} else {
|
||||
$('#card-view-container').hide();
|
||||
$('#table-view-container').show();
|
||||
table.columns.adjust().responsive.recalc();
|
||||
}
|
||||
});
|
||||
|
||||
// Select all checkbox
|
||||
$('#select-all').on('change', function() {
|
||||
$('.template-checkbox').prop('checked', this.checked);
|
||||
});
|
||||
|
||||
// Search functionality
|
||||
$('#search-input').on('keyup', function() {
|
||||
table.search(this.value).draw();
|
||||
filterCards(this.value);
|
||||
});
|
||||
|
||||
// Filter functionality
|
||||
$('#category-filter, #status-filter, #author-filter, #sort-filter').on('change', function() {
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
function filterCards(searchTerm) {
|
||||
const cards = document.querySelectorAll('.template-card');
|
||||
const term = searchTerm.toLowerCase();
|
||||
|
||||
cards.forEach(card => {
|
||||
const text = card.textContent.toLowerCase();
|
||||
const parent = card.closest('.col-lg-6');
|
||||
|
||||
if (text.includes(term)) {
|
||||
parent.style.display = '';
|
||||
} else {
|
||||
parent.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const category = document.getElementById('category-filter').value;
|
||||
const status = document.getElementById('status-filter').value;
|
||||
const author = document.getElementById('author-filter').value;
|
||||
const sort = document.getElementById('sort-filter').value;
|
||||
|
||||
// Apply filters to DataTable
|
||||
if (category) {
|
||||
table.column(2).search(category);
|
||||
} else {
|
||||
table.column(2).search('');
|
||||
}
|
||||
|
||||
if (status) {
|
||||
table.column(3).search(status);
|
||||
} else {
|
||||
table.column(3).search('');
|
||||
}
|
||||
|
||||
table.draw();
|
||||
|
||||
// Apply filters to cards
|
||||
filterCardsAdvanced(category, status, author, sort);
|
||||
}
|
||||
|
||||
function filterCardsAdvanced(category, status, author, sort) {
|
||||
const cards = Array.from(document.querySelectorAll('.template-card'));
|
||||
const container = document.querySelector('#card-view-container .row');
|
||||
|
||||
// Filter cards
|
||||
const filteredCards = cards.filter(card => {
|
||||
let show = true;
|
||||
|
||||
if (category) {
|
||||
const cardCategory = card.querySelector('.template-category').textContent.toLowerCase();
|
||||
if (!cardCategory.includes(category.toLowerCase())) {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (status) {
|
||||
const cardStatus = card.querySelector('.template-status').classList;
|
||||
if (!cardStatus.contains(`status-${status}`)) {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (author) {
|
||||
const cardAuthor = card.querySelector('.meta-item').textContent;
|
||||
if (!cardAuthor.includes(author)) {
|
||||
show = false;
|
||||
}
|
||||
}
|
||||
|
||||
return show;
|
||||
});
|
||||
|
||||
// Sort cards
|
||||
if (sort) {
|
||||
filteredCards.sort((a, b) => {
|
||||
switch (sort) {
|
||||
case 'name':
|
||||
return a.querySelector('h6').textContent.localeCompare(b.querySelector('h6').textContent);
|
||||
case 'usage':
|
||||
const usageA = parseInt(a.querySelector('.usage-item span:last-child').textContent);
|
||||
const usageB = parseInt(b.querySelector('.usage-item span:last-child').textContent);
|
||||
return usageB - usageA;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hide all cards first
|
||||
cards.forEach(card => {
|
||||
card.closest('.col-lg-6').style.display = 'none';
|
||||
});
|
||||
|
||||
// Show filtered cards
|
||||
filteredCards.forEach(card => {
|
||||
card.closest('.col-lg-6').style.display = '';
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
document.getElementById('category-filter').value = '';
|
||||
document.getElementById('status-filter').value = '';
|
||||
document.getElementById('author-filter').value = '';
|
||||
document.getElementById('sort-filter').value = 'name';
|
||||
document.getElementById('search-input').value = '';
|
||||
|
||||
// Clear DataTable filters
|
||||
table.search('').columns().search('').draw();
|
||||
|
||||
// Show all cards
|
||||
document.querySelectorAll('.template-card').forEach(card => {
|
||||
card.closest('.col-lg-6').style.display = '';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleFavorite(templateId, isFavorite) {
|
||||
fetch(`/operating-theatre/templates/${templateId}/favorite/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ is_favorite: isFavorite })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
showAlert('Error updating favorite status', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error updating favorite status', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function duplicateTemplate(templateId) {
|
||||
if (confirm('Create a copy of this template?')) {
|
||||
fetch(`/operating-theatre/templates/${templateId}/duplicate/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert('Template duplicated successfully', 'success');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showAlert('Error duplicating template', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error duplicating template', 'danger');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exportTemplate(templateId) {
|
||||
window.open(`/operating-theatre/templates/${templateId}/export/`, '_blank');
|
||||
}
|
||||
|
||||
function exportTemplates(format = 'json') {
|
||||
const selectedTemplates = Array.from(document.querySelectorAll('.template-checkbox:checked'))
|
||||
.map(cb => cb.value);
|
||||
|
||||
if (selectedTemplates.length === 0) {
|
||||
showAlert('Please select templates to export', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
format: format,
|
||||
templates: selectedTemplates.join(',')
|
||||
});
|
||||
|
||||
window.open(`/operating-theatre/templates/export/?${params}`, '_blank');
|
||||
}
|
||||
|
||||
function importTemplate() {
|
||||
new bootstrap.Modal(document.getElementById('importModal')).show();
|
||||
}
|
||||
|
||||
function processImport() {
|
||||
const form = document.getElementById('import-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
fetch('/operating-theatre/templates/import/', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showAlert(`Successfully imported ${data.count} templates`, 'success');
|
||||
bootstrap.Modal.getInstance(document.getElementById('importModal')).hide();
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
} else {
|
||||
showAlert(data.message || 'Error importing templates', 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showAlert('Error importing templates', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function printTemplates() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
||||
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(alertDiv);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentNode) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -0,0 +1,603 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Delete Template - {{ template.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.delete-header {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.warning-section {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.impact-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;
|
||||
}
|
||||
|
||||
.impact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.impact-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.impact-critical { background: #dc3545; }
|
||||
.impact-warning { background: #ffc107; }
|
||||
.impact-info { background: #17a2b8; }
|
||||
|
||||
.impact-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.impact-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.impact-description {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.alternative-actions {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.confirmation-section {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.type-to-confirm {
|
||||
background: white;
|
||||
border: 2px solid #dc3545;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.blocking-conditions {
|
||||
background: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.template-info {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.delete-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.impact-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.impact-icon {
|
||||
margin-right: 0;
|
||||
margin-bottom: 0.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"><a href="{% url 'operating_theatre:template_list' %}">Templates</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'operating_theatre:template_detail' template.pk %}">{{ template.name }}</a></li>
|
||||
<li class="breadcrumb-item active">Delete</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-trash me-2"></i>Delete Surgical Template
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'operating_theatre:template_detail' template.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Header -->
|
||||
<div class="delete-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Delete "{{ template.name }}"
|
||||
</h2>
|
||||
<p class="mb-0">
|
||||
You are about to permanently delete this surgical template. This action cannot be undone.
|
||||
Please review the impact analysis below before proceeding.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<i class="fas fa-file-medical fa-4x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Information -->
|
||||
<div class="template-info">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>Template Information
|
||||
</h5>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Template Name</div>
|
||||
<div class="info-value">{{ template.name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Specialty</div>
|
||||
<div class="info-value">{{ template.get_specialty_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Complexity</div>
|
||||
<div class="info-value">{{ template.get_complexity_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Status</div>
|
||||
<div class="info-value">
|
||||
{% if template.status == 'active' %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% elif template.status == 'draft' %}
|
||||
<span class="badge bg-secondary">Draft</span>
|
||||
{% elif template.status == 'under_review' %}
|
||||
<span class="badge bg-warning">Under Review</span>
|
||||
{% elif template.status == 'archived' %}
|
||||
<span class="badge bg-dark">Archived</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Created By</div>
|
||||
<div class="info-value">{{ template.created_by.get_full_name }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Created Date</div>
|
||||
<div class="info-value">{{ template.created_at|date:"M d, Y" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Total Uses</div>
|
||||
<div class="info-value">{{ template.usage_count|default:0 }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Last Used</div>
|
||||
<div class="info-value">{{ template.last_used|date:"M d, Y"|default:"Never" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blocking Conditions -->
|
||||
{% if blocking_conditions %}
|
||||
<div class="blocking-conditions">
|
||||
<h5 class="text-danger mb-3">
|
||||
<i class="fas fa-ban me-2"></i>Cannot Delete Template
|
||||
</h5>
|
||||
<p class="mb-3">This template cannot be deleted due to the following conditions:</p>
|
||||
<ul class="mb-0">
|
||||
{% for condition in blocking_conditions %}
|
||||
<li class="text-danger">{{ condition }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div class="mt-3">
|
||||
<a href="{% url 'operating_theatre:template_detail' template.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Return to Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<!-- Warning Section -->
|
||||
<div class="warning-section">
|
||||
<h5 class="text-warning mb-3">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Warning
|
||||
</h5>
|
||||
<p class="mb-0">
|
||||
Deleting this template will permanently remove all associated data including procedure steps,
|
||||
equipment requirements, and team configurations. This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Impact Analysis -->
|
||||
<div class="impact-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-chart-line me-2"></i>Impact Analysis
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon impact-critical">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
</div>
|
||||
<div class="impact-details">
|
||||
<div class="impact-title">Procedure Steps</div>
|
||||
<div class="impact-description">
|
||||
{{ template.procedure_steps.count }} procedure steps will be permanently deleted
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon impact-warning">
|
||||
<i class="fas fa-tools"></i>
|
||||
</div>
|
||||
<div class="impact-details">
|
||||
<div class="impact-title">Equipment Requirements</div>
|
||||
<div class="impact-description">
|
||||
{{ template.equipment_list.count }} equipment requirements will be removed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon impact-info">
|
||||
<i class="fas fa-users"></i>
|
||||
</div>
|
||||
<div class="impact-details">
|
||||
<div class="impact-title">Team Requirements</div>
|
||||
<div class="impact-description">
|
||||
{{ template.team_requirements.count }} team member requirements will be removed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if template.active_cases.count > 0 %}
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon impact-critical">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
</div>
|
||||
<div class="impact-details">
|
||||
<div class="impact-title">Active Cases</div>
|
||||
<div class="impact-description">
|
||||
{{ template.active_cases.count }} active surgical cases are using this template
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if template.scheduled_cases.count > 0 %}
|
||||
<div class="impact-item">
|
||||
<div class="impact-icon impact-warning">
|
||||
<i class="fas fa-clock"></i>
|
||||
</div>
|
||||
<div class="impact-details">
|
||||
<div class="impact-title">Scheduled Cases</div>
|
||||
<div class="impact-description">
|
||||
{{ template.scheduled_cases.count }} scheduled cases reference this template
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alternative Actions -->
|
||||
<div class="alternative-actions">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-lightbulb me-2"></i>Alternative Actions
|
||||
</h5>
|
||||
<p class="mb-3">Consider these alternatives instead of permanent deletion:</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-outline-secondary action-button" onclick="archiveTemplate()">
|
||||
<i class="fas fa-archive me-2"></i>
|
||||
<div>
|
||||
<strong>Archive Template</strong><br>
|
||||
<small>Hide template but preserve data for reference</small>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-outline-info action-button" onclick="deactivateTemplate()">
|
||||
<i class="fas fa-pause me-2"></i>
|
||||
<div>
|
||||
<strong>Deactivate Template</strong><br>
|
||||
<small>Prevent new uses while keeping existing data</small>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-outline-success action-button" onclick="exportTemplate()">
|
||||
<i class="fas fa-download me-2"></i>
|
||||
<div>
|
||||
<strong>Export Template</strong><br>
|
||||
<small>Create backup before deletion</small>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<button class="btn btn-outline-warning action-button" onclick="transferTemplate()">
|
||||
<i class="fas fa-exchange-alt me-2"></i>
|
||||
<div>
|
||||
<strong>Transfer Ownership</strong><br>
|
||||
<small>Assign template to another user</small>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Section -->
|
||||
<div class="confirmation-section">
|
||||
<h5 class="text-danger mb-3">
|
||||
<i class="fas fa-shield-alt me-2"></i>Deletion Confirmation
|
||||
</h5>
|
||||
|
||||
<form method="post" id="deleteForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="type-to-confirm">
|
||||
<label class="form-label">
|
||||
<strong>Type the template name to confirm deletion:</strong>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="confirmationInput"
|
||||
placeholder="Enter: {{ template.name }}" required>
|
||||
<small class="text-muted">
|
||||
This helps prevent accidental deletions
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reason for Deletion</label>
|
||||
<select class="form-select" name="deletion_reason" required>
|
||||
<option value="">Select reason...</option>
|
||||
<option value="obsolete">Template is obsolete</option>
|
||||
<option value="duplicate">Duplicate template</option>
|
||||
<option value="incorrect">Incorrect information</option>
|
||||
<option value="replaced">Replaced by newer template</option>
|
||||
<option value="unused">No longer used</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Additional Notes</label>
|
||||
<textarea class="form-control" name="deletion_notes" rows="3"
|
||||
placeholder="Optional: Provide additional context for this deletion..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="notifyUsers" name="notify_users" value="1">
|
||||
<label class="form-check-label" for="notifyUsers">
|
||||
Notify users who have used this template
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="createBackup" name="create_backup" value="1" checked>
|
||||
<label class="form-check-label" for="createBackup">
|
||||
Create backup before deletion
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="{% url 'operating_theatre:template_detail' template.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger" id="deleteButton" disabled>
|
||||
<i class="fas fa-trash me-1"></i>Delete Template Permanently
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Enable delete button only when confirmation text matches
|
||||
$('#confirmationInput').on('input', function() {
|
||||
const inputValue = $(this).val();
|
||||
const templateName = '{{ template.name }}';
|
||||
const deleteButton = $('#deleteButton');
|
||||
|
||||
if (inputValue === templateName) {
|
||||
deleteButton.prop('disabled', false);
|
||||
deleteButton.removeClass('btn-secondary').addClass('btn-danger');
|
||||
} else {
|
||||
deleteButton.prop('disabled', true);
|
||||
deleteButton.removeClass('btn-danger').addClass('btn-secondary');
|
||||
}
|
||||
});
|
||||
|
||||
// Form submission confirmation
|
||||
$('#deleteForm').on('submit', function(e) {
|
||||
const confirmed = confirm(
|
||||
'Are you absolutely sure you want to delete this template? ' +
|
||||
'This action cannot be undone and will permanently remove all associated data.'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function archiveTemplate() {
|
||||
if (confirm('Archive this template? It will be hidden but data will be preserved.')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:template_archive" template.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Template archived successfully!');
|
||||
window.location.href = '{% url "operating_theatre:template_list" %}';
|
||||
} else {
|
||||
alert('Error archiving template: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error archiving template');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deactivateTemplate() {
|
||||
if (confirm('Deactivate this template? It will prevent new uses but preserve existing data.')) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:template_deactivate" template.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Template deactivated successfully!');
|
||||
window.location.href = '{% url "operating_theatre:template_detail" template.pk %}';
|
||||
} else {
|
||||
alert('Error deactivating template: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error deactivating template');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exportTemplate() {
|
||||
window.location.href = '{% url "operating_theatre:template_export" template.pk %}';
|
||||
}
|
||||
|
||||
function transferTemplate() {
|
||||
const newOwner = prompt('Enter the username or email of the new owner:');
|
||||
if (newOwner) {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:template_transfer" template.pk %}',
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}',
|
||||
'new_owner': newOwner
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Template ownership transferred successfully!');
|
||||
window.location.href = '{% url "operating_theatre:template_list" %}';
|
||||
} else {
|
||||
alert('Error transferring template: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error transferring template');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
$(document).keydown(function(e) {
|
||||
// Escape key to cancel
|
||||
if (e.keyCode === 27) {
|
||||
window.location.href = '{% url "operating_theatre:template_detail" template.pk %}';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
628
templates/operating_theatre/templates/template_detail.html
Normal file
628
templates/operating_theatre/templates/template_detail.html
Normal file
@ -0,0 +1,628 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ template.name }} - Surgical Template{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.template-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 0.5rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.info-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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-left: 1rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.step-duration {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.equipment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.equipment-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.equipment-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.equipment-name {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.equipment-details {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.team-member {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.member-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.member-info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.member-role {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.member-requirements {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.usage-stats {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.template-tags {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.template-tag {
|
||||
display: inline-block;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.complexity-simple { background: #d4edda; color: #155724; }
|
||||
.complexity-moderate { background: #fff3cd; color: #856404; }
|
||||
.complexity-complex { background: #f8d7da; color: #721c24; }
|
||||
.complexity-highly-complex { background: #d1ecf1; color: #0c5460; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.template-header {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.step-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
margin-left: 0;
|
||||
margin-top: 0.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"><a href="{% url 'operating_theatre:template_list' %}">Templates</a></li>
|
||||
<li class="breadcrumb-item active">{{ template.name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-file-medical me-2"></i>{{ template.name }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group me-2">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'operating_theatre:template_export_pdf' template.pk %}">
|
||||
<i class="fas fa-file-pdf me-2"></i>Export as PDF
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'operating_theatre:template_export_json' template.pk %}">
|
||||
<i class="fas fa-file-code me-2"></i>Export as JSON
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="{% url 'operating_theatre:template_edit' template.pk %}" class="btn btn-outline-primary me-2">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button class="btn btn-success" onclick="useTemplate()">
|
||||
<i class="fas fa-play me-1"></i>Use Template
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Header -->
|
||||
<div class="template-header">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-3">{{ template.name }}</h2>
|
||||
<p class="mb-2">{{ template.description }}</p>
|
||||
<div class="template-tags">
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-user-md me-1"></i>{{ template.get_specialty_display }}
|
||||
</span>
|
||||
<span class="template-tag complexity-{{ template.complexity }}">
|
||||
<i class="fas fa-layer-group me-1"></i>{{ template.get_complexity_display }}
|
||||
</span>
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-clock me-1"></i>{{ template.estimated_duration }} minutes
|
||||
</span>
|
||||
{% if template.requires_anesthesia %}
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-syringe me-1"></i>Anesthesia Required
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if template.is_emergency_procedure %}
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>Emergency Procedure
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<div class="mb-2">
|
||||
{% if template.status == 'active' %}
|
||||
<span class="badge bg-success fs-6">Active</span>
|
||||
{% elif template.status == 'draft' %}
|
||||
<span class="badge bg-secondary fs-6">Draft</span>
|
||||
{% elif template.status == 'under_review' %}
|
||||
<span class="badge bg-warning fs-6">Under Review</span>
|
||||
{% elif template.status == 'archived' %}
|
||||
<span class="badge bg-dark fs-6">Archived</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-white-50">
|
||||
Created by {{ template.created_by.get_full_name }}<br>
|
||||
{{ template.created_at|date:"M d, Y" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Statistics -->
|
||||
<div class="usage-stats">
|
||||
<h5 class="mb-3">
|
||||
<i class="fas fa-chart-line me-2"></i>Usage Statistics
|
||||
</h5>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-primary">{{ usage_stats.total_uses }}</div>
|
||||
<div class="stat-label">Total Uses</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-success">{{ usage_stats.this_month }}</div>
|
||||
<div class="stat-label">This Month</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-info">{{ usage_stats.avg_duration }}</div>
|
||||
<div class="stat-label">Avg Duration (min)</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-warning">{{ usage_stats.success_rate }}%</div>
|
||||
<div class="stat-label">Success Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Procedure Type</div>
|
||||
<div class="info-value">{{ template.get_procedure_type_display }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Estimated Duration</div>
|
||||
<div class="info-value">{{ template.estimated_duration }} minutes</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Required Team Size</div>
|
||||
<div class="info-value">{{ template.required_team_size }} members</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Anesthesia Type</div>
|
||||
<div class="info-value">
|
||||
{% if template.anesthesia_type %}
|
||||
{{ template.get_anesthesia_type_display }}
|
||||
{% else %}
|
||||
Not specified
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Patient Position</div>
|
||||
<div class="info-value">{{ template.patient_position|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Room Requirements</div>
|
||||
<div class="info-value">{{ template.room_requirements|default:"Standard OR" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Procedure Steps -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-list-ol me-2"></i>Procedure Steps
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% for step in template.procedure_steps.all %}
|
||||
<div class="step-item">
|
||||
<div class="step-header">
|
||||
<div class="step-number">{{ step.step_number }}</div>
|
||||
<div class="step-title">{{ step.title }}</div>
|
||||
<div class="step-duration">{{ step.estimated_duration }} min</div>
|
||||
</div>
|
||||
<div class="step-description">
|
||||
{{ step.description }}
|
||||
</div>
|
||||
{% if step.critical_points %}
|
||||
<div class="mt-2">
|
||||
<small class="text-danger">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
<strong>Critical Points:</strong> {{ step.critical_points }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-list-ol fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted">No procedure steps defined</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Required Equipment -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-tools me-2"></i>Required Equipment
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% for equipment in template.equipment_list.all %}
|
||||
<div class="equipment-item">
|
||||
<div class="equipment-icon">
|
||||
<i class="fas fa-tools"></i>
|
||||
</div>
|
||||
<div class="equipment-info">
|
||||
<div class="equipment-name">{{ equipment.name }}</div>
|
||||
<div class="equipment-details">
|
||||
{{ equipment.category|title }} |
|
||||
Quantity: {{ equipment.quantity|default:1 }} |
|
||||
{% if equipment.is_critical %}
|
||||
<span class="text-danger">Critical</span>
|
||||
{% else %}
|
||||
<span class="text-muted">Standard</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if equipment.alternatives %}
|
||||
<div class="equipment-alternatives">
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-exchange-alt me-1"></i>
|
||||
Alternatives: {{ equipment.alternatives }}
|
||||
</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-tools fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted">No equipment requirements defined</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Required Team -->
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-users me-2"></i>Required Team
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% for member in template.team_requirements.all %}
|
||||
<div class="team-member">
|
||||
<div class="member-avatar">
|
||||
{{ member.role|first|upper }}
|
||||
</div>
|
||||
<div class="member-info">
|
||||
<div class="member-role">{{ member.get_role_display }}</div>
|
||||
<div class="member-requirements">
|
||||
{% if member.specialization %}
|
||||
Specialization: {{ member.specialization }} |
|
||||
{% endif %}
|
||||
Experience: {{ member.min_experience_years|default:"Any" }} years
|
||||
{% if member.certifications_required %}
|
||||
| Certifications: {{ member.certifications_required }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-count">
|
||||
<span class="badge bg-primary">{{ member.count|default:1 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-3">
|
||||
<i class="fas fa-users fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted">No team requirements defined</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pre-operative Requirements -->
|
||||
{% if template.preop_requirements %}
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-clipboard-check me-2"></i>Pre-operative Requirements
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Patient Preparation</h6>
|
||||
<ul class="list-unstyled">
|
||||
{% for req in template.preop_requirements.patient_prep %}
|
||||
<li class="mb-1">
|
||||
<i class="fas fa-check text-success me-2"></i>{{ req }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Required Tests</h6>
|
||||
<ul class="list-unstyled">
|
||||
{% for test in template.preop_requirements.required_tests %}
|
||||
<li class="mb-1">
|
||||
<i class="fas fa-vial text-info me-2"></i>{{ test }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Post-operative Care -->
|
||||
{% if template.postop_care %}
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-heart me-2"></i>Post-operative Care
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Immediate Care</h6>
|
||||
<ul class="list-unstyled">
|
||||
{% for care in template.postop_care.immediate %}
|
||||
<li class="mb-1">
|
||||
<i class="fas fa-arrow-right text-primary me-2"></i>{{ care }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Recovery Instructions</h6>
|
||||
<ul class="list-unstyled">
|
||||
{% for instruction in template.postop_care.recovery %}
|
||||
<li class="mb-1">
|
||||
<i class="fas fa-info-circle text-warning me-2"></i>{{ instruction }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Notes and Warnings -->
|
||||
{% if template.notes or template.warnings %}
|
||||
<div class="info-section">
|
||||
<div class="section-header">
|
||||
<i class="fas fa-sticky-note me-2"></i>Additional Information
|
||||
</div>
|
||||
<div class="section-content">
|
||||
{% if template.warnings %}
|
||||
<div class="alert alert-warning">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Warnings
|
||||
</h6>
|
||||
<p class="mb-0">{{ template.warnings }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if template.notes %}
|
||||
<div class="alert alert-info">
|
||||
<h6 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>Notes
|
||||
</h6>
|
||||
<p class="mb-0">{{ template.notes }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function useTemplate() {
|
||||
if (confirm('Use this template to create a new surgical case?')) {
|
||||
window.location.href = `/operating_theatre/cases/create/?template={{ template.pk }}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Print functionality
|
||||
$(document).keydown(function(e) {
|
||||
// Ctrl/Cmd + P for print
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 80) {
|
||||
e.preventDefault();
|
||||
window.print();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
816
templates/operating_theatre/templates/template_form.html
Normal file
816
templates/operating_theatre/templates/template_form.html
Normal file
@ -0,0 +1,816 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% if template.pk %}Edit Template - {{ template.name }}{% else %}Create Surgical Template{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.form-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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: between;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.step-builder {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: between;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.equipment-item {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.team-member-item {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.25rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.duration-calculator {
|
||||
background: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
background: white;
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding: 1.5rem;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.complexity-indicator {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.complexity-simple { background: #d4edda; color: #155724; }
|
||||
.complexity-moderate { background: #fff3cd; color: #856404; }
|
||||
.complexity-complex { background: #f8d7da; color: #721c24; }
|
||||
.complexity-highly-complex { background: #d1ecf1; color: #0c5460; }
|
||||
|
||||
.drag-handle {
|
||||
cursor: move;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.step-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</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"><a href="{% url 'operating_theatre:template_list' %}">Templates</a></li>
|
||||
<li class="breadcrumb-item active">
|
||||
{% if template.pk %}Edit Template{% else %}Create Template{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-file-medical me-2"></i>
|
||||
{% if template.pk %}Edit Template{% else %}Create Surgical Template{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<button type="button" class="btn btn-outline-secondary me-2" onclick="previewTemplate()">
|
||||
<i class="fas fa-eye me-1"></i>Preview
|
||||
</button>
|
||||
<a href="{% url 'operating_theatre:template_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to List
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" id="templateForm">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Template Name *</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small">{{ form.name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Specialty *</label>
|
||||
{{ form.specialty }}
|
||||
{% if form.specialty.errors %}
|
||||
<div class="text-danger small">{{ form.specialty.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="text-danger small">{{ form.description.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Procedure Type</label>
|
||||
{{ form.procedure_type }}
|
||||
{% if form.procedure_type.errors %}
|
||||
<div class="text-danger small">{{ form.procedure_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Complexity</label>
|
||||
{{ form.complexity }}
|
||||
{% if form.complexity.errors %}
|
||||
<div class="text-danger small">{{ form.complexity.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<div id="complexityIndicator" class="mt-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Status</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}
|
||||
<div class="text-danger small">{{ form.status.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Duration and Requirements -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<i class="fas fa-clock me-2"></i>Duration and Requirements
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="duration-calculator">
|
||||
<h6 class="mb-3">
|
||||
<i class="fas fa-calculator me-2"></i>Duration Calculator
|
||||
</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Estimated Duration (minutes)</label>
|
||||
{{ form.estimated_duration }}
|
||||
{% if form.estimated_duration.errors %}
|
||||
<div class="text-danger small">{{ form.estimated_duration.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Setup Time (minutes)</label>
|
||||
<input type="number" class="form-control" id="setupTime" value="15" min="0">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Cleanup Time (minutes)</label>
|
||||
<input type="number" class="form-control" id="cleanupTime" value="10" min="0">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Total Time</label>
|
||||
<input type="text" class="form-control" id="totalTime" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Required Team Size</label>
|
||||
{{ form.required_team_size }}
|
||||
{% if form.required_team_size.errors %}
|
||||
<div class="text-danger small">{{ form.required_team_size.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Anesthesia Type</label>
|
||||
{{ form.anesthesia_type }}
|
||||
{% if form.anesthesia_type.errors %}
|
||||
<div class="text-danger small">{{ form.anesthesia_type.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Patient Position</label>
|
||||
{{ form.patient_position }}
|
||||
{% if form.patient_position.errors %}
|
||||
<div class="text-danger small">{{ form.patient_position.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
{{ form.requires_anesthesia }}
|
||||
<label class="form-check-label" for="{{ form.requires_anesthesia.id_for_label }}">
|
||||
Requires Anesthesia
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
{{ form.is_emergency_procedure }}
|
||||
<label class="form-check-label" for="{{ form.is_emergency_procedure.id_for_label }}">
|
||||
Emergency Procedure
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Procedure Steps -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<i class="fas fa-list-ol me-2"></i>Procedure Steps
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="addStep()">
|
||||
<i class="fas fa-plus me-1"></i>Add Step
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div id="stepsContainer">
|
||||
<!-- Steps will be dynamically added here -->
|
||||
</div>
|
||||
<div class="text-center py-3" id="noStepsMessage">
|
||||
<i class="fas fa-list-ol fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted">No procedure steps added yet</p>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="addStep()">
|
||||
<i class="fas fa-plus me-1"></i>Add First Step
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Required Equipment -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<i class="fas fa-tools me-2"></i>Required Equipment
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="addEquipment()">
|
||||
<i class="fas fa-plus me-1"></i>Add Equipment
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div id="equipmentContainer">
|
||||
<!-- Equipment will be dynamically added here -->
|
||||
</div>
|
||||
<div class="text-center py-3" id="noEquipmentMessage">
|
||||
<i class="fas fa-tools fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted">No equipment requirements added yet</p>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="addEquipment()">
|
||||
<i class="fas fa-plus me-1"></i>Add Equipment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Required Team -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<i class="fas fa-users me-2"></i>Required Team
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-primary" onclick="addTeamMember()">
|
||||
<i class="fas fa-plus me-1"></i>Add Team Member
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div id="teamContainer">
|
||||
<!-- Team members will be dynamically added here -->
|
||||
</div>
|
||||
<div class="text-center py-3" id="noTeamMessage">
|
||||
<i class="fas fa-users fa-2x text-muted mb-2"></i>
|
||||
<p class="text-muted">No team requirements added yet</p>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="addTeamMember()">
|
||||
<i class="fas fa-plus me-1"></i>Add Team Member
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<i class="fas fa-sticky-note me-2"></i>Additional Information
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-content">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Room Requirements</label>
|
||||
{{ form.room_requirements }}
|
||||
{% if form.room_requirements.errors %}
|
||||
<div class="text-danger small">{{ form.room_requirements.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Usage Frequency</label>
|
||||
{{ form.usage_frequency }}
|
||||
{% if form.usage_frequency.errors %}
|
||||
<div class="text-danger small">{{ form.usage_frequency.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Notes</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="text-danger small">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Warnings</label>
|
||||
{{ form.warnings }}
|
||||
{% if form.warnings.errors %}
|
||||
<div class="text-danger small">{{ form.warnings.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="form-actions">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="saveDraft()">
|
||||
<i class="fas fa-save me-1"></i>Save as Draft
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'operating_theatre:template_list' %}" class="btn btn-outline-secondary me-2">
|
||||
<i class="fas fa-times me-1"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" form="templateForm" class="btn btn-primary">
|
||||
<i class="fas fa-check me-1"></i>
|
||||
{% if template.pk %}Update Template{% else %}Create Template{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>Template 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
let stepCounter = 0;
|
||||
let equipmentCounter = 0;
|
||||
let teamCounter = 0;
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initialize duration calculator
|
||||
updateTotalTime();
|
||||
$('#setupTime, #cleanupTime').on('input', updateTotalTime);
|
||||
$('#id_estimated_duration').on('input', updateTotalTime);
|
||||
|
||||
// Initialize complexity indicator
|
||||
updateComplexityIndicator();
|
||||
$('#id_complexity').on('change', updateComplexityIndicator);
|
||||
|
||||
// Load existing data if editing
|
||||
{% if template.pk %}
|
||||
loadExistingData();
|
||||
{% endif %}
|
||||
});
|
||||
|
||||
function updateTotalTime() {
|
||||
const procedureTime = parseInt($('#id_estimated_duration').val()) || 0;
|
||||
const setupTime = parseInt($('#setupTime').val()) || 0;
|
||||
const cleanupTime = parseInt($('#cleanupTime').val()) || 0;
|
||||
const total = procedureTime + setupTime + cleanupTime;
|
||||
|
||||
$('#totalTime').val(total + ' minutes');
|
||||
}
|
||||
|
||||
function updateComplexityIndicator() {
|
||||
const complexity = $('#id_complexity').val();
|
||||
const indicator = $('#complexityIndicator');
|
||||
|
||||
indicator.removeClass('complexity-simple complexity-moderate complexity-complex complexity-highly-complex');
|
||||
|
||||
if (complexity) {
|
||||
indicator.addClass('complexity-' + complexity.replace('_', '-'));
|
||||
indicator.text(complexity.replace('_', ' ').toUpperCase());
|
||||
indicator.show();
|
||||
} else {
|
||||
indicator.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function addStep() {
|
||||
stepCounter++;
|
||||
const stepHtml = `
|
||||
<div class="step-builder" data-step="${stepCounter}">
|
||||
<div class="step-header">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="step-number">${stepCounter}</div>
|
||||
<span class="ms-2 fw-bold">Step ${stepCounter}</span>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary me-1 drag-handle">
|
||||
<i class="fas fa-grip-vertical"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeStep(${stepCounter})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="step-content">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Step Title</label>
|
||||
<input type="text" class="form-control" name="step_${stepCounter}_title" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Duration (minutes)</label>
|
||||
<input type="number" class="form-control" name="step_${stepCounter}_duration" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" name="step_${stepCounter}_description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Critical Points</label>
|
||||
<textarea class="form-control" name="step_${stepCounter}_critical_points" rows="2"
|
||||
placeholder="Important safety considerations or critical steps..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#stepsContainer').append(stepHtml);
|
||||
$('#noStepsMessage').hide();
|
||||
}
|
||||
|
||||
function removeStep(stepId) {
|
||||
if (confirm('Remove this step?')) {
|
||||
$(`.step-builder[data-step="${stepId}"]`).remove();
|
||||
|
||||
if ($('#stepsContainer .step-builder').length === 0) {
|
||||
$('#noStepsMessage').show();
|
||||
}
|
||||
|
||||
// Renumber steps
|
||||
renumberSteps();
|
||||
}
|
||||
}
|
||||
|
||||
function renumberSteps() {
|
||||
$('#stepsContainer .step-builder').each(function(index) {
|
||||
const newNumber = index + 1;
|
||||
$(this).find('.step-number').text(newNumber);
|
||||
$(this).find('.step-header span').text(`Step ${newNumber}`);
|
||||
});
|
||||
}
|
||||
|
||||
function addEquipment() {
|
||||
equipmentCounter++;
|
||||
const equipmentHtml = `
|
||||
<div class="equipment-item" data-equipment="${equipmentCounter}">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h6 class="mb-0">Equipment ${equipmentCounter}</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeEquipment(${equipmentCounter})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Equipment Name</label>
|
||||
<input type="text" class="form-control" name="equipment_${equipmentCounter}_name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Category</label>
|
||||
<select class="form-select" name="equipment_${equipmentCounter}_category">
|
||||
<option value="">Select Category</option>
|
||||
<option value="surgical_instruments">Surgical Instruments</option>
|
||||
<option value="monitoring">Monitoring Equipment</option>
|
||||
<option value="anesthesia">Anesthesia Equipment</option>
|
||||
<option value="imaging">Imaging Equipment</option>
|
||||
<option value="electrosurgical">Electrosurgical</option>
|
||||
<option value="positioning">Positioning Equipment</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" class="form-control" name="equipment_${equipmentCounter}_quantity" value="1" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Alternatives</label>
|
||||
<input type="text" class="form-control" name="equipment_${equipmentCounter}_alternatives"
|
||||
placeholder="Alternative equipment options...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mt-4">
|
||||
<input type="checkbox" class="form-check-input" name="equipment_${equipmentCounter}_critical" value="1">
|
||||
<label class="form-check-label">Critical Equipment</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#equipmentContainer').append(equipmentHtml);
|
||||
$('#noEquipmentMessage').hide();
|
||||
}
|
||||
|
||||
function removeEquipment(equipmentId) {
|
||||
if (confirm('Remove this equipment requirement?')) {
|
||||
$(`.equipment-item[data-equipment="${equipmentId}"]`).remove();
|
||||
|
||||
if ($('#equipmentContainer .equipment-item').length === 0) {
|
||||
$('#noEquipmentMessage').show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addTeamMember() {
|
||||
teamCounter++;
|
||||
const teamHtml = `
|
||||
<div class="team-member-item" data-team="${teamCounter}">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<h6 class="mb-0">Team Member ${teamCounter}</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeTeamMember(${teamCounter})">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Role</label>
|
||||
<select class="form-select" name="team_${teamCounter}_role" required>
|
||||
<option value="">Select Role</option>
|
||||
<option value="surgeon">Surgeon</option>
|
||||
<option value="assistant_surgeon">Assistant Surgeon</option>
|
||||
<option value="anesthesiologist">Anesthesiologist</option>
|
||||
<option value="nurse_anesthetist">Nurse Anesthetist</option>
|
||||
<option value="scrub_nurse">Scrub Nurse</option>
|
||||
<option value="circulating_nurse">Circulating Nurse</option>
|
||||
<option value="surgical_technologist">Surgical Technologist</option>
|
||||
<option value="perfusionist">Perfusionist</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Specialization</label>
|
||||
<input type="text" class="form-control" name="team_${teamCounter}_specialization"
|
||||
placeholder="Required specialization...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Count</label>
|
||||
<input type="number" class="form-control" name="team_${teamCounter}_count" value="1" min="1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Min Experience (years)</label>
|
||||
<input type="number" class="form-control" name="team_${teamCounter}_experience" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Required Certifications</label>
|
||||
<input type="text" class="form-control" name="team_${teamCounter}_certifications"
|
||||
placeholder="Required certifications...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#teamContainer').append(teamHtml);
|
||||
$('#noTeamMessage').hide();
|
||||
}
|
||||
|
||||
function removeTeamMember(teamId) {
|
||||
if (confirm('Remove this team member requirement?')) {
|
||||
$(`.team-member-item[data-team="${teamId}"]`).remove();
|
||||
|
||||
if ($('#teamContainer .team-member-item').length === 0) {
|
||||
$('#noTeamMessage').show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function previewTemplate() {
|
||||
// Collect form data and show preview
|
||||
const formData = new FormData($('#templateForm')[0]);
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:template_preview" %}',
|
||||
method: 'POST',
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(response) {
|
||||
$('#previewContent').html(response.html);
|
||||
$('#previewModal').modal('show');
|
||||
},
|
||||
error: function() {
|
||||
alert('Error generating preview');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveDraft() {
|
||||
$('#id_status').val('draft');
|
||||
$('#templateForm').submit();
|
||||
}
|
||||
|
||||
function loadExistingData() {
|
||||
// Load existing steps, equipment, and team data
|
||||
// This would be populated from the backend
|
||||
}
|
||||
|
||||
// Form validation
|
||||
$('#templateForm').on('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
// Validate required fields
|
||||
$(this).find('[required]').each(function() {
|
||||
if (!$(this).val()) {
|
||||
isValid = false;
|
||||
$(this).addClass('is-invalid');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
alert('Please fill in all required fields');
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-save functionality
|
||||
let autoSaveTimer;
|
||||
$('#templateForm input, #templateForm textarea, #templateForm select').on('input change', function() {
|
||||
clearTimeout(autoSaveTimer);
|
||||
autoSaveTimer = setTimeout(function() {
|
||||
// Auto-save logic here
|
||||
console.log('Auto-saving...');
|
||||
}, 30000); // Auto-save every 30 seconds
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
602
templates/operating_theatre/templates/template_list.html
Normal file
602
templates/operating_theatre/templates/template_list.html
Normal file
@ -0,0 +1,602 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Surgical Templates{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<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-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #6c757d;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.template-header {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.template-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.template-meta {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.template-tags {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.template-tag {
|
||||
display: inline-block;
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.template-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
background: #f8f9fa;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.usage-indicator {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.usage-high {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.usage-medium {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.usage-low {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.template-header {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.template-actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</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 Templates</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">
|
||||
<i class="fas fa-file-medical me-2"></i>Surgical Templates
|
||||
</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<button class="btn btn-outline-secondary me-2" onclick="exportTemplates()">
|
||||
<i class="fas fa-download me-1"></i>Export
|
||||
</button>
|
||||
<a href="{% url 'operating_theatre:template_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="stats-card">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mb-2">Surgical Template Management</h3>
|
||||
<p class="mb-0">Manage standardized surgical procedure templates to ensure consistency and efficiency in the operating theatre.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<i class="fas fa-file-medical fa-3x opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-primary">{{ stats.total_templates }}</div>
|
||||
<div class="stat-label">Total Templates</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-success">{{ stats.active_templates }}</div>
|
||||
<div class="stat-label">Active Templates</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-info">{{ stats.used_this_month }}</div>
|
||||
<div class="stat-label">Used This Month</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value text-warning">{{ stats.pending_review }}</div>
|
||||
<div class="stat-label">Pending Review</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="search-section">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<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 templates by name, procedure, or specialty...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#filterSection">
|
||||
<i class="fas fa-filter me-1"></i>Filters
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Section -->
|
||||
<div class="collapse" id="filterSection">
|
||||
<div class="filter-section">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Specialty</label>
|
||||
<select class="form-select" id="specialtyFilter">
|
||||
<option value="">All Specialties</option>
|
||||
<option value="general_surgery">General Surgery</option>
|
||||
<option value="orthopedic">Orthopedic</option>
|
||||
<option value="cardiac">Cardiac</option>
|
||||
<option value="neurosurgery">Neurosurgery</option>
|
||||
<option value="plastic">Plastic Surgery</option>
|
||||
<option value="urology">Urology</option>
|
||||
<option value="gynecology">Gynecology</option>
|
||||
<option value="ophthalmology">Ophthalmology</option>
|
||||
<option value="ent">ENT</option>
|
||||
<option value="vascular">Vascular</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Status</label>
|
||||
<select class="form-select" id="statusFilter">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="under_review">Under Review</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Complexity</label>
|
||||
<select class="form-select" id="complexityFilter">
|
||||
<option value="">All Complexities</option>
|
||||
<option value="simple">Simple</option>
|
||||
<option value="moderate">Moderate</option>
|
||||
<option value="complex">Complex</option>
|
||||
<option value="highly_complex">Highly Complex</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Duration Range</label>
|
||||
<select class="form-select" id="durationFilter">
|
||||
<option value="">All Durations</option>
|
||||
<option value="0-60">0-60 minutes</option>
|
||||
<option value="60-120">1-2 hours</option>
|
||||
<option value="120-240">2-4 hours</option>
|
||||
<option value="240+">4+ hours</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-primary" onclick="applyFilters()">
|
||||
<i class="fas fa-check me-1"></i>Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Templates Grid -->
|
||||
<div class="template-grid" id="templatesContainer">
|
||||
{% for template in templates %}
|
||||
<div class="template-card" data-template-id="{{ template.id }}">
|
||||
<div class="template-header">
|
||||
<div class="flex-grow-1">
|
||||
<div class="template-title">{{ template.name }}</div>
|
||||
<div class="template-meta">
|
||||
<i class="fas fa-user-md me-1"></i>{{ template.specialty|title }} |
|
||||
<i class="fas fa-clock me-1"></i>{{ template.estimated_duration }} min |
|
||||
<i class="fas fa-calendar me-1"></i>Created {{ template.created_at|date:"M d, Y" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-2">
|
||||
{% if template.status == 'active' %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% elif template.status == 'draft' %}
|
||||
<span class="badge bg-secondary">Draft</span>
|
||||
{% elif template.status == 'under_review' %}
|
||||
<span class="badge bg-warning">Under Review</span>
|
||||
{% elif template.status == 'archived' %}
|
||||
<span class="badge bg-dark">Archived</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="template-description mb-3">
|
||||
{{ template.description|truncatewords:20 }}
|
||||
</div>
|
||||
|
||||
<div class="template-tags">
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-layer-group me-1"></i>{{ template.get_complexity_display }}
|
||||
</span>
|
||||
{% if template.requires_anesthesia %}
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-syringe me-1"></i>Anesthesia Required
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if template.is_emergency_procedure %}
|
||||
<span class="template-tag">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>Emergency
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="template-tag usage-{{ template.usage_frequency|lower }}">
|
||||
<i class="fas fa-chart-line me-1"></i>{{ template.get_usage_frequency_display }} Usage
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Equipment Items:</small>
|
||||
<div class="fw-bold">{{ template.equipment_list.count }}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<small class="text-muted">Team Members:</small>
|
||||
<div class="fw-bold">{{ template.required_team_size }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="template-actions">
|
||||
<a href="{% url 'operating_theatre:template_detail' template.pk %}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
<a href="{% url 'operating_theatre:template_edit' template.pk %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button class="btn btn-outline-success btn-sm" onclick="useTemplate('{{ template.pk }}')">
|
||||
<i class="fas fa-play me-1"></i>Use
|
||||
</button>
|
||||
<button class="btn btn-outline-info btn-sm" onclick="duplicateTemplate('{{ template.pk }}')">
|
||||
<i class="fas fa-copy me-1"></i>Duplicate
|
||||
</button>
|
||||
<div class="dropdown d-inline">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'operating_theatre:template_export' template.pk %}">
|
||||
<i class="fas fa-download me-2"></i>Export
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'operating_theatre:template_share' template.pk %}">
|
||||
<i class="fas fa-share me-2"></i>Share
|
||||
</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'operating_theatre:template_delete' template.pk %}">
|
||||
<i class="fas fa-trash me-2"></i>Delete
|
||||
</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-file-medical fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No surgical templates found</h5>
|
||||
<p class="text-muted">Create your first surgical template to standardize procedures.</p>
|
||||
<a href="{% url 'operating_theatre:template_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Create Template
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Templates pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">« First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="page-item active">
|
||||
<span class="page-link">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last »</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize search
|
||||
$('#searchInput').on('input', function() {
|
||||
filterTemplates();
|
||||
});
|
||||
|
||||
// Load statistics
|
||||
loadStats();
|
||||
});
|
||||
|
||||
function loadStats() {
|
||||
$.ajax({
|
||||
url: '{% url "operating_theatre:template_stats" %}',
|
||||
success: function(data) {
|
||||
$('.stat-item:nth-child(1) .stat-value').text(data.total_templates);
|
||||
$('.stat-item:nth-child(2) .stat-value').text(data.active_templates);
|
||||
$('.stat-item:nth-child(3) .stat-value').text(data.used_this_month);
|
||||
$('.stat-item:nth-child(4) .stat-value').text(data.pending_review);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterTemplates() {
|
||||
const searchTerm = $('#searchInput').val().toLowerCase();
|
||||
const specialty = $('#specialtyFilter').val();
|
||||
const status = $('#statusFilter').val();
|
||||
const complexity = $('#complexityFilter').val();
|
||||
const duration = $('#durationFilter').val();
|
||||
|
||||
$('.template-card').each(function() {
|
||||
const card = $(this);
|
||||
const title = card.find('.template-title').text().toLowerCase();
|
||||
const description = card.find('.template-description').text().toLowerCase();
|
||||
const cardSpecialty = card.data('specialty');
|
||||
const cardStatus = card.data('status');
|
||||
const cardComplexity = card.data('complexity');
|
||||
const cardDuration = card.data('duration');
|
||||
|
||||
let show = true;
|
||||
|
||||
// Search filter
|
||||
if (searchTerm && !title.includes(searchTerm) && !description.includes(searchTerm)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Specialty filter
|
||||
if (specialty && cardSpecialty !== specialty) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (status && cardStatus !== status) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Complexity filter
|
||||
if (complexity && cardComplexity !== complexity) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Duration filter
|
||||
if (duration && !matchesDurationRange(cardDuration, duration)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
card.toggle(show);
|
||||
});
|
||||
}
|
||||
|
||||
function matchesDurationRange(duration, range) {
|
||||
const durationNum = parseInt(duration);
|
||||
|
||||
switch(range) {
|
||||
case '0-60':
|
||||
return durationNum <= 60;
|
||||
case '60-120':
|
||||
return durationNum > 60 && durationNum <= 120;
|
||||
case '120-240':
|
||||
return durationNum > 120 && durationNum <= 240;
|
||||
case '240+':
|
||||
return durationNum > 240;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
filterTemplates();
|
||||
$('#filterSection').collapse('hide');
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#searchInput').val('');
|
||||
$('#specialtyFilter').val('');
|
||||
$('#statusFilter').val('');
|
||||
$('#complexityFilter').val('');
|
||||
$('#durationFilter').val('');
|
||||
filterTemplates();
|
||||
}
|
||||
|
||||
function useTemplate(templateId) {
|
||||
if (confirm('Use this template to create a new surgical case?')) {
|
||||
window.location.href = `/operating_theatre/cases/create/?template=${templateId}`;
|
||||
}
|
||||
}
|
||||
|
||||
function duplicateTemplate(templateId) {
|
||||
if (confirm('Create a copy of this template?')) {
|
||||
$.ajax({
|
||||
url: `{% url "operating_theatre:template_duplicate" "0" %}`.replace('0', templateId),
|
||||
method: 'POST',
|
||||
data: {
|
||||
'csrfmiddlewaretoken': '{{ csrf_token }}'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert('Template duplicated successfully!');
|
||||
window.location.href = response.edit_url;
|
||||
} else {
|
||||
alert('Error duplicating template: ' + response.error);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Error duplicating template');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function exportTemplates() {
|
||||
const selectedTemplates = [];
|
||||
$('.template-card input[type="checkbox"]:checked').each(function() {
|
||||
selectedTemplates.push($(this).closest('.template-card').data('template-id'));
|
||||
});
|
||||
|
||||
if (selectedTemplates.length === 0) {
|
||||
// Export all visible templates
|
||||
window.location.href = '{% url "operating_theatre:templates_export" %}';
|
||||
} else {
|
||||
// Export selected templates
|
||||
window.location.href = `{% url "operating_theatre:templates_export" %}?ids=${selectedTemplates.join(',')}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
$(document).keydown(function(e) {
|
||||
// Ctrl/Cmd + K for search
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 75) {
|
||||
e.preventDefault();
|
||||
$('#searchInput').focus();
|
||||
}
|
||||
|
||||
// Ctrl/Cmd + N for new template
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === 78) {
|
||||
e.preventDefault();
|
||||
window.location.href = '{% url "operating_theatre:template_create" %}';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
1186
templates/patients/dashboard.html
Normal file
1186
templates/patients/dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user