update
This commit is contained in:
parent
0422966e14
commit
b9b8c69129
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||
|
||||
@ -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'
|
||||
)
|
||||
|
||||
@ -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
BIN
operating_theatre/.DS_Store
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
operating_theatre/management/.DS_Store
vendored
Normal file
BIN
operating_theatre/management/.DS_Store
vendored
Normal file
Binary file not shown.
0
operating_theatre/management/__init__.py
Normal file
0
operating_theatre/management/__init__.py
Normal file
Binary file not shown.
0
operating_theatre/management/commands/__init__.py
Normal file
0
operating_theatre/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
585
operating_theatre/management/commands/or_data.py
Normal file
585
operating_theatre/management/commands/or_data.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@ -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
866
or_data.py
Normal 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
BIN
templates/.DS_Store
vendored
Binary file not shown.
1313
templates/inventory/orders/purchase_order_detail.html
Normal file
1313
templates/inventory/orders/purchase_order_detail.html
Normal file
File diff suppressed because it is too large
Load Diff
1505
templates/inventory/orders/purchase_order_form.html
Normal file
1505
templates/inventory/orders/purchase_order_form.html
Normal file
File diff suppressed because it is too large
Load Diff
981
templates/inventory/orders/purchase_order_list.html
Normal file
981
templates/inventory/orders/purchase_order_list.html
Normal 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">ê</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">ê</span>{{ order.total_amount|floatformat:'2g' }}</div>
|
||||
{% if order.tax_amount %}
|
||||
<small class="text-muted">+<span class="symbol">ê</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 %}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" }}';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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 }}';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?')) {
|
||||
|
||||
@ -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.')) {
|
||||
|
||||
@ -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.')) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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]');
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?')) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user