This commit is contained in:
Marwan Alwali 2025-08-31 10:47:23 +03:00
parent 0422966e14
commit b9b8c69129
52 changed files with 9225 additions and 302 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -366,7 +366,7 @@ class InventoryStockForm(forms.ModelForm):
self.fields['location'].queryset = InventoryLocation.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('location_name')
).order_by('name')
def clean_quantity_on_hand(self):
quantity_on_hand = self.cleaned_data.get('quantity_on_hand')
@ -463,12 +463,12 @@ class PurchaseOrderForm(forms.ModelForm):
self.fields['supplier'].queryset = Supplier.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('supplier_name')
).order_by('name')
self.fields['delivery_location'].queryset = InventoryLocation.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('location_name')
).order_by('name')
def clean_order_date(self):
order_date = self.cleaned_data.get('order_date')

View File

@ -35,7 +35,48 @@ class InventoryItem(models.Model):
('FURNITURE', 'Furniture'),
('OTHER', 'Other'),
]
ITEM_TYPE_CHOICES = [
('CONSUMABLE', 'Consumable'),
('REUSABLE', 'Reusable'),
('EQUIPMENT', 'Equipment'),
('MEDICATION', 'Medication'),
('IMPLANT', 'Implant'),
('DEVICE', 'Medical Device'),
('SUPPLY', 'Supply'),
('ASSET', 'Asset'),
]
UNIT_OF_MEASURE_CHOICES = [
('EACH', 'Each'),
('BOX', 'Box'),
('CASE', 'Case'),
('BOTTLE', 'Bottle'),
('VIAL', 'Vial'),
('TUBE', 'Tube'),
('PACK', 'Pack'),
('KIT', 'Kit'),
('ROLL', 'Roll'),
('SHEET', 'Sheet'),
('POUND', 'Pound'),
('KILOGRAM', 'Kilogram'),
('LITER', 'Liter'),
('MILLILITER', 'Milliliter'),
('METER', 'Meter'),
('FOOT', 'Foot'),
]
PACKAGE_TYPE_CHOICES = [
('INDIVIDUAL', 'Individual'),
('BULK', 'Bulk'),
('STERILE', 'Sterile Package'),
('NON_STERILE', 'Non-Sterile Package'),
]
DEA_SCHEDULE_CHOICES = [
('CI', 'Schedule I'),
('CII', 'Schedule II'),
('CIII', 'Schedule III'),
('CIV', 'Schedule IV'),
('CV', 'Schedule V'),
]
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -82,16 +123,7 @@ class InventoryItem(models.Model):
# Item Type
item_type = models.CharField(
max_length=20,
choices=[
('CONSUMABLE', 'Consumable'),
('REUSABLE', 'Reusable'),
('EQUIPMENT', 'Equipment'),
('MEDICATION', 'Medication'),
('IMPLANT', 'Implant'),
('DEVICE', 'Medical Device'),
('SUPPLY', 'Supply'),
('ASSET', 'Asset'),
],
choices=ITEM_TYPE_CHOICES,
help_text='Item type'
)
@ -138,24 +170,7 @@ class InventoryItem(models.Model):
# Unit Information
unit_of_measure = models.CharField(
max_length=20,
choices=[
('EACH', 'Each'),
('BOX', 'Box'),
('CASE', 'Case'),
('BOTTLE', 'Bottle'),
('VIAL', 'Vial'),
('TUBE', 'Tube'),
('PACK', 'Pack'),
('KIT', 'Kit'),
('ROLL', 'Roll'),
('SHEET', 'Sheet'),
('POUND', 'Pound'),
('KILOGRAM', 'Kilogram'),
('LITER', 'Liter'),
('MILLILITER', 'Milliliter'),
('METER', 'Meter'),
('FOOT', 'Foot'),
],
choices=UNIT_OF_MEASURE_CHOICES,
default='EACH',
help_text='Unit of measure'
)
@ -167,12 +182,7 @@ class InventoryItem(models.Model):
)
package_type = models.CharField(
max_length=20,
choices=[
('INDIVIDUAL', 'Individual'),
('BULK', 'Bulk'),
('STERILE', 'Sterile Package'),
('NON_STERILE', 'Non-Sterile Package'),
],
choices=PACKAGE_TYPE_CHOICES,
default='INDIVIDUAL',
help_text='Package type'
)
@ -246,13 +256,7 @@ class InventoryItem(models.Model):
)
dea_schedule = models.CharField(
max_length=5,
choices=[
('CI', 'Schedule I'),
('CII', 'Schedule II'),
('CIII', 'Schedule III'),
('CIV', 'Schedule IV'),
('CV', 'Schedule V'),
],
choices=DEA_SCHEDULE_CHOICES,
blank=True,
null=True,
help_text='DEA schedule (for controlled substances)'
@ -380,7 +384,15 @@ class InventoryStock(models.Model):
"""
Inventory stock model for tracking stock levels by location and lot.
"""
QUALITY_STATUS_CHOICES = [
('GOOD', 'Good'),
('QUARANTINE', 'Quarantine'),
('EXPIRED', 'Expired'),
('DAMAGED', 'Damaged'),
('RECALLED', 'Recalled'),
('REJECTED', 'Rejected'),
]
# Inventory Item relationship
inventory_item = models.ForeignKey(
InventoryItem,
@ -462,14 +474,7 @@ class InventoryStock(models.Model):
# Quality Information
quality_status = models.CharField(
max_length=20,
choices=[
('GOOD', 'Good'),
('QUARANTINE', 'Quarantine'),
('EXPIRED', 'Expired'),
('DAMAGED', 'Damaged'),
('RECALLED', 'Recalled'),
('REJECTED', 'Rejected'),
],
choices=QUALITY_STATUS_CHOICES,
default='GOOD',
help_text='Quality status'
)
@ -574,7 +579,13 @@ class InventoryLocation(models.Model):
('SHIPPING', 'Shipping'),
('OTHER', 'Other'),
]
ACCESS_CONTROL_CHOICES = [
('OPEN', 'Open Access'),
('BADGE', 'Badge Access'),
('KEY', 'Key Access'),
('BIOMETRIC', 'Biometric Access'),
('DUAL_CONTROL', 'Dual Control'),
]
# Tenant relationship
tenant = models.ForeignKey(
@ -715,13 +726,7 @@ class InventoryLocation(models.Model):
)
access_control = models.CharField(
max_length=20,
choices=[
('OPEN', 'Open Access'),
('BADGE', 'Badge Access'),
('KEY', 'Key Access'),
('BIOMETRIC', 'Biometric Access'),
('DUAL_CONTROL', 'Dual Control'),
],
choices=ACCESS_CONTROL_CHOICES,
default='OPEN',
help_text='Access control method'
)
@ -814,7 +819,42 @@ class PurchaseOrder(models.Model):
"""
Purchase order model for procurement workflows.
"""
ORDER_TYPE_CHOICES = [
('STANDARD', 'Standard Order'),
('RUSH', 'Rush Order'),
('EMERGENCY', 'Emergency Order'),
('BLANKET', 'Blanket Order'),
('CONTRACT', 'Contract Order'),
('CONSIGNMENT', 'Consignment'),
]
PRIORITY_CHOICES = [
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
]
STATUS_CHOICES = [
('DRAFT', 'Draft'),
('PENDING_APPROVAL', 'Pending Approval'),
('APPROVED', 'Approved'),
('SENT', 'Sent to Supplier'),
('ACKNOWLEDGED', 'Acknowledged'),
('PARTIAL_RECEIVED', 'Partially Received'),
('RECEIVED', 'Received'),
('INVOICED', 'Invoiced'),
('PAID', 'Paid'),
('CANCELLED', 'Cancelled'),
('CLOSED', 'Closed'),
]
PAYMENT_TERMS_CHOICES = [
('NET_30', 'Net 30 Days'),
('NET_60', 'Net 60 Days'),
('NET_90', 'Net 90 Days'),
('COD', 'Cash on Delivery'),
('PREPAID', 'Prepaid'),
('CREDIT_CARD', 'Credit Card'),
]
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -868,14 +908,7 @@ class PurchaseOrder(models.Model):
# Order Type
order_type = models.CharField(
max_length=20,
choices=[
('STANDARD', 'Standard Order'),
('RUSH', 'Rush Order'),
('EMERGENCY', 'Emergency Order'),
('BLANKET', 'Blanket Order'),
('CONTRACT', 'Contract Order'),
('CONSIGNMENT', 'Consignment'),
],
choices=ORDER_TYPE_CHOICES,
default='STANDARD',
help_text='Order type'
)
@ -883,12 +916,7 @@ class PurchaseOrder(models.Model):
# Priority
priority = models.CharField(
max_length=10,
choices=[
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
],
choices=PRIORITY_CHOICES,
default='NORMAL',
help_text='Order priority'
)
@ -922,19 +950,7 @@ class PurchaseOrder(models.Model):
# Order Status
status = models.CharField(
max_length=20,
choices=[
('DRAFT', 'Draft'),
('PENDING_APPROVAL', 'Pending Approval'),
('APPROVED', 'Approved'),
('SENT', 'Sent to Supplier'),
('ACKNOWLEDGED', 'Acknowledged'),
('PARTIAL_RECEIVED', 'Partially Received'),
('RECEIVED', 'Received'),
('INVOICED', 'Invoiced'),
('PAID', 'Paid'),
('CANCELLED', 'Cancelled'),
('CLOSED', 'Closed'),
],
choices=STATUS_CHOICES,
default='DRAFT',
help_text='Order status'
)
@ -957,14 +973,7 @@ class PurchaseOrder(models.Model):
# Payment Terms
payment_terms = models.CharField(
max_length=20,
choices=[
('NET_30', 'Net 30 Days'),
('NET_60', 'Net 60 Days'),
('NET_90', 'Net 90 Days'),
('COD', 'Cash on Delivery'),
('PREPAID', 'Prepaid'),
('CREDIT_CARD', 'Credit Card'),
],
choices=PAYMENT_TERMS_CHOICES,
default='NET_30',
help_text='Payment terms'
)
@ -1071,7 +1080,14 @@ class PurchaseOrderItem(models.Model):
"""
Purchase order item model for individual line items.
"""
STATUS_CHOICES = [
('PENDING', 'Pending'),
('ORDERED', 'Ordered'),
('PARTIAL_RECEIVED', 'Partially Received'),
('RECEIVED', 'Received'),
('CANCELLED', 'Cancelled'),
]
# Purchase Order relationship
purchase_order = models.ForeignKey(
PurchaseOrder,
@ -1134,13 +1150,7 @@ class PurchaseOrderItem(models.Model):
# Item Status
status = models.CharField(
max_length=20,
choices=[
('PENDING', 'Pending'),
('ORDERED', 'Ordered'),
('PARTIAL_RECEIVED', 'Partially Received'),
('RECEIVED', 'Received'),
('CANCELLED', 'Cancelled'),
],
choices=STATUS_CHOICES,
default='PENDING',
help_text='Item status'
)

View File

@ -685,7 +685,7 @@ class InventoryStockCreateView(LoginRequiredMixin, CreateView):
"""
model = InventoryStock
form_class = InventoryStockForm
template_name = 'inventory/stock_form.html'
template_name = 'inventory/stock/stock_form.html'
success_url = reverse_lazy('inventory:stock_list')
def get_form_kwargs(self):
@ -745,7 +745,7 @@ class PurchaseOrderListView(LoginRequiredMixin, ListView):
List all purchase orders with filtering.
"""
model = PurchaseOrder
template_name = 'inventory/purchase_order_list.html'
template_name = 'inventory/orders/purchase_order_list.html'
context_object_name = 'orders'
paginate_by = 20
@ -780,7 +780,7 @@ class PurchaseOrderListView(LoginRequiredMixin, ListView):
context['suppliers'] = Supplier.objects.filter(
tenant=self.request.user.tenant,
is_active=True
).order_by('supplier_name')
).order_by('name')
return context
@ -789,7 +789,7 @@ class PurchaseOrderDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a purchase order.
"""
model = PurchaseOrder
template_name = 'inventory/purchase_order_detail.html'
template_name = 'inventory/orders/purchase_order_detail.html'
context_object_name = 'order'
def get_queryset(self):
@ -804,7 +804,7 @@ class PurchaseOrderDetailView(LoginRequiredMixin, DetailView):
# Order items
context['order_items'] = PurchaseOrderItem.objects.filter(
purchase_order=order
).select_related('item').order_by('item__item_name')
).select_related('inventory_item').order_by('inventory_item__item_name')
return context
@ -815,7 +815,7 @@ class PurchaseOrderCreateView(LoginRequiredMixin, CreateView):
"""
model = PurchaseOrder
form_class = PurchaseOrderForm
template_name = 'inventory/purchase_order_form.html'
template_name = 'inventory/orders/purchase_order_form.html'
success_url = reverse_lazy('inventory:purchase_order_list')
def get_form_kwargs(self):
@ -848,7 +848,7 @@ class PurchaseOrderUpdateView(LoginRequiredMixin, UpdateView):
"""
model = PurchaseOrder
form_class = PurchaseOrderForm
template_name = 'inventory/purchase_order_form.html'
template_name = 'inventory/orders/purchase_order_form.html'
def get_queryset(self):
return PurchaseOrder.objects.filter(tenant=self.request.user.tenant)

File diff suppressed because it is too large Load Diff

BIN
operating_theatre/.DS_Store vendored Normal file

Binary file not shown.

BIN
operating_theatre/management/.DS_Store vendored Normal file

Binary file not shown.

View File

View File

@ -0,0 +1,585 @@
"""
Django management command to generate Saudi-influenced Operating Theatre data
Place this file in: operating_theatre/management/commands/generate_saudi_or_data.py
"""
import random
import uuid
from datetime import datetime, timedelta, date, time
from decimal import Decimal
from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from faker import Faker
# Import your models
from operating_theatre.models import (
OperatingRoom,
ORBlock,
SurgicalCase,
SurgicalNote,
EquipmentUsage,
SurgicalNoteTemplate
)
from core.models import Tenant
from patients.models import PatientProfile
from django.contrib.auth import get_user_model
User = get_user_model()
fake = Faker('en_US')
class Command(BaseCommand):
help = 'Generate Saudi-influenced Operating Theatre test data'
# Saudi cultural data
SAUDI_MALE_FIRST_NAMES = [
"Mohammed", "Abdullah", "Abdulrahman", "Khalid", "Fahad",
"Sultan", "Salman", "Saud", "Faisal", "Turki", "Ahmed",
"Omar", "Youssef", "Ibrahim", "Hamad", "Nasser", "Bandar",
"Mansour", "Majed", "Waleed", "Talal", "Rakan", "Yazeed"
]
SAUDI_FEMALE_FIRST_NAMES = [
"Nora", "Fatima", "Aisha", "Mariam", "Sarah", "Reem",
"Lama", "Hind", "Mona", "Amal", "Dalal", "Jawaher",
"Latifa", "Hessa", "Nouf", "Asma", "Khadija", "Layla"
]
SAUDI_FAMILY_NAMES = [
"Al-Saud", "Al-Rasheed", "Al-Qahtani", "Al-Otaibi", "Al-Dossari",
"Al-Harbi", "Al-Zahrani", "Al-Ghamdi", "Al-Shehri", "Al-Asmari",
"Al-Mutairi", "Al-Enezi", "Al-Shamari", "Al-Maliki", "Al-Johani"
]
SAUDI_HOSPITALS = [
"King Faisal Specialist Hospital",
"King Fahad Medical City",
"King Abdulaziz University Hospital",
"Prince Sultan Military Medical City",
"King Saud Medical City"
]
SAUDI_CITIES = [
"Riyadh", "Jeddah", "Mecca", "Medina", "Dammam",
"Khobar", "Dhahran", "Taif", "Tabuk", "Buraidah"
]
SURGICAL_PROCEDURES = {
"GENERAL": [
"Laparoscopic Cholecystectomy",
"Appendectomy",
"Hernia Repair",
"Bowel Resection",
"Gastric Bypass"
],
"CARDIAC": [
"Coronary Artery Bypass Grafting",
"Valve Replacement",
"Pacemaker Insertion"
],
"ORTHOPEDIC": [
"Total Knee Replacement",
"Total Hip Replacement",
"Spinal Fusion",
"ACL Reconstruction"
],
"NEURO": [
"Craniotomy",
"Brain Tumor Resection",
"Spinal Decompression"
]
}
MEDICAL_EQUIPMENT = [
"Da Vinci Surgical Robot",
"C-Arm Fluoroscopy",
"Zeiss Surgical Microscope",
"Harmonic Scalpel",
"LigaSure Device"
]
def add_arguments(self, parser):
parser.add_argument(
'--tenant',
type=str,
help='Tenant ID to use for data generation'
)
parser.add_argument(
'--rooms',
type=int,
default=5,
help='Number of operating rooms to create'
)
parser.add_argument(
'--blocks',
type=int,
default=3,
help='Number of OR blocks per room'
)
parser.add_argument(
'--cases',
type=int,
default=2,
help='Number of cases per block'
)
parser.add_argument(
'--clear',
action='store_true',
help='Clear existing OR data before generating'
)
def handle(self, *args, **options):
self.stdout.write(self.style.SUCCESS('Starting Saudi OR data generation...'))
# Get or create tenant
tenant = self.get_or_create_tenant(options.get('tenant'))
# Clear existing data if requested
if options['clear']:
self.clear_existing_data(tenant)
# Generate data
with transaction.atomic():
# Create users (surgeons, nurses, etc.)
users = self.create_medical_staff(tenant, 20)
# Create patients
patients = self.create_patients(tenant, 30)
# Create surgical note templates
self.create_surgical_note_templates(tenant)
# Create operating rooms
rooms = self.create_operating_rooms(tenant, options['rooms'])
# Create OR blocks and surgical cases
for room in rooms:
self.create_or_blocks_with_cases(
room,
users,
patients,
options['blocks'],
options['cases']
)
self.stdout.write(self.style.SUCCESS('Data generation complete!'))
self.print_summary(tenant)
def get_or_create_tenant(self, tenant_id=1):
"""Get existing tenant or create a new one."""
if tenant_id:
try:
return Tenant.objects.get(id=tenant_id)
except Tenant.DoesNotExist:
self.stdout.write(self.style.WARNING(f'Tenant {tenant_id} not found, creating new one'))
# Create a new tenant
hospital_name = random.choice(self.SAUDI_HOSPITALS)
tenant, created = Tenant.objects.get_or_create(
name=hospital_name,
defaults={
'domain': hospital_name.lower().replace(' ', '-') + '.sa',
'is_active': True,
'settings': {
'country': 'Saudi Arabia',
'city': random.choice(self.SAUDI_CITIES),
'timezone': 'Asia/Riyadh',
'currency': 'SAR'
}
}
)
if created:
self.stdout.write(self.style.SUCCESS(f'Created tenant: {tenant.name}'))
return tenant
def clear_existing_data(self, tenant):
"""Clear existing OR data for the tenant."""
self.stdout.write('Clearing existing data...')
OperatingRoom.objects.filter(tenant=tenant).delete()
SurgicalNoteTemplate.objects.filter(tenant=tenant).delete()
self.stdout.write(self.style.SUCCESS('Existing data cleared'))
def create_medical_staff(self, tenant, count=20):
"""Create medical staff users."""
self.stdout.write('Creating medical staff...')
users = []
for i in range(count):
is_female = i % 3 == 0 # 1/3 female staff
if is_female:
first_name = random.choice(self.SAUDI_FEMALE_FIRST_NAMES)
title = random.choice(['Dr.', 'Nurse'])
else:
first_name = random.choice(self.SAUDI_MALE_FIRST_NAMES)
title = 'Dr.'
last_name = random.choice(self.SAUDI_FAMILY_NAMES)
username = f"{first_name.lower()}.{last_name.lower().replace('-', '')}_{i}"
email = f"{username}@{tenant.domain}"
user, created = User.objects.get_or_create(
username=username,
defaults={
'email': email,
'first_name': first_name,
'last_name': last_name,
'is_active': True
}
)
if created:
user.set_password('password123')
user.save()
users.append(user)
self.stdout.write(self.style.SUCCESS(f'Created {len(users)} medical staff'))
return users
def create_patients(self, tenant, count=30):
"""Create patient profiles."""
self.stdout.write('Creating patients...')
patients = []
for i in range(count):
is_female = random.random() > 0.6
if is_female:
first_name = random.choice(self.SAUDI_FEMALE_FIRST_NAMES)
gender = 'F'
else:
first_name = random.choice(self.SAUDI_MALE_FIRST_NAMES)
gender = 'M'
last_name = random.choice(self.SAUDI_FAMILY_NAMES)
# Create user for patient
username = f"patient_{first_name.lower()}_{i}"
user, _ = User.objects.get_or_create(
username=username,
defaults={
'email': f"{username}@example.com",
'first_name': first_name,
'last_name': last_name
}
)
# Create patient profile
patient, created = PatientProfile.objects.get_or_create(
user=user,
defaults={
'tenant': tenant,
'patient_id': f"P{datetime.now().year}{i:06d}",
'date_of_birth': fake.date_of_birth(minimum_age=18, maximum_age=80),
'gender': gender,
'blood_group': random.choice(['A+', 'A-', 'B+', 'B-', 'O+', 'O-', 'AB+', 'AB-']),
'phone': f"+966{random.randint(500000000, 599999999)}",
'email': user.email,
'address': f"{random.randint(1, 999)} {random.choice(['King Fahd Road', 'Olaya Street', 'Tahlia Street'])}, {random.choice(self.SAUDI_CITIES)}",
'emergency_contact_name': random.choice(self.SAUDI_MALE_FIRST_NAMES) + ' ' + last_name,
'emergency_contact_phone': f"+966{random.randint(500000000, 599999999)}",
'national_id': f"{random.randint(1000000000, 2999999999)}",
'insurance_provider': random.choice(['Bupa', 'Tawuniya', 'MedGulf', 'Allianz', 'AXA']),
'insurance_number': f"INS{random.randint(100000, 999999)}"
}
)
patients.append(patient)
self.stdout.write(self.style.SUCCESS(f'Created {len(patients)} patients'))
return patients
def create_surgical_note_templates(self, tenant):
"""Create surgical note templates."""
self.stdout.write('Creating surgical note templates...')
specialties = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC']
for specialty in specialties:
for template_type in ['Standard', 'Complex', 'Emergency']:
SurgicalNoteTemplate.objects.get_or_create(
tenant=tenant,
name=f"{specialty} - {template_type} Template",
defaults={
'description': f"Standard {template_type.lower()} template for {specialty.lower()} procedures",
'specialty': specialty,
'procedure_type': random.choice(self.SURGICAL_PROCEDURES.get(specialty, ['General'])),
'preoperative_diagnosis_template': "Preoperative Diagnosis: [Enter diagnosis]",
'planned_procedure_template': "Planned Procedure: [Enter procedure]",
'indication_template': "Indication: Patient presents with [symptoms] requiring intervention",
'procedure_performed_template': "Procedure: [Describe procedure performed]",
'surgical_approach_template': "Approach: [Describe approach]",
'findings_template': "Findings: [Describe findings]",
'technique_template': "Technique: Standard surgical technique employed",
'postoperative_diagnosis_template': "Postoperative Diagnosis: [Enter diagnosis]",
'is_active': True,
'is_default': template_type == 'Standard'
}
)
self.stdout.write(self.style.SUCCESS('Created surgical note templates'))
def create_operating_rooms(self, tenant, count=5):
"""Create operating rooms."""
self.stdout.write('Creating operating rooms...')
rooms = []
room_types = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC']
for i in range(1, count + 1):
room_type = room_types[(i-1) % len(room_types)]
room = OperatingRoom.objects.create(
tenant=tenant,
room_number=f"OR-{i:03d}",
room_name=f"{room_type.title()} Operating Room {i}",
room_type=room_type,
status='AVAILABLE',
floor_number=random.randint(1, 4),
room_size=random.uniform(40, 80),
ceiling_height=random.uniform(3.0, 4.5),
temperature_min=18.0,
temperature_max=24.0,
humidity_min=30.0,
humidity_max=60.0,
air_changes_per_hour=random.randint(20, 25),
positive_pressure=True,
equipment_list=self._get_equipment_list(room_type),
special_features=self._get_special_features(room_type),
has_c_arm=room_type in ['ORTHOPEDIC', 'NEURO'],
has_ct=room_type in ['NEURO', 'CARDIAC'] and random.random() > 0.5,
has_mri=room_type == 'NEURO' and random.random() > 0.7,
has_ultrasound=True,
has_neuromonitoring=room_type in ['NEURO', 'ORTHOPEDIC'],
supports_robotic=room_type in ['GENERAL', 'CARDIAC'] and random.random() > 0.4,
supports_laparoscopic=room_type in ['GENERAL', 'OBSTETRIC'],
supports_microscopy=room_type in ['NEURO', 'OPHTHALMOLOGY'],
max_case_duration=random.randint(240, 720),
turnover_time=random.randint(20, 45),
cleaning_time=random.randint(30, 60),
required_nurses=random.randint(2, 4),
required_techs=random.randint(1, 3),
is_active=True,
accepts_emergency=True,
building=random.choice(['Main', 'East Wing', 'West Wing']),
wing=random.choice(['North', 'South', 'Central'])
)
rooms.append(room)
self.stdout.write(self.style.SUCCESS(f'Created {len(rooms)} operating rooms'))
return rooms
def _get_equipment_list(self, room_type):
"""Get equipment list for room type."""
basic = [
"Anesthesia Machine",
"Patient Monitor",
"Surgical Lights",
"Operating Table",
"Electrocautery Unit"
]
specialized = {
'CARDIAC': ["Heart-Lung Machine", "IABP"],
'NEURO': ["Surgical Microscope", "Neuronavigation"],
'ORTHOPEDIC': ["C-Arm", "Power Tools"],
'GENERAL': ["Laparoscopic Tower", "Harmonic Scalpel"]
}
equipment = basic.copy()
if room_type in specialized:
equipment.extend(specialized[room_type])
return equipment
def _get_special_features(self, room_type):
"""Get special features for room type."""
features = ["Laminar Flow", "HEPA Filtration"]
special = {
'CARDIAC': ["Hybrid OR Capability"],
'NEURO': ["Intraoperative MRI Compatible"],
'ORTHOPEDIC': ["Class 100 Clean Room"],
'OBSTETRIC': ["Neonatal Resuscitation Area"]
}
if room_type in special:
features.extend(special[room_type])
return features
def create_or_blocks_with_cases(self, room, users, patients, num_blocks, num_cases):
"""Create OR blocks with surgical cases."""
self.stdout.write(f'Creating blocks for {room.room_number}...')
surgeons = [u for u in users if 'Dr.' in u.first_name or random.random() > 0.5]
nurses = [u for u in users if u not in surgeons]
for block_num in range(num_blocks):
# Create OR block
block_date = timezone.now().date() + timedelta(days=random.randint(1, 30))
start_hour = random.choice([7, 8, 9, 13])
duration = random.randint(2, 6)
block = ORBlock.objects.create(
operating_room=room,
date=block_date,
start_time=time(start_hour, 0),
end_time=time((start_hour + duration) % 24, 0),
block_type='SCHEDULED',
primary_surgeon=random.choice(surgeons),
service=room.room_type,
status=random.choice(['SCHEDULED', 'ACTIVE', 'COMPLETED']),
allocated_minutes=duration * 60,
used_minutes=random.randint(duration * 45, duration * 60),
special_equipment=random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(0, 2)),
notes=f"Block for {room.room_type} procedures"
)
# Add assistant surgeons
if surgeons:
assistants = random.sample(surgeons, k=min(random.randint(0, 2), len(surgeons)))
block.assistant_surgeons.set(assistants)
# Create surgical cases for this block
for case_num in range(random.randint(1, num_cases)):
self.create_surgical_case(block, patients, surgeons, nurses)
def create_surgical_case(self, block, patients, surgeons, nurses):
"""Create a surgical case."""
procedure_type = block.service
procedures = self.SURGICAL_PROCEDURES.get(procedure_type, ['General Surgery'])
case_time = datetime.combine(block.date, block.start_time) + timedelta(minutes=random.randint(0, 120))
duration = random.randint(30, 240)
case = SurgicalCase.objects.create(
or_block=block,
patient=random.choice(patients),
primary_surgeon=block.primary_surgeon,
anesthesiologist=random.choice(surgeons) if surgeons else block.primary_surgeon,
circulating_nurse=random.choice(nurses) if nurses else None,
scrub_nurse=random.choice(nurses) if nurses else None,
primary_procedure=random.choice(procedures),
secondary_procedures=[],
procedure_codes=[f"CPT{random.randint(10000, 99999)}" for _ in range(random.randint(1, 3))],
case_type=random.choice(['ELECTIVE', 'URGENT', 'EMERGENCY']),
approach=random.choice(['OPEN', 'LAPAROSCOPIC', 'ROBOTIC']),
anesthesia_type=random.choice(['GENERAL', 'REGIONAL', 'SPINAL']),
scheduled_start=timezone.make_aware(case_time),
estimated_duration=duration,
status=random.choice(['SCHEDULED', 'COMPLETED', 'IN_PROGRESS']),
diagnosis=self._get_diagnosis(),
diagnosis_codes=[f"ICD10-{random.choice(['K', 'I', 'M'])}{random.randint(10, 99)}.{random.randint(0, 9)}"],
clinical_notes="Patient prepared for surgery as per protocol",
special_equipment=random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(0, 2)),
patient_position=random.choice(['SUPINE', 'PRONE', 'LATERAL']),
estimated_blood_loss=random.randint(10, 500) if random.random() > 0.5 else None
)
# Add assistant surgeons
if surgeons:
assistants = random.sample(surgeons, k=min(random.randint(0, 2), len(surgeons)))
case.assistant_surgeons.set(assistants)
# Create surgical note for completed cases
if case.status == 'COMPLETED':
self.create_surgical_note(case)
# Create equipment usage
self.create_equipment_usage(case)
return case
def _get_diagnosis(self):
"""Get a random diagnosis."""
diagnoses = [
"Acute Appendicitis",
"Cholelithiasis",
"Inguinal Hernia",
"Coronary Artery Disease",
"Spinal Stenosis",
"Osteoarthritis",
"Brain Tumor",
"Breast Cancer"
]
return random.choice(diagnoses)
def create_surgical_note(self, case):
"""Create surgical note for completed case."""
SurgicalNote.objects.create(
surgical_case=case,
surgeon=case.primary_surgeon,
preoperative_diagnosis=case.diagnosis,
planned_procedure=case.primary_procedure,
indication="Symptomatic disease requiring surgical intervention",
procedure_performed=case.primary_procedure,
surgical_approach=f"{case.approach} approach utilized",
findings="As per preoperative diagnosis",
technique="Standard surgical technique employed",
postoperative_diagnosis=case.diagnosis,
condition=random.choice(['STABLE', 'GOOD', 'FAIR']),
disposition=random.choice(['RECOVERY', 'ICU', 'WARD']),
complications="None",
estimated_blood_loss=case.estimated_blood_loss or random.randint(10, 200),
specimens="Sent to pathology" if random.random() > 0.5 else None,
closure="Layered closure with absorbable sutures",
postop_instructions="Standard postoperative care protocol",
follow_up="2 weeks in surgical clinic",
status='SIGNED',
signed_datetime=timezone.now()
)
def create_equipment_usage(self, case):
"""Create equipment usage records."""
equipment_types = [
('Surgical Drape Set', 'DISPOSABLE', 1, 'SET', 150.00),
('Harmonic Scalpel', 'SURGICAL_INSTRUMENT', 1, 'EACH', 2500.00),
('Suture Pack', 'DISPOSABLE', 3, 'PACK', 75.00),
('Surgical Gloves', 'DISPOSABLE', 10, 'PAIR', 5.00)
]
for name, eq_type, qty, unit, cost in random.sample(equipment_types, k=random.randint(2, 4)):
EquipmentUsage.objects.create(
surgical_case=case,
equipment_name=name,
equipment_type=eq_type,
manufacturer=random.choice(['Medtronic', 'Johnson & Johnson', 'Stryker']),
quantity_used=qty,
unit_of_measure=unit,
unit_cost=Decimal(str(cost)),
total_cost=Decimal(str(cost * qty)),
lot_number=f"LOT{random.randint(10000, 99999)}",
expiration_date=timezone.now().date() + timedelta(days=random.randint(180, 730)),
sterilization_date=timezone.now().date() - timedelta(days=random.randint(1, 7)),
recorded_by=case.primary_surgeon
)
def print_summary(self, tenant):
"""Print summary of generated data."""
self.stdout.write("\n" + "="*60)
self.stdout.write(self.style.SUCCESS("Data Generation Summary"))
self.stdout.write("="*60)
rooms = OperatingRoom.objects.filter(tenant=tenant)
blocks = ORBlock.objects.filter(operating_room__tenant=tenant)
cases = SurgicalCase.objects.filter(or_block__operating_room__tenant=tenant)
notes = SurgicalNote.objects.filter(surgical_case__or_block__operating_room__tenant=tenant)
equipment = EquipmentUsage.objects.filter(surgical_case__or_block__operating_room__tenant=tenant)
templates = SurgicalNoteTemplate.objects.filter(tenant=tenant)
self.stdout.write(f"Tenant: {tenant.name}")
self.stdout.write(f"Operating Rooms: {rooms.count()}")
self.stdout.write(f"OR Blocks: {blocks.count()}")
self.stdout.write(f"Surgical Cases: {cases.count()}")
self.stdout.write(f"Surgical Notes: {notes.count()}")
self.stdout.write(f"Equipment Usage Records: {equipment.count()}")
self.stdout.write(f"Surgical Templates: {templates.count()}")
self.stdout.write("="*60)

View File

@ -60,10 +60,10 @@ urlpatterns = [
# ============================================================================
# EQUIPMENT USAGE URLS (LIMITED CRUD - Operational Data)
# ============================================================================
path('equipment/', views.EquipmentUsageListView.as_view(), name='equipment_usage_list'),
path('equipment/create/', views.EquipmentUsageCreateView.as_view(), name='equipment_usage_create'),
path('equipment/<int:pk>/', views.EquipmentUsageDetailView.as_view(), name='equipment_usage_detail'),
path('equipment/<int:pk>/update/', views.EquipmentUsageUpdateView.as_view(), name='equipment_usage_update'),
path('equipment/', views.EquipmentUsageListView.as_view(), name='equipment_list'),
path('equipment/create/', views.EquipmentUsageCreateView.as_view(), name='equipment_create'),
path('equipment/<int:pk>/', views.EquipmentUsageDetailView.as_view(), name='equipment_detail'),
path('equipment/<int:pk>/update/', views.EquipmentUsageUpdateView.as_view(), name='equipment_update'),
# Note: No delete view for equipment usage - operational tracking data
# ============================================================================

View File

@ -354,7 +354,7 @@ class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView):
List all surgical note templates with filtering and search.
"""
model = SurgicalNoteTemplate
template_name = 'operating_theatre/surgical_note_template_list.html'
template_name = 'operating_theatre/templates/surgical_note_template_list.html'
context_object_name = 'surgical_note_templates'
paginate_by = 25
@ -380,7 +380,7 @@ class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView):
if active_only:
queryset = queryset.filter(is_active=True)
return queryset.order_by('template_name')
return queryset.order_by('name')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -396,7 +396,7 @@ class SurgicalNoteTemplateDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a surgical note template.
"""
model = SurgicalNoteTemplate
template_name = 'operating_theatre/surgical_note_template_detail.html'
template_name = 'operating_theatre/templates/surgical_note_template_detail.html'
context_object_name = 'surgical_note_template'
def get_queryset(self):
@ -421,7 +421,7 @@ class SurgicalNoteTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin
"""
model = SurgicalNoteTemplate
form_class = SurgicalNoteTemplateForm
template_name = 'operating_theatre/surgical_note_template_form.html'
template_name = 'operating_theatre/templates/surgical_note_template_form.html'
permission_required = 'operating_theatre.add_surgicalnotetemplate'
success_url = reverse_lazy('operating_theatre:surgical_note_template_list')
@ -452,7 +452,7 @@ class SurgicalNoteTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin
"""
model = SurgicalNoteTemplate
form_class = SurgicalNoteTemplateForm
template_name = 'operating_theatre/surgical_note_template_form.html'
template_name = 'operating_theatre/templates/surgical_note_template_form.html'
permission_required = 'operating_theatre.change_surgicalnotetemplate'
def get_queryset(self):
@ -485,7 +485,7 @@ class SurgicalNoteTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin
Delete a surgical note template (soft delete by deactivating).
"""
model = SurgicalNoteTemplate
template_name = 'operating_theatre/surgical_note_template_confirm_delete.html'
template_name = 'operating_theatre/templates/surgical_note_template_confirm_delete.html'
permission_required = 'operating_theatre.delete_surgicalnotetemplate'
success_url = reverse_lazy('operating_theatre:surgical_note_template_list')
@ -500,7 +500,7 @@ class SurgicalNoteTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin
self.object.save()
# Log the action
AuditLogger.log_action(
AuditLogger.log_event(
user=request.user,
action='SURGICAL_NOTE_TEMPLATE_DEACTIVATED',
model='SurgicalNoteTemplate',
@ -521,12 +521,12 @@ class ORBlockListView(LoginRequiredMixin, ListView):
List all OR blocks with filtering and search.
"""
model = ORBlock
template_name = 'operating_theatre/or_block_list.html'
template_name = 'operating_theatre/blocks/block_list.html'
context_object_name = 'or_blocks'
paginate_by = 25
def get_queryset(self):
queryset = ORBlock.objects.filter(tenant=self.request.user.tenant)
queryset = ORBlock.objects.filter(operating_room__tenant=self.request.user.tenant)
# Filter by date range
date_from = self.request.GET.get('date_from')
@ -566,7 +566,7 @@ class ORBlockDetailView(LoginRequiredMixin, DetailView):
Display detailed information about an OR block.
"""
model = ORBlock
template_name = 'operating_theatre/or_block_detail.html'
template_name = 'operating_theatre/blocks/block_detail.html'
context_object_name = 'or_block'
def get_queryset(self):
@ -609,7 +609,7 @@ class ORBlockCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
"""
model = ORBlock
form_class = ORBlockForm
template_name = 'operating_theatre/or_block_form.html'
template_name = 'operating_theatre/blocks/block_form.html'
permission_required = 'operating_theatre.add_orblock'
success_url = reverse_lazy('operating_theatre:or_block_list')
@ -855,12 +855,12 @@ class SurgicalNoteListView(LoginRequiredMixin, ListView):
List all surgical notes with filtering and search.
"""
model = SurgicalNote
template_name = 'operating_theatre/surgical_note_list.html'
template_name = 'operating_theatre/notes/operative_note_list.html'
context_object_name = 'surgical_notes'
paginate_by = 25
def get_queryset(self):
queryset = SurgicalNote.objects.filter(tenant=self.request.user.tenant)
queryset = SurgicalNote.objects.filter(surgeon__tenant=self.request.user.tenant)
# Search functionality
search = self.request.GET.get('search')
@ -895,8 +895,8 @@ class SurgicalNoteListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'note_types': SurgicalNote._meta.get_field('note_type').choices,
'statuses': SurgicalNote._meta.get_field('status').choices,
# 'note_types': SurgicalNote._meta.get_field('note_type').choices,
'statuses': SurgicalNote.STATUS_CHOICES,
})
return context
@ -906,7 +906,7 @@ class SurgicalNoteDetailView(LoginRequiredMixin, DetailView):
Display detailed information about a surgical note.
"""
model = SurgicalNote
template_name = 'operating_theatre/surgical_note_detail.html'
template_name = 'operating_theatre/notes/operative_note_detail.html'
context_object_name = 'surgical_note'
def get_queryset(self):
@ -919,7 +919,7 @@ class SurgicalNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
"""
model = SurgicalNote
form_class = SurgicalNoteForm
template_name = 'operating_theatre/surgical_note_form.html'
template_name = 'operating_theatre/notes/surgical_note_form.html'
permission_required = 'operating_theatre.add_surgicalnote'
success_url = reverse_lazy('operating_theatre:surgical_note_list')
@ -956,12 +956,12 @@ class EquipmentUsageListView(LoginRequiredMixin, ListView):
List all equipment usage records with filtering and search.
"""
model = EquipmentUsage
template_name = 'operating_theatre/equipment_usage_list.html'
template_name = 'operating_theatre/equipment/equipment_list.html'
context_object_name = 'equipment_usage_records'
paginate_by = 25
def get_queryset(self):
queryset = EquipmentUsage.objects.filter(tenant=self.request.user.tenant)
queryset = EquipmentUsage.objects.filter(surgical_case__admission__tenant=self.request.user.tenant)
# Filter by equipment type
equipment_type = self.request.GET.get('equipment_type')
@ -993,8 +993,8 @@ class EquipmentUsageListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'equipment_types': EquipmentUsage._meta.get_field('equipment_type').choices,
'statuses': EquipmentUsage._meta.get_field('status').choices,
'equipment_types': EquipmentUsage.EQUIPMENT_TYPE_CHOICES,
# 'statuses': EquipmentUsage._meta.get_field('status').choices,
'operating_rooms': OperatingRoom.objects.filter(
tenant=self.request.user.tenant,
is_active=True
@ -1031,7 +1031,7 @@ class EquipmentUsageCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
"""
model = EquipmentUsage
form_class = EquipmentUsageForm
template_name = 'operating_theatre/equipment_usage_form.html'
template_name = 'operating_theatre/equipment/equipment_form.html'
permission_required = 'operating_theatre.add_equipmentusage'
success_url = reverse_lazy('operating_theatre:equipment_usage_list')
@ -1114,18 +1114,18 @@ def operating_theatre_stats(request):
status='IN_USE'
).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(),
}

866
or_data.py Normal file
View File

@ -0,0 +1,866 @@
"""
Saudi-influenced Operating Theatre Data Generator
Generates realistic test data for hospital operating room management system
with Saudi Arabian cultural context and medical practices.
"""
import random
import uuid
from datetime import datetime, timedelta, date, time
from decimal import Decimal
from faker import Faker
from typing import List, Dict, Any, Optional
# Initialize Faker
fake = Faker('en_US')
class SaudiOperatingTheatreDataGenerator:
"""
Generates Saudi-influenced data for Operating Theatre models.
"""
# Saudi hospital names (in English)
SAUDI_HOSPITALS = [
"King Faisal Specialist Hospital",
"King Fahad Medical City",
"King Abdulaziz University Hospital",
"King Khaled Eye Specialist Hospital",
"Prince Sultan Military Medical City",
"King Abdullah Medical Complex",
"King Saud Medical City",
"National Guard Hospital",
"Prince Mohammed Bin Abdulaziz Hospital",
"King Salman Hospital",
"Security Forces Hospital",
"Aramco Medical Center",
"Dr. Sulaiman Al Habib Medical Group",
"Saudi German Hospital",
"International Medical Center"
]
# Saudi cities
SAUDI_CITIES = [
"Riyadh", "Jeddah", "Mecca", "Medina", "Dammam",
"Khobar", "Dhahran", "Taif", "Tabuk", "Buraidah",
"Khamis Mushait", "Hofuf", "Jubail", "Yanbu", "Abha",
"Najran", "Jizan", "Hail", "Al-Qassim", "Qatif"
]
# Saudi regions
SAUDI_REGIONS = [
"Riyadh Region", "Makkah Region", "Eastern Province",
"Asir Region", "Madinah Region", "Qassim Region",
"Tabuk Region", "Hail Region", "Northern Borders",
"Jazan Region", "Najran Region", "Al-Baha Region"
]
# Common Saudi male first names
SAUDI_MALE_FIRST_NAMES = [
"Mohammed", "Abdullah", "Abdulrahman", "Khalid", "Fahad",
"Sultan", "Salman", "Saud", "Faisal", "Turki", "Ahmed",
"Omar", "Youssef", "Ibrahim", "Hamad", "Nasser", "Bandar",
"Mansour", "Majed", "Waleed", "Talal", "Rakan", "Yazeed",
"Meshal", "Naif", "Abdulaziz", "Saad", "Ali", "Hassan"
]
# Common Saudi female first names
SAUDI_FEMALE_FIRST_NAMES = [
"Nora", "Fatima", "Aisha", "Mariam", "Sarah", "Reem",
"Lama", "Hind", "Mona", "Amal", "Dalal", "Jawaher",
"Latifa", "Hessa", "Nouf", "Asma", "Khadija", "Layla",
"Rana", "Dina", "Hala", "Salma", "Yasmin", "Zainab",
"Lubna", "Hanaa", "Samira", "Najla", "Afaf", "Ghada"
]
# Common Saudi family names
SAUDI_FAMILY_NAMES = [
"Al-Saud", "Al-Rasheed", "Al-Qahtani", "Al-Otaibi", "Al-Dossari",
"Al-Harbi", "Al-Zahrani", "Al-Ghamdi", "Al-Shehri", "Al-Asmari",
"Al-Mutairi", "Al-Enezi", "Al-Shamari", "Al-Maliki", "Al-Johani",
"Al-Subaie", "Al-Hajri", "Al-Khaldi", "Al-Turki", "Al-Obaid",
"Al-Hassan", "Al-Sheikh", "Al-Najjar", "Al-Omari", "Al-Bakri"
]
# Medical specialties common in Saudi Arabia
MEDICAL_SPECIALTIES = [
"General Surgery", "Cardiac Surgery", "Neurosurgery",
"Orthopedic Surgery", "Pediatric Surgery", "Vascular Surgery",
"Thoracic Surgery", "Plastic Surgery", "Bariatric Surgery",
"Transplant Surgery", "Ophthalmology", "ENT Surgery",
"Urology", "Obstetrics", "Maxillofacial Surgery"
]
# Common surgical procedures in Saudi context
SURGICAL_PROCEDURES = {
"GENERAL": [
"Laparoscopic Cholecystectomy",
"Appendectomy",
"Hernia Repair",
"Bowel Resection",
"Gastric Bypass",
"Sleeve Gastrectomy",
"Thyroidectomy",
"Hemorrhoidectomy"
],
"CARDIAC": [
"Coronary Artery Bypass Grafting",
"Valve Replacement",
"Atrial Septal Defect Repair",
"Pacemaker Insertion",
"Angioplasty",
"Heart Transplant"
],
"ORTHOPEDIC": [
"Total Knee Replacement",
"Total Hip Replacement",
"Spinal Fusion",
"ACL Reconstruction",
"Rotator Cuff Repair",
"Fracture Fixation",
"Arthroscopy"
],
"NEURO": [
"Craniotomy",
"Brain Tumor Resection",
"Spinal Decompression",
"VP Shunt Placement",
"Aneurysm Clipping",
"Deep Brain Stimulation"
],
"OBSTETRIC": [
"Cesarean Section",
"Hysterectomy",
"Myomectomy",
"Ovarian Cystectomy",
"Tubal Ligation",
"D&C Procedure"
]
}
# Equipment commonly used in Saudi hospitals
MEDICAL_EQUIPMENT = [
"Da Vinci Surgical Robot",
"C-Arm Fluoroscopy",
"Zeiss Surgical Microscope",
"Harmonic Scalpel",
"LigaSure Device",
"Medtronic Neuromonitoring System",
"Stryker Navigation System",
"Karl Storz Laparoscopic Tower",
"GE Ultrasound Machine",
"Phillips CT Scanner"
]
def __init__(self, tenant_id: Optional[str] = None):
"""Initialize the data generator."""
self.tenant_id = tenant_id or str(uuid.uuid4())
self.generated_surgeons = []
self.generated_patients = []
self.generated_rooms = []
def generate_saudi_name(self, gender: str = 'male') -> Dict[str, str]:
"""Generate a Saudi-style name."""
if gender.lower() == 'male':
first_name = random.choice(self.SAUDI_MALE_FIRST_NAMES)
else:
first_name = random.choice(self.SAUDI_FEMALE_FIRST_NAMES)
family_name = random.choice(self.SAUDI_FAMILY_NAMES)
# Sometimes add a middle name (father's name for males)
if gender.lower() == 'male' and random.random() > 0.5:
middle_name = random.choice(self.SAUDI_MALE_FIRST_NAMES)
full_name = f"Dr. {first_name} {middle_name} {family_name}"
else:
full_name = f"Dr. {first_name} {family_name}"
return {
'first_name': first_name,
'last_name': family_name,
'full_name': full_name
}
def generate_operating_room(self, room_number: int) -> Dict[str, Any]:
"""Generate operating room data with Saudi context."""
room_types = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC',
'PEDIATRIC', 'OPHTHALMOLOGY', 'ENT', 'UROLOGY']
room_type = random.choice(room_types)
hospital = random.choice(self.SAUDI_HOSPITALS)
# Advanced equipment more common in Saudi hospitals
has_advanced_equipment = random.random() > 0.3
room_data = {
'tenant': self.tenant_id,
'room_id': str(uuid.uuid4()),
'room_number': f"OR-{room_number:03d}",
'room_name': f"{hospital} - {room_type.title()} OR {room_number}",
'room_type': room_type,
'status': random.choice(['AVAILABLE', 'OCCUPIED', 'CLEANING', 'SETUP']),
'floor_number': random.randint(1, 5),
'room_size': round(random.uniform(40, 80), 2),
'ceiling_height': round(random.uniform(3.0, 4.5), 2),
'temperature_min': 18.0,
'temperature_max': 24.0,
'humidity_min': 30.0,
'humidity_max': 60.0,
'air_changes_per_hour': random.randint(20, 25),
'positive_pressure': True,
'equipment_list': self._generate_equipment_list(room_type, has_advanced_equipment),
'special_features': self._generate_special_features(room_type),
'has_c_arm': room_type in ['ORTHOPEDIC', 'NEURO', 'VASCULAR'] or random.random() > 0.5,
'has_ct': room_type in ['NEURO', 'CARDIAC'] and has_advanced_equipment,
'has_mri': room_type == 'NEURO' and has_advanced_equipment and random.random() > 0.7,
'has_ultrasound': True,
'has_neuromonitoring': room_type in ['NEURO', 'ORTHOPEDIC'],
'supports_robotic': has_advanced_equipment and room_type in ['GENERAL', 'UROLOGY', 'CARDIAC'],
'supports_laparoscopic': room_type in ['GENERAL', 'OBSTETRIC', 'UROLOGY'],
'supports_microscopy': room_type in ['NEURO', 'OPHTHALMOLOGY', 'ENT'],
'supports_laser': room_type in ['OPHTHALMOLOGY', 'ENT', 'UROLOGY'],
'max_case_duration': random.randint(240, 720),
'turnover_time': random.randint(20, 45),
'cleaning_time': random.randint(30, 60),
'required_nurses': random.randint(2, 4),
'required_techs': random.randint(1, 3),
'is_active': True,
'accepts_emergency': room_type != 'OPHTHALMOLOGY',
'building': f"Building {random.choice(['A', 'B', 'C', 'Main', 'East', 'West'])}",
'wing': random.choice(['North', 'South', 'East', 'West', 'Central']),
'created_at': fake.date_time_between(start_date='-2y', end_date='now'),
'updated_at': fake.date_time_between(start_date='-30d', end_date='now')
}
return room_data
def _generate_equipment_list(self, room_type: str, has_advanced: bool) -> List[str]:
"""Generate equipment list based on room type."""
basic_equipment = [
"Anesthesia Machine",
"Patient Monitor",
"Surgical Lights",
"Operating Table",
"Electrocautery Unit",
"Suction System",
"Instrument Tables"
]
specialized_equipment = {
'CARDIAC': ["Heart-Lung Machine", "IABP", "TEE Machine"],
'NEURO': ["Surgical Microscope", "Neuronavigation", "CUSA"],
'ORTHOPEDIC': ["C-Arm", "Power Tools", "Traction Table"],
'OPHTHALMOLOGY': ["Phaco Machine", "OCT", "YAG Laser"],
'ENT': ["ENT Microscope", "Shaver System", "Navigation"],
'UROLOGY': ["Cystoscopy Tower", "Laser System", "Lithotripter"]
}
equipment = basic_equipment.copy()
if room_type in specialized_equipment:
equipment.extend(specialized_equipment[room_type])
if has_advanced:
equipment.extend(random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(2, 4)))
return equipment
def _generate_special_features(self, room_type: str) -> List[str]:
"""Generate special features for OR."""
features = ["Laminar Flow", "HEPA Filtration", "Integrated Video System"]
special_features = {
'CARDIAC': ["Hybrid OR Capability", "Cardiac Catheterization"],
'NEURO': ["Intraoperative MRI Compatible", "Neuromonitoring"],
'ORTHOPEDIC': ["Laminar Flow Class 100", "Biplane Imaging"],
'OPHTHALMOLOGY': ["Laser Safety", "Microscope Integration"],
'OBSTETRIC': ["Neonatal Resuscitation Area", "Fetal Monitoring"]
}
if room_type in special_features:
features.extend(special_features[room_type])
return features
def generate_or_block(self, operating_room_id: str, block_date: date) -> Dict[str, Any]:
"""Generate OR block schedule."""
surgeon = self.generate_saudi_name('male')
services = ['GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC', 'OBSTETRIC',
'OPHTHALMOLOGY', 'ENT', 'UROLOGY']
# Saudi working hours typically 7 AM to 3 PM or 8 AM to 4 PM
start_hours = [7, 8, 9, 13, 14]
start_hour = random.choice(start_hours)
duration_hours = random.randint(2, 8)
block_data = {
'operating_room': operating_room_id,
'block_id': str(uuid.uuid4()),
'date': block_date,
'start_time': time(start_hour, 0),
'end_time': time((start_hour + duration_hours) % 24, 0),
'block_type': random.choice(['SCHEDULED', 'EMERGENCY', 'RESERVED']),
'primary_surgeon': surgeon['full_name'],
'service': random.choice(services),
'status': random.choice(['SCHEDULED', 'ACTIVE', 'COMPLETED']),
'allocated_minutes': duration_hours * 60,
'used_minutes': random.randint(duration_hours * 45, duration_hours * 60),
'special_equipment': random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(0, 3)),
'special_setup': self._generate_special_setup(),
'notes': self._generate_block_notes(),
'created_at': fake.date_time_between(start_date='-30d', end_date='now'),
'updated_at': fake.date_time_between(start_date='-7d', end_date='now')
}
return block_data
def _generate_special_setup(self) -> str:
"""Generate special setup requirements."""
setups = [
"Prone positioning required",
"Lateral positioning with bean bag",
"Beach chair position",
"Microscope setup required",
"Robot docking from patient left",
"Fluoroscopy setup",
"Neuromonitoring setup required",
"Cell saver required",
"Warming blanket needed"
]
return random.choice(setups) if random.random() > 0.5 else ""
def _generate_block_notes(self) -> str:
"""Generate block notes."""
notes = [
"Complex case - allow extra time",
"Teaching case for residents",
"VIP patient - special protocols",
"Latex allergy - latex-free environment",
"Multi-specialty case",
"Consultant from King Faisal Hospital attending",
"Research protocol case",
"International patient - translator needed"
]
return random.choice(notes) if random.random() > 0.3 else ""
def generate_surgical_case(self, or_block_id: str, patient_id: str) -> Dict[str, Any]:
"""Generate surgical case data."""
case_type = random.choice(['ELECTIVE', 'URGENT', 'EMERGENCY'])
service_type = random.choice(list(self.SURGICAL_PROCEDURES.keys()))
procedure = random.choice(self.SURGICAL_PROCEDURES[service_type])
# Generate Saudi medical team
primary_surgeon = self.generate_saudi_name('male')
anesthesiologist = self.generate_saudi_name(random.choice(['male', 'female']))
nurse = self.generate_saudi_name('female') # Nursing often female in Saudi
scheduled_start = fake.date_time_between(start_date='now', end_date='+30d')
duration = random.randint(30, 360)
case_data = {
'or_block': or_block_id,
'case_id': str(uuid.uuid4()),
'case_number': f"SURG-{datetime.now().strftime('%Y%m%d')}-{random.randint(1, 9999):04d}",
'patient': patient_id,
'primary_surgeon': primary_surgeon['full_name'],
'anesthesiologist': anesthesiologist['full_name'],
'circulating_nurse': nurse['full_name'],
'scrub_nurse': self.generate_saudi_name('female')['full_name'],
'primary_procedure': procedure,
'secondary_procedures': self._generate_secondary_procedures(service_type),
'procedure_codes': self._generate_procedure_codes(),
'case_type': case_type,
'approach': random.choice(['OPEN', 'LAPAROSCOPIC', 'ROBOTIC', 'ENDOSCOPIC']),
'anesthesia_type': random.choice(['GENERAL', 'REGIONAL', 'SPINAL', 'LOCAL']),
'scheduled_start': scheduled_start,
'estimated_duration': duration,
'actual_start': scheduled_start + timedelta(minutes=random.randint(-15, 30)) if random.random() > 0.5 else None,
'actual_end': scheduled_start + timedelta(minutes=duration + random.randint(-30, 60)) if random.random() > 0.5 else None,
'status': random.choice(['SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'DELAYED']),
'diagnosis': self._generate_diagnosis(),
'diagnosis_codes': self._generate_diagnosis_codes(),
'clinical_notes': self._generate_clinical_notes(),
'special_equipment': random.sample(self.MEDICAL_EQUIPMENT, k=random.randint(1, 3)),
'blood_products': self._generate_blood_products() if random.random() > 0.7 else [],
'implants': self._generate_implants(service_type) if random.random() > 0.6 else [],
'patient_position': random.choice(['SUPINE', 'PRONE', 'LATERAL', 'LITHOTOMY']),
'complications': [] if random.random() > 0.1 else ["Minor bleeding controlled"],
'estimated_blood_loss': random.randint(10, 500) if random.random() > 0.3 else None,
'created_at': fake.date_time_between(start_date='-7d', end_date='now'),
'updated_at': fake.date_time_between(start_date='-1d', end_date='now')
}
return case_data
def _generate_secondary_procedures(self, service_type: str) -> List[str]:
"""Generate secondary procedures."""
if random.random() > 0.6:
procedures = self.SURGICAL_PROCEDURES.get(service_type, [])
return random.sample(procedures, k=min(random.randint(1, 2), len(procedures)))
return []
def _generate_procedure_codes(self) -> List[str]:
"""Generate CPT procedure codes."""
# Sample CPT codes
codes = []
for _ in range(random.randint(1, 3)):
codes.append(f"{random.randint(10000, 99999)}")
return codes
def _generate_diagnosis(self) -> str:
"""Generate diagnosis."""
diagnoses = [
"Acute Appendicitis",
"Cholelithiasis",
"Inguinal Hernia",
"Coronary Artery Disease",
"Brain Tumor",
"Degenerative Disc Disease",
"Osteoarthritis",
"Breast Cancer",
"Colorectal Cancer",
"Thyroid Nodule",
"Uterine Fibroids",
"Kidney Stones",
"Prostate Hyperplasia"
]
return random.choice(diagnoses)
def _generate_diagnosis_codes(self) -> List[str]:
"""Generate ICD-10 diagnosis codes."""
# Sample ICD-10 codes
codes = []
for _ in range(random.randint(1, 3)):
letter = random.choice(['K', 'I', 'M', 'C', 'N', 'E'])
codes.append(f"{letter}{random.randint(10, 99)}.{random.randint(0, 9)}")
return codes
def _generate_clinical_notes(self) -> str:
"""Generate clinical notes."""
notes = [
"Patient with history of diabetes mellitus type 2, controlled on metformin",
"No known drug allergies. Previous surgery without complications",
"Hypertensive patient on ACE inhibitors, blood pressure stable",
"Patient fasting since midnight as per protocol",
"Preoperative antibiotics administered",
"Patient counseled about procedure risks and benefits",
"Informed consent obtained in Arabic and English"
]
return random.choice(notes)
def _generate_blood_products(self) -> List[str]:
"""Generate blood product requirements."""
products = []
if random.random() > 0.5:
products.append(f"PRBC {random.randint(1, 4)} units")
if random.random() > 0.7:
products.append(f"FFP {random.randint(1, 2)} units")
if random.random() > 0.8:
products.append(f"Platelets {random.randint(1, 2)} units")
return products
def _generate_implants(self, service_type: str) -> List[str]:
"""Generate implant requirements based on service type."""
implants = {
'ORTHOPEDIC': [
"Total Knee Prosthesis - Zimmer",
"Total Hip Prosthesis - Stryker",
"Spinal Fusion Cage - Medtronic",
"ACL Graft",
"Fracture Plate and Screws"
],
'CARDIAC': [
"Mechanical Valve - St. Jude",
"Bioprosthetic Valve",
"Pacemaker - Medtronic",
"Coronary Stent",
"Vascular Graft"
],
'NEURO': [
"VP Shunt",
"Deep Brain Stimulator",
"Cranial Plate",
"Aneurysm Clip"
],
'GENERAL': [
"Mesh for Hernia Repair",
"Gastric Band",
"Biliary Stent"
]
}
if service_type in implants:
return random.sample(implants[service_type], k=1)
return []
def generate_surgical_note(self, surgical_case_id: str, surgeon_id: str) -> Dict[str, Any]:
"""Generate surgical note."""
note_data = {
'surgical_case': surgical_case_id,
'note_id': str(uuid.uuid4()),
'surgeon': surgeon_id,
'preoperative_diagnosis': self._generate_diagnosis(),
'planned_procedure': random.choice([proc for procs in self.SURGICAL_PROCEDURES.values() for proc in procs]),
'indication': self._generate_indication(),
'procedure_performed': self._generate_procedure_details(),
'surgical_approach': self._generate_surgical_approach(),
'findings': self._generate_findings(),
'technique': self._generate_technique(),
'postoperative_diagnosis': self._generate_diagnosis(),
'condition': random.choice(['STABLE', 'GOOD', 'FAIR']),
'disposition': random.choice(['RECOVERY', 'ICU', 'WARD']),
'complications': "None" if random.random() > 0.1 else "Minor bleeding, controlled",
'estimated_blood_loss': random.randint(10, 500),
'blood_transfusion': "None" if random.random() > 0.8 else "1 unit PRBC",
'specimens': self._generate_specimens() if random.random() > 0.5 else None,
'implants': self._generate_implant_details() if random.random() > 0.6 else None,
'drains': "JP drain placed" if random.random() > 0.5 else None,
'closure': self._generate_closure_details(),
'postop_instructions': self._generate_postop_instructions(),
'follow_up': "Follow up in 2 weeks in surgical clinic",
'status': random.choice(['DRAFT', 'COMPLETED', 'SIGNED']),
'signed_datetime': fake.date_time_between(start_date='-7d', end_date='now') if random.random() > 0.3 else None,
'created_at': fake.date_time_between(start_date='-7d', end_date='now'),
'updated_at': fake.date_time_between(start_date='-1d', end_date='now')
}
return note_data
def _generate_indication(self) -> str:
"""Generate surgical indication."""
indications = [
"Symptomatic for 6 months, failed conservative management",
"Progressive symptoms despite medical therapy",
"Acute presentation with signs of peritonitis",
"Elective procedure for quality of life improvement",
"Urgent intervention to prevent complications",
"Diagnostic and therapeutic intervention",
"Staged procedure as per treatment protocol"
]
return random.choice(indications)
def _generate_procedure_details(self) -> str:
"""Generate detailed procedure description."""
return "Procedure performed as planned using standard technique with no intraoperative complications"
def _generate_surgical_approach(self) -> str:
"""Generate surgical approach description."""
approaches = [
"Midline laparotomy incision",
"Laparoscopic approach with 4 ports",
"Lateral thoracotomy through 5th intercostal space",
"Posterior approach to spine",
"Deltopectoral approach",
"Bikini incision for cesarean section"
]
return random.choice(approaches)
def _generate_findings(self) -> str:
"""Generate intraoperative findings."""
findings = [
"Findings consistent with preoperative diagnosis",
"Adhesions from previous surgery noted and lysed",
"No evidence of metastatic disease",
"Inflammation noted in surrounding tissues",
"Anatomy normal, procedure proceeded as planned"
]
return random.choice(findings)
def _generate_technique(self) -> str:
"""Generate surgical technique description."""
return "Standard surgical technique employed with meticulous hemostasis throughout the procedure"
def _generate_specimens(self) -> str:
"""Generate specimen details."""
specimens = [
"Appendix sent to pathology",
"Gallbladder sent to pathology",
"Lymph nodes sent for frozen section",
"Tissue biopsy sent to pathology",
"Tumor specimen sent with margins marked"
]
return random.choice(specimens)
def _generate_implant_details(self) -> str:
"""Generate implant details."""
return "Implant placed per manufacturer protocol, position confirmed with imaging"
def _generate_closure_details(self) -> str:
"""Generate closure details."""
closures = [
"Layered closure with absorbable sutures",
"Skin closed with staples",
"Subcuticular closure with absorbable sutures",
"Wound closed in layers, dressing applied",
"Closure with 3-0 Vicryl and skin adhesive"
]
return random.choice(closures)
def _generate_postop_instructions(self) -> str:
"""Generate postoperative instructions."""
instructions = [
"NPO until bowel sounds return, advance diet as tolerated",
"Ambulate within 6 hours, incentive spirometry every hour",
"Pain control with PCA, transition to oral when tolerating diet",
"DVT prophylaxis with heparin, sequential compression devices",
"Monitor vitals every 4 hours, daily labs",
"Antibiotics for 24 hours postoperatively",
"Foley catheter to be removed POD 1"
]
return "; ".join(random.sample(instructions, k=random.randint(2, 4)))
def generate_equipment_usage(self, surgical_case_id: str) -> List[Dict[str, Any]]:
"""Generate equipment usage records."""
equipment_records = []
num_equipment = random.randint(3, 8)
for _ in range(num_equipment):
equipment_type = random.choice([
'SURGICAL_INSTRUMENT', 'MONITORING_DEVICE', 'ELECTROCAUTERY',
'LASER', 'MICROSCOPE', 'ULTRASOUND', 'DISPOSABLE'
])
equipment_name = self._get_equipment_name(equipment_type)
usage_data = {
'surgical_case': surgical_case_id,
'usage_id': str(uuid.uuid4()),
'equipment_name': equipment_name,
'equipment_type': equipment_type,
'manufacturer': self._get_manufacturer(),
'model': f"Model-{random.randint(100, 999)}",
'serial_number': f"SN{random.randint(100000, 999999)}",
'quantity_used': random.randint(1, 5),
'unit_of_measure': random.choice(['EACH', 'SET', 'PACK', 'BOX']),
'start_time': fake.date_time_between(start_date='-1d', end_date='now'),
'end_time': fake.date_time_between(start_date='now', end_date='+4h'),
'unit_cost': Decimal(str(round(random.uniform(10, 5000), 2))),
'lot_number': f"LOT{random.randint(10000, 99999)}",
'expiration_date': fake.date_between(start_date='+30d', end_date='+2y'),
'sterilization_date': fake.date_between(start_date='-7d', end_date='today'),
'notes': self._generate_equipment_notes(),
'created_at': fake.date_time_between(start_date='-1d', end_date='now'),
'updated_at': fake.date_time_between(start_date='-1h', end_date='now')
}
equipment_records.append(usage_data)
return equipment_records
def _get_equipment_name(self, equipment_type: str) -> str:
"""Get equipment name based on type."""
equipment_names = {
'SURGICAL_INSTRUMENT': [
"Harmonic Scalpel", "LigaSure Device", "Surgical Stapler",
"Laparoscopic Grasper Set", "Retractor Set", "Scalpel Set"
],
'MONITORING_DEVICE': [
"Cardiac Monitor", "Pulse Oximeter", "BIS Monitor",
"Arterial Line Monitor", "Central Line Kit"
],
'ELECTROCAUTERY': [
"Bovie Electrocautery", "Bipolar Forceps", "Monopolar Cautery"
],
'LASER': [
"CO2 Laser", "YAG Laser", "Holmium Laser", "Argon Laser"
],
'MICROSCOPE': [
"Zeiss OPMI", "Leica Surgical Microscope", "Pentero Microscope"
],
'ULTRASOUND': [
"GE Ultrasound", "Phillips EPIQ", "Sonosite Edge"
],
'DISPOSABLE': [
"Surgical Drape Set", "Gown Pack", "Suture Set",
"Surgical Gloves", "Sponge Pack"
]
}
return random.choice(equipment_names.get(equipment_type, ["Generic Equipment"]))
def _get_manufacturer(self) -> str:
"""Get equipment manufacturer."""
manufacturers = [
"Medtronic", "Johnson & Johnson", "Stryker", "Boston Scientific",
"Abbott", "GE Healthcare", "Siemens", "Phillips", "Karl Storz",
"Olympus", "Zimmer Biomet", "Smith & Nephew", "B. Braun"
]
return random.choice(manufacturers)
def _generate_equipment_notes(self) -> str:
"""Generate equipment usage notes."""
notes = [
"Equipment functioning properly",
"Calibrated before use",
"Backup equipment available",
"Special settings documented",
"Used per protocol",
""
]
return random.choice(notes)
def generate_surgical_note_template(self) -> Dict[str, Any]:
"""Generate surgical note template."""
specialties = ['ALL', 'GENERAL', 'CARDIAC', 'NEURO', 'ORTHOPEDIC',
'OBSTETRIC', 'OPHTHALMOLOGY', 'ENT', 'UROLOGY']
specialty = random.choice(specialties)
template_data = {
'tenant': self.tenant_id,
'template_id': str(uuid.uuid4()),
'name': f"{specialty} Surgery Template - {random.choice(['Standard', 'Complex', 'Emergency'])}",
'description': f"Standard template for {specialty.lower()} surgical procedures",
'procedure_type': random.choice(self.SURGICAL_PROCEDURES.get(specialty, ["General Procedure"])) if specialty != 'ALL' else None,
'specialty': specialty,
'preoperative_diagnosis_template': "Preoperative Diagnosis: [Diagnosis]",
'planned_procedure_template': "Planned Procedure: [Procedure Name]",
'indication_template': "Indication: Patient presents with [symptoms] requiring surgical intervention",
'procedure_performed_template': "Procedure Performed: [Actual procedure]",
'surgical_approach_template': "Approach: [Describe surgical approach]",
'findings_template': "Findings: [Describe intraoperative findings]",
'technique_template': "Technique: [Describe surgical technique in detail]",
'postoperative_diagnosis_template': "Postoperative Diagnosis: [Final diagnosis]",
'complications_template': "Complications: [None/Describe if any]",
'specimens_template': "Specimens: [List specimens sent to pathology]",
'implants_template': "Implants: [List any implants used]",
'closure_template': "Closure: [Describe closure technique]",
'postop_instructions_template': "Postoperative Instructions: [List instructions]",
'is_active': True,
'is_default': random.random() > 0.7,
'usage_count': random.randint(0, 100),
'created_at': fake.date_time_between(start_date='-1y', end_date='now'),
'updated_at': fake.date_time_between(start_date='-30d', end_date='now')
}
return template_data
def generate_complete_dataset(self,
num_rooms: int = 10,
num_blocks_per_room: int = 5,
num_cases_per_block: int = 3) -> Dict[str, List]:
"""Generate complete dataset for all models."""
dataset = {
'operating_rooms': [],
'or_blocks': [],
'surgical_cases': [],
'surgical_notes': [],
'equipment_usage': [],
'surgical_note_templates': []
}
# Generate operating rooms
for i in range(1, num_rooms + 1):
room = self.generate_operating_room(i)
dataset['operating_rooms'].append(room)
self.generated_rooms.append(room)
# Generate OR blocks for each room
for j in range(num_blocks_per_room):
block_date = fake.date_between(start_date='today', end_date='+30d')
block = self.generate_or_block(room['room_id'], block_date)
dataset['or_blocks'].append(block)
# Generate surgical cases for each block
for k in range(random.randint(1, num_cases_per_block)):
# Generate a patient ID (placeholder)
patient_id = str(uuid.uuid4())
case = self.generate_surgical_case(block['block_id'], patient_id)
dataset['surgical_cases'].append(case)
# Generate surgical note for completed cases
if case['status'] == 'COMPLETED':
note = self.generate_surgical_note(case['case_id'], block['primary_surgeon'])
dataset['surgical_notes'].append(note)
# Generate equipment usage
equipment_usage = self.generate_equipment_usage(case['case_id'])
dataset['equipment_usage'].extend(equipment_usage)
# Generate surgical note templates
for _ in range(15):
template = self.generate_surgical_note_template()
dataset['surgical_note_templates'].append(template)
return dataset
def print_statistics(self, dataset: Dict[str, List]) -> None:
"""Print statistics about generated data."""
print("\n" + "="*60)
print("Saudi Operating Theatre Data Generation Complete")
print("="*60)
print(f"Operating Rooms Generated: {len(dataset['operating_rooms'])}")
print(f"OR Blocks Generated: {len(dataset['or_blocks'])}")
print(f"Surgical Cases Generated: {len(dataset['surgical_cases'])}")
print(f"Surgical Notes Generated: {len(dataset['surgical_notes'])}")
print(f"Equipment Usage Records: {len(dataset['equipment_usage'])}")
print(f"Surgical Note Templates: {len(dataset['surgical_note_templates'])}")
print("="*60)
# Room type distribution
room_types = {}
for room in dataset['operating_rooms']:
room_type = room['room_type']
room_types[room_type] = room_types.get(room_type, 0) + 1
print("\nOperating Room Types:")
for room_type, count in sorted(room_types.items()):
print(f" {room_type}: {count}")
# Case type distribution
case_types = {}
for case in dataset['surgical_cases']:
case_type = case['case_type']
case_types[case_type] = case_types.get(case_type, 0) + 1
print("\nSurgical Case Types:")
for case_type, count in sorted(case_types.items()):
print(f" {case_type}: {count}")
# Status distribution
case_statuses = {}
for case in dataset['surgical_cases']:
status = case['status']
case_statuses[status] = case_statuses.get(status, 0) + 1
print("\nCase Status Distribution:")
for status, count in sorted(case_statuses.items()):
print(f" {status}: {count}")
# Example usage
if __name__ == "__main__":
# Initialize generator
generator = SaudiOperatingTheatreDataGenerator()
# Generate complete dataset
dataset = generator.generate_complete_dataset(
num_rooms=8,
num_blocks_per_room=4,
num_cases_per_block=3
)
# Print statistics
generator.print_statistics(dataset)
# Example: Print first operating room
if dataset['operating_rooms']:
print("\n" + "="*60)
print("Sample Operating Room:")
print("="*60)
room = dataset['operating_rooms'][0]
for key, value in room.items():
if key not in ['equipment_list', 'special_features']:
print(f"{key}: {value}")
# Example: Print first surgical case
if dataset['surgical_cases']:
print("\n" + "="*60)
print("Sample Surgical Case:")
print("="*60)
case = dataset['surgical_cases'][0]
for key, value in case.items():
if key not in ['special_equipment', 'blood_products', 'implants']:
print(f"{key}: {value}")

BIN
templates/.DS_Store vendored

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,981 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Purchase Orders{% endblock %}
{% block css %}
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.page-header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 0.5rem;
padding: 2rem;
margin-bottom: 2rem;
}
.stats-cards {
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.5rem;
padding: 1.5rem;
text-align: center;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--card-color);
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1rem;
color: white;
font-size: 1.25rem;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #495057;
margin-bottom: 0.5rem;
}
.stat-label {
color: #6c757d;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.filters-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 2rem;
}
.filter-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
align-items: end;
}
.orders-table-section {
background: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
}
.section-header {
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
padding: 1rem 1.5rem;
font-weight: 600;
color: #495057;
display: flex;
justify-content: between;
align-items: center;
}
.status-badge {
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.status-draft { background: #f8f9fa; color: #6c757d; }
.status-pending { background: #fff3cd; color: #856404; }
.status-approved { background: #d1ecf1; color: #0c5460; }
.status-ordered { background: #d4edda; color: #155724; }
.status-received { background: #d4edda; color: #155724; }
.status-cancelled { 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-urgent { background: #f5c6cb; color: #721c24; }
.action-buttons {
display: flex;
gap: 0.25rem;
}
.btn-action {
padding: 0.375rem 0.5rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.btn-view { background: #e3f2fd; color: #1976d2; }
.btn-edit { background: #fff3e0; color: #f57c00; }
.btn-delete { background: #ffebee; color: #d32f2f; }
.btn-approve { background: #e8f5e8; color: #2e7d32; }
.btn-action:hover {
transform: scale(1.05);
opacity: 0.8;
}
.bulk-actions {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
display: none;
}
.bulk-actions.show {
display: block;
}
.quick-filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.quick-filter {
padding: 0.5rem 1rem;
border: 1px solid #dee2e6;
background: white;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
text-decoration: none;
color: #495057;
}
.quick-filter:hover, .quick-filter.active {
background: #007bff;
color: white;
border-color: #007bff;
text-decoration: none;
}
.order-summary {
display: flex;
align-items: center;
gap: 1rem;
}
.order-amount {
font-weight: bold;
color: #28a745;
}
.order-items {
font-size: 0.875rem;
color: #6c757d;
}
.supplier-info {
display: flex;
align-items: center;
gap: 0.5rem;
}
.supplier-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #007bff;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
}
.table-actions {
display: flex;
justify-content: between;
align-items: center;
padding: 1rem 1.5rem;
background: #f8f9fa;
border-top: 1px solid #dee2e6;
}
@media (max-width: 768px) {
.page-header-section {
padding: 1.5rem;
}
.stats-cards {
grid-template-columns: repeat(2, 1fr);
}
.filter-row {
grid-template-columns: 1fr;
}
.quick-filters {
justify-content: center;
}
.action-buttons {
flex-direction: column;
}
.table-actions {
flex-direction: column;
gap: 1rem;
}
}
@media print {
.filters-section, .bulk-actions, .action-buttons, .table-actions {
display: none !important;
}
.section-header {
background: none;
border-bottom: 2px solid #000;
color: #000;
}
}
</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 'inventory:dashboard' %}">Inventory</a></li>
<li class="breadcrumb-item active">Purchase Orders</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-shopping-cart me-2"></i>Purchase Orders
</h1>
</div>
<div class="ms-auto">
<button type="button" class="btn btn-outline-secondary me-2" onclick="exportOrders()">
<i class="fas fa-download me-1"></i>Export
</button>
<button type="button" class="btn btn-outline-info me-2" onclick="importOrders()">
<i class="fas fa-upload me-1"></i>Import
</button>
<a href="{% url 'inventory:purchase_order_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>New Purchase Order
</a>
</div>
</div>
<!-- Statistics Cards -->
<div class="stats-cards">
<div class="stat-card" style="--card-color: #007bff;">
<div class="stat-icon" style="background: #007bff;">
<i class="fas fa-file-alt"></i>
</div>
<div class="stat-number">{{ stats.total_orders|default:0 }}</div>
<div class="stat-label">Total Orders</div>
</div>
<div class="stat-card" style="--card-color: #ffc107;">
<div class="stat-icon" style="background: #ffc107;">
<i class="fas fa-clock"></i>
</div>
<div class="stat-number">{{ stats.pending_orders|default:0 }}</div>
<div class="stat-label">Pending Orders</div>
</div>
<div class="stat-card" style="--card-color: #28a745;">
<div class="stat-icon" style="background: #28a745;">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-number">{{ stats.approved_orders|default:0 }}</div>
<div class="stat-label">Approved Orders</div>
</div>
<div class="stat-card" style="--card-color: #17a2b8;">
<div class="stat-icon" style="background: #17a2b8;">
<i class="fas fa-truck"></i>
</div>
<div class="stat-number">{{ stats.received_orders|default:0 }}</div>
<div class="stat-label">Received Orders</div>
</div>
<div class="stat-card" style="--card-color: #6f42c1;">
<div class="stat-icon" style="background: #6f42c1;">
<i class="fas fa-dollar-sign"></i>
</div>
<div class="stat-number"><span class="symbol">&#xea;</span>{{ stats.total_value|default:0|floatformat:0 }}</div>
<div class="stat-label">Total Value</div>
</div>
</div>
<!-- Quick Filters -->
<div class="quick-filters">
<a href="?status=all" class="quick-filter {% if not request.GET.status or request.GET.status == 'all' %}active{% endif %}">
<i class="fas fa-list me-1"></i>All Orders
</a>
<a href="?status=draft" class="quick-filter {% if request.GET.status == 'draft' %}active{% endif %}">
<i class="fas fa-edit me-1"></i>Draft
</a>
<a href="?status=pending" class="quick-filter {% if request.GET.status == 'pending' %}active{% endif %}">
<i class="fas fa-clock me-1"></i>Pending
</a>
<a href="?status=approved" class="quick-filter {% if request.GET.status == 'approved' %}active{% endif %}">
<i class="fas fa-check me-1"></i>Approved
</a>
<a href="?status=ordered" class="quick-filter {% if request.GET.status == 'ordered' %}active{% endif %}">
<i class="fas fa-shopping-cart me-1"></i>Ordered
</a>
<a href="?status=received" class="quick-filter {% if request.GET.status == 'received' %}active{% endif %}">
<i class="fas fa-truck me-1"></i>Received
</a>
<a href="?priority=urgent" class="quick-filter {% if request.GET.priority == 'urgent' %}active{% endif %}">
<i class="fas fa-exclamation-triangle me-1"></i>Urgent
</a>
</div>
<!-- Filters Section -->
<div class="filters-section">
<h6 class="mb-3">
<i class="fas fa-filter me-2"></i>Advanced Filters
</h6>
<form method="get" id="filter-form">
<div class="filter-row">
<div>
<label class="form-label">Order Number</label>
<input type="text" class="form-control" name="order_number"
value="{{ request.GET.order_number }}" placeholder="PO-2024-001">
</div>
<div>
<label class="form-label">Supplier</label>
<select class="form-select" name="supplier">
<option value="">All Suppliers</option>
{% for supplier in suppliers %}
<option value="{{ supplier.id }}" {% if request.GET.supplier == supplier.id|stringformat:"s" %}selected{% endif %}>
{{ supplier.name }}
</option>
{% endfor %}
</select>
</div>
<div>
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="">All Statuses</option>
<option value="draft" {% if request.GET.status == 'draft' %}selected{% endif %}>Draft</option>
<option value="pending" {% if request.GET.status == 'pending' %}selected{% endif %}>Pending</option>
<option value="approved" {% if request.GET.status == 'approved' %}selected{% endif %}>Approved</option>
<option value="ordered" {% if request.GET.status == 'ordered' %}selected{% endif %}>Ordered</option>
<option value="received" {% if request.GET.status == 'received' %}selected{% endif %}>Received</option>
<option value="cancelled" {% if request.GET.status == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
<div>
<label class="form-label">Priority</label>
<select class="form-select" name="priority">
<option value="">All Priorities</option>
<option value="low" {% if request.GET.priority == 'low' %}selected{% endif %}>Low</option>
<option value="medium" {% if request.GET.priority == 'medium' %}selected{% endif %}>Medium</option>
<option value="high" {% if request.GET.priority == 'high' %}selected{% endif %}>High</option>
<option value="urgent" {% if request.GET.priority == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
<div>
<label class="form-label">Date From</label>
<input type="date" class="form-control" name="date_from" value="{{ request.GET.date_from }}">
</div>
<div>
<label class="form-label">Date To</label>
<input type="date" class="form-control" name="date_to" value="{{ request.GET.date_to }}">
</div>
<div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-search me-1"></i>Filter
</button>
<a href="{% url 'inventory:purchase_order_list' %}" class="btn btn-outline-secondary ms-2">
<i class="fas fa-times me-1"></i>Clear
</a>
</div>
</div>
</form>
</div>
<!-- Bulk Actions -->
<div class="bulk-actions" id="bulk-actions">
<div class="d-flex align-items-center justify-content-between">
<div>
<span id="selected-count">0</span> orders selected
</div>
<div>
<button type="button" class="btn btn-outline-primary btn-sm me-2" onclick="bulkApprove()">
<i class="fas fa-check me-1"></i>Approve Selected
</button>
<button type="button" class="btn btn-outline-warning btn-sm me-2" onclick="bulkExport()">
<i class="fas fa-download me-1"></i>Export Selected
</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="bulkCancel()">
<i class="fas fa-times me-1"></i>Cancel Selected
</button>
</div>
</div>
</div>
<!-- Orders Table -->
<div class="orders-table-section">
<div class="section-header">
<div>
<i class="fas fa-table me-2"></i>Purchase Orders List
</div>
<div class="d-flex align-items-center gap-2">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="auto-refresh">
<label class="form-check-label" for="auto-refresh">Auto Refresh</label>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshTable()">
<i class="fas fa-sync"></i>
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0" id="orders-table">
<thead class="table-light">
<tr>
<th width="40">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="select-all">
</div>
</th>
<th>Order Number</th>
<th>Supplier</th>
<th>Date Created</th>
<th>Expected Delivery</th>
<th>Items</th>
<th>Total Amount</th>
<th>Priority</th>
<th>Status</th>
<th>Created By</th>
<th width="120">Actions</th>
</tr>
</thead>
<tbody>
{% for order in orders %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input order-checkbox" type="checkbox" value="{{ order.id }}">
</div>
</td>
<td>
<div class="fw-bold text-primary">{{ order.order_number }}</div>
{% if order.reference_number %}
<small class="text-muted">Ref: {{ order.reference_number }}</small>
{% endif %}
</td>
<td>
<div class="supplier-info">
<div class="supplier-avatar">
{{ order.supplier.name.0|upper }}
</div>
<div>
<div class="fw-bold">{{ order.supplier.name }}</div>
<small class="text-muted">{{ order.supplier.contact_person|default:"" }}</small>
</div>
</div>
</td>
<td>
<div class="fw-bold">{{ order.created_at|date:"M d, Y" }}</div>
<small class="text-muted">{{ order.created_at|time:"g:i A" }}</small>
</td>
<td>
{% if order.expected_delivery_date %}
<div class="fw-bold">{{ order.expected_delivery_date|date:"M d, Y" }}</div>
<small class="text-muted">
{% if order.is_overdue %}
<span class="text-danger">Overdue</span>
{% else %}
{{ order.days_until_delivery }} days
{% endif %}
</small>
{% else %}
<span class="text-muted">Not set</span>
{% endif %}
</td>
<td>
<div class="order-summary">
<div>
<div class="fw-bold">{{ order.total_items }} items</div>
<small class="text-muted">{{ order.unique_items }} unique</small>
</div>
</div>
</td>
<td>
<div class="order-amount"><span class="symbol">&#xea;</span>{{ order.total_amount|floatformat:'2g' }}</div>
{% if order.tax_amount %}
<small class="text-muted">+<span class="symbol">&#xea;</span>{{ order.tax_amount|floatformat:'2g' }} VAT</small>
{% endif %}
</td>
<td>
<span class="priority-badge priority-{{ order.priority }}">
{{ order.get_priority_display }}
</span>
</td>
<td>
<span class="status-badge status-{{ order.status }}">
{{ order.get_status_display }}
</span>
</td>
<td>
<div class="fw-bold">{{ order.created_by.get_full_name }}</div>
<small class="text-muted">{{ order.created_by.department|default:"" }}</small>
</td>
<td>
<div class="action-buttons">
<a href="{% url 'inventory:purchase_order_detail' order.pk %}"
class="btn-action btn-view" title="View Details">
<i class="fas fa-eye"></i>
</a>
{% if order.can_edit %}
<a href="{% url 'inventory:purchase_order_edit' order.pk %}"
class="btn-action btn-edit" title="Edit Order">
<i class="fas fa-edit"></i>
</a>
{% endif %}
{% if order.can_approve %}
<button type="button" class="btn-action btn-approve"
onclick="approveOrder({{ order.pk }})" title="Approve Order">
<i class="fas fa-check"></i>
</button>
{% endif %}
{% if order.can_delete %}
<button type="button" class="btn-action btn-delete"
onclick="deleteOrder({{ order.pk }})" title="Delete Order">
<i class="fas fa-trash"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="11" class="text-center py-4">
<div class="text-muted">
<i class="fas fa-shopping-cart fa-3x mb-3"></i>
<h5>No Purchase Orders Found</h5>
<p>No purchase orders match your current filters.</p>
<a href="{% url 'inventory:purchase_order_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create First Order
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Table Actions -->
<div class="d-flex align-items-center gap-2 mb-2">
<!-- Pagination -->
{% if is_paginated %}
{% include 'partial/pagination.html'%}
{% endif %}
</div>
</div>
</div>
<!-- Approval Modal -->
<div class="modal fade" id="approvalModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-check-circle me-2"></i>Approve Purchase Order
</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>
Are you sure you want to approve this purchase order? This action will send the order to the supplier.
</div>
<div class="mb-3">
<label class="form-label">Approval Notes (Optional)</label>
<textarea class="form-control" id="approval-notes" rows="3"
placeholder="Add any notes about this approval..."></textarea>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="send-notification">
<label class="form-check-label" for="send-notification">
Send notification to supplier
</label>
</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-success" onclick="confirmApproval()">
<i class="fas fa-check me-1"></i>Approve Order
</button>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-exclamation-triangle me-2 text-danger"></i>Delete Purchase Order
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Are you sure you want to delete this purchase order? This action cannot be undone.
</div>
<div class="mb-3">
<label class="form-label">Reason for Deletion</label>
<textarea class="form-control" id="deletion-reason" rows="3"
placeholder="Please provide a reason for deleting this order..." required></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-danger" onclick="confirmDeletion()">
<i class="fas fa-trash me-1"></i>Delete Order
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js' %}"></script>
<script>
let currentOrderId = null;
$(document).ready(function() {
// Initialize DataTable
$('#orders-table').DataTable({
responsive: true,
pageLength: 25,
order: [[3, 'desc']], // Sort by date created
columnDefs: [
{ orderable: false, targets: [0, 10] } // Disable sorting for checkbox and actions
]
});
// Handle select all checkbox
$('#select-all').change(function() {
$('.order-checkbox').prop('checked', this.checked);
updateBulkActions();
});
// Handle individual checkboxes
$('.order-checkbox').change(function() {
updateBulkActions();
// Update select all checkbox
const totalCheckboxes = $('.order-checkbox').length;
const checkedCheckboxes = $('.order-checkbox:checked').length;
$('#select-all').prop('checked', totalCheckboxes === checkedCheckboxes);
});
// Auto-refresh functionality
let autoRefreshInterval;
$('#auto-refresh').change(function() {
if (this.checked) {
autoRefreshInterval = setInterval(refreshTable, 30000); // Refresh every 30 seconds
} else {
clearInterval(autoRefreshInterval);
}
});
});
function updateBulkActions() {
const selectedCount = $('.order-checkbox:checked').length;
$('#selected-count').text(selectedCount);
if (selectedCount > 0) {
$('#bulk-actions').addClass('show');
} else {
$('#bulk-actions').removeClass('show');
}
}
function refreshTable() {
location.reload();
}
function changePageSize(size) {
const url = new URL(window.location);
url.searchParams.set('page_size', size);
url.searchParams.delete('page'); // Reset to first page
window.location.href = url.toString();
}
function exportOrders() {
const selectedOrders = $('.order-checkbox:checked').map(function() {
return this.value;
}).get();
let url = '/inventory/orders/export/';
if (selectedOrders.length > 0) {
url += '?orders=' + selectedOrders.join(',');
}
window.open(url, '_blank');
}
function importOrders() {
// Open import modal or redirect to import page
window.location.href = '/inventory/orders/import/';
}
function approveOrder(orderId) {
currentOrderId = orderId;
new bootstrap.Modal(document.getElementById('approvalModal')).show();
}
function confirmApproval() {
const notes = document.getElementById('approval-notes').value;
const sendNotification = document.getElementById('send-notification').checked;
fetch(`/inventory/orders/${currentOrderId}/approve/`, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify({
notes: notes,
send_notification: sendNotification
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('Purchase order approved successfully', 'success');
setTimeout(() => location.reload(), 1500);
} else {
showAlert('Error approving purchase order', 'danger');
}
})
.catch(error => {
showAlert('Error approving purchase order', 'danger');
});
bootstrap.Modal.getInstance(document.getElementById('approvalModal')).hide();
}
function deleteOrder(orderId) {
currentOrderId = orderId;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
function confirmDeletion() {
const reason = document.getElementById('deletion-reason').value;
if (!reason.trim()) {
showAlert('Please provide a reason for deletion', 'warning');
return;
}
fetch(`/inventory/orders/${currentOrderId}/delete/`, {
method: 'DELETE',
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('Purchase order deleted successfully', 'success');
setTimeout(() => location.reload(), 1500);
} else {
showAlert('Error deleting purchase order', 'danger');
}
})
.catch(error => {
showAlert('Error deleting purchase order', 'danger');
});
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
}
function bulkApprove() {
const selectedOrders = $('.order-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedOrders.length === 0) {
showAlert('Please select orders to approve', 'warning');
return;
}
if (confirm(`Are you sure you want to approve ${selectedOrders.length} selected orders?`)) {
fetch('/inventory/orders/bulk-approve/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify({
order_ids: selectedOrders
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(`${data.approved_count} orders approved successfully`, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showAlert('Error approving orders', 'danger');
}
})
.catch(error => {
showAlert('Error approving orders', 'danger');
});
}
}
function bulkExport() {
const selectedOrders = $('.order-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedOrders.length === 0) {
showAlert('Please select orders to export', 'warning');
return;
}
const url = '/inventory/orders/export/?orders=' + selectedOrders.join(',');
window.open(url, '_blank');
}
function bulkCancel() {
const selectedOrders = $('.order-checkbox:checked').map(function() {
return this.value;
}).get();
if (selectedOrders.length === 0) {
showAlert('Please select orders to cancel', 'warning');
return;
}
if (confirm(`Are you sure you want to cancel ${selectedOrders.length} selected orders?`)) {
fetch('/inventory/orders/bulk-cancel/', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify({
order_ids: selectedOrders
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert(`${data.cancelled_count} orders cancelled successfully`, 'success');
setTimeout(() => location.reload(), 1500);
} else {
showAlert('Error cancelling orders', 'danger');
}
})
.catch(error => {
showAlert('Error cancelling orders', 'danger');
});
}
}
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 %}

View File

@ -24,14 +24,18 @@
<div class="row">
<div class="col-xl-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">
<i class="fas fa-boxes me-2"></i>
Stock Information
</h4>
</div>
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title"><i class="fas fa-boxes me-2"></i> {{ _("Stock Information")}}</h4>
<div class="panel-heading-btn">
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>#}
</div>
</div>
<div class="panel-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
@ -289,14 +293,18 @@
<div class="col-xl-4">
<!-- Stock Level Guidelines -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-info-circle me-2"></i>
Stock Level Guidelines
</h5>
</div>
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="index-2">
<div class="panel-heading">
<h4 class="panel-title"><i class="fas fa-info-circle me-2"></i> {{ _("Stock Level Guidelines")}}</h4>
<div class="panel-heading-btn">
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>#}
</div>
</div>
<div class="panel-body">
<div class="mb-3">
<h6><i class="fas fa-exclamation-triangle text-danger me-2"></i>Minimum Quantity</h6>
<p class="small text-muted">
@ -357,14 +365,18 @@
</div>
<!-- Quick Actions -->
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-bolt me-2"></i>
Quick Actions
</h5>
</div>
<div class="card-body">
<div class="panel panel-inverse" data-sortable-id="index-3">
<div class="panel-heading">
<h4 class="panel-title"><i class="fas fa-bolt me-2"></i> {{ _("Quick Actions")}}</h4>
<div class="panel-heading-btn">
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>#}
{# <a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>#}
</div>
</div>
<div class="panel-body">
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-primary" onclick="calculateRecommendedLevels()">
<i class="fas fa-calculator me-2"></i>Calculate Recommended Levels

View File

@ -139,19 +139,19 @@
<table id="stock-table" class="table table-striped table-bordered align-middle">
<thead>
<tr>
<th width="5%">
<th>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="select-all">
</div>
</th>
<th width="25%">Item</th>
<th width="15%">Location</th>
<th width="10%">Lot Number</th>
<th width="10%">Current Stock</th>
<th width="10%">Available</th>
<th width="10%">Expiry Date</th>
<th width="10%">Status</th>
<th width="5%">Actions</th>
<th>Item</th>
<th>Location</th>
<th>Lot Number</th>
<th>Current Stock</th>
<th>Available</th>
<th>Expiry Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@ -159,7 +159,7 @@
<tr data-stock-status="{{ stock.stock_status }}" data-location="{{ stock.location.id }}" data-category="{{ stock.inventory_item.category }}">
<td>
<div class="form-check">
<input class="form-check-input row-checkbox" type="checkbox" value="{{ stock.id }}">
<input class="form-check-input row-checkbox" type="checkbox" value="{{ stock.stock_id }}">
</div>
</td>
<td>
@ -172,8 +172,8 @@
</div>
{% endif %}
<div>
<div class="fw-bold">{{ stock.inventory_item.name }}</div>
<div class="small text-muted">{{ stock.inventory_item.code }}</div>
<div class="fw-bold">{{ stock.inventory_item.item_name }}</div>
<div class="small text-muted">{{ stock.inventory_item.item_code }}</div>
</div>
</div>
</td>
@ -189,13 +189,13 @@
<div class="small text-muted">{{ stock.inventory_item.unit_of_measure }}</div>
</td>
<td>
<div class="fw-bold">{{ stock.available_quantity }}</div>
<div class="fw-bold">{{ stock.quantity_available }}</div>
<div class="small text-muted">Available</div>
</td>
<td>
{% if stock.expiry_date %}
{% if stock.expiration_date %}
<div class="{% if stock.is_expired %}text-danger{% elif stock.is_expiring_soon %}text-warning{% endif %}">
{{ stock.expiry_date|date:"M d, Y" }}
{{ stock.expiration_date|date:"M d, Y" }}
</div>
{% if stock.is_expired %}
<div class="small text-danger">Expired</div>
@ -207,12 +207,23 @@
{% endif %}
</td>
<td>
<span class="badge bg-{% if stock.stock_status == 'IN_STOCK' %}success{% elif stock.stock_status == 'LOW_STOCK' %}warning{% elif stock.stock_status == 'OUT_OF_STOCK' %}danger{% elif stock.stock_status == 'EXPIRED' %}dark{% else %}secondary{% endif %}">
{{ stock.get_stock_status_display }}
</span>
{% if stock.quantity_available >= stock.inventory_item.reorder_point %}
<span class="badge bg-success">AVAILABLE</span><br>
<small class="text-muted"> reorder point {{ stock.inventory_item.reorder_point }}</small>
{% elif stock.quantity_available <= stock.inventory_item.reorder_point %}
<span class="badge bg-warning">LOW STOCK</span><br>
<small class="text-muted"> reorder point {{ stock.inventory_item.reorder_point }}</small>
{% elif stock.quantity_available == 0 %}
<span class="badge bg-danger">OUT OF STOCK</span><br>
<small class="text-muted"> reorder point {{ stock.inventory_item.reorder_point }}</small>
{% else %}
<span class="badge bg-secondary">{{ stock.inventory_item.reorder_point }}</span>
{% endif %}
{% if stock.is_reserved %}
<div class="small mt-1">
<span class="badge bg-info badge-sm">{{ stock.reserved_quantity }} Reserved</span>
<span class="badge bg-info badge-sm">{{ stock.quantity_reserved }} Reserved</span>
</div>
{% endif %}
</td>

View File

@ -461,9 +461,9 @@ function printSupplierInfo() {
window.print();
}
function exportSupplierData() {
window.location.href = '{% url "inventory:supplier_export" supplier.pk %}';
}
{#function exportSupplierData() {#}
{# window.location.href = '{% url "inventory:supplier_export" supplier.pk %}';#}
{# }#}
function scheduleCall() {
// Implement call scheduling functionality

View File

@ -3,7 +3,7 @@
{% block title %}Delete OR Block - {{ block.operating_room.name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.delete-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
@ -442,7 +442,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
const expectedText = 'DELETE {{ block.operating_room.name }} {{ block.date|date:"Y-m-d" }}';

View File

@ -3,7 +3,7 @@
{% block title %}OR Block - {{ block.operating_room.name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.block-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -456,7 +456,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Utilization Chart

View File

@ -3,7 +3,7 @@
{% block title %}{% if form.instance.pk %}Edit{% else %}Create{% endif %} OR Block{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.form-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -116,7 +116,7 @@
<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:or_block_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">
@ -124,7 +124,7 @@
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'operating_theatre:block_schedule_list' %}" class="btn btn-outline-secondary">
<a href="{% url 'operating_theatre:or_block_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Schedule
</a>
</div>
@ -405,7 +405,7 @@
<!-- 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">
<a href="{% url 'operating_theatre:or_block_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
</div>
@ -423,7 +423,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
// Initialize form

View File

@ -3,7 +3,7 @@
{% block title %}OR Block Schedule{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.schedule-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -214,7 +214,7 @@
</div>
<div class="ms-auto">
<div class="btn-group">
<a href="{% url 'operating_theatre:block_create' %}" class="btn btn-primary">
<a href="{% url 'operating_theatre:or_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">
@ -491,7 +491,7 @@
<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">
<a href="{% url 'operating_theatre:or_block_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create First Block
</a>
</div>
@ -532,7 +532,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
function changeDate() {
const selectedDate = document.getElementById('datePicker').value;

View File

@ -382,7 +382,7 @@
</a>
</div>
<div class="col-6">
<a href="{% url 'operating_theatre:equipment_usage_list' %}" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center">
<a href="{% url 'operating_theatre:equipment_list' %}" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center">
<i class="fas fa-tools fa-2x mb-2"></i>
<span>Equipment</span>
</a>

View File

@ -3,7 +3,7 @@
{% block title %}Delete Equipment - {{ equipment.name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.delete-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
@ -418,7 +418,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
const expectedText = 'DELETE {{ equipment.name }} {{ equipment.serial_number }}';

View File

@ -3,7 +3,7 @@
{% block title %}{{ equipment.name }} - Equipment Details{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.equipment-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -595,7 +595,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Usage Chart

View File

@ -3,7 +3,7 @@
{% block title %}{% if form.instance.pk %}Edit{% else %}Add{% endif %} Equipment{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.form-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -564,7 +564,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
// Initialize calculations

View File

@ -3,7 +3,7 @@
{% block title %}OR Equipment Management{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.equipment-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -469,38 +469,12 @@
<!-- 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>
{% include 'partial/pagination.html' %}
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
function reserveEquipment(equipmentId) {
if (confirm('Reserve this equipment?')) {

View File

@ -3,7 +3,7 @@
{% block title %}Operative Note - {{ note.patient.get_full_name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.note-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -580,7 +580,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
function signNote() {
if (confirm('Sign this operative note? Once signed, the note cannot be edited without creating an amendment.')) {

View File

@ -3,7 +3,7 @@
{% block title %}Operative Notes{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -125,7 +125,7 @@
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'operating_theatre:operative_note_create' %}" class="btn btn-primary">
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>New Note
</a>
</div>
@ -246,7 +246,7 @@
<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">
<a href="{% url 'operating_theatre:surgical_note_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Clear
</a>
</div>
@ -378,7 +378,7 @@
<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">
<a href="{% url 'operating_theatre:surgical_note_create' %}" class="btn btn-primary">
<i class="fas fa-plus me-1"></i>Create First Note
</a>
</div>
@ -419,7 +419,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
function signNote(noteId) {
if (confirm('Sign this operative note? Once signed, the note cannot be edited without creating an amendment.')) {

View File

@ -3,7 +3,7 @@
{% block title %}Delete Surgical Note - {{ note.patient.get_full_name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.delete-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
@ -476,7 +476,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
// Enable/disable delete button based on confirmations

View File

@ -3,7 +3,7 @@
{% block title %}Surgical Note - {{ note.patient.get_full_name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.note-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -532,7 +532,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
function printNote() {
window.print();

View File

@ -3,7 +3,7 @@
{% block title %}{% if note.pk %}Edit{% else %}Create{% endif %} Surgical Note{% endblock %}
{% block extra_css %}
{% block css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-timepicker/css/bootstrap-timepicker.min.css' %}" rel="stylesheet" />
@ -679,7 +679,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-timepicker/js/bootstrap-timepicker.min.js' %}"></script>

View File

@ -3,7 +3,7 @@
{% block title %}Surgical Notes{% endblock %}
{% block extra_css %}
{% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
@ -366,7 +366,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>

View File

@ -66,7 +66,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="h2 mb-1">{{ available_rooms|default:0 }}</div>
<div class="h2 mb-1">{{ rooms_available }}</div>
<div class="small">Available</div>
</div>
<div class="fa-3x opacity-50">

View File

@ -3,7 +3,7 @@
{% block title %}Delete Template - {{ template.name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.delete-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
@ -673,7 +673,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
// Enable/disable delete button based on confirmations

View File

@ -3,7 +3,7 @@
{% block title %}{{ template.name }} - Template Details{% endblock %}
{% block extra_css %}
{% block css %}
<link href="{% static 'assets/plugins/prismjs/themes/prism.min.css' %}" rel="stylesheet" />
<style>
.template-header {
@ -831,7 +831,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script src="{% static 'assets/plugins/prismjs/prism.min.js' %}"></script>
<script src="{% static 'assets/plugins/chart.js/chart.min.js' %}"></script>

View File

@ -3,9 +3,9 @@
{% block title %}{% if template.pk %}Edit{% else %}Create{% endif %} Surgical Note Template{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/summernote/summernote-bs5.min.css' %}" rel="stylesheet" />
{% block css %}
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/summernote/summernote-bs5.min.css' %}" rel="stylesheet" />
<style>
.template-form-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -715,9 +715,9 @@
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/summernote/summernote-bs5.min.js' %}"></script>
{% block js %}
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'plugins/summernote/summernote-bs5.min.js' %}"></script>
<script>
$(document).ready(function() {
@ -839,7 +839,7 @@ function toggleEditor(mode) {
}
function insertVariable(variable) {
const variableText = `{{${variable}}}`;
const variableText = `{${variable}}`;
if (document.getElementById('wysiwyg-editor').style.display !== 'none') {
$('#id_content').summernote('insertText', variableText);
@ -940,7 +940,7 @@ function updateLivePreview(content) {
};
Object.keys(sampleData).forEach(key => {
const regex = new RegExp(`{{${key}}}`, 'g');
const regex = new RegExp(`{${key}}`, 'g');
previewContent = previewContent.replace(regex, sampleData[key]);
});
@ -1027,30 +1027,30 @@ function validateTemplate() {
new bootstrap.Modal(document.getElementById('validationModal')).show();
}
function testTemplate() {
const content = $('#id_content').summernote('code');
fetch('{% url "operating_theatre:surgical_note_template_test" %}', {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
'Content-Type': 'application/json'
},
body: JSON.stringify({ content: content })
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('preview-modal-content').innerHTML = data.rendered_content;
new bootstrap.Modal(document.getElementById('previewModal')).show();
} else {
showAlert('Error testing template', 'danger');
}
})
.catch(error => {
showAlert('Error testing template', 'danger');
});
}
{#function testTemplate() {#}
{# const content = $('#id_content').summernote('code');#}
{# #}
{# fetch('{% url "operating_theatre:" %}', {#}
{# method: 'POST',#}
{# headers: {#}
{# 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,#}
{# 'Content-Type': 'application/json'#}
{# },#}
{# body: JSON.stringify({ content: content })#}
{# })#}
{# .then(response => response.json())#}
{# .then(data => {#}
{# if (data.success) {#}
{# document.getElementById('preview-modal-content').innerHTML = data.rendered_content;#}
{# new bootstrap.Modal(document.getElementById('previewModal')).show();#}
{# } else {#}
{# showAlert('Error testing template', 'danger');#}
{# }#}
{# })#}
{# .catch(error => {#}
{# showAlert('Error testing template', 'danger');#}
{# });#}
{# }#}
function validateRequiredFields() {
const requiredFields = document.querySelectorAll('[required]');

View File

@ -3,7 +3,7 @@
{% block title %}Surgical Note Templates{% endblock %}
{% block extra_css %}
{% block css %}
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
@ -651,7 +651,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>

View File

@ -3,7 +3,7 @@
{% block title %}Delete Template - {{ template.name }}{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.delete-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
@ -485,7 +485,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
// Enable delete button only when confirmation text matches

View File

@ -3,7 +3,7 @@
{% block title %}{{ template.name }} - Surgical Template{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.template-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -607,7 +607,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
function useTemplate() {
if (confirm('Use this template to create a new surgical case?')) {

View File

@ -5,7 +5,7 @@
{% if template.pk %}Edit Template - {{ template.name }}{% else %}Create Surgical Template{% endif %}
{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.form-section {
background: white;
@ -490,7 +490,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
let stepCounter = 0;
let equipmentCounter = 0;

View File

@ -3,7 +3,7 @@
{% block title %}Surgical Templates{% endblock %}
{% block extra_css %}
{% block css %}
<style>
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@ -437,7 +437,7 @@
</div>
{% endblock %}
{% block extra_js %}
{% block js %}
<script>
$(document).ready(function() {
// Initialize search