# import random # import uuid # from datetime import datetime, timedelta # from decimal import Decimal # from django.utils import timezone as django_timezone # from django.contrib.auth import get_user_model # # from core.models import Tenant # from inventory.models import InventoryItem, InventoryStock, InventoryLocation, PurchaseOrder, PurchaseOrderItem, \ # Supplier # # User = get_user_model() # # # Saudi Arabian Inventory Data # SAUDI_MEDICAL_CATEGORIES = [ # 'Pharmaceuticals', # 'Medical Devices', # 'Surgical Instruments', # 'Laboratory Supplies', # 'PPE & Safety', # 'IV Therapy', # 'Emergency Supplies' # ] # # SAUDI_SUPPLIERS = [ # 'Saudi Medical Supply Co.', # 'Gulf Medical Equipment', # 'Arabian Healthcare Supplies', # 'Riyadh Medical Trading', # 'Al-Dawaa Medical', # 'Nahdi Medical Company', # 'United Pharmaceuticals' # ] # # SAUDI_CITIES = ['Riyadh', 'Jeddah', 'Dammam', 'Medina', 'Taif', 'Khobar'] # # MEDICAL_ITEMS = [ # {'name': 'Paracetamol 500mg', 'category': 'Pharmaceuticals', 'unit': 'TAB'}, # {'name': 'Disposable Syringe 5ml', 'category': 'Medical Devices', 'unit': 'PCS'}, # {'name': 'Surgical Gloves Size M', 'category': 'PPE & Safety', 'unit': 'PAIR'}, # {'name': 'Blood Collection Tube', 'category': 'Laboratory Supplies', 'unit': 'PCS'}, # {'name': 'IV Bag Normal Saline', 'category': 'IV Therapy', 'unit': 'BAG'}, # {'name': 'Emergency Oxygen Mask', 'category': 'Emergency Supplies', 'unit': 'PCS'} # ] # # # def create_saudi_suppliers(tenants): # """Create Saudi suppliers""" # suppliers = [] # # for tenant in tenants: # print(f"Creating suppliers for {tenant.name}...") # # for i, supplier_name in enumerate(SAUDI_SUPPLIERS): # supplier_code = f"SUP-{tenant.id}-{i + 1:03d}" # # try: # supplier = Supplier.objects.create( # tenant=tenant, # supplier_code=supplier_code, # name=supplier_name, # supplier_type='DISTRIBUTOR', # city=random.choice(SAUDI_CITIES), # country='Saudi Arabia', # is_active=True # ) # suppliers.append(supplier) # print(f" ✓ Created supplier: {supplier_name}") # # except Exception as e: # print(f" ✗ Error creating supplier {supplier_name}: {e}") # continue # # print(f"Created {len(suppliers)} suppliers") # return suppliers # # # def create_saudi_inventory_locations(tenants): # """Create Saudi inventory locations""" # locations = [] # # storage_rooms = ['Pharmacy', 'Central Supply', 'OR Storage', 'ICU Supply', 'Ward Storage'] # # for tenant in tenants: # print(f"Creating locations for {tenant.name}...") # # for i, room in enumerate(storage_rooms): # location_code = f"LOC-{tenant.id}-{i + 1:03d}" # # try: # location = InventoryLocation.objects.create( # tenant=tenant, # location_code=location_code, # name=f"{room} - {tenant.city}", # description=f"Storage location in {room}", # location_type='WAREHOUSE', # building='Main Hospital', # floor='Ground Floor', # room=room, # is_active=True # ) # locations.append(location) # print(f" ✓ Created location: {location.name}") # # except Exception as e: # print(f" ✗ Error creating location {room}: {e}") # continue # # print(f"Created {len(locations)} locations") # return locations # # # def create_saudi_inventory_items(tenants): # """Create Saudi inventory items""" # items = [] # # for tenant in tenants: # print(f"Creating items for {tenant.name}...") # # for i, item_data in enumerate(MEDICAL_ITEMS): # item_code = f"ITM-{tenant.id}-{i + 1:03d}" # # try: # item = InventoryItem.objects.create( # tenant=tenant, # item_code=item_code, # item_name=item_data['name'], # description=f"Medical item: {item_data['name']}", # category=item_data['category'], # subcategory=item_data['category'], # item_type='STOCK', # manufacturer='Saudi Medical Industries', # unit_of_measure=item_data['unit'], # package_size=1, # unit_cost=Decimal(str(random.uniform(10, 100))), # list_price=Decimal(str(random.uniform(15, 150))), # has_expiration=item_data['category'] == 'Pharmaceuticals', # is_active=True, # is_tracked=True, # reorder_point=random.randint(10, 50), # reorder_quantity=random.randint(100, 500), # max_stock_level=random.randint(500, 1000) # ) # items.append(item) # print(f" ✓ Created item: {item.item_name}") # # except Exception as e: # print(f" ✗ Error creating item {item_data['name']}: {e}") # continue # # print(f"Created {len(items)} items") # return items # # # def create_saudi_inventory_stock(items, locations): # """Create Saudi inventory stock entries""" # stocks = [] # # for item in items: # print(f"Creating stock for {item.item_name}...") # # # Get locations for this tenant # tenant_locations = [loc for loc in locations if loc.tenant == item.tenant] # if not tenant_locations: # continue # # location = random.choice(tenant_locations) # # try: # stock = InventoryStock.objects.create( # inventory_item=item, # location=location, # quantity_on_hand=random.randint(50, 500), # quantity_reserved=random.randint(0, 20), # received_date=django_timezone.now().date() - timedelta(days=random.randint(1, 90)), # expiration_date=django_timezone.now().date() + timedelta(days=365) if item.has_expiration else None, # unit_cost=item.unit_cost, # quality_status='AVAILABLE' # ) # stocks.append(stock) # print(f" ✓ Created stock for: {item.item_name}") # # except Exception as e: # print(f" ✗ Error creating stock for {item.item_name}: {e}") # continue # # print(f"Created {len(stocks)} stock entries") # return stocks # # # def create_saudi_purchase_orders(tenants, suppliers): # """Create Saudi purchase orders""" # orders = [] # # for tenant in tenants: # print(f"Creating purchase orders for {tenant.name}...") # # # Get suppliers for this tenant # tenant_suppliers = [supplier for supplier in suppliers if supplier.tenant == tenant] # if not tenant_suppliers: # print(f" No suppliers found for {tenant.name}, skipping...") # continue # # # Get delivery locations # try: # locations = InventoryLocation.objects.filter(tenant=tenant) # delivery_location = locations.first() if locations.exists() else None # except: # delivery_location = None # # for i in range(3): # Create 3 orders per tenant # po_number = f"PO-{tenant.id}-{django_timezone.now().year}-{i + 1:04d}" # supplier = random.choice(tenant_suppliers) # # try: # order = PurchaseOrder.objects.create( # tenant=tenant, # po_number=po_number, # supplier=supplier, # order_date=django_timezone.now().date() - timedelta(days=random.randint(1, 30)), # requested_delivery_date=django_timezone.now().date() + timedelta(days=random.randint(7, 30)), # order_type='STANDARD', # priority='NORMAL', # subtotal=Decimal(str(random.uniform(1000, 10000))), # tax_amount=Decimal('0.00'), # shipping_amount=Decimal('0.00'), # total_amount=Decimal(str(random.uniform(1000, 10000))), # status='DRAFT', # delivery_location=delivery_location, # payment_terms='NET_30' # ) # orders.append(order) # print(f" ✓ Created PO: {po_number}") # # except Exception as e: # print(f" ✗ Error creating PO {po_number}: {e}") # continue # # print(f"Created {len(orders)} purchase orders") # return orders # # # def create_saudi_purchase_order_items(orders, items): # """Create Saudi purchase order items""" # po_items = [] # # for order in orders: # print(f"Creating items for PO {order.po_number}...") # # # Get items for this tenant # tenant_items = [item for item in items if item.tenant == order.tenant] # if not tenant_items: # continue # # # Create 2-3 items per order # num_items = min(3, len(tenant_items)) # selected_items = random.sample(tenant_items, num_items) # # for line_num, item in enumerate(selected_items, 1): # quantity_ordered = random.randint(10, 100) # unit_price = item.unit_cost * Decimal(str(random.uniform(0.9, 1.1))) # total_price = unit_price * quantity_ordered # # try: # po_item = PurchaseOrderItem.objects.create( # purchase_order=order, # line_number=line_num, # inventory_item=item, # quantity_ordered=quantity_ordered, # quantity_received=0, # unit_price=unit_price, # total_price=total_price, # requested_delivery_date=order.requested_delivery_date, # status='PENDING' # ) # po_items.append(po_item) # print(f" ✓ Created PO item: {item.item_name}") # # except Exception as e: # print(f" ✗ Error creating PO item for {item.item_name}: {e}") # continue # # print(f"Created {len(po_items)} purchase order items") # return po_items # # # def main(): # """Main function to create all Saudi inventory data""" # print("🏥 Starting Saudi Inventory Data Generation...") # # # Get tenants # try: # tenants = list(Tenant.objects.filter(is_active=True)[:5]) # Limit to first 5 tenants # if not tenants: # print("❌ No active tenants found. Please run core_data.py first.") # return # # print(f"📋 Found {len(tenants)} active tenants") # except Exception as e: # print(f"❌ Error getting tenants: {e}") # return # # # Create data step by step # print("\n1️⃣ Creating Suppliers...") # suppliers = create_saudi_suppliers(tenants) # if not suppliers: # print("❌ No suppliers created. Stopping.") # return # # print("\n2️⃣ Creating Locations...") # locations = create_saudi_inventory_locations(tenants) # if not locations: # print("❌ No locations created. Stopping.") # return # # print("\n3️⃣ Creating Items...") # items = create_saudi_inventory_items(tenants) # if not items: # print("❌ No items created. Stopping.") # return # # print("\n4️⃣ Creating Stock...") # stocks = create_saudi_inventory_stock(items, locations) # # print("\n5️⃣ Creating Purchase Orders...") # orders = create_saudi_purchase_orders(tenants, suppliers) # # print("\n6️⃣ Creating Purchase Order Items...") # po_items = create_saudi_purchase_order_items(orders, items) # # print("\n🎉 Saudi Inventory Data Generation Complete!") # print(f"📊 Summary:") # print(f" - Suppliers: {len(suppliers)}") # print(f" - Locations: {len(locations)}") # print(f" - Items: {len(items)}") # print(f" - Stock Entries: {len(stocks)}") # print(f" - Purchase Orders: {len(orders)}") # print(f" - PO Items: {len(po_items)}") # # # if __name__ == "__main__": # main() class Employee(models.Model): # """ # Employee model for hospital staff management. # """ # GENDER_CHOICES = [ # ('MALE', 'Male'), # ('FEMALE', 'Female'), # ('OTHER', 'Other'), # ('UNKNOWN', 'Unknown'), # ] # MARITAL_STATUS_CHOICES = [ # ('SINGLE', 'Single'), # ('MARRIED', 'Married'), # ('DIVORCED', 'Divorced'), # ('WIDOWED', 'Widowed'), # ('SEPARATED', 'Separated'), # ('OTHER', 'Other'), # ] # EMPLOYMENT_TYPE_CHOICES = [ # ('FULL_TIME', 'Full Time'), # ('PART_TIME', 'Part Time'), # ('CONTRACT', 'Contract'), # ('TEMPORARY', 'Temporary'), # ('INTERN', 'Intern'), # ('VOLUNTEER', 'Volunteer'), # ('PER_DIEM', 'Per Diem'), # ('CONSULTANT', 'Consultant'), # ] # EMPLOYMENT_STATUS_CHOICES = [ # ('ACTIVE', 'Active'), # ('INACTIVE', 'Inactive'), # ('TERMINATED', 'Terminated'), # ('SUSPENDED', 'Suspended'), # ('LEAVE', 'On Leave'), # ('RETIRED', 'Retired'), # ] # # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='employees', # help_text='Organization tenant' # ) # # # User relationship (optional - for employees who have system access) # user = models.OneToOneField( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='employee_profile', # help_text='Associated user account' # ) # # # Employee Information # employee_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique employee identifier' # ) # employee_number = models.CharField( # max_length=20, # help_text='Employee number' # ) # # # Personal Information # first_name = models.CharField( # max_length=50, # help_text='First name' # ) # last_name = models.CharField( # max_length=50, # help_text='Last name' # ) # middle_name = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Middle name' # ) # preferred_name = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Preferred name' # ) # # # Contact Information # email = models.EmailField( # blank=True, # null=True, # help_text='Email address' # ) # phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Phone number' # ) # mobile_phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Mobile phone number' # ) # # # Address Information # address_line_1 = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Address line 1' # ) # address_line_2 = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Address line 2' # ) # city = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='City' # ) # state = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='State/Province' # ) # postal_code = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Postal/ZIP code' # ) # country = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Country' # ) # national_id = models.CharField( # max_length=10, # blank=True, # null=True, # unique=True, # help_text='National ID' # ) # # Personal Details # date_of_birth = models.DateField( # blank=True, # null=True, # help_text='Date of birth' # ) # gender = models.CharField( # max_length=10, # choices=GENDER_CHOICES, # blank=True, # null=True, # help_text='Gender' # ) # marital_status = models.CharField( # max_length=20, # choices=MARITAL_STATUS_CHOICES, # blank=True, # null=True, # help_text='Marital status' # ) # # # Employment Information # department = models.ForeignKey( # 'Department', # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='employees', # help_text='Department' # ) # job_title = models.CharField( # max_length=100, # help_text='Job title' # ) # employment_type = models.CharField( # max_length=20, # choices=EMPLOYMENT_TYPE_CHOICES, # help_text='Employment type' # ) # employment_status = models.CharField( # max_length=20, # choices=EMPLOYMENT_STATUS_CHOICES, # default='ACTIVE', # help_text='Employment status' # ) # # # Employment Dates # hire_date = models.DateField( # help_text='Hire date' # ) # termination_date = models.DateField( # blank=True, # null=True, # help_text='Termination date' # ) # # # Supervisor Information # supervisor = models.ForeignKey( # 'self', # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='direct_reports', # help_text='Direct supervisor' # ) # # # Work Schedule Information # standard_hours_per_week = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('40.00'), # help_text='Standard hours per week' # ) # fte_percentage = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('100.00'), # validators=[MinValueValidator(0), MaxValueValidator(100)], # help_text='FTE percentage' # ) # # # Compensation Information # hourly_rate = models.DecimalField( # max_digits=10, # decimal_places=2, # blank=True, # null=True, # help_text='Hourly rate' # ) # annual_salary = models.DecimalField( # max_digits=12, # decimal_places=2, # blank=True, # null=True, # help_text='Annual salary' # ) # # # Professional Information # license_number = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Professional license number' # ) # license_expiry_date = models.DateField( # blank=True, # null=True, # help_text='License expiry date' # ) # certifications = models.JSONField( # default=list, # help_text='Professional certifications' # ) # # # Emergency Contact # emergency_contact_name = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Emergency contact name' # ) # emergency_contact_relationship = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Emergency contact relationship' # ) # emergency_contact_phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Emergency contact phone' # ) # # # Notes # notes = models.TextField( # blank=True, # null=True, # help_text='Employee notes' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # created_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='created_employees', # help_text='User who created the employee record' # ) # # class Meta: # db_table = 'hr_employee' # verbose_name = 'Employee' # verbose_name_plural = 'Employees' # ordering = ['last_name', 'first_name'] # indexes = [ # models.Index(fields=['tenant', 'employment_status']), # models.Index(fields=['employee_number']), # models.Index(fields=['last_name', 'first_name']), # models.Index(fields=['department']), # models.Index(fields=['hire_date']), # ] # unique_together = ['tenant', 'employee_number'] # # def __str__(self): # return f"{self.employee_number} - {self.get_full_name()}" # # def get_full_name(self): # """ # Get employee's full name. # """ # if self.middle_name: # return f"{self.first_name} {self.middle_name} {self.last_name}" # return f"{self.first_name} {self.last_name}" # # def get_display_name(self): # """ # Get employee's display name (preferred name if available). # """ # if self.preferred_name: # return f"{self.preferred_name} {self.last_name}" # return self.get_full_name() # # @property # def age(self): # """ # Calculate employee's age. # """ # if self.date_of_birth: # today = date.today() # return today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) # return None # # @property # def years_of_service(self): # """ # Calculate years of service. # """ # if self.hire_date: # end_date = self.termination_date or date.today() # return (end_date - self.hire_date).days / 365.25 # return 0 # # @property # def is_license_expired(self): # """ # Check if professional license is expired. # """ # if self.license_expiry_date: # return self.license_expiry_date < date.today() # return False # # @property # def full_address(self): # """ # Get full address. # """ # parts = [ # self.address_line_1, # self.address_line_2, # f"{self.city}, {self.state} {self.postal_code}", # self.country # ] # return "\n".join([part for part in parts if part]) # class TrainingRecord(models.Model): # """ # Training record model for employee training and certifications. # """ # # class TrainingType(models.TextChoices): # ORIENTATION = 'ORIENTATION', 'Orientation' # MANDATORY = 'MANDATORY', 'Mandatory Training' # CONTINUING_ED = 'CONTINUING_ED', 'Continuing Education' # CERTIFICATION = 'CERTIFICATION', 'Certification' # SKILLS = 'SKILLS', 'Skills Training' # SAFETY = 'SAFETY', 'Safety Training' # COMPLIANCE = 'COMPLIANCE', 'Compliance Training' # LEADERSHIP = 'LEADERSHIP', 'Leadership Development' # TECHNICAL = 'TECHNICAL', 'Technical Training' # OTHER = 'OTHER', 'Other' # # class TrainingStatus(models.TextChoices): # SCHEDULED = 'SCHEDULED', 'Scheduled' # IN_PROGRESS = 'IN_PROGRESS', 'In Progress' # COMPLETED = 'COMPLETED', 'Completed' # CANCELLED = 'CANCELLED', 'Cancelled' # NO_SHOW = 'NO_SHOW', 'No Show' # FAILED = 'FAILED', 'Failed' # # # Employee relationship # employee = models.ForeignKey( # Employee, # on_delete=models.CASCADE, # related_name='training_records', # help_text='Employee' # ) # # # Training Information # record_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique training record identifier' # ) # training_name = models.CharField( # max_length=200, # help_text='Training name' # ) # training_description = models.TextField( # blank=True, # null=True, # help_text='Training description' # ) # # # Training Type # training_type = models.CharField( # max_length=20, # choices=TrainingType.choices, # help_text='Training type' # ) # # # Training Provider # training_provider = models.CharField( # max_length=200, # blank=True, # null=True, # help_text='Training provider' # ) # instructor = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Instructor name' # ) # # # Training Dates # training_date = models.DateField( # help_text='Training date' # ) # completion_date = models.DateField( # blank=True, # null=True, # help_text='Completion date' # ) # expiry_date = models.DateField( # blank=True, # null=True, # help_text='Certification expiry date' # ) # # # Training Details # duration_hours = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('0.00'), # help_text='Training duration in hours' # ) # credits_earned = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('0.00'), # help_text='Credits earned' # ) # # # Training Status # status = models.CharField( # max_length=20, # choices=TrainingStatus.choices, # default='SCHEDULED', # help_text='Training status' # ) # # # Results # score = models.DecimalField( # max_digits=5, # decimal_places=2, # blank=True, # null=True, # help_text='Training score/grade' # ) # passed = models.BooleanField( # default=False, # help_text='Training passed' # ) # is_certified = models.BooleanField( # default=False, # help_text='Training is certified' # ) # # Certification Information # certificate_number = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Certificate number' # ) # certification_body = models.CharField( # max_length=200, # blank=True, # null=True, # help_text='Certification body' # ) # # # Cost Information # training_cost = models.DecimalField( # max_digits=10, # decimal_places=2, # default=Decimal('0.00'), # help_text='Training cost' # ) # # # Notes # notes = models.TextField( # blank=True, # null=True, # help_text='Training notes' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # created_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='created_training_records', # help_text='User who created the training record' # ) # # class Meta: # db_table = 'hr_training_record' # verbose_name = 'Training Record' # verbose_name_plural = 'Training Records' # ordering = ['-training_date'] # indexes = [ # models.Index(fields=['employee', 'training_date']), # models.Index(fields=['training_type']), # models.Index(fields=['status']), # models.Index(fields=['expiry_date']), # ] # # def __str__(self): # return f"{self.employee.get_full_name()} - {self.training_name}" # # @property # def tenant(self): # """ # Get tenant from employee. # """ # return self.employee.tenant # # @property # def is_expired(self): # """ # Check if certification is expired. # """ # if self.expiry_date: # return self.expiry_date < date.today() # return False # # @property # def days_to_expiry(self): # """ # Calculate days to expiry. # """ # if self.expiry_date: # return (self.expiry_date - date.today()).days # return None # # @property # def is_due_for_renewal(self): # """ # Check if certification is due for renewal (within 30 days). # """ # if self.expiry_date: # return (self.expiry_date - date.today()).days <= 30 # return False # class Certification(models.Model): # tenant = models.ForeignKey('core.Tenant', on_delete=models.PROTECT, related_name='certifications') # name = models.CharField(max_length=100) # issuer = models.CharField(max_length=150, blank=True, null=True) # is_clinical = models.BooleanField(default=False) # # class Meta: # unique_together = [('tenant', 'name')] # # class EmployeeCertification(models.Model): # employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='employee_certifications') # certification = models.ForeignKey(Certification, on_delete=models.PROTECT) # credential_id = models.CharField(max_length=100, blank=True, null=True) # issued_on = models.DateField(blank=True, null=True) # expires_on = models.DateField(blank=True, null=True) # # class Meta: # constraints = [ # models.UniqueConstraint(fields=['employee', 'certification'], name='uq_employee_cert_once') # ] # class DrugInteraction(models.Model): # """ # Drug interaction model for tracking medication interactions. # """ # # class InteractionSeverity(models.TextChoices): # MINOR = 'MINOR', 'Minor' # MODERATE = 'MODERATE', 'Moderate' # MAJOR = 'MAJOR', 'Major' # CONTRAINDICATED = 'CONTRAINDICATED', 'Contraindicated' # # # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='drug_interactions', # help_text='Organization tenant' # ) # # # Patient relationship # patient = models.ForeignKey( # 'patients.PatientProfile', # on_delete=models.CASCADE, # related_name='drug_interactions', # help_text='Patient' # ) # # # Interaction details # interaction_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique interaction identifier' # ) # # drug1 = models.CharField( # max_length=100, # help_text='First drug name' # ) # drug2 = models.CharField( # max_length=100, # help_text='Second drug name' # ) # # severity = models.CharField( # max_length=20, # choices=InteractionSeverity.choices, # help_text='Interaction severity' # ) # # description = models.TextField( # help_text='Interaction description and clinical significance' # ) # # # Management # management = models.TextField( # blank=True, # null=True, # help_text='Management recommendations' # ) # # # Status # resolved = models.BooleanField( # default=False, # help_text='Interaction has been resolved' # ) # resolved_at = models.DateTimeField( # blank=True, # null=True, # help_text='Date interaction was resolved' # ) # resolved_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='resolved_interactions', # help_text='Provider who resolved the interaction' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # detected_at = models.DateTimeField( # default=timezone.now, # help_text='When interaction was detected' # ) # # class Meta: # db_table = 'emr_drug_interaction' # verbose_name = 'Drug Interaction' # verbose_name_plural = 'Drug Interactions' # ordering = ['-detected_at'] # indexes = [ # models.Index(fields=['tenant', 'resolved']), # models.Index(fields=['patient', 'resolved']), # models.Index(fields=['severity']), # models.Index(fields=['detected_at']), # ] # # def __str__(self): # return f"{self.patient.get_full_name()} - {self.drug1} + {self.drug2}" # class RiskAssessment(models.Model): # """ # Risk assessment model for tracking patient risk scores. # """ # # class RiskLevel(models.TextChoices): # LOW = 'LOW', 'Low Risk' # MODERATE = 'MODERATE', 'Moderate Risk' # HIGH = 'HIGH', 'High Risk' # CRITICAL = 'CRITICAL', 'Critical Risk' # # # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='risk_assessments', # help_text='Organization tenant' # ) # # # Patient relationship # patient = models.ForeignKey( # 'patients.PatientProfile', # on_delete=models.CASCADE, # related_name='risk_assessments', # help_text='Patient' # ) # # # Assessment details # assessment_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique assessment identifier' # ) # # assessment_type = models.CharField( # max_length=100, # help_text='Type of risk assessment (e.g., Fall Risk, Cardiac Risk)' # ) # # score = models.DecimalField( # max_digits=5, # decimal_places=2, # help_text='Risk score value' # ) # # risk_level = models.CharField( # max_length=20, # choices=RiskLevel.choices, # help_text='Calculated risk level' # ) # # description = models.TextField( # blank=True, # null=True, # help_text='Assessment description and interpretation' # ) # # # Assessment details # assessment_date = models.DateTimeField( # default=timezone.now, # help_text='Date and time of assessment' # ) # # factors = models.JSONField( # default=dict, # help_text='Risk factors considered' # ) # # recommendations = models.JSONField( # default=list, # help_text='Recommendations based on assessment' # ) # # # Provider information # assessed_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.CASCADE, # related_name='risk_assessments', # help_text='Provider who performed assessment' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # # class Meta: # db_table = 'emr_risk_assessment' # verbose_name = 'Risk Assessment' # verbose_name_plural = 'Risk Assessments' # ordering = ['-assessment_date'] # indexes = [ # models.Index(fields=['tenant']), # models.Index(fields=['patient']), # models.Index(fields=['assessment_type']), # models.Index(fields=['risk_level']), # models.Index(fields=['assessment_date']), # ] # # def __str__(self): # return f"{self.patient.get_full_name()} - {self.assessment_type}: {self.score}" # # # ClinicalNote -------------------------------------------------------------------- # # class ClinicalNoteListView(LoginRequiredMixin, TenantMixin, ListView): # model = ClinicalNote # template_name = 'emr/clinical_note_list.html' # context_object_name = 'notes' # paginate_by = 20 # # # class ClinicalNoteDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = ClinicalNote # template_name = 'emr/clinical_note_detail.html' # context_object_name = 'note' # # # class ClinicalNoteCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = ClinicalNote # form_class = ClinicalNoteForm # template_name = 'emr/clinical_note_form.html' # success_message = _('Clinical note created successfully.') # # def form_valid(self, form): # form.instance.author = self.request.user # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='ClinicalNote', # object_id=str(self.object.pk), # changes={'status': 'Clinical note created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) # # # class ClinicalNoteUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = ClinicalNote # form_class = ClinicalNoteForm # template_name = 'emr/clinical_note_form.html' # success_message = _('Clinical note updated successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='ClinicalNote', # object_id=str(self.object.pk), # changes={'status': 'Clinical note updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) # # # class ClinicalNoteDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = ClinicalNote # template_name = 'emr/clinical_note_confirm_delete.html' # success_url = reverse_lazy('emr:clinical_note_list') # success_message = _('Clinical note deleted successfully.') # # def delete(self, request, *args, **kwargs): # cn = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='ClinicalNote', # object_id=str(cn.pk), # changes={'status': 'Clinical note deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # NoteTemplate ------------------------------------------------------------------- # # # encounters/views.py # # from django.shortcuts import get_object_or_404, redirect, render # from django.urls import reverse_lazy # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin # from django.contrib import messages # from django.views.generic import ( # TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.views.generic.edit import FormMixin # from django.contrib.messages.views import SuccessMessageMixin # from django.db.models import Q, Avg # from django.utils import timezone # from django.http import JsonResponse # # from .models import * # from .forms import * # from core.models import AuditLogEntry # from patients.models import PatientProfile # from django.utils.translation import gettext_lazy as _ # # # # Mixins ------------------------------------------------------------------------- # # class TenantMixin: # def get_queryset(self): # qs = super().get_queryset() # tenant = getattr(self.request.user, 'tenant', None) # if tenant and not self.request.user.is_superuser: # # Models with patient FK: # if hasattr(qs.model, 'patient'): # return qs.filter(patient__tenant=tenant) # # NoteTemplate uses tenant directly: # return qs.filter(tenant=tenant) # return qs # # def get_object(self, queryset=None): # qs = queryset or self.get_queryset() # return super().get_object(qs) # # # class FormKwargsMixin: # def get_form_kwargs(self): # kw = super().get_form_kwargs() # kw['user'] = self.request.user # kw['tenant'] = getattr(self.request.user, 'tenant', None) # return kw # # # # Dashboard ---------------------------------------------------------------------- # # class DashboardView(LoginRequiredMixin, TemplateView): # template_name = 'emr/dashboard.html' # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # tenant = getattr(self.request.user, 'tenant', None) # today = timezone.now().date() # week_ago = today - timezone.timedelta(days=7) # # enc = Encounter.objects.filter(patient__tenant=tenant) # vs = VitalSigns.objects.filter(encounter__patient__tenant=tenant) # pb = ProblemList.objects.filter(patient__tenant=tenant) # cp = CarePlan.objects.filter(patient__tenant=tenant) # cn = ClinicalNote.objects.filter(encounter__patient__tenant=tenant) # # ctx.update({ # 'total_encounters': enc.count(), # 'encounters_today': enc.filter(scheduled_datetime__date=today).count(), # 'encounters_this_week': enc.filter(scheduled_datetime__date__gte=week_ago).count(), # 'active_encounters': enc.filter(status='IN_PROGRESS').count(), # # 'total_vital_signs': vs.count(), # 'vital_signs_today': vs.filter(recorded_at__date=today).count(), # 'avg_temp_week': vs.filter(recorded_at__date__gte=week_ago).aggregate(Avg('temperature'))['temperature__avg'], # 'avg_hr_week': vs.filter(recorded_at__date__gte=week_ago).aggregate(Avg('heart_rate'))['heart_rate__avg'], # # 'total_problems': pb.count(), # 'active_problems': pb.filter(status='ACTIVE').count(), # 'resolved_problems': pb.filter(status='RESOLVED').count(), # # 'total_care_plans': cp.count(), # 'active_care_plans': cp.filter(status='ACTIVE').count(), # 'completed_care_plans': cp.filter(status='COMPLETED').count(), # # 'total_notes': cn.count(), # 'notes_today': cn.filter(created_at__date=today).count(), # # 'recent_encounters': enc.select_related('patient','provider').order_by('-scheduled_datetime')[:5], # 'recent_vitals': vs.select_related('encounter','recorded_by').order_by('-recorded_at')[:5], # 'recent_notes': cn.select_related('encounter','author').order_by('-created_at')[:5], # }) # return ctx # # # # Encounter ---------------------------------------------------------------------- # # class EncounterListView(LoginRequiredMixin, TenantMixin, FormMixin, ListView): # model = Encounter # template_name = 'emr/encounter_list.html' # context_object_name = 'encounters' # paginate_by = 20 # form_class = EMRSearchForm # # def get_queryset(self): # qs = super().get_queryset().select_related('patient','provider').order_by('-scheduled_datetime') # if self.request.GET: # form = self.get_form() # if form.is_valid(): # cd = form.cleaned_data # if cd.get('search'): # qs = qs.filter( # Q(patient__first_name__icontains=cd['search']) | # Q(patient__last_name__icontains=cd['search']) | # Q(chief_complaint__icontains=cd['search']) # ) # for fld in ('patient','provider','encounter_type','status'): # if cd.get(fld): # qs = qs.filter(**{fld: cd[fld]}) # if cd.get('date_from'): # qs = qs.filter(scheduled_datetime__date__gte=cd['date_from']) # if cd.get('date_to'): # qs = qs.filter(scheduled_datetime__date__lte=cd['date_to']) # return qs # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # ctx['search_form'] = self.get_form() # ctx['total_count'] = self.get_queryset().count() # return ctx # # # class EncounterDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = Encounter # template_name = 'emr/encounter_detail.html' # context_object_name = 'encounter' # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # enc = self.object # ctx.update({ # 'vital_signs': VitalSigns.objects.filter(encounter=enc).order_by('-recorded_at'), # 'clinical_notes': ClinicalNote.objects.filter(encounter=enc).order_by('-created_at'), # 'problems': ProblemList.objects.filter(patient=enc.patient, status='ACTIVE'), # 'care_plans': CarePlan.objects.filter(patient=enc.patient, status='ACTIVE'), # }) # return ctx # # # class EncounterCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = Encounter # form_class = EncounterForm # template_name = 'emr/encounter_form.html' # success_message = _('Encounter for %(patient)s created successfully.') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='Encounter', # object_id=str(self.object.pk), # changes={'status': 'Encounter created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) # # # class EncounterUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = Encounter # form_class = EncounterForm # template_name = 'emr/encounter_form.html' # success_message = _('Encounter for %(patient)s updated successfully.') # # def dispatch(self, request, *args, **kwargs): # enc = self.get_object() # if enc.status == 'COMPLETED' and not request.user.is_superuser: # messages.error(request, _('Cannot modify a completed encounter.')) # return redirect('emr:encounter_detail', pk=enc.pk) # return super().dispatch(request, *args, **kwargs) # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='Encounter', # object_id=str(self.object.pk), # changes={'status': 'Encounter updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) # # # class EncounterDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = Encounter # template_name = 'emr/encounter_confirm_delete.html' # success_url = reverse_lazy('emr:encounter_list') # success_message = _('Encounter deleted successfully.') # # def delete(self, request, *args, **kwargs): # enc = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='Encounter', # object_id=str(enc.pk), # changes={'status': 'Encounter deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # VitalSigns ---------------------------------------------------------------------- # # class VitalSignsListView(LoginRequiredMixin, TenantMixin, ListView): # model = VitalSigns # template_name = 'emr/vital_signs_list.html' # context_object_name = 'vital_signs' # paginate_by = 20 # # def get_queryset(self): # qs = super().get_queryset().select_related('encounter','recorded_by').order_by('-recorded_at') # # (Search/filter logic would use a VitalSignsSearchForm — omitted for brevity) # return qs # # class VitalSignsUpdateView(LoginRequiredMixin, FormKwargsMixin, SuccessMessageMixin, TenantMixin, UpdateView): # model = VitalSigns # form_class = VitalSignsForm # template_name = 'emr/vital_signs_form.html' # success_message = _('Vital signs updated successfully.') # # implement form_valid, get_success_url, etc. # # class VitalSignsDeleteView(LoginRequiredMixin, TenantMixin, SuccessMessageMixin, DeleteView): # model = VitalSigns # template_name = 'emr/vital_signs_confirm_delete.html' # success_url = reverse_lazy('emr:vitalsigns_list') # success_message = _('Vital signs entry deleted.') # # """ # Radiology app views with healthcare-focused CRUD operations. # Implements appropriate access patterns for radiology and imaging workflows. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Avg, Sum, F # from django.utils import timezone # from django.contrib import messages # from django.urls import reverse_lazy, reverse # from django.core.paginator import Paginator # from django.template.loader import render_to_string # from datetime import datetime, timedelta, date # import json # # from core.utils import AuditLogger # from .models import ( # ImagingOrder, ImagingStudy, ImagingSeries, DICOMImage, # RadiologyReport, ReportTemplate # ) # from .forms import ( # ImagingOrderForm, ImagingStudyForm, RadiologyReportForm, # ReportTemplateForm, ImagingSeriesForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class RadiologyDashboardView(LoginRequiredMixin, TemplateView): # """ # Main radiology dashboard with key metrics and recent activity. # """ # template_name = 'radiology/dashboard.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # today = timezone.now().date() # # # Dashboard statistics # context.update({ # 'pending_orders': ImagingOrder.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'scheduled_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='SCHEDULED' # ).count(), # 'in_progress_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'studies_completed_today': ImagingStudy.objects.filter( # tenant=tenant, # completed_datetime__date=today, # status='COMPLETED' # ).count(), # 'reports_pending': RadiologyReport.objects.filter( # tenant=tenant, # status='DRAFT' # ).count(), # 'reports_signed_today': RadiologyReport.objects.filter( # tenant=tenant, # signed_datetime__date=today, # status='SIGNED' # ).count(), # 'critical_findings': RadiologyReport.objects.filter( # tenant=tenant, # has_critical_findings=True, # status='SIGNED', # signed_datetime__date=today # ).count(), # 'total_images_today': DICOMImage.objects.filter( # tenant=tenant, # created_at__date=today # ).count(), # }) # # # Recent orders # context['recent_orders'] = ImagingOrder.objects.filter( # tenant=tenant # ).select_related('patient', 'ordering_provider').order_by('-order_datetime')[:10] # # # Recent studies # context['recent_studies'] = ImagingStudy.objects.filter( # tenant=tenant # ).select_related('order__patient').order_by('-study_datetime')[:10] # # # Recent reports # context['recent_reports'] = RadiologyReport.objects.filter( # tenant=tenant, # status='SIGNED' # ).select_related('study__order__patient').order_by('-signed_datetime')[:10] # # return context # # # # ============================================================================ # # REPORT TEMPLATE VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class ReportTemplateListView(LoginRequiredMixin, ListView): # """ # List all radiology report templates with filtering and search. # """ # model = ReportTemplate # template_name = 'radiology/report_template_list.html' # context_object_name = 'report_templates' # paginate_by = 25 # # def get_queryset(self): # queryset = ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(template_name__icontains=search) | # Q(modality__icontains=search) | # Q(body_part__icontains=search) # ) # # # Filter by modality # modality = self.request.GET.get('modality') # if modality: # queryset = queryset.filter(modality=modality) # # # Filter by active status # active_only = self.request.GET.get('active_only') # if active_only: # queryset = queryset.filter(is_active=True) # # return queryset.order_by('template_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'modalities': ReportTemplate._meta.get_field('modality').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class ReportTemplateDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a report template. # """ # model = ReportTemplate # template_name = 'radiology/report_template_detail.html' # context_object_name = 'report_template' # # def get_queryset(self): # return ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # template = self.object # # # Get recent reports using this template # context['recent_reports'] = RadiologyReport.objects.filter( # template=template, # tenant=self.request.user.tenant # ).select_related('study__order__patient').order_by('-created_at')[:10] # # return context # # # class ReportTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new report template. # """ # model = ReportTemplate # form_class = ReportTemplateForm # template_name = 'radiology/report_template_form.html' # permission_required = 'radiology.add_reporttemplate' # success_url = reverse_lazy('radiology:report_template_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='REPORT_TEMPLATE_CREATED', # model='ReportTemplate', # object_id=str(self.object.id), # details={ # 'template_name': self.object.template_name, # 'modality': self.object.modality # } # ) # # messages.success(self.request, f'Report template "{self.object.template_name}" created successfully.') # return response # # # class ReportTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing report template. # """ # model = ReportTemplate # form_class = ReportTemplateForm # template_name = 'radiology/report_template_form.html' # permission_required = 'radiology.change_reporttemplate' # # def get_queryset(self): # return ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('radiology:report_template_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='REPORT_TEMPLATE_UPDATED', # model='ReportTemplate', # object_id=str(self.object.id), # details={ # 'template_name': self.object.template_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Report template "{self.object.template_name}" updated successfully.') # return response # # # class ReportTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete a report template (soft delete by deactivating). # """ # model = ReportTemplate # template_name = 'radiology/report_template_confirm_delete.html' # permission_required = 'radiology.delete_reporttemplate' # success_url = reverse_lazy('radiology:report_template_list') # # def get_queryset(self): # return ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Soft delete by deactivating instead of actual deletion # self.object.is_active = False # self.object.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='REPORT_TEMPLATE_DEACTIVATED', # model='ReportTemplate', # object_id=str(self.object.id), # details={'template_name': self.object.template_name} # ) # # messages.success(request, f'Report template "{self.object.template_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # IMAGING ORDER VIEWS (RESTRICTED CRUD - Clinical Orders) # # ============================================================================ # # class ImagingOrderListView(LoginRequiredMixin, ListView): # """ # List all imaging orders with filtering and search. # """ # model = ImagingOrder # template_name = 'radiology/imaging_order_list.html' # context_object_name = 'imaging_orders' # paginate_by = 25 # # def get_queryset(self): # queryset = ImagingOrder.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(study_description__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by modality # modality = self.request.GET.get('modality') # if modality: # queryset = queryset.filter(modality=modality) # # # Filter by priority # priority = self.request.GET.get('priority') # if priority: # queryset = queryset.filter(priority=priority) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(order_datetime__date__gte=date_from) # if date_to: # queryset = queryset.filter(order_datetime__date__lte=date_to) # # return queryset.select_related( # 'patient', 'ordering_provider' # ).order_by('-order_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': ImagingOrder._meta.get_field('status').choices, # 'modalities': ImagingOrder._meta.get_field('modality').choices, # 'priorities': ImagingOrder._meta.get_field('priority').choices, # }) # return context # # # class ImagingOrderDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an imaging order. # """ # model = ImagingOrder # template_name = 'radiology/imaging_order_detail.html' # context_object_name = 'imaging_order' # # def get_queryset(self): # return ImagingOrder.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # imaging_order = self.object # # # Get studies for this order # context['studies'] = imaging_order.studies.all().order_by('-study_datetime') # # # Get reports for this order # context['reports'] = RadiologyReport.objects.filter( # study__order=imaging_order, # tenant=self.request.user.tenant # ).order_by('-created_at') # # return context # # # class ImagingOrderCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new imaging order. # """ # model = ImagingOrder # form_class = ImagingOrderForm # template_name = 'radiology/imaging_order_form.html' # permission_required = 'radiology.add_imagingorder' # success_url = reverse_lazy('radiology:imaging_order_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.ordering_provider = self.request.user # form.instance.order_datetime = timezone.now() # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_ORDER_CREATED', # model='ImagingOrder', # object_id=str(self.object.order_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'modality': self.object.modality, # 'study_description': self.object.study_description, # 'priority': self.object.priority # } # ) # # messages.success(self.request, 'Imaging order created successfully.') # return response # # # class ImagingOrderUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update imaging order (limited to status and notes only). # """ # model = ImagingOrder # fields = ['status', 'notes'] # Restricted fields for clinical orders # template_name = 'radiology/imaging_order_update_form.html' # permission_required = 'radiology.change_imagingorder' # # def get_queryset(self): # return ImagingOrder.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('radiology:imaging_order_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_ORDER_UPDATED', # model='ImagingOrder', # object_id=str(self.object.order_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Imaging order updated successfully.') # return response # # # # ============================================================================ # # IMAGING STUDY VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class ImagingStudyListView(LoginRequiredMixin, ListView): # """ # List all imaging studies with filtering and search. # """ # model = ImagingStudy # template_name = 'radiology/imaging_study_list.html' # context_object_name = 'imaging_studies' # paginate_by = 25 # # def get_queryset(self): # queryset = ImagingStudy.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(study_id__icontains=search) | # Q(order__patient__first_name__icontains=search) | # Q(order__patient__last_name__icontains=search) | # Q(order__patient__mrn__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by modality # modality = self.request.GET.get('modality') # if modality: # queryset = queryset.filter(order__modality=modality) # # return queryset.select_related('order__patient').order_by('-study_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': ImagingStudy._meta.get_field('status').choices, # 'modalities': ImagingOrder._meta.get_field('modality').choices, # }) # return context # # # class ImagingStudyDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an imaging study. # """ # model = ImagingStudy # template_name = 'radiology/imaging_study_detail.html' # context_object_name = 'imaging_study' # # def get_queryset(self): # return ImagingStudy.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # imaging_study = self.object # # # Get series for this study # context['series'] = imaging_study.series.all().order_by('series_number') # # # Get reports for this study # context['reports'] = imaging_study.reports.all().order_by('-created_at') # # # Get total image count # context['total_images'] = DICOMImage.objects.filter( # series__study=imaging_study, # tenant=self.request.user.tenant # ).count() # # return context # # # class ImagingStudyCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new imaging study. # """ # model = ImagingStudy # form_class = ImagingStudyForm # template_name = 'radiology/imaging_study_form.html' # permission_required = 'radiology.add_imagingstudy' # success_url = reverse_lazy('radiology:imaging_study_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.technologist = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_STUDY_CREATED', # model='ImagingStudy', # object_id=str(self.object.study_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'modality': self.object.order.modality # } # ) # # messages.success(self.request, 'Imaging study created successfully.') # return response # # # class ImagingStudyUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update imaging study (limited to status and technical notes). # """ # model = ImagingStudy # fields = ['status', 'technical_notes'] # Restricted fields # template_name = 'radiology/imaging_study_update_form.html' # permission_required = 'radiology.change_imagingstudy' # # def get_queryset(self): # return ImagingStudy.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('radiology:imaging_study_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_STUDY_UPDATED', # model='ImagingStudy', # object_id=str(self.object.study_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Imaging study updated successfully.') # return response # # # # ============================================================================ # # IMAGING SERIES VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class ImagingSeriesListView(LoginRequiredMixin, ListView): # """ # List all imaging series with filtering and search. # """ # model = ImagingSeries # template_name = 'radiology/imaging_series_list.html' # context_object_name = 'imaging_series' # paginate_by = 25 # # def get_queryset(self): # queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant) # # # Filter by study # study_id = self.request.GET.get('study') # if study_id: # queryset = queryset.filter(study_id=study_id) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(series_description__icontains=search) | # Q(study__order__patient__first_name__icontains=search) | # Q(study__order__patient__last_name__icontains=search) # ) # # return queryset.select_related('study__order__patient').order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'studies': ImagingStudy.objects.filter( # tenant=self.request.user.tenant # ).select_related('order__patient').order_by('-study_datetime')[:50], # }) # return context # # # class ImagingSeriesDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an imaging series. # """ # model = ImagingSeries # template_name = 'radiology/imaging_series_detail.html' # context_object_name = 'imaging_series' # # def get_queryset(self): # return ImagingSeries.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # imaging_series = self.object # # # Get images for this series # context['images'] = imaging_series.images.all().order_by('instance_number') # # return context # # # class ImagingSeriesCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new imaging series. # """ # model = ImagingSeries # form_class = ImagingSeriesForm # template_name = 'radiology/imaging_series_form.html' # permission_required = 'radiology.add_imagingseries' # success_url = reverse_lazy('radiology:imaging_series_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_SERIES_CREATED', # model='ImagingSeries', # object_id=str(self.object.series_id), # details={ # 'series_description': self.object.series_description, # 'study_id': str(self.object.study.study_id) # } # ) # # messages.success(self.request, 'Imaging series created successfully.') # return response # # # # ============================================================================ # # DICOM IMAGE VIEWS (READ-ONLY - System Generated) # # ============================================================================ # # class DICOMImageListView(LoginRequiredMixin, ListView): # """ # List all DICOM images with filtering and search. # """ # model = DICOMImage # template_name = 'radiology/dicom_image_list.html' # context_object_name = 'dicom_images' # paginate_by = 50 # # def get_queryset(self): # queryset = DICOMImage.objects.filter(tenant=self.request.user.tenant) # # # Filter by series # series_id = self.request.GET.get('series') # if series_id: # queryset = queryset.filter(series_id=series_id) # # # Filter by study # study_id = self.request.GET.get('study') # if study_id: # queryset = queryset.filter(series__study_id=study_id) # # return queryset.select_related('series__study__order__patient').order_by('instance_number') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'series': ImagingSeries.objects.filter( # tenant=self.request.user.tenant # ).select_related('study__order__patient').order_by('-created_at')[:50], # }) # return context # # # class DICOMImageDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a DICOM image. # """ # model = DICOMImage # template_name = 'radiology/dicom_image_detail.html' # context_object_name = 'dicom_image' # # def get_queryset(self): # return DICOMImage.objects.filter(tenant=self.request.user.tenant) # # # # ============================================================================ # # RADIOLOGY REPORT VIEWS (APPEND-ONLY - Clinical Records) # # ============================================================================ # # class RadiologyReportListView(LoginRequiredMixin, ListView): # """ # List all radiology reports with filtering and search. # """ # model = RadiologyReport # template_name = 'radiology/radiology_report_list.html' # context_object_name = 'radiology_reports' # paginate_by = 25 # # def get_queryset(self): # queryset = RadiologyReport.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(study__order__patient__first_name__icontains=search) | # Q(study__order__patient__last_name__icontains=search) | # Q(study__order__patient__mrn__icontains=search) | # Q(findings__icontains=search) | # Q(impression__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by critical findings # critical_only = self.request.GET.get('critical_only') # if critical_only: # queryset = queryset.filter(has_critical_findings=True) # # # Filter by radiologist # radiologist_id = self.request.GET.get('radiologist') # if radiologist_id: # queryset = queryset.filter(radiologist_id=radiologist_id) # # return queryset.select_related( # 'study__order__patient', 'radiologist', 'template' # ).order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': RadiologyReport._meta.get_field('status').choices, # }) # return context # # # class RadiologyReportDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a radiology report. # """ # model = RadiologyReport # template_name = 'radiology/radiology_report_detail.html' # context_object_name = 'radiology_report' # # def get_queryset(self): # return RadiologyReport.objects.filter(tenant=self.request.user.tenant) # # # class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new radiology report. # """ # model = RadiologyReport # form_class = RadiologyReportForm # template_name = 'radiology/radiology_report_form.html' # permission_required = 'radiology.add_radiologyreport' # success_url = reverse_lazy('radiology:radiology_report_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.radiologist = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='RADIOLOGY_REPORT_CREATED', # model='RadiologyReport', # object_id=str(self.object.report_id), # details={ # 'patient_name': f"{self.object.study.order.patient.first_name} {self.object.study.order.patient.last_name}", # 'has_critical_findings': self.object.has_critical_findings # } # ) # # messages.success(self.request, 'Radiology report created successfully.') # return response # # # # Note: No UpdateView or DeleteView for RadiologyReport - Append-only for clinical records # # Reports can only be amended through addendum process after signing # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @login_required # def radiology_stats(request): # """ # HTMX endpoint for radiology statistics. # """ # tenant = request.user.tenant # today = timezone.now().date() # # stats = { # 'pending_orders': ImagingOrder.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'scheduled_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='SCHEDULED' # ).count(), # 'in_progress_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'reports_pending': RadiologyReport.objects.filter( # tenant=tenant, # status='DRAFT' # ).count(), # 'critical_findings': RadiologyReport.objects.filter( # tenant=tenant, # has_critical_findings=True, # status='SIGNED', # signed_datetime__date=today # ).count(), # } # # return render(request, 'radiology/partials/radiology_stats.html', {'stats': stats}) # # # @login_required # def order_search(request): # """ # HTMX endpoint for imaging order search. # """ # search = request.GET.get('search', '') # status = request.GET.get('status', '') # modality = request.GET.get('modality', '') # # queryset = ImagingOrder.objects.filter(tenant=request.user.tenant) # # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(study_description__icontains=search) # ) # # if status: # queryset = queryset.filter(status=status) # # if modality: # queryset = queryset.filter(modality=modality) # # orders = queryset.select_related( # 'patient', 'ordering_provider' # ).order_by('-order_datetime')[:20] # # return render(request, 'radiology/partials/order_list.html', {'orders': orders}) # # # # ============================================================================ # # ACTION VIEWS # # ============================================================================ # # @login_required # def schedule_study(request, order_id): # """ # Schedule an imaging study for an order. # """ # if request.method == 'POST': # order = get_object_or_404( # ImagingOrder, # id=order_id, # tenant=request.user.tenant # ) # # scheduled_datetime = request.POST.get('scheduled_datetime') # if scheduled_datetime: # scheduled_datetime = timezone.datetime.fromisoformat(scheduled_datetime) # # # Create or update study # study, created = ImagingStudy.objects.get_or_create( # order=order, # tenant=request.user.tenant, # defaults={ # 'study_datetime': scheduled_datetime, # 'status': 'SCHEDULED', # 'technologist': request.user # } # ) # # if not created: # study.study_datetime = scheduled_datetime # study.status = 'SCHEDULED' # study.save() # # # Update order status # order.status = 'SCHEDULED' # order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='STUDY_SCHEDULED', # model='ImagingOrder', # object_id=str(order.order_id), # details={ # 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", # 'scheduled_time': scheduled_datetime.isoformat() # } # ) # # messages.success(request, 'Study scheduled successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/order_status.html', {'order': order}) # # return redirect('radiology:imaging_order_detail', pk=order.pk) # # return JsonResponse({'success': False}) # # # @login_required # def start_study(request, study_id): # """ # Start an imaging study. # """ # if request.method == 'POST': # study = get_object_or_404( # ImagingStudy, # id=study_id, # tenant=request.user.tenant # ) # # study.status = 'IN_PROGRESS' # study.started_datetime = timezone.now() # study.technologist = request.user # study.save() # # # Update order status # study.order.status = 'IN_PROGRESS' # study.order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='STUDY_STARTED', # model='ImagingStudy', # object_id=str(study.study_id), # details={ # 'patient_name': f"{study.order.patient.first_name} {study.order.patient.last_name}", # 'modality': study.order.modality # } # ) # # messages.success(request, 'Study started successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/study_status.html', {'study': study}) # # return redirect('radiology:imaging_study_detail', pk=study.pk) # # return JsonResponse({'success': False}) # # # @login_required # def complete_study(request, study_id): # """ # Complete an imaging study. # """ # if request.method == 'POST': # study = get_object_or_404( # ImagingStudy, # id=study_id, # tenant=request.user.tenant # ) # # study.status = 'COMPLETED' # study.completed_datetime = timezone.now() # study.save() # # # Update order status # study.order.status = 'COMPLETED' # study.order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='STUDY_COMPLETED', # model='ImagingStudy', # object_id=str(study.study_id), # details={ # 'patient_name': f"{study.order.patient.first_name} {study.order.patient.last_name}", # 'modality': study.order.modality # } # ) # # messages.success(request, 'Study completed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/study_status.html', {'study': study}) # # return redirect('radiology:imaging_study_detail', pk=study.pk) # # return JsonResponse({'success': False}) # # # @login_required # def sign_report(request, report_id): # """ # Sign a radiology report. # """ # if request.method == 'POST': # report = get_object_or_404( # RadiologyReport, # id=report_id, # tenant=request.user.tenant # ) # # # Only allow signing if report is in draft status # if report.status != 'DRAFT': # messages.error(request, 'Only draft reports can be signed.') # return redirect('radiology:radiology_report_detail', pk=report.pk) # # report.status = 'SIGNED' # report.signed_datetime = timezone.now() # report.signed_by = request.user # report.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='REPORT_SIGNED', # model='RadiologyReport', # object_id=str(report.report_id), # details={ # 'patient_name': f"{report.study.order.patient.first_name} {report.study.order.patient.last_name}", # 'has_critical_findings': report.has_critical_findings # } # ) # # messages.success(request, 'Report signed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/report_status.html', {'report': report}) # # return redirect('radiology:radiology_report_detail', pk=report.pk) # # return JsonResponse({'success': False}) # # # @login_required # def dictate_report(request, study_id): # """ # Start dictating a report for a study. # """ # if request.method == 'POST': # study = get_object_or_404( # ImagingStudy, # id=study_id, # tenant=request.user.tenant # ) # # template_id = request.POST.get('template_id') # template = None # if template_id: # template = get_object_or_404( # ReportTemplate, # id=template_id, # tenant=request.user.tenant # ) # # # Create report if it doesn't exist # report, created = RadiologyReport.objects.get_or_create( # study=study, # tenant=request.user.tenant, # defaults={ # 'radiologist': request.user, # 'template': template, # 'status': 'DRAFT' # } # ) # # if created: # # Log the action # AuditLogger.log_action( # user=request.user, # action='REPORT_DICTATION_STARTED', # model='RadiologyReport', # object_id=str(report.report_id), # details={ # 'patient_name': f"{study.order.patient.first_name} {study.order.patient.last_name}", # 'template_used': template.template_name if template else None # } # ) # # messages.success(request, 'Report dictation started.') # # return redirect('radiology:radiology_report_detail', pk=report.pk) # # return JsonResponse({'success': False}) # # # # # @login_required # @require_POST # def clock_in(request, employee_id): # """ # Clock in an employee. If HTMX, return the updated controls partial (button -> time). # Otherwise return JSON. # """ # # Prefer the URL param; fall back to POST field for safety # emp_id = employee_id or request.POST.get('employee_id') # # try: # employee = Employee.objects.get(id=emp_id, tenant=request.user.tenant) # except Employee.DoesNotExist: # if request.headers.get('HX-Request'): # return HttpResponse("Employee not found.", status=404) # return JsonResponse({'success': False, 'message': 'Employee not found.'}, status=404) # # today = timezone.now().date() # open_entry = TimeEntry.objects.filter( # employee=employee, # work_date=today, # clock_out_time__isnull=True # ).first() # # if open_entry: # # Already clocked in — just re-render the controls so the UI shows the time # if request.headers.get('HX-Request'): # html = render_to_string( # 'hr/partials/clock_controls.html', # {'employee': employee, 'time_entry': open_entry}, # request=request # ) # return HttpResponse(html) # return JsonResponse({'success': False, 'message': 'Employee is already clocked in.'}, status=400) # # # Create new time entry # time_entry = TimeEntry.objects.create( # employee=employee, # work_date=today, # clock_in_time=timezone.now(), # status='DRAFT' # ) # # if request.headers.get('HX-Request'): # html = render_to_string( # 'hr/partials/clock_controls.html', # {'employee': employee, 'time_entry': time_entry}, # request=request # ) # return HttpResponse(html) # # return JsonResponse({ # 'success': True, # 'message': f'{employee.get_full_name()} clocked in successfully.', # 'time_entry_id': time_entry.id # }) # # # @login_required # @require_POST # def clock_out(request, employee_id): # """ # Clock out an employee. # If HTMX, return the updated controls partial (button -> times). # Otherwise, return JSON. # """ # emp_id = employee_id or request.POST.get('employee_id') # # # Find employee in same tenant # try: # employee = Employee.objects.get(id=emp_id, tenant=request.user.tenant) # except Employee.DoesNotExist: # if request.headers.get('HX-Request'): # # Re-render controls without an active entry (will show "Clock In") # html = render_to_string( # 'hr/partials/clock_controls.html', # {'employee': None, 'time_entry': None}, # or pass the real employee if you want the button still visible # request=request # ) # return HttpResponse(html, status=404) # return JsonResponse({'success': False, 'message': 'Employee not found.'}, status=404) # # # Locate today's open entry # today = timezone.now().date() # time_entry = TimeEntry.objects.filter( # employee=employee, # work_date=today, # clock_out_time__isnull=True # ).first() # # if not time_entry: # if request.headers.get('HX-Request'): # # No open entry → re-render to show "Clock In" # html = render_to_string( # 'hr/partials/clock_controls.html', # {'employee': employee, 'time_entry': None}, # request=request # ) # return HttpResponse(html, status=400) # return JsonResponse({'success': False, 'message': 'No active clock-in found for this employee.'}, status=400) # # # Close the entry # time_entry.clock_out_time = timezone.now() # time_entry.save() # if your model computes total_hours on save # # if request.headers.get('HX-Request'): # html = render_to_string( # 'hr/partials/clock_controls.html', # {'employee': employee, 'time_entry': time_entry}, # request=request # ) # return HttpResponse(html) # # return JsonResponse({ # 'success': True, # 'message': f'{employee.get_full_name()} clocked out successfully.', # 'time_entry_id': time_entry.id, # # If total_hours is Decimal, this makes it JSON-safe: # 'hours_worked': float(getattr(time_entry, 'total_hours', 0) or 0) # }) # Query patterns to use if needed # # All upcoming sessions for a tenant (next 30 days) # TrainingSession.objects.filter( # tenant=request.user.tenant, start_at__gte=timezone.now(), # start_at__lte=timezone.now() + timedelta(days=30) # ).select_related('program', 'instructor') # # # Employees due for renewal in 30 days # TrainingCertificates.objects.filter( # tenant=request.user.tenant, # expiry_date__lte=date.today() + timedelta(days=30), # expiry_date__gte=date.today() # ).select_related('employee', 'program') # # # Enroll an employee, respecting capacity # session = TrainingSession.objects.select_for_update().get(pk=session_pk, tenant=tenant) # if session.capacity and session.enrollments.count() >= session.capacity: # status = 'WAITLISTED' # else: # status = 'SCHEDULED' # enrollment = TrainingRecord.objects.create( # tenant=tenant, employee=emp, program=session.program, session=session, # status=status, created_by=request.user # ) # # # Mark completion + pass (auto-certificate will fire via signal) # enrollment.status = 'COMPLETED' # enrollment.passed = True # enrollment.completion_date = date.today() # enrollment.credits_earned = enrollment.hours # enrollment.save() # # from django.shortcuts import render, redirect, get_object_or_404 # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.urls import reverse, reverse_lazy # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.contrib import messages # from django.utils import timezone # from django.http import HttpResponse, JsonResponse # from django.db.models import Q, Count, Sum, Avg, F, ExpressionWrapper, fields # from django.template.loader import render_to_string # from django.core.paginator import Paginator # # from .models import ( # Department, Employee, Schedule, ScheduleAssignment, # TimeEntry, PerformanceReview, TrainingRecord # ) # from .forms import ( # DepartmentForm, EmployeeForm, ScheduleForm, ScheduleAssignmentForm, # TimeEntryForm, PerformanceReviewForm, TrainingRecordForm, # HRSearchForm, TimeEntrySearchForm # ) # # # class DashboardView(LoginRequiredMixin, ListView): # """ # HR dashboard view displaying summary statistics. # """ # template_name = 'hr/dashboard.html' # context_object_name = 'employees' # paginate_by = 10 # # def get_queryset(self): # """ # Get recent employees as primary queryset. # """ # return Employee.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('-hire_date')[:10] # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # # Employee statistics # context['total_employees'] = Employee.objects.filter( # tenant=tenant # ).count() # # context['active_employees'] = Employee.objects.filter( # tenant=tenant, # is_active=True # ).count() # # context['department_stats'] = Department.objects.filter( # tenant=tenant # ).annotate( # employee_count=Count('employees', filter=Q(employees__is_active=True)) # ).order_by('-employee_count')[:5] # # # Recent hires # context['recent_hires'] = Employee.objects.filter( # tenant=tenant # ).order_by('-hire_date')[:5] # # # Upcoming reviews # context['upcoming_reviews'] = PerformanceReview.objects.filter( # tenant=tenant, # status='PENDING', # scheduled_date__gte=timezone.now().date() # ).order_by('scheduled_date')[:5] # # # Employees on leave # context['employees_on_leave'] = Employee.objects.filter( # tenant=tenant, # employment_status='LEAVE' # ).order_by('last_name') # # # Training expiring soon # context['expiring_trainings'] = TrainingRecord.objects.filter( # tenant=tenant, # status='COMPLETED', # expiry_date__gte=timezone.now().date(), # expiry_date__lte=timezone.now().date() + timezone.timedelta(days=30) # ).order_by('expiry_date')[:5] # # # Time entry approvals pending # context['pending_time_approvals'] = TimeEntry.objects.filter( # tenant=tenant, # is_approved=False, # entry_date__gte=timezone.now().date() - timezone.timedelta(days=14) # ).count() # # # Search form # context['search_form'] = HRSearchForm(tenant=tenant) # # return context # # # class EmployeeListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # List view for employees. # """ # model = Employee # template_name = 'hr/employee_list.html' # context_object_name = 'employees' # paginate_by = 20 # permission_required = 'hr.view_employee' # # def get_queryset(self): # """ # Filter employees by tenant and apply search filters. # """ # queryset = Employee.objects.filter(tenant=self.request.user.tenant) # # # Apply search filters # form = HRSearchForm(self.request.GET, tenant=self.request.user.tenant) # if form.is_valid(): # data = form.cleaned_data # # # Search term # if data.get('search'): # search_term = data['search'] # queryset = queryset.filter( # Q(first_name__icontains=search_term) | # Q(last_name__icontains=search_term) | # Q(employee_number__icontains=search_term) | # Q(position__icontains=search_term) | # Q(personal_email__icontains=search_term) | # Q(work_email__icontains=search_term) # ) # # # Department filter # if data.get('department'): # queryset = queryset.filter(department=data['department']) # # # Employment status filter # if data.get('employment_status'): # queryset = queryset.filter(employment_status=data['employment_status']) # # # Employment type filter # if data.get('employment_type'): # queryset = queryset.filter(employment_type=data['employment_type']) # # # Position filter # if data.get('position'): # queryset = queryset.filter(position__icontains=data['position']) # # # Hire date range # if data.get('hire_date_from'): # queryset = queryset.filter(hire_date__gte=data['hire_date_from']) # # if data.get('hire_date_to'): # queryset = queryset.filter(hire_date__lte=data['hire_date_to']) # # # Active status # if data.get('is_active') == 'True': # queryset = queryset.filter(is_active=True) # elif data.get('is_active') == 'False': # queryset = queryset.filter(is_active=False) # # return queryset.select_related('department', 'supervisor') # # def get_context_data(self, **kwargs): # """ # Add search form to context. # """ # context = super().get_context_data(**kwargs) # context['search_form'] = HRSearchForm( # self.request.GET or None, # tenant=self.request.user.tenant # ) # return context # # # class EmployeeDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # Detail view for employees. # """ # model = Employee # template_name = 'hr/employee_detail.html' # context_object_name = 'employee' # permission_required = 'hr.view_employee' # # def get_queryset(self): # """ # Filter employee by tenant. # """ # return Employee.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'department', 'supervisor', 'user', 'created_by' # ) # # def get_context_data(self, **kwargs): # """ # Add related objects to context. # """ # context = super().get_context_data(**kwargs) # employee = self.object # # # Current schedule # context['current_schedule'] = Schedule.objects.filter( # employee=employee, # is_current=True, # is_active=True # ).first() # # # Recent time entries # context['recent_time_entries'] = TimeEntry.objects.filter( # employee=employee # ).order_by('-entry_date', '-created_at')[:10] # # # Performance reviews # context['performance_reviews'] = PerformanceReview.objects.filter( # employee=employee # ).order_by('-scheduled_date')[:5] # # # Training records # context['training_records'] = TrainingRecord.objects.filter( # employee=employee # ).order_by('-start_date')[:10] # # # Subordinates # context['subordinates'] = Employee.objects.filter( # supervisor=employee, # is_active=True # ).order_by('last_name', 'first_name') # # return context # # # class EmployeeCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create view for employees. # """ # model = Employee # form_class = EmployeeForm # template_name = 'hr/employee_form.html' # permission_required = 'hr.add_employee' # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Set tenant and user before saving. # """ # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # response = super().form_valid(form) # # messages.success( # self.request, # f"Employee {form.instance.get_full_name()} created successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to employee detail view. # """ # return reverse('hr:employee_detail', kwargs={'pk': self.object.pk}) # # # class EmployeeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update view for employees. # """ # model = Employee # form_class = EmployeeForm # template_name = 'hr/employee_form.html' # permission_required = 'hr.change_employee' # # def get_queryset(self): # """ # Filter employee by tenant. # """ # return Employee.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Handle successful form validation. # """ # response = super().form_valid(form) # # messages.success( # self.request, # f"Employee {form.instance.get_full_name()} updated successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to employee detail view. # """ # return reverse('hr:employee_detail', kwargs={'pk': self.object.pk}) # # # class EmployeeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete view for employees. # """ # model = Employee # template_name = 'hr/employee_confirm_delete.html' # permission_required = 'hr.delete_employee' # # def get_queryset(self): # """ # Filter employee by tenant. # """ # return Employee.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # """ # Override delete to show success message. # """ # self.object = self.get_object() # name = self.object.get_full_name() # # # Check if employee can be safely deleted # if self.object.managed_departments.exists(): # messages.error( # request, # f"Cannot delete {name} because they are assigned as a department manager." # ) # return redirect('hr:employee_detail', pk=self.object.pk) # # if self.object.subordinates.exists(): # messages.error( # request, # f"Cannot delete {name} because they are assigned as a supervisor." # ) # return redirect('hr:employee_detail', pk=self.object.pk) # # success_url = self.get_success_url() # self.object.delete() # # messages.success(request, f"Employee {name} deleted successfully.") # # return redirect(success_url) # # def get_success_url(self): # """ # Redirect to employee list view. # """ # return reverse_lazy('hr:employee_list') # # # class DepartmentListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # List view for departments. # """ # model = Department # template_name = 'hr/department_list.html' # context_object_name = 'departments' # paginate_by = 20 # permission_required = 'hr.view_department' # # def get_queryset(self): # """ # Filter departments by tenant. # """ # queryset = Department.objects.filter(tenant=self.request.user.tenant) # # # Apply search filters # search = self.request.GET.get('search', '') # if search: # queryset = queryset.filter( # Q(name__icontains=search) | # Q(code__icontains=search) | # Q(description__icontains=search) # ) # # department_type = self.request.GET.get('department_type', '') # if department_type: # queryset = queryset.filter(department_type=department_type) # # is_active = self.request.GET.get('is_active', '') # if is_active == 'True': # queryset = queryset.filter(is_active=True) # elif is_active == 'False': # queryset = queryset.filter(is_active=False) # # return queryset.select_related('manager', 'parent_department') # # def get_context_data(self, **kwargs): # """ # Add filter choices to context. # """ # context = super().get_context_data(**kwargs) # context['search'] = self.request.GET.get('search', '') # context['department_types'] = Department.DEPARTMENT_TYPES # context['selected_type'] = self.request.GET.get('department_type', '') # context['selected_active'] = self.request.GET.get('is_active', '') # return context # # # class DepartmentDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # Detail view for departments. # """ # model = Department # template_name = 'hr/department_detail.html' # context_object_name = 'department' # permission_required = 'hr.view_department' # # def get_queryset(self): # """ # Filter department by tenant. # """ # return Department.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'manager', 'parent_department', 'created_by' # ) # # def get_context_data(self, **kwargs): # """ # Add related objects to context. # """ # context = super().get_context_data(**kwargs) # department = self.object # # # Employees in department # context['employees'] = Employee.objects.filter( # department=department, # is_active=True # ).order_by('last_name', 'first_name') # # # Sub-departments # context['sub_departments'] = Department.objects.filter( # parent_department=department, # is_active=True # ).order_by('name') # # return context # # # class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create view for departments. # """ # model = Department # form_class = DepartmentForm # template_name = 'hr/department_form.html' # permission_required = 'hr.add_department' # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Set tenant and user before saving. # """ # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # response = super().form_valid(form) # # messages.success( # self.request, # f"Department {form.instance.name} created successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to department detail view. # """ # return reverse('hr:department_detail', kwargs={'pk': self.object.pk}) # # # class DepartmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update view for departments. # """ # model = Department # form_class = DepartmentForm # template_name = 'hr/department_form.html' # permission_required = 'hr.change_department' # # def get_queryset(self): # """ # Filter department by tenant. # """ # return Department.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Handle successful form validation. # """ # response = super().form_valid(form) # # messages.success( # self.request, # f"Department {form.instance.name} updated successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to department detail view. # """ # return reverse('hr:department_detail', kwargs={'pk': self.object.pk}) # # # class DepartmentDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete view for departments. # """ # model = Department # template_name = 'hr/department_confirm_delete.html' # permission_required = 'hr.delete_department' # # def get_queryset(self): # """ # Filter department by tenant. # """ # return Department.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # """ # Override delete to show success message. # """ # self.object = self.get_object() # name = self.object.name # # # Check if department can be safely deleted # if self.object.employees.exists(): # messages.error( # request, # f"Cannot delete {name} because it has employees assigned to it." # ) # return redirect('hr:department_detail', pk=self.object.pk) # # if self.object.sub_departments.exists(): # messages.error( # request, # f"Cannot delete {name} because it has sub-departments assigned to it." # ) # return redirect('hr:department_detail', pk=self.object.pk) # # success_url = self.get_success_url() # self.object.delete() # # messages.success(request, f"Department {name} deleted successfully.") # # return redirect(success_url) # # def get_success_url(self): # """ # Redirect to department list view. # """ # return reverse_lazy('hr:department_list') # # # class ScheduleListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # List view for schedules. # """ # model = Schedule # template_name = 'hr/schedule_list.html' # context_object_name = 'schedules' # paginate_by = 20 # permission_required = 'hr.view_schedule' # # def get_queryset(self): # """ # Filter schedules by tenant and apply search filters. # """ # queryset = Schedule.objects.filter( # employee__tenant=self.request.user.tenant # ) # # # Apply search filters # search = self.request.GET.get('search', '') # if search: # queryset = queryset.filter( # Q(name__icontains=search) | # Q(description__icontains=search) | # Q(employee__first_name__icontains=search) | # Q(employee__last_name__icontains=search) | # Q(employee__employee_number__icontains=search) # ) # # employee_id = self.request.GET.get('employee', '') # if employee_id: # queryset = queryset.filter(employee_id=employee_id) # # schedule_type = self.request.GET.get('schedule_type', '') # if schedule_type: # queryset = queryset.filter(schedule_type=schedule_type) # # is_active = self.request.GET.get('is_active', '') # if is_active == 'True': # queryset = queryset.filter(is_active=True) # elif is_active == 'False': # queryset = queryset.filter(is_active=False) # # is_current = self.request.GET.get('is_current', '') # if is_current == 'True': # queryset = queryset.filter(is_current=True) # # return queryset.select_related('employee', 'approved_by') # # def get_context_data(self, **kwargs): # """ # Add filter choices to context. # """ # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # context['search'] = self.request.GET.get('search', '') # context['employees'] = Employee.objects.filter( # tenant=tenant, # is_active=True # ).order_by('last_name', 'first_name') # context['schedule_types'] = Schedule.SCHEDULE_TYPES # context['selected_employee'] = self.request.GET.get('employee', '') # context['selected_type'] = self.request.GET.get('schedule_type', '') # context['selected_active'] = self.request.GET.get('is_active', '') # context['selected_current'] = self.request.GET.get('is_current', '') # # return context # # # class ScheduleDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # Detail view for schedules. # """ # model = Schedule # template_name = 'hr/schedule_detail.html' # context_object_name = 'schedule' # permission_required = 'hr.view_schedule' # # def get_queryset(self): # """ # Filter schedule by tenant. # """ # return Schedule.objects.filter( # employee__tenant=self.request.user.tenant # ).select_related( # 'employee', 'approved_by', 'created_by' # ) # # def get_context_data(self, **kwargs): # """ # Add related objects to context. # """ # context = super().get_context_data(**kwargs) # schedule = self.object # # # Assignments for this schedule # context['assignments'] = ScheduleAssignment.objects.filter( # schedule=schedule # ).order_by('assignment_date', 'start_time') # # # Group assignments by week # assignments_by_week = {} # for assignment in context['assignments']: # # Get week start date (Monday) # week_start = assignment.assignment_date - timezone.timedelta( # days=assignment.assignment_date.weekday() # ) # week_key = week_start.strftime('%Y-%m-%d') # # if week_key not in assignments_by_week: # assignments_by_week[week_key] = [] # # assignments_by_week[week_key].append(assignment) # # context['assignments_by_week'] = assignments_by_week # # return context # # # class ScheduleCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create view for schedules. # """ # model = Schedule # form_class = ScheduleForm # template_name = 'hr/schedule_form.html' # permission_required = 'hr.add_schedule' # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # # # Pre-select employee if provided in GET parameters # employee_id = self.request.GET.get('employee', None) # if employee_id: # kwargs['initial'] = kwargs.get('initial', {}) # kwargs['initial']['employee'] = employee_id # # return kwargs # # def form_valid(self, form): # """ # Set tenant and user before saving. # """ # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # # Set approval information # form.instance.approved_by = self.request.user # form.instance.approval_date = timezone.now() # # response = super().form_valid(form) # # messages.success( # self.request, # f"Schedule '{form.instance.name}' for {form.instance.employee.get_full_name()} created successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to schedule detail view. # """ # return reverse('hr:schedule_detail', kwargs={'pk': self.object.pk}) # # # class ScheduleUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update view for schedules. # """ # model = Schedule # form_class = ScheduleForm # template_name = 'hr/schedule_form.html' # permission_required = 'hr.change_schedule' # # def get_queryset(self): # """ # Filter schedule by tenant. # """ # return Schedule.objects.filter( # employee__tenant=self.request.user.tenant # ) # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Handle successful form validation. # """ # # If is_current status changed, update approval information # if 'is_current' in form.changed_data and form.instance.is_current: # form.instance.approved_by = self.request.user # form.instance.approval_date = timezone.now() # # response = super().form_valid(form) # # messages.success( # self.request, # f"Schedule '{form.instance.name}' updated successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to schedule detail view. # """ # return reverse('hr:schedule_detail', kwargs={'pk': self.object.pk}) # # # class ScheduleDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete view for schedules. # """ # model = Schedule # template_name = 'hr/schedule_confirm_delete.html' # permission_required = 'hr.delete_schedule' # # def get_queryset(self): # """ # Filter schedule by tenant. # """ # return Schedule.objects.filter( # employee__tenant=self.request.user.tenant # ) # # def delete(self, request, *args, **kwargs): # """ # Override delete to show success message. # """ # self.object = self.get_object() # employee_name = self.object.employee.get_full_name() # schedule_name = self.object.name # # # Check if schedule can be safely deleted # if self.object.is_current: # messages.error( # request, # f"Cannot delete schedule '{schedule_name}' because it is the current schedule for {employee_name}." # ) # return redirect('hr:schedule_detail', pk=self.object.pk) # # success_url = self.get_success_url() # self.object.delete() # # messages.success( # request, # f"Schedule '{schedule_name}' for {employee_name} deleted successfully." # ) # # return redirect(success_url) # # def get_success_url(self): # """ # Redirect to schedule list view. # """ # return reverse_lazy('hr:schedule_list') # # # class TimeEntryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # List view for time entries. # """ # model = TimeEntry # template_name = 'hr/time_entry_list.html' # context_object_name = 'time_entries' # paginate_by = 20 # permission_required = 'hr.view_timeentry' # # def get_queryset(self): # """ # Filter time entries by tenant and apply search filters. # """ # queryset = TimeEntry.objects.filter(tenant=self.request.user.tenant) # # # Apply search filters # form = TimeEntrySearchForm(self.request.GET, tenant=self.request.user.tenant) # if form.is_valid(): # data = form.cleaned_data # # # Search term # if data.get('search'): # search_term = data['search'] # queryset = queryset.filter( # Q(employee__first_name__icontains=search_term) | # Q(employee__last_name__icontains=search_term) | # Q(employee__employee_number__icontains=search_term) | # Q(description__icontains=search_term) # ) # # # Employee filter # if data.get('employee'): # queryset = queryset.filter(employee=data['employee']) # # # Department filter # if data.get('department'): # queryset = queryset.filter(department=data['department']) # # # Entry type filter # if data.get('entry_type'): # queryset = queryset.filter(entry_type=data['entry_type']) # # # Entry date range # if data.get('entry_date_from'): # queryset = queryset.filter(entry_date__gte=data['entry_date_from']) # # if data.get('entry_date_to'): # queryset = queryset.filter(entry_date__lte=data['entry_date_to']) # # # Approval status # if data.get('is_approved') == 'True': # queryset = queryset.filter(is_approved=True) # elif data.get('is_approved') == 'False': # queryset = queryset.filter(is_approved=False) # # # Payment status # if data.get('is_paid') == 'True': # queryset = queryset.filter(is_paid=True) # elif data.get('is_paid') == 'False': # queryset = queryset.filter(is_paid=False) # # return queryset.select_related( # 'employee', 'department', 'approved_by' # ).order_by('-entry_date', '-updated_at') # # def get_context_data(self, **kwargs): # """ # Add search form to context. # """ # context = super().get_context_data(**kwargs) # context['search_form'] = TimeEntrySearchForm( # self.request.GET or None, # tenant=self.request.user.tenant # ) # # # Calculate totals # entries = self.object_list # context['total_hours'] = sum(entry.total_hours for entry in entries) # context['total_approved'] = sum(1 for entry in entries if entry.is_approved) # context['total_pending'] = sum(1 for entry in entries if not entry.is_approved) # # return context # # # class TimeEntryDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # Detail view for time entries. # """ # model = TimeEntry # template_name = 'hr/time_entry_detail.html' # context_object_name = 'time_entry' # permission_required = 'hr.view_timeentry' # # def get_queryset(self): # """ # Filter time entry by tenant. # """ # return TimeEntry.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'employee', 'department', 'approved_by', 'created_by' # ) # # # class TimeEntryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create view for time entries. # """ # model = TimeEntry # form_class = TimeEntryForm # template_name = 'hr/time_entry_form.html' # permission_required = 'hr.add_timeentry' # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # # # Pre-select employee and date if provided in GET parameters # initial = {} # employee_id = self.request.GET.get('employee', None) # if employee_id: # initial['employee'] = employee_id # # entry_date = self.request.GET.get('entry_date', None) # if entry_date: # try: # initial['entry_date'] = timezone.datetime.strptime( # entry_date, '%Y-%m-%d' # ).date() # except (ValueError, TypeError): # pass # # if initial: # kwargs['initial'] = initial # # return kwargs # # def form_valid(self, form): # """ # Set tenant and user before saving. # """ # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # response = super().form_valid(form) # # messages.success( # self.request, # f"Time entry for {form.instance.employee.get_full_name()} on {form.instance.entry_date} created successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to time entry list view or employee detail based on origin. # """ # if 'employee' in self.request.GET: # return reverse('hr:employee_detail', kwargs={'pk': self.request.GET['employee']}) # return reverse('hr:time_entry_list') # # # class TimeEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update view for time entries. # """ # model = TimeEntry # form_class = TimeEntryForm # template_name = 'hr/time_entry_form.html' # permission_required = 'hr.change_timeentry' # # def get_queryset(self): # """ # Filter time entry by tenant. # """ # return TimeEntry.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Handle successful form validation. # """ # response = super().form_valid(form) # # messages.success( # self.request, # f"Time entry for {form.instance.employee.get_full_name()} on {form.instance.entry_date} updated successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to time entry detail view. # """ # return reverse('hr:time_entry_detail', kwargs={'pk': self.object.pk}) # # # @login_required # def approve_time_entry(request, pk): # """ # Approve a time entry. # """ # time_entry = get_object_or_404( # TimeEntry, # pk=pk, # tenant=request.user.tenant # ) # # if request.method == 'POST': # if not request.user.has_perm('hr.approve_timeentry'): # messages.error(request, "You don't have permission to approve time entries.") # return redirect('hr:time_entry_detail', pk=time_entry.pk) # # time_entry.is_approved = True # time_entry.approved_by = request.user # time_entry.approval_date = timezone.now() # time_entry.save() # # messages.success( # request, # f"Time entry for {time_entry.employee.get_full_name()} on {time_entry.entry_date} approved." # ) # # redirect_url = request.POST.get('next', reverse('hr:time_entry_detail', kwargs={'pk': time_entry.pk})) # return redirect(redirect_url) # # return redirect('hr:time_entry_detail', pk=time_entry.pk) # # # # # class PerformanceReviewListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # List view for performance reviews. # """ # model = PerformanceReview # template_name = 'hr/performance_review_list.html' # context_object_name = 'reviews' # paginate_by = 20 # permission_required = 'hr.view_performancereview' # # def get_queryset(self): # """ # Filter reviews by tenant and apply search filters. # """ # queryset = PerformanceReview.objects.filter(tenant=self.request.user.tenant) # # # Apply search filters # search = self.request.GET.get('search', '') # if search: # queryset = queryset.filter( # Q(employee__first_name__icontains=search) | # Q(employee__last_name__icontains=search) | # Q(employee__employee_number__icontains=search) | # Q(strengths__icontains=search) | # Q(areas_for_improvement__icontains=search) # ) # # employee_id = self.request.GET.get('employee', '') # if employee_id: # queryset = queryset.filter(employee_id=employee_id) # # review_type = self.request.GET.get('review_type', '') # if review_type: # queryset = queryset.filter(review_type=review_type) # # status = self.request.GET.get('status', '') # if status: # queryset = queryset.filter(status=status) # # # Date range filters # scheduled_from = self.request.GET.get('scheduled_from', '') # if scheduled_from: # try: # scheduled_from = timezone.datetime.strptime(scheduled_from, '%Y-%m-%d').date() # queryset = queryset.filter(scheduled_date__gte=scheduled_from) # except (ValueError, TypeError): # pass # # scheduled_to = self.request.GET.get('scheduled_to', '') # if scheduled_to: # try: # scheduled_to = timezone.datetime.strptime(scheduled_to, '%Y-%m-%d').date() # queryset = queryset.filter(scheduled_date__lte=scheduled_to) # except (ValueError, TypeError): # pass # # # Rating filter # rating = self.request.GET.get('rating', '') # if rating and rating.isdigit(): # queryset = queryset.filter(performance_rating=int(rating)) # # return queryset.select_related('employee', 'reviewer') # # def get_context_data(self, **kwargs): # """ # Add filter choices to context. # """ # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # context['search'] = self.request.GET.get('search', '') # context['employees'] = Employee.objects.filter( # tenant=tenant # ).order_by('last_name', 'first_name') # context['review_types'] = PerformanceReview.REVIEW_TYPES # context['review_statuses'] = PerformanceReview.REVIEW_STATUS # context['selected_employee'] = self.request.GET.get('employee', '') # context['selected_type'] = self.request.GET.get('review_type', '') # context['selected_status'] = self.request.GET.get('status', '') # context['scheduled_from'] = self.request.GET.get('scheduled_from', '') # context['scheduled_to'] = self.request.GET.get('scheduled_to', '') # context['selected_rating'] = self.request.GET.get('rating', '') # # return context # # # class PerformanceReviewDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # Detail view for performance reviews. # """ # model = PerformanceReview # template_name = 'hr/performance_review_detail.html' # context_object_name = 'review' # permission_required = 'hr.view_performancereview' # # def get_queryset(self): # """ # Filter review by tenant. # """ # return PerformanceReview.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'employee', 'reviewer', 'created_by' # ) # # # class PerformanceReviewCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create view for performance reviews. # """ # model = PerformanceReview # form_class = PerformanceReviewForm # template_name = 'hr/performance_review_form.html' # permission_required = 'hr.add_performancereview' # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # # # Pre-select employee if provided in GET parameters # employee_id = self.request.GET.get('employee', None) # if employee_id: # kwargs['initial'] = kwargs.get('initial', {}) # kwargs['initial']['employee'] = employee_id # kwargs['initial']['reviewer'] = self.request.user.id # # return kwargs # # def form_valid(self, form): # """ # Set tenant and user before saving. # """ # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # response = super().form_valid(form) # # messages.success( # self.request, # f"Performance review for {form.instance.employee.get_full_name()} created successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to review detail view. # """ # return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk}) # # # class PerformanceReviewUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update view for performance reviews. # """ # model = PerformanceReview # form_class = PerformanceReviewForm # template_name = 'hr/performance_review_form.html' # permission_required = 'hr.change_performancereview' # # def get_queryset(self): # """ # Filter review by tenant. # """ # return PerformanceReview.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Handle successful form validation. # """ # response = super().form_valid(form) # # messages.success( # self.request, # f"Performance review for {form.instance.employee.get_full_name()} updated successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to review detail view. # """ # return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk}) # # # class TrainingRecordListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # List view for training records. # """ # model = TrainingRecord # template_name = 'hr/training_record_list.html' # context_object_name = 'training_records' # paginate_by = 20 # permission_required = 'hr.view_trainingrecord' # # def get_queryset(self): # """ # Filter training records by tenant and apply search filters. # """ # queryset = TrainingRecord.objects.filter(tenant=self.request.user.tenant) # # # Apply search filters # search = self.request.GET.get('search', '') # if search: # queryset = queryset.filter( # Q(employee__first_name__icontains=search) | # Q(employee__last_name__icontains=search) | # Q(employee__employee_number__icontains=search) | # Q(training_title__icontains=search) | # Q(training_description__icontains=search) | # Q(provider__icontains=search) | # Q(trainer__icontains=search) # ) # # employee_id = self.request.GET.get('employee', '') # if employee_id: # queryset = queryset.filter(employee_id=employee_id) # # training_type = self.request.GET.get('training_type', '') # if training_type: # queryset = queryset.filter(training_type=training_type) # # status = self.request.GET.get('status', '') # if status: # queryset = queryset.filter(status=status) # # is_mandatory = self.request.GET.get('is_mandatory', '') # if is_mandatory == 'True': # queryset = queryset.filter(is_mandatory=True) # elif is_mandatory == 'False': # queryset = queryset.filter(is_mandatory=False) # # # Date range filters # start_from = self.request.GET.get('start_from', '') # if start_from: # try: # start_from = timezone.datetime.strptime(start_from, '%Y-%m-%d').date() # queryset = queryset.filter(start_date__gte=start_from) # except (ValueError, TypeError): # pass # # start_to = self.request.GET.get('start_to', '') # if start_to: # try: # start_to = timezone.datetime.strptime(start_to, '%Y-%m-%d').date() # queryset = queryset.filter(start_date__lte=start_to) # except (ValueError, TypeError): # pass # # # Expiry status filter # expiry_status = self.request.GET.get('expiry_status', '') # today = timezone.now().date() # if expiry_status == 'valid': # queryset = queryset.filter( # status='COMPLETED', # expiry_date__gt=today # ) # elif expiry_status == 'expiring': # queryset = queryset.filter( # status='COMPLETED', # expiry_date__gt=today, # expiry_date__lte=today + timezone.timedelta(days=30) # ) # elif expiry_status == 'expired': # queryset = queryset.filter( # Q(status='EXPIRED') | # Q(status='COMPLETED', expiry_date__lt=today) # ) # # return queryset.select_related('employee') # # def get_context_data(self, **kwargs): # """ # Add filter choices to context. # """ # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # context['search'] = self.request.GET.get('search', '') # context['employees'] = Employee.objects.filter( # tenant=tenant # ).order_by('last_name', 'first_name') # context['training_statuses'] = TrainingRecord.TRAINING_STATUS # context['selected_employee'] = self.request.GET.get('employee', '') # context['selected_type'] = self.request.GET.get('training_type', '') # context['selected_status'] = self.request.GET.get('status', '') # context['selected_mandatory'] = self.request.GET.get('is_mandatory', '') # context['start_from'] = self.request.GET.get('start_from', '') # context['start_to'] = self.request.GET.get('start_to', '') # context['selected_expiry'] = self.request.GET.get('expiry_status', '') # # # Collect unique training types from existing records # context['training_types'] = TrainingRecord.objects.filter( # tenant=tenant # ).values_list('training_type', flat=True).distinct().order_by('training_type') # # return context # # # class TrainingRecordDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # Detail view for training records. # """ # model = TrainingRecord # template_name = 'hr/training_record_detail.html' # context_object_name = 'training_record' # permission_required = 'hr.view_trainingrecord' # # def get_queryset(self): # """ # Filter training record by tenant. # """ # return TrainingRecord.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'employee', 'created_by' # ) # # # class TrainingRecordCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create view for training records. # """ # model = TrainingRecord # form_class = TrainingRecordForm # template_name = 'hr/training_record_form.html' # permission_required = 'hr.add_trainingrecord' # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # # # Pre-select employee if provided in GET parameters # employee_id = self.request.GET.get('employee', None) # if employee_id: # kwargs['initial'] = kwargs.get('initial', {}) # kwargs['initial']['employee'] = employee_id # # return kwargs # # def form_valid(self, form): # """ # Set tenant and user before saving. # """ # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # response = super().form_valid(form) # # messages.success( # self.request, # f"Training record '{form.instance.training_title}' for {form.instance.employee.get_full_name()} created successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to training record detail view. # """ # return reverse('hr:training_record_detail', kwargs={'pk': self.object.pk}) # # # class TrainingRecordUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update view for training records. # """ # model = TrainingRecord # form_class = TrainingRecordForm # template_name = 'hr/training_record_form.html' # permission_required = 'hr.change_trainingrecord' # # def get_queryset(self): # """ # Filter training record by tenant. # """ # return TrainingRecord.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # """ # Pass tenant and user to form. # """ # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """ # Handle successful form validation. # """ # response = super().form_valid(form) # # messages.success( # self.request, # f"Training record '{form.instance.training_title}' updated successfully." # ) # # return response # # def get_success_url(self): # """ # Redirect to training record detail view. # """ # return reverse('hr:training_record_detail', kwargs={'pk': self.object.pk}) # # # @login_required # def hr_stats(request): # """ # Return HR statistics for dashboard updates. # """ # context = { # 'total_employees': Employee.objects.filter(tenant=request.user.tenant).count(), # 'active_employees': Employee.objects.filter( # tenant=request.user.tenant, # employment_status='ACTIVE' # ).count(), # 'total_departments': Department.objects.filter(tenant=request.user.tenant).count(), # 'pending_reviews': PerformanceReview.objects.filter( # tenant=request.user.tenant, # status='PENDING' # ).count(), # 'employees_clocked_in': TimeEntry.objects.filter( # tenant=request.user.tenant, # clock_in_time__date=timezone.now().date(), # clock_out_time__isnull=True # ).count(), # } # # return render(request, 'hr/partials/hr_stats.html', context) # # # @login_required # def clock_in(request): # """ # Clock in the current user. # """ # # Check if user has an employee profile # try: # employee = request.user.employee_profile # except (AttributeError, Employee.DoesNotExist): # messages.error(request, "You don't have an employee profile. Please contact HR.") # return redirect('hr:dashboard') # # # Check if already clocked in # existing_entry = TimeEntry.objects.filter( # tenant=request.user.tenant, # employee=employee, # entry_date=timezone.now().date(), # clock_out_time__isnull=True # ).first() # # if existing_entry: # messages.warning(request, "You are already clocked in. Please clock out first.") # return redirect('hr:dashboard') # # # Create new time entry # time_entry = TimeEntry( # tenant=request.user.tenant, # employee=employee, # entry_date=timezone.now().date(), # entry_type='REGULAR', # clock_in_time=timezone.now(), # department=employee.department, # created_by=request.user # ) # time_entry.save() # # messages.success(request, f"Clocked in at {time_entry.clock_in_time.strftime('%H:%M:%S')}.") # return redirect('hr:dashboard') # # # @login_required # def clock_out(request): # """ # Clock out the current user. # """ # # Check if user has an employee profile # try: # employee = request.user.employee_profile # except (AttributeError, Employee.DoesNotExist): # messages.error(request, "You don't have an employee profile. Please contact HR.") # return redirect('hr:dashboard') # # # Find active time entry # time_entry = TimeEntry.objects.filter( # tenant=request.user.tenant, # employee=employee, # entry_date=timezone.now().date(), # clock_out_time__isnull=True # ).order_by('-clock_in_time').first() # # if not time_entry: # messages.warning(request, "You are not clocked in. Please clock in first.") # return redirect('hr:dashboard') # # # Update time entry # time_entry.clock_out_time = timezone.now() # time_entry.save() # This will calculate total_hours in the save method # # hours = time_entry.total_hours # messages.success( # request, # f"Clocked out at {time_entry.clock_out_time.strftime('%H:%M:%S')}. " # f"Total hours: {hours:.2f}" # ) # return redirect('hr:dashboard') # # # # # # ============================================================================ # # ACTION VIEWS # # ============================================================================ # # @login_required # def start_case(request, case_id): # """ # Start a surgical case. # """ # if request.method == 'POST': # case = get_object_or_404( # SurgicalCase, # id=case_id, # tenant=request.user.tenant # ) # # case.status = 'IN_PROGRESS' # case.actual_start_time = timezone.now() # case.save() # # # Update room status # if case.operating_room: # case.operating_room.status = 'IN_USE' # case.operating_room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_CASE_STARTED', # model='SurgicalCase', # object_id=str(case.case_id), # details={ # 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", # 'procedure_name': case.procedure_name # } # ) # # messages.success(request, 'Surgical case started successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) # # return redirect('operating_theatre:surgical_case_detail', pk=case.pk) # # return JsonResponse({'success': False}) # # # @login_required # def complete_case(request, case_id): # """ # Complete a surgical case. # """ # if request.method == 'POST': # case = get_object_or_404( # SurgicalCase, # id=case_id, # tenant=request.user.tenant # ) # # case.status = 'COMPLETED' # case.actual_end_time = timezone.now() # case.save() # # # Update room status # if case.operating_room: # case.operating_room.status = 'CLEANING' # case.operating_room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_CASE_COMPLETED', # model='SurgicalCase', # object_id=str(case.case_id), # details={ # 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", # 'procedure_name': case.procedure_name, # 'duration': str(case.actual_end_time - case.actual_start_time) if case.actual_start_time else None # } # ) # # messages.success(request, 'Surgical case completed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) # # return redirect('operating_theatre:surgical_case_detail', pk=case.pk) # # return JsonResponse({'success': False}) # # # # @login_required # def sign_note(request, note_id): # """ # Sign a surgical note. # """ # if request.method == 'POST': # note = get_object_or_404( # SurgicalNote, # id=note_id, # tenant=request.user.tenant # ) # # # Only allow signing if note is in draft status # if note.status != 'DRAFT': # messages.error(request, 'Only draft notes can be signed.') # return redirect('operating_theatre:surgical_note_detail', pk=note.pk) # # note.status = 'SIGNED' # note.signed_datetime = timezone.now() # note.signed_by = request.user # note.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_NOTE_SIGNED', # model='SurgicalNote', # object_id=str(note.note_id), # details={ # 'patient_name': f"{note.surgical_case.patient.first_name} {note.surgical_case.patient.last_name}", # 'note_type': note.note_type # } # ) # # messages.success(request, 'Surgical note signed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/note_status.html', {'note': note}) # # return redirect('operating_theatre:surgical_note_detail', pk=note.pk) # # return JsonResponse({'success': False}) # # # @login_required # def update_room_status(request, room_id): # """ # Update operating room status. # """ # if request.method == 'POST': # room = get_object_or_404( # OperatingRoom, # id=room_id, # tenant=request.user.tenant # ) # # new_status = request.POST.get('status') # if new_status in dict(OperatingRoom._meta.get_field('status').choices): # old_status = room.status # room.status = new_status # room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='OPERATING_ROOM_STATUS_UPDATED', # model='OperatingRoom', # object_id=str(room.id), # details={ # 'room_number': room.room_number, # 'old_status': old_status, # 'new_status': new_status # } # ) # # messages.success(request, f'Room {room.room_number} status updated to {room.get_status_display()}.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/room_status.html', {'room': room}) # # return redirect('operating_theatre:operating_room_detail', pk=room.pk) # # return JsonResponse({'success': False}) # # # # # # """ # Operating Theatre app views with healthcare-focused CRUD operations. # Implements appropriate access patterns for surgical and OR management workflows. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Avg, Sum, F # from django.utils import timezone # from django.contrib import messages # from django.urls import reverse_lazy, reverse # from django.core.paginator import Paginator # from django.template.loader import render_to_string # from datetime import datetime, timedelta, date # import json # # from core.utils import AuditLogger # from .models import ( # OperatingRoom, ORBlock, SurgicalCase, SurgicalNote, # EquipmentUsage, SurgicalNoteTemplate # ) # from .forms import ( # OperatingRoomForm, ORBlockForm, SurgicalCaseForm, SurgicalNoteForm, # EquipmentUsageForm, SurgicalNoteTemplateForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class OperatingTheatreDashboardView(LoginRequiredMixin, TemplateView): # """ # Main operating theatre dashboard with key metrics and recent activity. # """ # template_name = 'operating_theatre/dashboard.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # today = timezone.now().date() # # # Dashboard statistics # context.update({ # 'total_rooms': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True # ).count(), # 'rooms_available': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True, # status='AVAILABLE' # ).count(), # 'rooms_in_use': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True, # status='IN_USE' # ).count(), # 'rooms_maintenance': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True, # status='MAINTENANCE' # ).count(), # 'cases_today': SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today # ).count(), # 'cases_in_progress': SurgicalCase.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'cases_completed_today': SurgicalCase.objects.filter( # tenant=tenant, # actual_end_time__date=today, # status='COMPLETED' # ).count(), # 'emergency_cases_today': SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today, # priority='EMERGENCY' # ).count(), # 'blocks_today': ORBlock.objects.filter( # tenant=tenant, # date=today # ).count(), # 'equipment_in_use': EquipmentUsage.objects.filter( # tenant=tenant, # status='IN_USE' # ).count(), # 'notes_pending': SurgicalNote.objects.filter( # tenant=tenant, # status='DRAFT' # ).count(), # }) # # # Recent surgical cases # context['recent_cases'] = SurgicalCase.objects.filter( # tenant=tenant # ).select_related( # 'patient', 'primary_surgeon', 'operating_room' # ).order_by('-scheduled_start_time')[:10] # # # Today's schedule # context['todays_schedule'] = SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today # ).select_related( # 'patient', 'primary_surgeon', 'operating_room' # ).order_by('scheduled_start_time') # # # Room utilization # context['room_utilization'] = OperatingRoom.objects.filter( # tenant=tenant, # is_active=True # ).annotate( # cases_today=Count( # 'surgical_cases', # filter=Q(surgical_cases__scheduled_start_time__date=today) # ) # ).order_by('room_number') # # return context # # # # ============================================================================ # # OPERATING ROOM VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class OperatingRoomListView(LoginRequiredMixin, ListView): # """ # List all operating rooms with filtering and search. # """ # model = OperatingRoom # template_name = 'operating_theatre/operating_room_list.html' # context_object_name = 'operating_rooms' # paginate_by = 25 # # def get_queryset(self): # queryset = OperatingRoom.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(room_number__icontains=search) | # Q(room_name__icontains=search) | # Q(location__icontains=search) # ) # # # Filter by room type # room_type = self.request.GET.get('room_type') # if room_type: # queryset = queryset.filter(room_type=room_type) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by floor # floor = self.request.GET.get('floor') # if floor: # queryset = queryset.filter(floor=floor) # # # Filter by active status # active_only = self.request.GET.get('active_only') # if active_only: # queryset = queryset.filter(is_active=True) # # return queryset.order_by('room_number') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'room_types': OperatingRoom._meta.get_field('room_type').choices, # 'statuses': OperatingRoom._meta.get_field('status').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class OperatingRoomDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an operating room. # """ # model = OperatingRoom # template_name = 'operating_theatre/operating_room_detail.html' # context_object_name = 'operating_room' # # def get_queryset(self): # return OperatingRoom.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # operating_room = self.object # today = timezone.now().date() # # # Get today's cases for this room # context['todays_cases'] = operating_room.surgical_cases.filter( # scheduled_start_time__date=today # ).select_related('patient', 'primary_surgeon').order_by('scheduled_start_time') # # # Get recent cases # context['recent_cases'] = operating_room.surgical_cases.all().select_related( # 'patient', 'primary_surgeon' # ).order_by('-scheduled_start_time')[:10] # # # Get equipment usage # context['equipment_usage'] = EquipmentUsage.objects.filter( # operating_room=operating_room, # tenant=self.request.user.tenant # ).order_by('-start_time')[:10] # # # Room statistics # context['room_stats'] = { # 'total_cases': operating_room.surgical_cases.count(), # 'cases_this_month': operating_room.surgical_cases.filter( # scheduled_start_time__month=timezone.now().month, # scheduled_start_time__year=timezone.now().year # ).count(), # 'average_case_duration': operating_room.surgical_cases.filter( # actual_end_time__isnull=False # ).aggregate( # avg_duration=Avg( # F('actual_end_time') - F('actual_start_time') # ) # )['avg_duration'], # } # # return context # # # class OperatingRoomCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new operating room. # """ # model = OperatingRoom # form_class = OperatingRoomForm # template_name = 'operating_theatre/operating_room_form.html' # permission_required = 'operating_theatre.add_operatingroom' # success_url = reverse_lazy('operating_theatre:operating_room_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='OPERATING_ROOM_CREATED', # model='OperatingRoom', # object_id=str(self.object.id), # details={ # 'room_number': self.object.room_number, # 'room_name': self.object.room_name, # 'room_type': self.object.room_type # } # ) # # messages.success(self.request, f'Operating room "{self.object.room_number}" created successfully.') # return response # # # class OperatingRoomUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing operating room. # """ # model = OperatingRoom # form_class = OperatingRoomForm # template_name = 'operating_theatre/operating_room_form.html' # permission_required = 'operating_theatre.change_operatingroom' # # def get_queryset(self): # return OperatingRoom.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('operating_theatre:operating_room_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='OPERATING_ROOM_UPDATED', # model='OperatingRoom', # object_id=str(self.object.id), # details={ # 'room_number': self.object.room_number, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Operating room "{self.object.room_number}" updated successfully.') # return response # # # class OperatingRoomDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete an operating room (soft delete by deactivating). # """ # model = OperatingRoom # template_name = 'operating_theatre/operating_room_confirm_delete.html' # permission_required = 'operating_theatre.delete_operatingroom' # success_url = reverse_lazy('operating_theatre:operating_room_list') # # def get_queryset(self): # return OperatingRoom.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Check if room has active cases # active_cases = self.object.surgical_cases.filter( # status__in=['SCHEDULED', 'IN_PROGRESS'] # ).count() # # if active_cases > 0: # messages.error( # request, # f'Cannot deactivate room "{self.object.room_number}" - it has {active_cases} active cases.' # ) # return redirect('operating_theatre:operating_room_detail', pk=self.object.pk) # # # Soft delete by deactivating # self.object.is_active = False # self.object.status = 'OUT_OF_SERVICE' # self.object.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='OPERATING_ROOM_DEACTIVATED', # model='OperatingRoom', # object_id=str(self.object.id), # details={'room_number': self.object.room_number} # ) # # messages.success(request, f'Operating room "{self.object.room_number}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # SURGICAL NOTE TEMPLATE VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class SurgicalNoteTemplateListView(LoginRequiredMixin, ListView): # """ # List all surgical note templates with filtering and search. # """ # model = SurgicalNoteTemplate # template_name = 'operating_theatre/surgical_note_template_list.html' # context_object_name = 'surgical_note_templates' # paginate_by = 25 # # def get_queryset(self): # queryset = SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(template_name__icontains=search) | # Q(procedure_type__icontains=search) | # Q(specialty__icontains=search) # ) # # # Filter by specialty # specialty = self.request.GET.get('specialty') # if specialty: # queryset = queryset.filter(specialty=specialty) # # # Filter by active status # active_only = self.request.GET.get('active_only') # if active_only: # queryset = queryset.filter(is_active=True) # # return queryset.order_by('template_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'specialties': SurgicalNoteTemplate._meta.get_field('specialty').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class SurgicalNoteTemplateDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a surgical note template. # """ # model = SurgicalNoteTemplate # template_name = 'operating_theatre/surgical_note_template_detail.html' # context_object_name = 'surgical_note_template' # # def get_queryset(self): # return SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # template = self.object # # # Get recent notes using this template # context['recent_notes'] = SurgicalNote.objects.filter( # template=template, # tenant=self.request.user.tenant # ).select_related('surgical_case__patient').order_by('-created_at')[:10] # # return context # # # class SurgicalNoteTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new surgical note template. # """ # model = SurgicalNoteTemplate # form_class = SurgicalNoteTemplateForm # template_name = 'operating_theatre/surgical_note_template_form.html' # permission_required = 'operating_theatre.add_surgicalnotetemplate' # success_url = reverse_lazy('operating_theatre:surgical_note_template_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SURGICAL_NOTE_TEMPLATE_CREATED', # model='SurgicalNoteTemplate', # object_id=str(self.object.id), # details={ # 'template_name': self.object.template_name, # 'specialty': self.object.specialty # } # ) # # messages.success(self.request, f'Surgical note template "{self.object.template_name}" created successfully.') # return response # # # class SurgicalNoteTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing surgical note template. # """ # model = SurgicalNoteTemplate # form_class = SurgicalNoteTemplateForm # template_name = 'operating_theatre/surgical_note_template_form.html' # permission_required = 'operating_theatre.change_surgicalnotetemplate' # # def get_queryset(self): # return SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('operating_theatre:surgical_note_template_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SURGICAL_NOTE_TEMPLATE_UPDATED', # model='SurgicalNoteTemplate', # object_id=str(self.object.id), # details={ # 'template_name': self.object.template_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Surgical note template "{self.object.template_name}" updated successfully.') # return response # # # class SurgicalNoteTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete a surgical note template (soft delete by deactivating). # """ # model = SurgicalNoteTemplate # template_name = 'operating_theatre/surgical_note_template_confirm_delete.html' # permission_required = 'operating_theatre.delete_surgicalnotetemplate' # success_url = reverse_lazy('operating_theatre:surgical_note_template_list') # # def get_queryset(self): # return SurgicalNoteTemplate.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Soft delete by deactivating # self.object.is_active = False # self.object.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_NOTE_TEMPLATE_DEACTIVATED', # model='SurgicalNoteTemplate', # object_id=str(self.object.id), # details={'template_name': self.object.template_name} # ) # # messages.success(request, f'Surgical note template "{self.object.template_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # OR BLOCK VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class ORBlockListView(LoginRequiredMixin, ListView): # """ # List all OR blocks with filtering and search. # """ # model = ORBlock # template_name = 'operating_theatre/or_block_list.html' # context_object_name = 'or_blocks' # paginate_by = 25 # # def get_queryset(self): # queryset = ORBlock.objects.filter(tenant=self.request.user.tenant) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(date__gte=date_from) # if date_to: # queryset = queryset.filter(date__lte=date_to) # # # Filter by surgeon # surgeon_id = self.request.GET.get('surgeon') # if surgeon_id: # queryset = queryset.filter(surgeon_id=surgeon_id) # # # Filter by operating room # room_id = self.request.GET.get('room') # if room_id: # queryset = queryset.filter(operating_room_id=room_id) # # return queryset.select_related( # 'operating_room', 'surgeon' # ).order_by('-date', 'start_time') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'operating_rooms': OperatingRoom.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('room_number'), # }) # return context # # # class ORBlockDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an OR block. # """ # model = ORBlock # template_name = 'operating_theatre/or_block_detail.html' # context_object_name = 'or_block' # # def get_queryset(self): # return ORBlock.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # or_block = self.object # # # Get cases scheduled in this block # context['scheduled_cases'] = SurgicalCase.objects.filter( # operating_room=or_block.operating_room, # scheduled_start_time__date=or_block.date, # scheduled_start_time__time__gte=or_block.start_time, # scheduled_start_time__time__lt=or_block.end_time, # tenant=self.request.user.tenant # ).select_related('patient', 'primary_surgeon').order_by('scheduled_start_time') # # # Calculate utilization # total_block_minutes = ( # timezone.datetime.combine(timezone.now().date(), or_block.end_time) - # timezone.datetime.combine(timezone.now().date(), or_block.start_time) # ).total_seconds() / 60 # # used_minutes = 0 # for case in context['scheduled_cases']: # if case.estimated_duration_minutes: # used_minutes += case.estimated_duration_minutes # # context['utilization_percentage'] = ( # (used_minutes / total_block_minutes) * 100 if total_block_minutes > 0 else 0 # ) # # return context # # # class ORBlockCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new OR block. # """ # model = ORBlock # form_class = ORBlockForm # template_name = 'operating_theatre/or_block_form.html' # permission_required = 'operating_theatre.add_orblock' # success_url = reverse_lazy('operating_theatre:or_block_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='OR_BLOCK_CREATED', # model='ORBlock', # object_id=str(self.object.id), # details={ # 'date': str(self.object.date), # 'operating_room': self.object.operating_room.room_number, # 'surgeon': f"{self.object.surgeon.first_name} {self.object.surgeon.last_name}" # } # ) # # messages.success(self.request, 'OR block created successfully.') # return response # # # class ORBlockUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an OR block (limited to notes and time adjustments). # """ # model = ORBlock # fields = ['start_time', 'end_time', 'notes'] # Restricted fields # template_name = 'operating_theatre/or_block_update_form.html' # permission_required = 'operating_theatre.change_orblock' # # def get_queryset(self): # return ORBlock.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('operating_theatre:or_block_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='OR_BLOCK_UPDATED', # model='ORBlock', # object_id=str(self.object.id), # details={ # 'date': str(self.object.date), # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'OR block updated successfully.') # return response # # # # ============================================================================ # # SURGICAL CASE VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class SurgicalCaseListView(LoginRequiredMixin, ListView): # """ # List all surgical cases with filtering and search. # """ # model = SurgicalCase # template_name = 'operating_theatre/surgical_case_list.html' # context_object_name = 'surgical_cases' # paginate_by = 25 # # def get_queryset(self): # queryset = SurgicalCase.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(procedure_name__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by priority # priority = self.request.GET.get('priority') # if priority: # queryset = queryset.filter(priority=priority) # # # Filter by surgeon # surgeon_id = self.request.GET.get('surgeon') # if surgeon_id: # queryset = queryset.filter(primary_surgeon_id=surgeon_id) # # # Filter by operating room # room_id = self.request.GET.get('room') # if room_id: # queryset = queryset.filter(operating_room_id=room_id) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(scheduled_start_time__date__gte=date_from) # if date_to: # queryset = queryset.filter(scheduled_start_time__date__lte=date_to) # # return queryset.select_related( # 'patient', 'primary_surgeon', 'operating_room' # ).order_by('-scheduled_start_time') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': SurgicalCase._meta.get_field('status').choices, # 'priorities': SurgicalCase._meta.get_field('priority').choices, # 'operating_rooms': OperatingRoom.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('room_number'), # }) # return context # # # class SurgicalCaseDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a surgical case. # """ # model = SurgicalCase # template_name = 'operating_theatre/surgical_case_detail.html' # context_object_name = 'surgical_case' # # def get_queryset(self): # return SurgicalCase.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # surgical_case = self.object # # # Get surgical notes for this case # context['surgical_notes'] = surgical_case.surgical_notes.all().order_by('-created_at') # # # Get equipment usage for this case # context['equipment_usage'] = EquipmentUsage.objects.filter( # surgical_case=surgical_case, # tenant=self.request.user.tenant # ).order_by('-start_time') # # # Calculate actual duration if case is completed # if surgical_case.actual_start_time and surgical_case.actual_end_time: # context['actual_duration'] = surgical_case.actual_end_time - surgical_case.actual_start_time # # return context # # # class SurgicalCaseCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new surgical case. # """ # model = SurgicalCase # form_class = SurgicalCaseForm # template_name = 'operating_theatre/surgical_case_form.html' # permission_required = 'operating_theatre.add_surgicalcase' # success_url = reverse_lazy('operating_theatre:surgical_case_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SURGICAL_CASE_CREATED', # model='SurgicalCase', # object_id=str(self.object.case_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'procedure_name': self.object.procedure_name, # 'priority': self.object.priority # } # ) # # messages.success(self.request, 'Surgical case created successfully.') # return response # # # class SurgicalCaseUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update surgical case (limited to status and notes after surgery starts). # """ # model = SurgicalCase # template_name = 'operating_theatre/surgical_case_update_form.html' # permission_required = 'operating_theatre.change_surgicalcase' # # def get_queryset(self): # return SurgicalCase.objects.filter(tenant=self.request.user.tenant) # # def get_form_class(self): # # Limit fields based on case status # if self.object.status in ['IN_PROGRESS', 'COMPLETED']: # # Limited fields for cases that have started # class RestrictedSurgicalCaseForm(SurgicalCaseForm): # class Meta(SurgicalCaseForm.Meta): # fields = ['status', 'notes', 'complications'] # # return RestrictedSurgicalCaseForm # else: # return SurgicalCaseForm # # def get_success_url(self): # return reverse('operating_theatre:surgical_case_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SURGICAL_CASE_UPDATED', # model='SurgicalCase', # object_id=str(self.object.case_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Surgical case updated successfully.') # return response # # # # ============================================================================ # # SURGICAL NOTE VIEWS (APPEND-ONLY - Clinical Records) # # ============================================================================ # # class SurgicalNoteListView(LoginRequiredMixin, ListView): # """ # List all surgical notes with filtering and search. # """ # model = SurgicalNote # template_name = 'operating_theatre/surgical_note_list.html' # context_object_name = 'surgical_notes' # paginate_by = 25 # # def get_queryset(self): # queryset = SurgicalNote.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(surgical_case__patient__first_name__icontains=search) | # Q(surgical_case__patient__last_name__icontains=search) | # Q(surgical_case__patient__mrn__icontains=search) | # Q(surgical_case__procedure_name__icontains=search) | # Q(note_content__icontains=search) # ) # # # Filter by note type # note_type = self.request.GET.get('note_type') # if note_type: # queryset = queryset.filter(note_type=note_type) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by surgeon # surgeon_id = self.request.GET.get('surgeon') # if surgeon_id: # queryset = queryset.filter(surgeon_id=surgeon_id) # # return queryset.select_related( # 'surgical_case__patient', 'surgeon', 'template' # ).order_by('-created_at') # # 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, # }) # return context # # # class SurgicalNoteDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a surgical note. # """ # model = SurgicalNote # template_name = 'operating_theatre/surgical_note_detail.html' # context_object_name = 'surgical_note' # # def get_queryset(self): # return SurgicalNote.objects.filter(tenant=self.request.user.tenant) # # # class SurgicalNoteCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new surgical note. # """ # model = SurgicalNote # form_class = SurgicalNoteForm # template_name = 'operating_theatre/surgical_note_form.html' # permission_required = 'operating_theatre.add_surgicalnote' # success_url = reverse_lazy('operating_theatre:surgical_note_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.surgeon = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SURGICAL_NOTE_CREATED', # model='SurgicalNote', # object_id=str(self.object.note_id), # details={ # 'patient_name': f"{self.object.surgical_case.patient.first_name} {self.object.surgical_case.patient.last_name}", # 'note_type': self.object.note_type # } # ) # # messages.success(self.request, 'Surgical note created successfully.') # return response # # # # Note: No UpdateView or DeleteView for SurgicalNote - Append-only for clinical records # # # # ============================================================================ # # EQUIPMENT USAGE VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class EquipmentUsageListView(LoginRequiredMixin, ListView): # """ # List all equipment usage records with filtering and search. # """ # model = EquipmentUsage # template_name = 'operating_theatre/equipment_usage_list.html' # context_object_name = 'equipment_usage_records' # paginate_by = 25 # # def get_queryset(self): # queryset = EquipmentUsage.objects.filter(tenant=self.request.user.tenant) # # # Filter by equipment type # equipment_type = self.request.GET.get('equipment_type') # if equipment_type: # queryset = queryset.filter(equipment_type=equipment_type) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by operating room # room_id = self.request.GET.get('room') # if room_id: # queryset = queryset.filter(operating_room_id=room_id) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(start_time__date__gte=date_from) # if date_to: # queryset = queryset.filter(start_time__date__lte=date_to) # # return queryset.select_related( # 'operating_room', 'surgical_case__patient' # ).order_by('-start_time') # # 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, # 'operating_rooms': OperatingRoom.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('room_number'), # }) # return context # # # class EquipmentUsageDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about equipment usage. # """ # model = EquipmentUsage # template_name = 'operating_theatre/equipment_usage_detail.html' # context_object_name = 'equipment_usage' # # def get_queryset(self): # return EquipmentUsage.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # equipment_usage = self.object # # # Calculate usage duration if ended # if equipment_usage.end_time: # context['usage_duration'] = equipment_usage.end_time - equipment_usage.start_time # # return context # # # class EquipmentUsageCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new equipment usage record. # """ # model = EquipmentUsage # form_class = EquipmentUsageForm # template_name = 'operating_theatre/equipment_usage_form.html' # permission_required = 'operating_theatre.add_equipmentusage' # success_url = reverse_lazy('operating_theatre:equipment_usage_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='EQUIPMENT_USAGE_CREATED', # model='EquipmentUsage', # object_id=str(self.object.id), # details={ # 'equipment_name': self.object.equipment_name, # 'equipment_type': self.object.equipment_type, # 'operating_room': self.object.operating_room.room_number # } # ) # # messages.success(self.request, 'Equipment usage record created successfully.') # return response # # # class EquipmentUsageUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update equipment usage record (limited to status and end time). # """ # model = EquipmentUsage # fields = ['status', 'end_time', 'notes'] # Restricted fields # template_name = 'operating_theatre/equipment_usage_update_form.html' # permission_required = 'operating_theatre.change_equipmentusage' # # def get_queryset(self): # return EquipmentUsage.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('operating_theatre:equipment_usage_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='EQUIPMENT_USAGE_UPDATED', # model='EquipmentUsage', # object_id=str(self.object.id), # details={ # 'equipment_name': self.object.equipment_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Equipment usage record updated successfully.') # return response # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @login_required # def operating_theatre_stats(request): # """ # HTMX endpoint for operating theatre statistics. # """ # tenant = request.user.tenant # today = timezone.now().date() # # stats = { # 'rooms_available': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True, # status='AVAILABLE' # ).count(), # 'rooms_in_use': OperatingRoom.objects.filter( # tenant=tenant, # is_active=True, # status='IN_USE' # ).count(), # 'cases_in_progress': SurgicalCase.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'cases_completed_today': SurgicalCase.objects.filter( # tenant=tenant, # actual_end_time__date=today, # status='COMPLETED' # ).count(), # 'emergency_cases_today': SurgicalCase.objects.filter( # tenant=tenant, # scheduled_start_time__date=today, # priority='EMERGENCY' # ).count(), # } # # return render(request, 'operating_theatre/partials/or_stats.html', {'stats': stats}) # # # @login_required # def start_case(request, case_id): # """ # Start a surgical case. # """ # if request.method == 'POST': # case = get_object_or_404( # SurgicalCase, # id=case_id, # tenant=request.user.tenant # ) # # case.status = 'IN_PROGRESS' # case.actual_start_time = timezone.now() # case.save() # # # Update room status # if case.operating_room: # case.operating_room.status = 'IN_USE' # case.operating_room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_CASE_STARTED', # model='SurgicalCase', # object_id=str(case.case_id), # details={ # 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", # 'procedure_name': case.procedure_name # } # ) # # messages.success(request, 'Surgical case started successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) # # return redirect('operating_theatre:surgical_case_detail', pk=case.pk) # # return JsonResponse({'success': False}) # # # @login_required # def complete_case(request, case_id): # """ # Complete a surgical case. # """ # if request.method == 'POST': # case = get_object_or_404( # SurgicalCase, # id=case_id, # tenant=request.user.tenant # ) # # case.status = 'COMPLETED' # case.actual_end_time = timezone.now() # case.save() # # # Update room status # if case.operating_room: # case.operating_room.status = 'CLEANING' # case.operating_room.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SURGICAL_CASE_COMPLETED', # model='SurgicalCase', # object_id=str(case.case_id), # details={ # 'patient_name': f"{case.patient.first_name} {case.patient.last_name}", # 'procedure_name': case.procedure_name, # 'duration': str(case.actual_end_time - case.actual_start_time) if case.actual_start_time else None # } # ) # # messages.success(request, 'Surgical case completed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'operating_theatre/partials/case_status.html', {'case': case}) # # return redirect('operating_theatre:surgical_case_detail', pk=case.pk) # # return JsonResponse({'success': False}) # """ # Accounts app views for hospital management system with comprehensive CRUD operations. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth import authenticate, login, logout # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.views.generic import ( # TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.http import JsonResponse # from django.contrib import messages # from django.db.models import Q, Count # from django.utils import timezone # from django.urls import reverse_lazy, reverse # from django.core.paginator import Paginator # from datetime import timedelta # from .models import * # from .forms import * # from core.utils import AuditLogger # # # ============================================================================ # # USER VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # from core.utils import AuditLogger # # # # ============================================================================ # # USER VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class UserListView(LoginRequiredMixin, ListView): # """ # User listing view. # """ # model = User # template_name = 'accounts/user_list.html' # context_object_name = 'users' # paginate_by = 25 # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return User.objects.none() # # queryset = User.objects.filter(tenant=tenant) # # # Apply filters # role = self.request.GET.get('role') # if role: # queryset = queryset.filter(role=role) # # department = self.request.GET.get('department') # if department: # queryset = queryset.filter(department=department) # # status = self.request.GET.get('status') # if status == 'active': # queryset = queryset.filter(is_active=True) # elif status == 'inactive': # queryset = queryset.filter(is_active=False) # elif status == 'pending': # queryset = queryset.filter(is_approved=False) # # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(username__icontains=search) | # Q(email__icontains=search) | # Q(first_name__icontains=search) | # Q(last_name__icontains=search) | # Q(employee_id__icontains=search) # ) # # return queryset.order_by('last_name', 'first_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # if tenant: # # Get filter options # context.update({ # 'roles': User.objects.filter(tenant=tenant).values_list('role', flat=True).distinct(), # 'departments': User.objects.filter(tenant=tenant).values_list('department', flat=True).distinct(), # 'search_form': AccountsSearchForm(self.request.GET), # }) # # return context # # # class UserDetailView(LoginRequiredMixin, DetailView): # """ # User detail view. # """ # model = User # template_name = 'accounts/user_detail.html' # context_object_name = 'user_profile' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return User.objects.none() # return User.objects.filter(tenant=tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # user_profile = self.get_object() # # # Get user's sessions # context['active_sessions'] = UserSession.objects.filter( # user=user_profile, # is_active=True # ).order_by('-created_at') # # # Get user's two-factor devices # context['two_factor_devices'] = TwoFactorDevice.objects.filter( # user=user_profile, # is_active=True # ).order_by('-created_at') # # # Get user's social accounts # context['social_accounts'] = SocialAccount.objects.filter( # user=user_profile, # is_active=True # ).order_by('-created_at') # # # Get password history # context['password_history'] = PasswordHistory.objects.filter( # user=user_profile # ).order_by('-created_at')[:5] # # # Get recent audit logs # from core.models import AuditLogEntry # context['recent_activity'] = AuditLogEntry.objects.filter( # tenant=user_profile.tenant, # user=user_profile # ).order_by('-timestamp')[:10] # # return context # # # class UserCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create new user. # """ # model = User # form_class = UserCreateForm # template_name = 'accounts/user_create.html' # permission_required = 'accounts.add_user' # success_url = reverse_lazy('accounts:user_list') # # def form_valid(self, form): # # Set tenant # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log user creation # AuditLogger.log_event( # tenant=form.instance.tenant, # event_type='CREATE', # event_category='USER_MANAGEMENT', # action='Create User', # description=f'Created new user: {self.object.username}', # user=self.request.user, # content_object=self.object, # request=self.request # ) # # messages.success(self.request, f'User "{self.object.username}" created successfully.') # return response # # # class UserUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update user information. # """ # model = User # form_class = UserForm # template_name = 'accounts/user_form.html' # permission_required = 'accounts.change_user' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return User.objects.none() # return User.objects.filter(tenant=tenant) # # def get_success_url(self): # return reverse('accounts:user_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log user update # AuditLogger.log_event( # tenant=self.object.tenant, # event_type='UPDATE', # event_category='USER_MANAGEMENT', # action='Update User', # description=f'Updated user: {self.object.username}', # user=self.request.user, # content_object=self.object, # request=self.request # ) # # messages.success(self.request, f'User "{self.object.username}" updated successfully.') # return response # # # class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete user (soft delete to inactive). # """ # model = User # template_name = 'accounts/user_confirm_delete.html' # permission_required = 'accounts.delete_user' # success_url = reverse_lazy('accounts:user_list') # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return User.objects.none() # return User.objects.filter(tenant=tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Soft delete - set to inactive # self.object.is_active = False # self.object.save() # # # Log user deletion # AuditLogger.log_event( # tenant=self.object.tenant, # event_type='DELETE', # event_category='USER_MANAGEMENT', # action='Deactivate User', # description=f'Deactivated user: {self.object.username}', # user=request.user, # content_object=self.object, # request=request # ) # # messages.success(request, f'User "{self.object.username}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # TWO FACTOR DEVICE VIEWS (FULL CRUD - Security Data) # # ============================================================================ # # class TwoFactorDeviceListView(LoginRequiredMixin, ListView): # """ # List two-factor devices. # """ # model = TwoFactorDevice # template_name = 'accounts/two_factor_device_list.html' # context_object_name = 'devices' # paginate_by = 20 # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return TwoFactorDevice.objects.none() # # queryset = TwoFactorDevice.objects.filter(user__tenant=tenant) # # # Apply filters # user_id = self.request.GET.get('user_id') # if user_id: # queryset = queryset.filter(user_id=user_id) # # device_type = self.request.GET.get('device_type') # if device_type: # queryset = queryset.filter(device_type=device_type) # # status = self.request.GET.get('status') # if status == 'active': # queryset = queryset.filter(is_active=True) # elif status == 'inactive': # queryset = queryset.filter(is_active=False) # # return queryset.order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # if tenant: # context.update({ # 'device_types': TwoFactorDevice.DEVICE_TYPE_CHOICES, # 'users': User.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'), # }) # # return context # # # class TwoFactorDeviceDetailView(LoginRequiredMixin, DetailView): # """ # Display two-factor device details. # """ # model = TwoFactorDevice # template_name = 'accounts/two_factor_device_detail.html' # context_object_name = 'device' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return TwoFactorDevice.objects.none() # return TwoFactorDevice.objects.filter(user__tenant=tenant) # # # class TwoFactorDeviceCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create new two-factor device. # """ # model = TwoFactorDevice # form_class = TwoFactorDeviceForm # template_name = 'accounts/two_factor_device_form.html' # permission_required = 'accounts.add_twofactordevice' # success_url = reverse_lazy('accounts:two_factor_device_list') # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # response = super().form_valid(form) # # # Generate secret key for TOTP devices # if form.instance.device_type == 'TOTP': # import secrets # form.instance.secret_key = secrets.token_urlsafe(32) # form.instance.save() # # # Log device creation # AuditLogger.log_event( # tenant=self.request.user.tenant, # event_type='CREATE', # event_category='SECURITY', # action='Create Two-Factor Device', # description=f'Created two-factor device: {self.object.name}', # user=self.request.user, # content_object=self.object, # request=self.request # ) # # messages.success(self.request, f'Two-factor device "{self.object.name}" created successfully.') # return response # # # class TwoFactorDeviceUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update two-factor device. # """ # model = TwoFactorDevice # form_class = TwoFactorDeviceForm # template_name = 'accounts/two_factor_device_form.html' # permission_required = 'accounts.change_twofactordevice' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return TwoFactorDevice.objects.none() # return TwoFactorDevice.objects.filter(user__tenant=tenant) # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_success_url(self): # return reverse('accounts:two_factor_device_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log device update # AuditLogger.log_event( # tenant=self.request.user.tenant, # event_type='UPDATE', # event_category='SECURITY', # action='Update Two-Factor Device', # description=f'Updated two-factor device: {self.object.name}', # user=self.request.user, # content_object=self.object, # request=self.request # ) # # messages.success(self.request, f'Two-factor device "{self.object.name}" updated successfully.') # return response # # # class TwoFactorDeviceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete two-factor device. # """ # model = TwoFactorDevice # template_name = 'accounts/two_factor_device_confirm_delete.html' # permission_required = 'accounts.delete_twofactordevice' # success_url = reverse_lazy('accounts:two_factor_device_list') # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return TwoFactorDevice.objects.none() # return TwoFactorDevice.objects.filter(user__tenant=tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # device_name = self.object.name # # # Log device deletion # AuditLogger.log_event( # tenant=getattr(request, 'tenant', None), # event_type='DELETE', # event_category='SECURITY', # action='Delete Two-Factor Device', # description=f'Deleted two-factor device: {device_name}', # user=request.user, # content_object=self.object, # request=request # ) # # messages.success(request, f'Two-factor device "{device_name}" deleted successfully.') # return super().delete(request, *args, **kwargs) # # # # ============================================================================ # # SOCIAL ACCOUNT VIEWS (FULL CRUD - Integration Data) # # ============================================================================ # # class SocialAccountListView(LoginRequiredMixin, ListView): # """ # List social accounts. # """ # model = SocialAccount # template_name = 'accounts/social_account_list.html' # context_object_name = 'social_accounts' # paginate_by = 20 # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return SocialAccount.objects.none() # # queryset = SocialAccount.objects.filter(user__tenant=tenant) # # # Apply filters # user_id = self.request.GET.get('user_id') # if user_id: # queryset = queryset.filter(user_id=user_id) # # provider = self.request.GET.get('provider') # if provider: # queryset = queryset.filter(provider=provider) # # status = self.request.GET.get('status') # if status == 'active': # queryset = queryset.filter(is_active=True) # elif status == 'inactive': # queryset = queryset.filter(is_active=False) # # return queryset.order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # if tenant: # context.update({ # 'providers': SocialAccount.objects.filter( # user__tenant=tenant # ).values_list('provider', flat=True).distinct(), # 'users': User.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'), # }) # # return context # # # class SocialAccountDetailView(LoginRequiredMixin, DetailView): # """ # Display social account details. # """ # model = SocialAccount # template_name = 'accounts/social_account_detail.html' # context_object_name = 'social_account' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return SocialAccount.objects.none() # return SocialAccount.objects.filter(user__tenant=tenant) # # # class SocialAccountCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create new social account. # """ # model = SocialAccount # form_class = SocialAccountForm # template_name = 'accounts/social_account_form.html' # permission_required = 'accounts.add_socialaccount' # success_url = reverse_lazy('accounts:social_account_list') # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # response = super().form_valid(form) # # # Log social account creation # AuditLogger.log_event( # tenant=self.request.user.tenant, # event_type='CREATE', # event_category='INTEGRATION', # action='Create Social Account', # description=f'Created social account: {self.object.provider}', # user=self.request.user, # content_object=self.object, # request=self.request # ) # # messages.success(self.request, f'Social account "{self.object.provider}" created successfully.') # return response # # # class SocialAccountUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update social account. # """ # model = SocialAccount # form_class = SocialAccountForm # template_name = 'accounts/social_account_form.html' # permission_required = 'accounts.change_socialaccount' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return SocialAccount.objects.none() # return SocialAccount.objects.filter(user__tenant=tenant) # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_success_url(self): # return reverse('accounts:social_account_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log social account update # AuditLogger.log_event( # tenant=self.request.user.tenant, # event_type='UPDATE', # event_category='INTEGRATION', # action='Update Social Account', # description=f'Updated social account: {self.object.provider}', # user=self.request.user, # content_object=self.object, # request=self.request # ) # # messages.success(self.request, f'Social account "{self.object.provider}" updated successfully.') # return response # # # class SocialAccountDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete social account. # """ # model = SocialAccount # template_name = 'accounts/social_account_confirm_delete.html' # permission_required = 'accounts.delete_socialaccount' # success_url = reverse_lazy('accounts:social_account_list') # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return SocialAccount.objects.none() # return SocialAccount.objects.filter(user__tenant=tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # provider = self.object.provider # # # Log social account deletion # AuditLogger.log_event( # tenant=getattr(request, 'tenant', None), # event_type='DELETE', # event_category='INTEGRATION', # action='Delete Social Account', # description=f'Deleted social account: {provider}', # user=request.user, # content_object=self.object, # request=request # ) # # messages.success(request, f'Social account "{provider}" deleted successfully.') # return super().delete(request, *args, **kwargs) # # # # ============================================================================ # # USER SESSION VIEWS (READ-ONLY - System Generated) # # ============================================================================ # # class UserSessionListView(LoginRequiredMixin, ListView): # """ # List user sessions. # """ # model = UserSession # template_name = 'accounts/user_session_list.html' # context_object_name = 'sessions' # paginate_by = 50 # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return UserSession.objects.none() # # queryset = UserSession.objects.filter(user__tenant=tenant) # # # Apply filters # user_id = self.request.GET.get('user_id') # if user_id: # queryset = queryset.filter(user_id=user_id) # # status = self.request.GET.get('status') # if status == 'active': # queryset = queryset.filter(is_active=True) # elif status == 'expired': # queryset = queryset.filter(expires_at__lt=timezone.now()) # # device_type = self.request.GET.get('device_type') # if device_type: # queryset = queryset.filter(device_type=device_type) # # date_from = self.request.GET.get('date_from') # if date_from: # queryset = queryset.filter(created_at__gte=date_from) # # date_to = self.request.GET.get('date_to') # if date_to: # queryset = queryset.filter(created_at__lte=date_to) # # return queryset.order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # if tenant: # context.update({ # 'device_types': UserSession.objects.filter( # user__tenant=tenant # ).values_list('device_type', flat=True).distinct(), # 'users': User.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'), # }) # # return context # # # class UserSessionDetailView(LoginRequiredMixin, DetailView): # """ # Display user session details. # """ # model = UserSession # template_name = 'accounts/user_session_detail.html' # context_object_name = 'session' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return UserSession.objects.none() # return UserSession.objects.filter(user__tenant=tenant) # # # # ============================================================================ # # PASSWORD HISTORY VIEWS (READ-ONLY - System Generated) # # ============================================================================ # # class PasswordHistoryListView(LoginRequiredMixin, ListView): # """ # List password history. # """ # model = PasswordHistory # template_name = 'accounts/password_history_list.html' # context_object_name = 'password_history' # paginate_by = 50 # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return PasswordHistory.objects.none() # # queryset = PasswordHistory.objects.filter(user__tenant=tenant) # # # Apply filters # user_id = self.request.GET.get('user_id') # if user_id: # queryset = queryset.filter(user_id=user_id) # # date_from = self.request.GET.get('date_from') # if date_from: # queryset = queryset.filter(created_at__gte=date_from) # # date_to = self.request.GET.get('date_to') # if date_to: # queryset = queryset.filter(created_at__lte=date_to) # # return queryset.order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # # if tenant: # context.update({ # 'users': User.objects.filter(tenant=tenant, is_active=True).order_by('last_name', 'first_name'), # }) # # return context # # # class PasswordHistoryDetailView(LoginRequiredMixin, DetailView): # """ # Display password history details. # """ # model = PasswordHistory # template_name = 'accounts/password_history_detail.html' # context_object_name = 'password_history' # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return PasswordHistory.objects.none() # return PasswordHistory.objects.filter(user__tenant=tenant) # # # # ============================================================================ # # PROFILE AND SESSION MANAGEMENT VIEWS # # ============================================================================ # # class UserProfileView(LoginRequiredMixin, TemplateView): # """ # User profile management view. # """ # template_name = 'accounts/user_profile.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # user = self.request.user # # # Get user's sessions # context['active_sessions'] = UserSession.objects.filter( # user=user, # is_active=True # ).order_by('-created_at') # # # Get user's two-factor devices # context['two_factor_devices'] = TwoFactorDevice.objects.filter( # user=user, # is_active=True # ).order_by('-created_at') # # # Get user's social accounts # context['social_accounts'] = SocialAccount.objects.filter( # user=user, # is_active=True # ).order_by('-created_at') # # # Get password history # context['password_history'] = PasswordHistory.objects.filter( # user=user # ).order_by('-created_at')[:5] # # return context # # # class SessionManagementView(LoginRequiredMixin, ListView): # """ # Session management view. # """ # model = UserSession # template_name = 'accounts/session_management.html' # context_object_name = 'sessions' # paginate_by = 50 # # def get_queryset(self): # tenant = self.request.user.tenant # if not tenant: # return UserSession.objects.none() # # queryset = UserSession.objects.filter(user__tenant=tenant) # # # Apply filters # user_id = self.request.GET.get('user_id') # if user_id: # queryset = queryset.filter(user_id=user_id) # # status = self.request.GET.get('status') # if status == 'active': # queryset = queryset.filter(is_active=True) # elif status == 'expired': # queryset = queryset.filter(expires_at__lt=timezone.now()) # # device_type = self.request.GET.get('device_type') # if device_type: # queryset = queryset.filter(device_type=device_type) # # return queryset.order_by('-created_at') # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @login_required # def user_search(request): # """ # HTMX view for user search. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # search_query = request.GET.get('search', '') # queryset = User.objects.filter(tenant=tenant) # # if search_query: # queryset = queryset.filter( # Q(username__icontains=search_query) | # Q(email__icontains=search_query) | # Q(first_name__icontains=search_query) | # Q(last_name__icontains=search_query) | # Q(employee_id__icontains=search_query) # ) # # users = queryset.order_by('last_name', 'first_name')[:20] # # return render(request, 'accounts/partials/user_list.html', { # 'users': users # }) # # # @login_required # def user_stats(request): # """ # HTMX view for user statistics. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # # Calculate user statistics # total_users = User.objects.filter(tenant=tenant).count() # active_users = User.objects.filter(tenant=tenant, is_active=True).count() # pending_approval = User.objects.filter(tenant=tenant, is_approved=False).count() # # # Active sessions in last 24 hours # yesterday = timezone.now() - timedelta(days=1) # active_sessions = UserSession.objects.filter( # user__tenant=tenant, # last_activity_at__gte=yesterday, # is_active=True # ).count() # # # Users by role # role_stats = User.objects.filter(tenant=tenant).values('role').annotate( # count=Count('id') # ).order_by('-count')[:5] # # # Two-factor adoption # two_factor_users = User.objects.filter( # tenant=tenant, # twofactordevice__is_active=True # ).distinct().count() # # stats = { # 'total_users': total_users, # 'active_users': active_users, # 'pending_approval': pending_approval, # 'active_sessions': active_sessions, # 'two_factor_users': two_factor_users, # 'role_stats': role_stats, # } # # return render(request, 'accounts/partials/user_stats.html', {'stats': stats}) # # # @login_required # def session_list(request): # """ # HTMX view for session list. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # user_id = request.GET.get('user_id') # if user_id: # sessions = UserSession.objects.filter( # user_id=user_id, # user__tenant=tenant # ).order_by('-created_at')[:10] # else: # sessions = UserSession.objects.filter( # user__tenant=tenant, # is_active=True # ).order_by('-created_at')[:20] # # return render(request, 'accounts/partials/session_list.html', { # 'sessions': sessions # }) # # # @login_required # def two_factor_setup(request): # """ # HTMX view for two-factor authentication setup. # """ # user = request.user # # if request.method == 'POST': # device_type = request.POST.get('device_type') # device_name = request.POST.get('device_name') # # if device_type and device_name: # # Create new two-factor device # device = TwoFactorDevice.objects.create( # user=user, # name=device_name, # device_type=device_type, # phone_number=request.POST.get('phone_number'), # email_address=request.POST.get('email_address'), # ) # # # Generate secret key for TOTP devices # if device_type == 'TOTP': # import secrets # device.secret_key = secrets.token_urlsafe(32) # device.save() # # # Log the setup # AuditLogger.log_event( # tenant=getattr(request, 'tenant', None), # event_type='CREATE', # event_category='SECURITY', # action='Setup Two-Factor Device', # description=f'User setup two-factor device: {device_name}', # user=request.user, # content_object=device, # request=request # ) # # return JsonResponse({'status': 'created', 'device_id': str(device.device_id)}) # # return render(request, 'accounts/partials/two_factor_setup.html') # # # # ============================================================================ # # ACTION VIEWS FOR WORKFLOW OPERATIONS # # ============================================================================ # # @login_required # def end_session(request, session_id): # """ # End a user session. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # session = get_object_or_404( # UserSession, # session_id=session_id, # user__tenant=tenant # ) # # # End the session # session.end_session() # # # Log the action # AuditLogger.log_event( # tenant=tenant, # event_type='UPDATE', # event_category='AUTHENTICATION', # action='End User Session', # description=f'Ended session for user: {session.user.username}', # user=request.user, # content_object=session, # additional_data={ # 'target_user': session.user.username, # 'session_ip': session.ip_address, # }, # request=request # ) # # messages.success(request, f'Session for {session.user.username} ended successfully.') # return redirect('accounts:session_management') # # # @login_required # def user_profile_update(request): # """ # HTMX view for user profile update. # """ # if request.method == 'POST': # user = request.user # # # Update basic information # user.first_name = request.POST.get('first_name', user.first_name) # user.last_name = request.POST.get('last_name', user.last_name) # user.email = request.POST.get('email', user.email) # user.phone_number = request.POST.get('phone_number', user.phone_number) # user.mobile_number = request.POST.get('mobile_number', user.mobile_number) # user.bio = request.POST.get('bio', user.bio) # # # Update preferences # user.timezone = request.POST.get('timezone', user.timezone) # user.language = request.POST.get('language', user.language) # user.theme = request.POST.get('theme', user.theme) # # user.save() # # # Log the update # AuditLogger.log_event( # tenant=getattr(request, 'tenant', None), # event_type='UPDATE', # event_category='DATA_MODIFICATION', # action='Update User Profile', # description=f'User updated their profile: {user.username}', # user=request.user, # content_object=user, # request=request # ) # # messages.success(request, 'Profile updated successfully.') # return JsonResponse({'status': 'success'}) # # return JsonResponse({'error': 'Invalid request'}, status=400) # # # @login_required # def remove_two_factor_device(request, device_id): # """ # Remove a two-factor device. # """ # device = get_object_or_404( # TwoFactorDevice, # device_id=device_id, # user=request.user # ) # # device_name = device.name # device.delete() # # # Log the removal # AuditLogger.log_event( # tenant=getattr(request, 'tenant', None), # event_type='DELETE', # event_category='SECURITY', # action='Remove Two-Factor Device', # description=f'User removed two-factor device: {device_name}', # user=request.user, # additional_data={'device_name': device_name}, # request=request # ) # # messages.success(request, f'Two-factor device "{device_name}" removed successfully.') # return redirect('accounts:user_profile') # # # @login_required # def user_activity_log(request, user_id): # """ # HTMX view for user activity log. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # user_profile = get_object_or_404(User, id=user_id, tenant=tenant) # # # Get recent audit logs for this user # from core.models import AuditLogEntry # audit_logs = AuditLogEntry.objects.filter( # tenant=tenant, # user=user_profile # ).order_by('-timestamp')[:20] # # return render(request, 'accounts/partials/user_activity_log.html', { # 'audit_logs': audit_logs, # 'user_profile': user_profile # }) # # # @login_required # def approve_user(request, pk): # """ # Approve a user account. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # messages.error(request, 'No tenant found.') # return redirect('accounts:user_list') # # user = get_object_or_404(User, pk=pk, tenant=tenant) # # user.is_approved = True # user.save() # # # Log approval # AuditLogger.log_event( # tenant=tenant, # event_type='UPDATE', # event_category='USER_MANAGEMENT', # action='Approve User', # description=f'Approved user: {user.username}', # user=request.user, # content_object=user, # request=request # ) # # messages.success(request, f'User "{user.username}" approved successfully.') # return redirect('accounts:user_detail', pk=pk) # # # @login_required # def activate_user(request, pk): # """ # Activate a user account. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # messages.error(request, 'No tenant found.') # return redirect('accounts:user_list') # # user = get_object_or_404(User, pk=pk, tenant=tenant) # # user.is_active = True # user.save() # # # Log activation # AuditLogger.log_event( # tenant=tenant, # event_type='UPDATE', # event_category='USER_MANAGEMENT', # action='Activate User', # description=f'Activated user: {user.username}', # user=request.user, # content_object=user, # request=request # ) # # messages.success(request, f'User "{user.username}" activated successfully.') # return redirect('accounts:user_detail', pk=pk) # # # @login_required # def reset_user_password(request, pk): # """ # Reset user password. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # messages.error(request, 'No tenant found.') # return redirect('accounts:user_list') # # user = get_object_or_404(User, pk=pk, tenant=tenant) # # # Generate temporary password # import secrets # import string # temp_password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12)) # # # Set password and force change on next login # user.set_password(temp_password) # user.password_change_required = True # user.save() # # # Log password reset # AuditLogger.log_event( # tenant=tenant, # event_type='UPDATE', # event_category='SECURITY', # action='Reset User Password', # description=f'Reset password for user: {user.username}', # user=request.user, # content_object=user, # request=request # ) # # messages.success(request, f'Password reset for "{user.username}". Temporary password: {temp_password}') # return redirect('accounts:user_detail', pk=pk) # # def available_slots(request): # """ # HTMX view for available slots. # """ # tenant = getattr(request, 'tenant', None) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # provider = request.GET.get('provider') # date_str = request.GET.get('date') # # if not provider or not date_str: # return render(request, 'appointments/partials/available_slots.html', {'slots': []}) # # try: # selected_date = datetime.strptime(date_str, '%Y-%m-%d').date() # except ValueError: # return render(request, 'appointments/partials/available_slots.html', {'slots': []}) # # # Get available slots for the provider on the selected date # slots = SlotAvailability.objects.filter( # provider_id=provider, # provider__tenant=tenant, # date=selected_date, # # is_available=True # ).order_by('start_time') # # return render(request, 'appointments/partials/available_slots.html', { # 'slots': slots, # 'selected_date': selected_date # }) # class AppointmentListView(LoginRequiredMixin, ListView): # """ # List view for appointments. # """ # model = AppointmentRequest # template_name = 'appointments/requests/appointment_list.html' # context_object_name = 'appointments' # paginate_by = 20 # # def get_queryset(self): # return AppointmentRequest.objects.filter( # tenant=self.request.user.tenant # ).order_by('-created_at') # # # class AppointmentDetailView(LoginRequiredMixin, DetailView): # """ # Detail view for appointments. # """ # model = AppointmentRequest # template_name = 'appointments/requests/appointment_request_detail.html' # context_object_name = 'appointment' # # def get_queryset(self): # return AppointmentRequest.objects.filter( # tenant=self.request.user.tenant # ) # # from django.shortcuts import render, get_object_or_404, redirect # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.contrib.auth.decorators import login_required # from django.http import JsonResponse, HttpResponseForbidden # from django.urls import reverse, reverse_lazy # from django.utils import timezone # from django.utils.translation import gettext_lazy as _ # from django.contrib import messages # from django.db.models import Q, Count, Avg # from django.template.loader import render_to_string # from datetime import datetime, timedelta # # from .models import ( # AppointmentRequest, SlotAvailability, WaitingQueue, # QueueEntry, TelemedicineSession, AppointmentTemplate # ) # from .forms import ( # AppointmentRequestForm, SlotAvailabilityForm, WaitingQueueForm, # QueueEntryForm, TelemedicineSessionForm, AppointmentTemplateForm, # AppointmentSearchForm # ) # from patients.models import PatientProfile # from accounts.models import User # from core.utils import get_tenant_from_request # # # class TenantRequiredMixin: # """Mixin to require a tenant for views.""" # # def dispatch(self, request, *args, **kwargs): # """Check for tenant before dispatching.""" # self.tenant = get_tenant_from_request(request) # if not self.tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # return super().dispatch(request, *args, **kwargs) # # # # --- AppointmentRequest Views --- # # class AppointmentRequestListView(LoginRequiredMixin, TenantRequiredMixin, ListView): # """List view for appointment requests.""" # model = AppointmentRequest # template_name = 'appointments/appointment_list.html' # context_object_name = 'appointments' # paginate_by = 20 # # def get_queryset(self): # """Get filtered queryset based on search parameters.""" # queryset = AppointmentRequest.objects.filter(tenant=self.tenant) # # # Initialize the search form # self.form = AppointmentSearchForm(self.request.GET, tenant=self.tenant) # # # Apply filters if the form is valid # if self.form.is_valid(): # data = self.form.cleaned_data # # # Text search # if data.get('search'): # search_term = data['search'] # queryset = queryset.filter( # Q(patient__first_name__icontains=search_term) | # Q(patient__last_name__icontains=search_term) | # Q(provider__first_name__icontains=search_term) | # Q(provider__last_name__icontains=search_term) | # Q(reason__icontains=search_term) # ) # # # Status filter # if data.get('status'): # queryset = queryset.filter(status=data['status']) # # # Appointment type filter # if data.get('appointment_type'): # queryset = queryset.filter(appointment_type=data['appointment_type']) # # # Provider filter # if data.get('provider'): # queryset = queryset.filter(provider=data['provider']) # # # Department filter # if data.get('department'): # queryset = queryset.filter(department=data['department']) # # # Date range filters # if data.get('date_from'): # queryset = queryset.filter(scheduled_datetime__date__gte=data['date_from']) # # if data.get('date_to'): # queryset = queryset.filter(scheduled_datetime__date__lte=data['date_to']) # # # Default ordering # return queryset.select_related('patient', 'provider', 'department').order_by( # 'scheduled_datetime' # ) # # def get_context_data(self, **kwargs): # """Add search form and filters to context.""" # context = super().get_context_data(**kwargs) # context['form'] = self.form # # # Add filter summaries # context['active_filters'] = {} # if self.form.is_valid(): # data = self.form.cleaned_data # for field, value in data.items(): # if value: # if field == 'provider' and value: # context['active_filters'][field] = value.get_full_name() # elif field == 'department' and value: # context['active_filters'][field] = value.name # elif field in ['status', 'appointment_type'] and value: # # Get display value for choice fields # choices_dict = dict(getattr(AppointmentRequest, f"{field.upper()}_CHOICES", [])) # context['active_filters'][field] = choices_dict.get(value, value) # else: # context['active_filters'][field] = value # # # Add appointment counts by status # context['appointment_counts'] = { # 'total': AppointmentRequest.objects.filter(tenant=self.tenant).count(), # 'upcoming': AppointmentRequest.objects.filter( # tenant=self.tenant, # scheduled_datetime__gte=timezone.now(), # status__in=['SCHEDULED', 'CONFIRMED'] # ).count(), # 'today': AppointmentRequest.objects.filter( # tenant=self.tenant, # scheduled_datetime__date=timezone.now().date() # ).count(), # 'pending': AppointmentRequest.objects.filter( # tenant=self.tenant, # status='REQUESTED' # ).count(), # } # # return context # # # class AppointmentRequestDetailView(LoginRequiredMixin, TenantRequiredMixin, DetailView): # """Detail view for an appointment request.""" # model = AppointmentRequest # template_name = 'appointments/appointment_detail.html' # context_object_name = 'appointment' # # def get_queryset(self): # """Filter by tenant.""" # return AppointmentRequest.objects.filter( # tenant=self.tenant # ).select_related( # 'patient', 'provider', 'department', 'telemedicine_session', # 'template', 'requested_by', 'cancelled_by' # ) # # def get_context_data(self, **kwargs): # """Add additional context data.""" # context = super().get_context_data(**kwargs) # # # Add related queue entries # context['queue_entries'] = self.object.queue_entries.all() # # # Check if patient has other appointments # context['other_appointments'] = AppointmentRequest.objects.filter( # tenant=self.tenant, # patient=self.object.patient # ).exclude( # pk=self.object.pk # ).order_by('-scheduled_datetime')[:5] # # return context # # # class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, CreateView): # """Create view for a new appointment request.""" # model = AppointmentRequest # form_class = AppointmentRequestForm # template_name = 'appointments/appointment_form.html' # permission_required = 'appointments.add_appointmentrequest' # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_initial(self): # """Pre-populate form with initial values.""" # initial = super().get_initial() # # # Pre-populate patient if provided in query params # patient_id = self.request.GET.get('patient_id') # if patient_id: # try: # patient = PatientProfile.objects.get( # tenant=self.tenant, # patient_id=patient_id # ) # initial['patient'] = patient # except PatientProfile.DoesNotExist: # pass # # # Pre-populate provider if provided in query params # provider_id = self.request.GET.get('provider_id') # if provider_id: # try: # provider = User.objects.get( # tenant=self.tenant, # id=provider_id # ) # initial['provider'] = provider # except User.DoesNotExist: # pass # # # Pre-populate date/time if provided # date_str = self.request.GET.get('date') # time_str = self.request.GET.get('time') # if date_str: # try: # if time_str: # # Combine date and time # date_time = datetime.strptime( # f"{date_str} {time_str}", # '%Y-%m-%d %H:%M' # ) # else: # # Just date, use default time (9:00 AM) # date_time = datetime.strptime( # f"{date_str} 09:00", # '%Y-%m-%d %H:%M' # ) # # # Convert to timezone-aware datetime # date_time = timezone.make_aware(date_time) # initial['scheduled_datetime'] = date_time # except ValueError: # pass # # # Set default status # initial['status'] = 'SCHEDULED' # # # Set default appointment type # initial['appointment_type'] = 'IN_PERSON' # # # Set default duration # initial['duration_minutes'] = 30 # # return initial # # def form_valid(self, form): # """Process the valid form.""" # # Set tenant # form.instance.tenant = self.tenant # # # Set requested_by to current user if not set # if not form.instance.requested_by: # form.instance.requested_by = self.request.user # # # Save the form # response = super().form_valid(form) # # # Log the creation # messages.success( # self.request, # _("Appointment for {} with {} has been scheduled for {}.").format( # form.instance.patient.get_full_name(), # form.instance.provider.get_full_name(), # form.instance.scheduled_datetime.strftime('%Y-%m-%d %H:%M') # ) # ) # # return response # # def get_success_url(self): # """Redirect to appointment detail view.""" # return reverse('appointments:appointment_detail', kwargs={'pk': self.object.pk}) # # # class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, UpdateView): # """Update view for an appointment request.""" # model = AppointmentRequest # form_class = AppointmentRequestForm # template_name = 'appointments/appointment_form.html' # permission_required = 'appointments.change_appointmentrequest' # # def get_queryset(self): # """Filter by tenant.""" # return AppointmentRequest.objects.filter(tenant=self.tenant) # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Check if status changed # old_status = AppointmentRequest.objects.get(pk=self.object.pk).status # new_status = form.cleaned_data.get('status') # # # Save the form # response = super().form_valid(form) # # # Add appropriate message # if old_status != new_status: # messages.success( # self.request, # _("Appointment status updated from {} to {}.").format( # dict(AppointmentRequest.STATUS_CHOICES).get(old_status, old_status), # dict(AppointmentRequest.STATUS_CHOICES).get(new_status, new_status), # ) # ) # else: # messages.success( # self.request, # _("Appointment updated successfully.") # ) # # return response # # def get_success_url(self): # """Redirect to appointment detail view.""" # return reverse('appointments:appointment_detail', kwargs={'pk': self.object.pk}) # # # class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, DeleteView): # """Delete view for an appointment request.""" # model = AppointmentRequest # template_name = 'appointments/appointment_confirm_delete.html' # permission_required = 'appointments.delete_appointmentrequest' # success_url = reverse_lazy('appointments:appointment_list') # # def get_queryset(self): # """Filter by tenant.""" # return AppointmentRequest.objects.filter(tenant=self.tenant) # # def delete(self, request, *args, **kwargs): # """Override delete to add custom message.""" # self.object = self.get_object() # success_url = self.get_success_url() # # # Store patient name for message # patient_name = self.object.patient.get_full_name() if self.object.patient else "Unknown" # appointment_date = self.object.scheduled_datetime.strftime( # '%Y-%m-%d %H:%M') if self.object.scheduled_datetime else "Unscheduled" # # # Delete the object # self.object.delete() # # # Add success message # messages.success( # request, # _("Appointment for {} on {} has been deleted.").format( # patient_name, appointment_date # ) # ) # # return redirect(success_url) # # # # --- SlotAvailability Views --- # # class SlotAvailabilityListView(LoginRequiredMixin, TenantRequiredMixin, ListView): # """List view for provider availability slots.""" # model = SlotAvailability # template_name = 'appointments/slot_availability_list.html' # context_object_name = 'slots' # paginate_by = 20 # # def get_queryset(self): # """Filter by tenant and provider.""" # queryset = SlotAvailability.objects.filter(tenant=self.tenant) # # # Filter by provider if specified # provider_id = self.request.GET.get('provider_id') # if provider_id: # queryset = queryset.filter(provider_id=provider_id) # # # Filter by date range # date_from = self.request.GET.get('date_from') # if date_from: # try: # date_from = datetime.strptime(date_from, '%Y-%m-%d').date() # queryset = queryset.filter(start_time__date__gte=date_from) # except ValueError: # pass # # date_to = self.request.GET.get('date_to') # if date_to: # try: # date_to = datetime.strptime(date_to, '%Y-%m-%d').date() # queryset = queryset.filter(start_time__date__lte=date_to) # except ValueError: # pass # # # Default ordering # return queryset.select_related('provider').order_by('start_time') # # def get_context_data(self, **kwargs): # """Add filter context.""" # context = super().get_context_data(**kwargs) # # # Add provider filter # provider_id = self.request.GET.get('provider_id') # if provider_id: # try: # context['selected_provider'] = User.objects.get(id=provider_id) # except User.DoesNotExist: # pass # # # Add providers for filter dropdown # context['providers'] = User.objects.filter( # tenant=self.tenant, # role__in=['DOCTOR', 'NURSE', 'PHYSICIAN_ASSISTANT'] # ).order_by('first_name', 'last_name') # # # Add date filters # context['date_from'] = self.request.GET.get('date_from', '') # context['date_to'] = self.request.GET.get('date_to', '') # # return context # # # class SlotAvailabilityDetailView(LoginRequiredMixin, TenantRequiredMixin, DetailView): # """Detail view for a provider availability slot.""" # model = SlotAvailability # template_name = 'appointments/slot_availability_detail.html' # context_object_name = 'slot' # # def get_queryset(self): # """Filter by tenant.""" # return SlotAvailability.objects.filter(tenant=self.tenant).select_related('provider') # # def get_context_data(self, **kwargs): # """Add additional context data.""" # context = super().get_context_data(**kwargs) # # # Get appointments during this slot # context['appointments'] = self.object.get_appointments() # # return context # # # class SlotAvailabilityCreateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, CreateView): # """Create view for a new availability slot.""" # model = SlotAvailability # form_class = SlotAvailabilityForm # template_name = 'appointments/slot_availability_form.html' # permission_required = 'appointments.add_slotavailability' # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_initial(self): # """Pre-populate form with initial values.""" # initial = super().get_initial() # # # Pre-populate provider if provided in query params # provider_id = self.request.GET.get('provider_id') # if provider_id: # try: # provider = User.objects.get(tenant=self.tenant, id=provider_id) # initial['provider'] = provider # except User.DoesNotExist: # pass # # # Pre-populate date/time if provided # date_str = self.request.GET.get('date') # if date_str: # try: # # Parse date and set default times (9:00 AM - 5:00 PM) # date_obj = datetime.strptime(date_str, '%Y-%m-%d').date() # start_time = timezone.make_aware(datetime.combine(date_obj, datetime.min.time().replace(hour=9))) # end_time = timezone.make_aware(datetime.combine(date_obj, datetime.min.time().replace(hour=17))) # # initial['start_time'] = start_time # initial['end_time'] = end_time # except ValueError: # pass # # # Set default availability # initial['is_available'] = True # # return initial # # def form_valid(self, form): # """Process the valid form.""" # # Set tenant # form.instance.tenant = self.tenant # # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Availability slot for {} from {} to {} has been created.").format( # form.instance.provider.get_full_name(), # form.instance.start_time.strftime('%Y-%m-%d %H:%M'), # form.instance.end_time.strftime('%H:%M') # ) # ) # # return response # # def get_success_url(self): # """Redirect to slot list view.""" # return reverse('appointments:slot_availability_list') # # # class SlotAvailabilityUpdateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, UpdateView): # """Update view for an availability slot.""" # model = SlotAvailability # form_class = SlotAvailabilityForm # template_name = 'appointments/slot_availability_form.html' # permission_required = 'appointments.change_slotavailability' # # def get_queryset(self): # """Filter by tenant.""" # return SlotAvailability.objects.filter(tenant=self.tenant) # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Availability slot updated successfully.") # ) # # return response # # def get_success_url(self): # """Redirect to slot detail view.""" # return reverse('appointments:slot_availability_detail', kwargs={'pk': self.object.pk}) # # # class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, DeleteView): # """Delete view for an availability slot.""" # model = SlotAvailability # template_name = 'appointments/slot_availability_confirm_delete.html' # permission_required = 'appointments.delete_slotavailability' # success_url = reverse_lazy('appointments:slot_availability_list') # # def get_queryset(self): # """Filter by tenant.""" # return SlotAvailability.objects.filter(tenant=self.tenant) # # def delete(self, request, *args, **kwargs): # """Override delete to add custom message.""" # self.object = self.get_object() # success_url = self.get_success_url() # # # Store information for message # provider_name = self.object.provider.get_full_name() if self.object.provider else "Unknown" # slot_date = self.object.start_time.strftime('%Y-%m-%d') # # # Delete the object # self.object.delete() # # # Add success message # messages.success( # request, # _("Availability slot for {} on {} has been deleted.").format( # provider_name, slot_date # ) # ) # # return redirect(success_url) # # # # --- WaitingQueue Views --- # # class WaitingQueueListView(LoginRequiredMixin, TenantRequiredMixin, ListView): # """List view for waiting queues.""" # model = WaitingQueue # template_name = 'appointments/waiting_queue_list.html' # context_object_name = 'queues' # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter( # tenant=self.tenant # ).annotate( # waiting_count=Count('entries', filter=Q(entries__status='WAITING')) # ).select_related('department', 'provider').order_by('name') # # def get_context_data(self, **kwargs): # """Add department filter context.""" # context = super().get_context_data(**kwargs) # # # Add department filter # department_id = self.request.GET.get('department_id') # if department_id: # context['queues'] = context['queues'].filter(department_id=department_id) # try: # context['selected_department'] = department_id # except: # pass # # return context # # # class WaitingQueueDetailView(LoginRequiredMixin, TenantRequiredMixin, DetailView): # """Detail view for a waiting queue.""" # model = WaitingQueue # template_name = 'appointments/waiting_queue_detail.html' # context_object_name = 'queue' # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter(tenant=self.tenant).select_related('department', 'provider') # # def get_context_data(self, **kwargs): # """Add entries to context.""" # context = super().get_context_data(**kwargs) # # # Get waiting entries # context['waiting_entries'] = self.object.entries.filter( # status='WAITING' # ).select_related('patient').order_by('priority', 'added_time') # # # Get called/in-progress entries # context['active_entries'] = self.object.entries.filter( # status__in=['CALLED', 'IN_PROGRESS'] # ).select_related('patient').order_by('called_time') # # # Get completed entries from today # today = timezone.now().date() # context['completed_entries'] = self.object.entries.filter( # status='COMPLETED', # completed_time__date=today # ).select_related('patient').order_by('-completed_time') # # # Get statistics # context['stats'] = { # 'avg_wait_time': self.object.entries.filter( # status__in=['COMPLETED', 'CALLED', 'IN_PROGRESS'], # wait_time__isnull=False # ).aggregate(avg=Avg('wait_time'))['avg'] or 0, # # 'today_count': self.object.entries.filter( # added_time__date=today # ).count(), # # 'completed_count': self.object.entries.filter( # status='COMPLETED', # completed_time__date=today # ).count(), # } # # # Add form for adding a new entry # context['entry_form'] = QueueEntryForm( # user=self.request.user, # initial={'queue': self.object, 'priority': 'ROUTINE'} # ) # # return context # # # class WaitingQueueCreateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, CreateView): # """Create view for a new waiting queue.""" # model = WaitingQueue # form_class = WaitingQueueForm # template_name = 'appointments/waiting_queue_form.html' # permission_required = 'appointments.add_waitingqueue' # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Set tenant # form.instance.tenant = self.tenant # # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Waiting queue '{}' has been created.").format( # form.instance.name # ) # ) # # return response # # def get_success_url(self): # """Redirect to queue detail view.""" # return reverse('appointments:waiting_queue_detail', kwargs={'pk': self.object.pk}) # # # class WaitingQueueUpdateView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, UpdateView): # """Update view for a waiting queue.""" # model = WaitingQueue # form_class = WaitingQueueForm # template_name = 'appointments/waiting_queue_form.html' # permission_required = 'appointments.change_waitingqueue' # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter(tenant=self.tenant) # # def get_form_kwargs(self): # """Add the current user to form kwargs.""" # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # """Process the valid form.""" # # Save the form # response = super().form_valid(form) # # # Add success message # messages.success( # self.request, # _("Waiting queue '{}' has been updated.").format( # form.instance.name # ) # ) # # return response # # def get_success_url(self): # """Redirect to queue detail view.""" # return reverse('appointments:waiting_queue_detail', kwargs={'pk': self.object.pk}) # # # class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, TenantRequiredMixin, DeleteView): # """Delete view for a waiting queue.""" # model = WaitingQueue # template_name = 'appointments/waiting_queue_confirm_delete.html' # permission_required = 'appointments.delete_waitingqueue' # success_url = reverse_lazy('appointments:waiting_queue_list') # # def get_queryset(self): # """Filter by tenant.""" # return WaitingQueue.objects.filter(tenant=self.tenant) # # def delete(self, request, *args, **kwargs): # """Override delete to add custom message.""" # self.object = self.get_object() # success_url = self.get_success_url() # # # Store information for message # queue_name = self.object.name # # # Check if there are active entries # active_entries = self.object.entries.filter( # status__in=['WAITING', 'CALLED', 'IN_PROGRESS'] # ).exists() # # if active_entries: # messages.error( # request, # _("Cannot delete queue '{}' because it has active entries.").format(queue_name) # ) # return redirect('appointments:waiting_queue_detail', pk=self.object.pk) # # # Delete the object # self.object.delete() # # # Add success message # messages.success( # request, # _("Waiting queue '{}' has been deleted.").format(queue_name) # ) # # return redirect(success_url) # # # # --- Calendar Views --- # # @login_required # def calendar_view(request): # """View for the appointment calendar.""" # tenant = get_tenant_from_request(request) # if not tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # # # Get providers for filter # providers = User.objects.filter( # tenant=tenant, # role__in=['DOCTOR', 'NURSE', 'PHYSICIAN_ASSISTANT'] # ).order_by('first_name', 'last_name') # # # Get selected date (default to today) # date_str = request.GET.get('date') # if not date_str: # date_str = timezone.now().date().strftime('%Y-%m-%d') # # try: # selected_date = datetime.strptime(date_str, '%Y-%m-%d').date() # except ValueError: # selected_date = timezone.now().date() # # # Get selected provider (optional) # provider_id = request.GET.get('provider_id') # selected_provider = None # if provider_id: # try: # selected_provider = User.objects.get(id=provider_id, tenant=tenant) # except User.DoesNotExist: # pass # # # Calendar navigation # prev_date = selected_date - timedelta(days=1) # next_date = selected_date + timedelta(days=1) # # # Week navigation # start_of_week = selected_date - timedelta(days=selected_date.weekday()) # week_dates = [start_of_week + timedelta(days=i) for i in range(7)] # # context = { # 'providers': providers, # 'selected_date': selected_date, # 'selected_provider': selected_provider, # 'prev_date': prev_date, # 'next_date': next_date, # 'week_dates': week_dates, # } # # return render(request, 'appointments/calendar.html', context) # # # @login_required # def calendar_appointments(request): # """ # HTMX view for calendar appointments. # """ # tenant = get_tenant_from_request(request) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # date_str = request.GET.get('date') # provider_id = request.GET.get('provider_id') # # if not date_str: # date_str = timezone.now().date().strftime('%Y-%m-%d') # # try: # selected_date = datetime.strptime(date_str, '%Y-%m-%d').date() # except ValueError: # selected_date = timezone.now().date() # # # Get appointments for the selected date # queryset = AppointmentRequest.objects.filter( # tenant=tenant, # scheduled_datetime__date=selected_date # ) # # if provider_id: # queryset = queryset.filter(provider_id=provider_id) # # appointments = queryset.select_related('patient', 'provider').order_by('scheduled_datetime') # # # Get time slots (30-minute intervals from 8:00 to 18:00) # start_hour = 8 # end_hour = 18 # time_slots = [] # # for hour in range(start_hour, end_hour): # for minute in [0, 30]: # time_slots.append({ # 'time': f"{hour:02d}:{minute:02d}", # 'datetime': datetime.combine(selected_date, datetime.min.time().replace(hour=hour, minute=minute)) # }) # # # Organize appointments by time slot # for slot in time_slots: # slot_time = timezone.make_aware(slot['datetime']) # slot_end = slot_time + timedelta(minutes=30) # # # Find appointments that overlap with this slot # slot['appointments'] = [ # appt for appt in appointments if ( # appt.scheduled_datetime < slot_end and # appt.scheduled_datetime + timedelta(minutes=appt.duration_minutes) > slot_time # ) # ] # # return render(request, 'appointments/partials/calendar_appointments.html', { # 'appointments': appointments, # 'selected_date': selected_date, # 'time_slots': time_slots # }) # # # # # --- HTMX Queue Management Views --- # # @login_required # def queue_status(request, queue_id): # """ # HTMX view for queue status updates. # """ # tenant = get_tenant_from_request(request) # if not tenant: # return JsonResponse({'error': 'No tenant found'}, status=400) # # queue = get_object_or_404(WaitingQueue, queue_id=queue_id, tenant=tenant) # # # Get waiting entries # waiting_entries = queue.entries.filter( # status='WAITING' # ).select_related('patient').order_by('priority', 'added_time') # # # Get called/in-progress entries # active_entries = queue.entries.filter( # status__in=['CALLED', 'IN_PROGRESS'] # ).select_related('patient').order_by('called_time') # # return render(request, 'appointments/partials/queue_status.html', { # 'queue': queue, # 'waiting_entries': waiting_entries, # 'active_entries': active_entries # }) # # # # --- Action Views --- # # @login_required # def cancel_appointment(request, pk): # """ # View for cancelling an appointment. # """ # tenant = get_tenant_from_request(request) # if not tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # # appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant) # # # Check if the user has permission # if not request.user.has_perm('appointments.change_appointmentrequest'): # messages.error(request, _("You don't have permission to cancel appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # if request.method == 'POST': # reason = request.POST.get('reason', '') # # # Cancel the appointment # appointment.cancel(request.user, reason) # # messages.success(request, _("Appointment has been cancelled.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # return render(request, 'appointments/cancel_appointment.html', { # 'appointment': appointment # }) # # # def reschedule_appointment(request, pk): # """ # Reschedule an appointment. # """ # tenant = request.user.tenant # appointment = get_object_or_404(AppointmentRequest,pk=pk,tenant=tenant) # providers = User.objects.filter(tenant=tenant, role__in=['DOCTOR', 'NURSE', 'SPECIALIST']).order_by('last_name', 'first_name') # # new_date = request.POST.get('new_date') # new_time = request.POST.get('new_time') # # if new_date and new_time: # appointment.scheduled_date = new_date # appointment.scheduled_time = new_time # appointment.status = 'RESCHEDULED' # appointment.save() # # messages.success(request, 'Appointment has been rescheduled') # return redirect('appointments:appointment_detail', pk=appointment.pk) # # return render(request, 'appointments/reschedule_appointment.html', { # 'appointment': appointment, # 'providers': providers # }) # @login_required # def reschedule_appointment(request, pk): # """ # View for rescheduling an appointment. # """ # tenant = get_tenant_from_request(request) # if not tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # # appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant) # # # Check if the user has permission # if not request.user.has_perm('appointments.change_appointmentrequest'): # messages.error(request, _("You don't have permission to reschedule appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Check if the appointment can be rescheduled # if appointment.status in ['COMPLETED', 'CANCELLED', 'NO_SHOW']: # messages.error(request, _("This appointment cannot be rescheduled.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # if request.method == 'POST': # form = AppointmentRequestForm(request.POST, instance=appointment, user=request.user) # if form.is_valid(): # # Update the appointment # appointment = form.save() # # # Set status to RESCHEDULED if it was already scheduled # if appointment.status in ['SCHEDULED', 'CONFIRMED']: # appointment.status = 'RESCHEDULED' # appointment.save() # # messages.success(request, _("Appointment has been rescheduled.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # else: # form = AppointmentRequestForm(instance=appointment, user=request.user) # # return render(request, 'appointments/appointment_reschedule.html', { # 'form': form, # 'appointment': appointment # }) # # @login_required # def start_appointment(request, pk): # """ # View for starting an appointment. # """ # tenant = get_tenant_from_request(request) # if not tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # # appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant) # # # Check if the user has permission # if not request.user.has_perm('appointments.change_appointmentrequest'): # messages.error(request, _("You don't have permission to start appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Check if the appointment can be started # if appointment.status != 'CHECKED_IN': # messages.error(request, _("This appointment cannot be started.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Start the appointment # appointment.start_appointment() # # messages.success(request, _("Appointment has been started.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # @login_required # def complete_appointment(request, pk): # """ # View for completing an appointment. # """ # tenant = get_tenant_from_request(request) # if not tenant: # messages.error(request, _("No tenant found. Please contact an administrator.")) # return redirect('core:dashboard') # # appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant) # # # Check if the user has permission # if not request.user.has_perm('appointments.change_appointmentrequest'): # messages.error(request, _("You don't have permission to complete appointments.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Check if the appointment can be completed # if appointment.status != 'IN_PROGRESS': # messages.error(request, _("This appointment cannot be marked as completed.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # # # Complete the appointment # appointment.complete_appointment() # # messages.success(request, _("Appointment has been completed.")) # return redirect('appointments:appointment_detail', pk=appointment.pk) # class Department(models.Model): # """ # Hospital department model for organizational structure. # Represents different departments within a healthcare organization. # """ # # DEPARTMENT_TYPE_CHOICES = [ # ('CLINICAL', 'Clinical Department'), # ('ANCILLARY', 'Ancillary Services'), # ('SUPPORT', 'Support Services'), # ('ADMINISTRATIVE', 'Administrative'), # ('DIAGNOSTIC', 'Diagnostic Services'), # ('THERAPEUTIC', 'Therapeutic Services'), # ('EMERGENCY', 'Emergency Services'), # ('SURGICAL', 'Surgical Services'), # ('MEDICAL', 'Medical Services'), # ('NURSING', 'Nursing Services'), # ('PHARMACY', 'Pharmacy'), # ('LABORATORY', 'Laboratory'), # ('RADIOLOGY', 'Radiology'), # ('REHABILITATION', 'Rehabilitation'), # ('MENTAL_HEALTH', 'Mental Health'), # ('PEDIATRIC', 'Pediatric'), # ('OBSTETRIC', 'Obstetric'), # ('ONCOLOGY', 'Oncology'), # ('CARDIOLOGY', 'Cardiology'), # ('NEUROLOGY', 'Neurology'), # ('ORTHOPEDIC', 'Orthopedic'), # ('OTHER', 'Other'), # ] # # # Tenant relationship # tenant = models.ForeignKey( # Tenant, # on_delete=models.CASCADE, # related_name='core_departments', # help_text='Organization tenant' # ) # # # Department Information # department_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique department identifier' # ) # code = models.CharField( # max_length=20, # help_text='Department code (e.g., CARD, EMER, SURG)' # ) # name = models.CharField( # max_length=100, # help_text='Department name' # ) # description = models.TextField( # blank=True, # null=True, # help_text='Department description' # ) # # # Department Classification # department_type = models.CharField( # max_length=30, # choices=DEPARTMENT_TYPE_CHOICES, # help_text='Type of department' # ) # # # Organizational Structure # parent_department = models.ForeignKey( # 'self', # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='sub_departments', # help_text='Parent department (for hierarchical structure)' # ) # # # Management # department_head = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='headed_departments', # help_text='Department head/manager' # ) # # # Contact Information # phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Department phone number' # ) # extension = models.CharField( # max_length=10, # blank=True, # null=True, # help_text='Phone extension' # ) # email = models.EmailField( # blank=True, # null=True, # help_text='Department email' # ) # # # Location # building = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Building name or number' # ) # floor = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Floor number or name' # ) # wing = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Wing or section' # ) # room_numbers = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Room numbers (e.g., 101-110, 201A-205C)' # ) # # # Operational Information # is_active = models.BooleanField( # default=True, # help_text='Department is active' # ) # is_24_hour = models.BooleanField( # default=False, # help_text='Department operates 24 hours' # ) # operating_hours = models.JSONField( # default=dict, # blank=True, # help_text='Operating hours by day of week' # ) # # # Budget and Cost Center # cost_center_code = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Cost center code for financial tracking' # ) # budget_code = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Budget code' # ) # # # Staffing # authorized_positions = models.PositiveIntegerField( # default=0, # help_text='Number of authorized positions' # ) # current_staff_count = models.PositiveIntegerField( # default=0, # help_text='Current number of staff members' # ) # # # Quality and Compliance # accreditation_required = models.BooleanField( # default=False, # help_text='Department requires special accreditation' # ) # accreditation_body = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Accrediting body (e.g., Joint Commission, CAP)' # ) # last_inspection_date = models.DateField( # blank=True, # null=True, # help_text='Last inspection date' # ) # next_inspection_date = models.DateField( # blank=True, # null=True, # help_text='Next scheduled inspection date' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # created_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='created_departments', # help_text='User who created the department' # ) # # class Meta: # db_table = 'core_department' # verbose_name = 'Department' # verbose_name_plural = 'Departments' # ordering = ['name'] # indexes = [ # models.Index(fields=['tenant', 'department_type']), # models.Index(fields=['code']), # models.Index(fields=['is_active']), # models.Index(fields=['parent_department']), # ] # unique_together = ['tenant', 'code'] # # def __str__(self): # return f"{self.name} ({self.code})" # # @property # def full_name(self): # """Return full department name with parent if applicable""" # if self.parent_department: # return f"{self.parent_department.name} - {self.name}" # return self.name # # @property # def staffing_percentage(self): # """Calculate current staffing percentage""" # if self.authorized_positions > 0: # return (self.current_staff_count / self.authorized_positions) * 100 # return 0 # # def get_all_sub_departments(self): # """Get all sub-departments recursively""" # sub_departments = [] # for sub_dept in self.sub_departments.all(): # sub_departments.append(sub_dept) # sub_departments.extend(sub_dept.get_all_sub_departments()) # return sub_departments # from django.shortcuts import render, redirect, get_object_or_404 # from django.contrib.auth.decorators import login_required, permission_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.contrib import messages # from django.views.generic import ( # CreateView, UpdateView, DeleteView, DetailView, ListView, FormView # ) # from django.urls import reverse_lazy, reverse # from django.http import JsonResponse, HttpResponse # from django.utils import timezone # from django.db import transaction # from django.core.mail import send_mail # from django.conf import settings # from django.db.models import Q, Count # from viewflow.views import CreateProcessView, UpdateProcessView # from datetime import datetime, timedelta # import json # # from .models import Appointment, AppointmentConfirmation, Queue, TelemedicineSession # from .forms import ( # AppointmentSchedulingForm, AppointmentConfirmationForm, QueueManagementForm, # TelemedicineSetupForm, AppointmentRescheduleForm, AppointmentCancellationForm, # AppointmentCheckInForm, BulkAppointmentForm # ) # from .flows import AppointmentSchedulingFlow, AppointmentConfirmationFlow, QueueManagementFlow, TelemedicineSetupFlow # from patients.models import Patient # # # class AppointmentSchedulingView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for appointment scheduling workflow # """ # model = Appointment # form_class = AppointmentSchedulingForm # template_name = 'appointments/appointment_scheduling.html' # permission_required = 'appointments.can_schedule_appointments' # flow_class = AppointmentSchedulingFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # return kwargs # # def form_valid(self, form): # with transaction.atomic(): # # Create appointment # appointment = form.save(commit=False) # appointment.tenant = self.request.user.tenant # appointment.scheduled_by = self.request.user # appointment.status = 'scheduled' # appointment.save() # # # Start appointment scheduling workflow # process = self.flow_class.start.run( # appointment=appointment, # send_confirmation=form.cleaned_data.get('send_confirmation', True), # send_reminder=form.cleaned_data.get('send_reminder', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Appointment scheduled successfully for {appointment.patient.get_full_name()} ' # f'on {appointment.appointment_date} at {appointment.appointment_time}.' # ) # # return redirect('appointments:appointment_detail', pk=appointment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Schedule Appointment' # context['breadcrumbs'] = [ # {'name': 'Home', 'url': reverse('core:dashboard')}, # {'name': 'Appointments', 'url': reverse('appointments:appointment_list')}, # {'name': 'Schedule Appointment', 'url': ''} # ] # context['available_slots'] = self.get_available_slots() # return context # # def get_available_slots(self): # """Get available appointment slots for the next 7 days""" # slots = [] # today = timezone.now().date() # # for i in range(7): # date = today + timedelta(days=i) # day_slots = self.get_slots_for_date(date) # if day_slots: # slots.append({ # 'date': date, # 'slots': day_slots # }) # # return slots # # def get_slots_for_date(self, date): # """Get available slots for a specific date""" # # This would implement slot availability logic # return [] # # # class AppointmentConfirmationView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for appointment confirmation workflow # """ # model = AppointmentConfirmation # form_class = AppointmentConfirmationForm # template_name = 'appointments/appointment_confirmation.html' # permission_required = 'appointments.can_confirm_appointments' # flow_class = AppointmentConfirmationFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # return kwargs # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # # with transaction.atomic(): # # Create confirmation record # confirmation = form.save(commit=False) # confirmation.appointment = appointment # confirmation.confirmed_by = self.request.user # confirmation.confirmed_at = timezone.now() # confirmation.save() # # # Update appointment status # if form.cleaned_data.get('reschedule_requested'): # appointment.status = 'reschedule_requested' # appointment.save() # # # Start rescheduling process # messages.info( # self.request, # 'Patient requested reschedule. Please process the reschedule request.' # ) # return redirect('appointments:appointment_reschedule', pk=appointment.pk) # else: # appointment.status = 'confirmed' # appointment.save() # # # Start confirmation workflow # process = self.flow_class.start.run( # appointment=appointment, # confirmation=confirmation, # created_by=self.request.user # ) # # messages.success( # self.request, # f'Appointment confirmed for {appointment.patient.get_full_name()}.' # ) # # return redirect('appointments:appointment_detail', pk=appointment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # context['title'] = 'Confirm Appointment' # return context # # # class QueueManagementView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for queue management workflow # """ # model = Queue # form_class = QueueManagementForm # template_name = 'appointments/queue_management.html' # permission_required = 'appointments.can_manage_queue' # flow_class = QueueManagementFlow # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # # with transaction.atomic(): # # Create or update queue entry # queue_entry, created = Queue.objects.get_or_create( # appointment=appointment, # defaults={ # 'tenant': self.request.user.tenant, # 'patient': appointment.patient, # 'provider': appointment.provider, # 'department': appointment.department, # 'created_by': self.request.user # } # ) # # # Update queue entry with form data # for field in form.cleaned_data: # if hasattr(queue_entry, field): # setattr(queue_entry, field, form.cleaned_data[field]) # # queue_entry.save() # # # Start queue management workflow # process = self.flow_class.start.run( # queue_entry=queue_entry, # appointment=appointment, # notify_patient=form.cleaned_data.get('notify_patient', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Queue updated for {appointment.patient.get_full_name()}. ' # f'Position: {queue_entry.position}, Wait time: {queue_entry.estimated_wait_time} minutes.' # ) # # return redirect('appointments:queue_list') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # context['title'] = 'Manage Queue' # context['current_queue'] = self.get_current_queue() # return context # # def get_current_queue(self): # """Get current queue status""" # return Queue.objects.filter( # tenant=self.request.user.tenant, # status='waiting' # ).order_by('priority', 'created_at') # # # class TelemedicineSetupView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for telemedicine setup workflow # """ # model = TelemedicineSession # form_class = TelemedicineSetupForm # template_name = 'appointments/telemedicine_setup.html' # permission_required = 'appointments.can_setup_telemedicine' # flow_class = TelemedicineSetupFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # return kwargs # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # # with transaction.atomic(): # # Create telemedicine session # session = form.save(commit=False) # session.appointment = appointment # session.tenant = self.request.user.tenant # session.created_by = self.request.user # session.save() # # # Update appointment type # appointment.appointment_type = 'telemedicine' # appointment.save() # # # Start telemedicine setup workflow # process = self.flow_class.start.run( # session=session, # appointment=appointment, # test_connection=form.cleaned_data.get('test_connection', True), # send_instructions=form.cleaned_data.get('send_instructions', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Telemedicine session setup completed for {appointment.patient.get_full_name()}. ' # f'Meeting details have been sent to the patient.' # ) # # return redirect('appointments:appointment_detail', pk=appointment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['appointment_id']) # context['title'] = 'Setup Telemedicine' # return context # # # class AppointmentRescheduleView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for appointment rescheduling # """ # form_class = AppointmentRescheduleForm # template_name = 'appointments/appointment_reschedule.html' # permission_required = 'appointments.can_reschedule_appointments' # # def get_success_url(self): # return reverse('appointments:appointment_detail', kwargs={'pk': self.kwargs['pk']}) # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # kwargs['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # return kwargs # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['pk']) # # with transaction.atomic(): # # Store original appointment details # original_date = appointment.appointment_date # original_time = appointment.appointment_time # original_provider = appointment.provider # # # Update appointment # appointment.appointment_date = form.cleaned_data['new_date'] # appointment.appointment_time = form.cleaned_data['new_time'] # if form.cleaned_data.get('new_provider'): # appointment.provider = form.cleaned_data['new_provider'] # appointment.status = 'rescheduled' # appointment.save() # # # Log reschedule # self.log_reschedule(appointment, original_date, original_time, original_provider, form.cleaned_data) # # # Send notifications # if form.cleaned_data.get('notify_patient'): # self.send_reschedule_notification(appointment, form.cleaned_data) # # messages.success( # self.request, # f'Appointment rescheduled successfully. New date: {appointment.appointment_date}, ' # f'New time: {appointment.appointment_time}.' # ) # # return super().form_valid(form) # # def log_reschedule(self, appointment, original_date, original_time, original_provider, form_data): # """Log reschedule details""" # from core.models import AuditLogEntry # AuditLogEntry.objects.create( # tenant=appointment.tenant, # user=self.request.user, # event_type='APPOINTMENT_RESCHEDULE', # action='UPDATE', # object_type='Appointment', # object_id=str(appointment.id), # details={ # 'original_date': original_date.isoformat(), # 'original_time': original_time.isoformat(), # 'original_provider': original_provider.get_full_name() if original_provider else None, # 'new_date': form_data['new_date'].isoformat(), # 'new_time': form_data['new_time'].isoformat(), # 'new_provider': form_data['new_provider'].get_full_name() if form_data.get('new_provider') else None, # 'reason': form_data['reschedule_reason'], # 'notes': form_data.get('notes', '') # }, # ip_address=self.request.META.get('REMOTE_ADDR'), # user_agent=self.request.META.get('HTTP_USER_AGENT', '') # ) # # def send_reschedule_notification(self, appointment, form_data): # """Send reschedule notification to patient""" # if appointment.patient.email: # send_mail( # subject='Appointment Rescheduled', # message=f'Your appointment has been rescheduled to {appointment.appointment_date} at {appointment.appointment_time}.', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # context['title'] = 'Reschedule Appointment' # return context # # # class AppointmentCancellationView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for appointment cancellation # """ # form_class = AppointmentCancellationForm # template_name = 'appointments/appointment_cancellation.html' # permission_required = 'appointments.can_cancel_appointments' # # def get_success_url(self): # return reverse('appointments:appointment_list') # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['pk']) # # with transaction.atomic(): # # Update appointment status # appointment.status = 'cancelled' # appointment.cancelled_at = timezone.now() # appointment.cancelled_by = self.request.user # appointment.cancellation_reason = form.cleaned_data['cancellation_reason'] # appointment.cancellation_notes = form.cleaned_data.get('cancellation_notes', '') # appointment.save() # # # Handle follow-up actions # if form.cleaned_data.get('offer_reschedule'): # self.offer_reschedule(appointment) # # if form.cleaned_data.get('notify_patient'): # self.send_cancellation_notification(appointment, form.cleaned_data) # # if form.cleaned_data.get('refund_required'): # self.process_refund(appointment) # # messages.success( # self.request, # f'Appointment cancelled for {appointment.patient.get_full_name()}.' # ) # # return super().form_valid(form) # # def offer_reschedule(self, appointment): # """Offer reschedule options to patient""" # # This would implement reschedule offering logic # pass # # def send_cancellation_notification(self, appointment, form_data): # """Send cancellation notification to patient""" # if appointment.patient.email: # send_mail( # subject='Appointment Cancelled', # message=f'Your appointment on {appointment.appointment_date} has been cancelled. Reason: {form_data["cancellation_reason"]}', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def process_refund(self, appointment): # """Process refund if required""" # # This would implement refund processing logic # pass # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # context['title'] = 'Cancel Appointment' # return context # # # class AppointmentCheckInView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for appointment check-in # """ # form_class = AppointmentCheckInForm # template_name = 'appointments/appointment_checkin.html' # permission_required = 'appointments.can_checkin_patients' # # def get_success_url(self): # return reverse('appointments:appointment_detail', kwargs={'pk': self.kwargs['pk']}) # # def form_valid(self, form): # appointment = get_object_or_404(Appointment, pk=self.kwargs['pk']) # # with transaction.atomic(): # # Update appointment status # appointment.status = 'checked_in' # appointment.checked_in_at = timezone.now() # appointment.checked_in_by = self.request.user # appointment.arrival_time = form.cleaned_data['arrival_time'] # appointment.save() # # # Create queue entry if needed # if not hasattr(appointment, 'queue_entry'): # Queue.objects.create( # appointment=appointment, # tenant=appointment.tenant, # patient=appointment.patient, # provider=appointment.provider, # department=appointment.department, # queue_type='check_in', # priority='normal', # status='waiting', # created_by=self.request.user # ) # # # Handle pre-visit tasks # self.process_checkin_tasks(appointment, form.cleaned_data) # # messages.success( # self.request, # f'{appointment.patient.get_full_name()} checked in successfully.' # ) # # return super().form_valid(form) # # def process_checkin_tasks(self, appointment, form_data): # """Process check-in tasks""" # tasks = [] # # if form_data.get('insurance_verified'): # tasks.append('Insurance verified') # if form_data.get('copay_collected'): # tasks.append('Copay collected') # if form_data.get('forms_completed'): # tasks.append('Forms completed') # if form_data.get('vitals_required'): # tasks.append('Vitals required') # # # Log completed tasks # if tasks: # from core.models import AuditLogEntry # AuditLogEntry.objects.create( # tenant=appointment.tenant, # user=self.request.user, # event_type='APPOINTMENT_CHECKIN', # action='UPDATE', # object_type='Appointment', # object_id=str(appointment.id), # details={ # 'completed_tasks': tasks, # 'special_needs': form_data.get('special_needs', '') # }, # ip_address=self.request.META.get('REMOTE_ADDR'), # user_agent=self.request.META.get('HTTP_USER_AGENT', '') # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['appointment'] = get_object_or_404(Appointment, pk=self.kwargs['pk']) # context['title'] = 'Check In Patient' # return context # # # class BulkAppointmentView(LoginRequiredMixin, PermissionRequiredMixin, FormView): # """ # View for bulk appointment operations # """ # form_class = BulkAppointmentForm # template_name = 'appointments/bulk_appointment.html' # permission_required = 'appointments.can_bulk_manage_appointments' # # def get_success_url(self): # return reverse('appointments:appointment_list') # # def form_valid(self, form): # appointment_ids = form.cleaned_data['appointment_ids'].split(',') # appointments = Appointment.objects.filter( # id__in=appointment_ids, # tenant=self.request.user.tenant # ) # # action = form.cleaned_data['action'] # # with transaction.atomic(): # if action == 'confirm': # self.bulk_confirm(appointments, form.cleaned_data) # elif action == 'reschedule': # self.bulk_reschedule(appointments, form.cleaned_data) # elif action == 'cancel': # self.bulk_cancel(appointments, form.cleaned_data) # elif action == 'send_reminders': # self.bulk_send_reminders(appointments, form.cleaned_data) # # messages.success( # self.request, # f'Bulk operation "{action}" completed for {appointments.count()} appointments.' # ) # # return super().form_valid(form) # # def bulk_confirm(self, appointments, form_data): # """Bulk confirm appointments""" # for appointment in appointments: # appointment.status = 'confirmed' # appointment.save() # # if form_data.get('notify_patients'): # self.send_confirmation_notification(appointment) # # def bulk_reschedule(self, appointments, form_data): # """Bulk reschedule appointments""" # new_date = form_data.get('bulk_date') # if new_date: # for appointment in appointments: # appointment.appointment_date = new_date # appointment.status = 'rescheduled' # appointment.save() # # if form_data.get('notify_patients'): # self.send_reschedule_notification(appointment, form_data) # # def bulk_cancel(self, appointments, form_data): # """Bulk cancel appointments""" # for appointment in appointments: # appointment.status = 'cancelled' # appointment.cancelled_at = timezone.now() # appointment.cancelled_by = self.request.user # appointment.cancellation_reason = 'bulk_cancellation' # appointment.cancellation_notes = form_data.get('bulk_reason', '') # appointment.save() # # if form_data.get('notify_patients'): # self.send_cancellation_notification(appointment, form_data) # # def bulk_send_reminders(self, appointments, form_data): # """Bulk send reminders""" # for appointment in appointments: # if form_data.get('notify_patients'): # self.send_reminder_notification(appointment) # # def send_confirmation_notification(self, appointment): # """Send confirmation notification""" # if appointment.patient.email: # send_mail( # subject='Appointment Confirmed', # message=f'Your appointment on {appointment.appointment_date} at {appointment.appointment_time} has been confirmed.', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def send_reminder_notification(self, appointment): # """Send reminder notification""" # if appointment.patient.email: # send_mail( # subject='Appointment Reminder', # message=f'Reminder: You have an appointment on {appointment.appointment_date} at {appointment.appointment_time}.', # from_email=settings.DEFAULT_FROM_EMAIL, # recipient_list=[appointment.patient.email], # fail_silently=True # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Bulk Appointment Operations' # return context # # # class AppointmentListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # View for listing appointments # """ # model = Appointment # template_name = 'appointments/appointment_list.html' # context_object_name = 'appointments' # permission_required = 'appointments.view_appointment' # paginate_by = 25 # # def get_queryset(self): # queryset = Appointment.objects.filter(tenant=self.request.user.tenant) # # # Apply filters # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(provider__first_name__icontains=search) | # Q(provider__last_name__icontains=search) | # Q(reason__icontains=search) # ) # # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # date_from = self.request.GET.get('date_from') # if date_from: # queryset = queryset.filter(appointment_date__gte=date_from) # # date_to = self.request.GET.get('date_to') # if date_to: # queryset = queryset.filter(appointment_date__lte=date_to) # # provider = self.request.GET.get('provider') # if provider: # queryset = queryset.filter(provider_id=provider) # # return queryset.order_by('appointment_date', 'appointment_time') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Appointments' # context['providers'] = self.get_providers() # context['search'] = self.request.GET.get('search', '') # context['selected_status'] = self.request.GET.get('status', '') # context['selected_provider'] = self.request.GET.get('provider', '') # context['date_from'] = self.request.GET.get('date_from', '') # context['date_to'] = self.request.GET.get('date_to', '') # return context # # def get_providers(self): # """Get providers for filter""" # from django.contrib.auth.models import User # return User.objects.filter( # tenant=self.request.user.tenant, # groups__name__in=['Doctors', 'Nurses', 'Specialists'] # ).distinct() # # # class AppointmentDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # View for appointment details # """ # model = Appointment # template_name = 'appointments/appointment_detail.html' # context_object_name = 'appointment' # permission_required = 'appointments.view_appointment' # # def get_queryset(self): # return Appointment.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # appointment = self.object # context['title'] = f'Appointment - {appointment.patient.get_full_name()}' # context['confirmation'] = getattr(appointment, 'confirmation', None) # context['queue_entry'] = getattr(appointment, 'queue_entry', None) # context['telemedicine_session'] = getattr(appointment, 'telemedicine_session', None) # context['can_edit'] = self.request.user.has_perm('appointments.change_appointment') # context['can_cancel'] = self.request.user.has_perm('appointments.can_cancel_appointments') # context['can_reschedule'] = self.request.user.has_perm('appointments.can_reschedule_appointments') # return context # # # class QueueListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # View for listing queue entries # """ # model = Queue # template_name = 'appointments/queue_list.html' # context_object_name = 'queue_entries' # permission_required = 'appointments.view_queue' # # def get_queryset(self): # queryset = Queue.objects.filter(tenant=self.request.user.tenant) # # # Apply filters # status = self.request.GET.get('status', 'waiting') # if status: # queryset = queryset.filter(status=status) # # department = self.request.GET.get('department') # if department: # queryset = queryset.filter(department_id=department) # # return queryset.order_by('priority', 'created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Patient Queue' # context['departments'] = self.get_departments() # context['selected_status'] = self.request.GET.get('status', 'waiting') # context['selected_department'] = self.request.GET.get('department', '') # context['queue_stats'] = self.get_queue_stats() # return context # # def get_departments(self): # """Get departments for filter""" # from core.models import Department # return Department.objects.filter(tenant=self.request.user.tenant) # # def get_queue_stats(self): # """Get queue statistics""" # return { # 'total_waiting': Queue.objects.filter( # tenant=self.request.user.tenant, # status='waiting' # ).count(), # 'average_wait_time': 25, # Would be calculated # 'longest_wait': 45 # Would be calculated # } # # # # AJAX Views # @login_required # @permission_required('appointments.view_appointment') # def appointment_availability_ajax(request): # """AJAX view to check appointment availability""" # date = request.GET.get('date') # provider_id = request.GET.get('provider_id') # # if not date or not provider_id: # return JsonResponse({'success': False, 'message': 'Missing parameters'}) # # try: # from django.contrib.auth.models import User # provider = User.objects.get(id=provider_id, tenant=request.user.tenant) # appointment_date = datetime.strptime(date, '%Y-%m-%d').date() # # # Get existing appointments for the date # existing_appointments = Appointment.objects.filter( # provider=provider, # appointment_date=appointment_date, # status__in=['scheduled', 'confirmed', 'in_progress'] # ) # # # Generate available slots # available_slots = generate_available_slots(appointment_date, existing_appointments) # # return JsonResponse({ # 'success': True, # 'slots': available_slots # }) # except Exception as e: # return JsonResponse({'success': False, 'message': str(e)}) # # # @login_required # @permission_required('appointments.view_patient') # def patient_search_ajax(request): # """AJAX view for patient search""" # query = request.GET.get('q', '') # if len(query) < 2: # return JsonResponse({'patients': []}) # # patients = Patient.objects.filter( # tenant=request.user.tenant # ).filter( # Q(first_name__icontains=query) | # Q(last_name__icontains=query) | # Q(patient_id__icontains=query) | # Q(phone_number__icontains=query) # )[:10] # # patient_data = [ # { # 'id': patient.id, # 'name': patient.get_full_name(), # 'patient_id': patient.patient_id, # 'phone': patient.phone_number, # 'email': patient.email # } # for patient in patients # ] # # return JsonResponse({'patients': patient_data}) # # # @login_required # @permission_required('appointments.can_manage_queue') # def update_queue_position_ajax(request): # """AJAX view to update queue position""" # if request.method == 'POST': # try: # data = json.loads(request.body) # queue_id = data.get('queue_id') # new_position = data.get('new_position') # # queue_entry = Queue.objects.get( # id=queue_id, # tenant=request.user.tenant # ) # # queue_entry.position = new_position # queue_entry.save() # # return JsonResponse({ # 'success': True, # 'message': 'Queue position updated successfully.' # }) # except Queue.DoesNotExist: # return JsonResponse({ # 'success': False, # 'message': 'Queue entry not found.' # }) # except Exception as e: # return JsonResponse({ # 'success': False, # 'message': str(e) # }) # # return JsonResponse({'success': False, 'message': 'Invalid request.'}) # # # def generate_available_slots(date, existing_appointments): # """Generate available appointment slots for a date""" # slots = [] # start_time = datetime.strptime('09:00', '%H:%M').time() # end_time = datetime.strptime('17:00', '%H:%M').time() # slot_duration = 30 # minutes # # current_time = datetime.combine(date, start_time) # end_datetime = datetime.combine(date, end_time) # # while current_time < end_datetime: # slot_time = current_time.time() # # # Check if slot is available # is_available = True # for appointment in existing_appointments: # appointment_start = datetime.combine(date, appointment.appointment_time) # appointment_end = appointment_start + timedelta(minutes=appointment.duration) # # slot_start = current_time # slot_end = current_time + timedelta(minutes=slot_duration) # # if slot_start < appointment_end and slot_end > appointment_start: # is_available = False # break # # if is_available: # slots.append({ # 'time': slot_time.strftime('%H:%M'), # 'available': True # }) # # current_time += timedelta(minutes=slot_duration) # # return slots # # from django import forms # from django.core.exceptions import ValidationError # from django.utils import timezone # from django.contrib.auth.models import User # from crispy_forms.helper import FormHelper # from crispy_forms.layout import Layout, Fieldset, Submit, Row, Column, HTML, Div # from crispy_forms.bootstrap import FormActions # from datetime import datetime, timedelta # # from .models import Appointment, AppointmentConfirmation, Queue, TelemedicineSession # from patients.models import Patient # from core.models import Department # # # class AppointmentSchedulingForm(forms.ModelForm): # """ # Form for appointment scheduling # """ # patient_search = forms.CharField( # required=False, # widget=forms.TextInput(attrs={ # 'class': 'form-control', # 'placeholder': 'Search patient by name, ID, or phone...', # 'data-toggle': 'patient-search' # }) # ) # preferred_time_1 = forms.TimeField( # required=False, # widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}) # ) # preferred_time_2 = forms.TimeField( # required=False, # widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}) # ) # preferred_time_3 = forms.TimeField( # required=False, # widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}) # ) # special_instructions = forms.CharField( # required=False, # widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}) # ) # send_confirmation = forms.BooleanField( # required=False, # initial=True, # widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) # ) # send_reminder = forms.BooleanField( # required=False, # initial=True, # widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) # ) # # class Meta: # model = Appointment # fields = [ # 'patient', 'provider', 'department', 'appointment_type', # 'appointment_date', 'appointment_time', 'duration', # 'urgency', 'reason', 'notes', 'special_instructions', # 'send_confirmation', 'send_reminder' # ] # widgets = { # 'patient': forms.Select(attrs={'class': 'form-control'}), # 'provider': forms.Select(attrs={'class': 'form-control'}), # 'department': forms.Select(attrs={'class': 'form-control'}), # 'appointment_type': forms.Select(attrs={'class': 'form-control'}), # 'appointment_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}), # 'appointment_time': forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}), # 'duration': forms.NumberInput(attrs={'class': 'form-control'}), # 'urgency': forms.Select(attrs={'class': 'form-control'}), # 'reason': forms.TextInput(attrs={'class': 'form-control'}), # 'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}) # } # # def __init__(self, *args, **kwargs): # tenant = kwargs.pop('tenant', None) # super().__init__(*args, **kwargs) # # if tenant: # self.fields['patient'].queryset = Patient.objects.filter(tenant=tenant) # self.fields['provider'].queryset = User.objects.filter( # tenant=tenant, # groups__name__in=['Doctors', 'Nurses', 'Specialists'] # ) # self.fields['department'].queryset = Department.objects.filter(tenant=tenant) # # # Set minimum date to today # self.fields['appointment_date'].widget.attrs['min'] = timezone.now().date().isoformat() # # self.helper = FormHelper() # self.helper.layout = Layout( # Fieldset( # 'Patient Information', # 'patient_search', # 'patient' # ), # Fieldset( # 'Appointment Details', # Row( # Column('provider', css_class='form-group col-md-6 mb-0'), # Column('department', css_class='form-group col-md-6 mb-0'), # css_class='form-row' # ), # Row( # Column('appointment_type', css_class='form-group col-md-6 mb-0'), # Column('urgency', css_class='form-group col-md-6 mb-0'), # css_class='form-row' # ), # Row( # Column('appointment_date', css_class='form-group col-md-4 mb-0'), # Column('appointment_time', css_class='form-group col-md-4 mb-0'), # Column('duration', css_class='form-group col-md-4 mb-0'), # css_class='form-row' # ), # 'reason', # 'notes', # 'special_instructions' # ), # Fieldset( # 'Preferred Times (Alternative Options)', # Row( # Column('preferred_time_1', css_class='form-group col-md-4 mb-0'), # Column('preferred_time_2', css_class='form-group col-md-4 mb-0'), # Column('preferred_time_3', css_class='form-group col-md-4 mb-0'), # css_class='form-row' # ) # ), # Fieldset( # 'Notifications', # HTML('
Real-time bed occupancy and management dashboard
Monitor and manage bed occupancy across all wards in real-time