""" Signal handlers for appointments app. This module contains signal handlers for automating various tasks related to appointments, queue management, and notifications. """ from django.db.models.signals import post_save, pre_save, post_delete from django.dispatch import receiver, Signal from django.utils import timezone from django.core.mail import send_mail from django.conf import settings from .models import ( AppointmentRequest, QueueEntry, WaitingQueue, TelemedicineSession, WaitingList ) from core.utils import AuditLogger # Custom signals appointment_status_changed = Signal() queue_position_updated = Signal() appointment_reminder_due = Signal() telemedicine_session_started = Signal() @receiver(pre_save, sender=AppointmentRequest) def track_appointment_status_change(sender, instance, **kwargs): """ Track appointment status changes and trigger appropriate actions. This signal handler: - Detects status changes - Sends custom signal for status changes - Updates related records """ if instance.pk: try: old_instance = AppointmentRequest.objects.get(pk=instance.pk) # Check if status changed if old_instance.status != instance.status: # Send custom signal appointment_status_changed.send( sender=sender, instance=instance, old_status=old_instance.status, new_status=instance.status ) # Update timestamps based on status if instance.status == 'CHECKED_IN' and not instance.checked_in_at: instance.checked_in_at = timezone.now() elif instance.status == 'COMPLETED' and not instance.completed_at: instance.completed_at = timezone.now() elif instance.status == 'CANCELLED' and not instance.cancelled_at: instance.cancelled_at = timezone.now() except AppointmentRequest.DoesNotExist: pass @receiver(post_save, sender=AppointmentRequest) def handle_appointment_creation(sender, instance, created, **kwargs): """ Handle actions after appointment is created. This signal handler: - Sends confirmation notifications - Creates audit log entry - Updates related queue entries """ if created: # Log appointment creation AuditLogger.log_event( tenant=instance.tenant, event_type='CREATE', event_category='APPOINTMENT_MANAGEMENT', action='Appointment Created', description=f'New appointment created for {instance.patient} with {instance.provider}', content_object=instance ) # Send confirmation notification (if configured) if hasattr(settings, 'SEND_APPOINTMENT_CONFIRMATIONS') and settings.SEND_APPOINTMENT_CONFIRMATIONS: send_appointment_confirmation(instance) @receiver(appointment_status_changed) def handle_appointment_status_change(sender, instance, old_status, new_status, **kwargs): """ Handle appointment status changes. This signal handler: - Sends status change notifications - Updates queue entries - Logs status changes """ # Log status change AuditLogger.log_event( tenant=instance.tenant, event_type='UPDATE', event_category='APPOINTMENT_MANAGEMENT', action='Appointment Status Changed', description=f'Appointment status changed from {old_status} to {new_status}', content_object=instance ) # Update related queue entry if exists try: queue_entry = QueueEntry.objects.get(appointment=instance) # Map appointment status to queue entry status status_mapping = { 'CHECKED_IN': 'WAITING', 'IN_PROGRESS': 'IN_SERVICE', 'COMPLETED': 'COMPLETED', 'CANCELLED': 'CANCELLED', 'NO_SHOW': 'NO_SHOW' } if new_status in status_mapping: queue_entry.status = status_mapping[new_status] queue_entry.save() except QueueEntry.DoesNotExist: pass # Send notification based on status if new_status in ['CONFIRMED', 'RESCHEDULED', 'CANCELLED']: send_status_notification(instance, new_status) @receiver(post_save, sender=QueueEntry) def handle_queue_entry_update(sender, instance, created, **kwargs): """ Handle queue entry creation and updates. This signal handler: - Updates queue positions - Sends queue notifications - Logs queue changes """ if created: # Log queue entry creation AuditLogger.log_event( tenant=instance.queue.tenant, event_type='CREATE', event_category='QUEUE_MANAGEMENT', action='Patient Added to Queue', description=f'{instance.patient} added to {instance.queue.name}', content_object=instance ) # Update positions for all entries in the queue update_queue_positions(instance.queue) # Send queue position notification if hasattr(settings, 'SEND_QUEUE_NOTIFICATIONS') and settings.SEND_QUEUE_NOTIFICATIONS: send_queue_notification(instance) @receiver(pre_save, sender=QueueEntry) def track_queue_status_change(sender, instance, **kwargs): """ Track queue entry status changes. This signal handler: - Detects status changes - Updates timestamps - Sends custom signal """ if instance.pk: try: old_instance = QueueEntry.objects.get(pk=instance.pk) # Check if status changed if old_instance.status != instance.status: # Update timestamps based on status if instance.status == 'CALLED' and not instance.called_at: instance.called_at = timezone.now() elif instance.status == 'IN_SERVICE' and not instance.served_at: instance.served_at = timezone.now() elif instance.status == 'COMPLETED' and not instance.completed_at: instance.completed_at = timezone.now() # Send custom signal queue_position_updated.send( sender=sender, instance=instance, old_status=old_instance.status, new_status=instance.status ) except QueueEntry.DoesNotExist: pass @receiver(queue_position_updated) def handle_queue_position_change(sender, instance, old_status, new_status, **kwargs): """ Handle queue position/status changes. This signal handler: - Logs position changes - Sends notifications - Updates related appointments """ # Log status change AuditLogger.log_event( tenant=instance.queue.tenant, event_type='UPDATE', event_category='QUEUE_MANAGEMENT', action='Queue Status Changed', description=f'Queue entry status changed from {old_status} to {new_status}', content_object=instance ) # Update related appointment if exists if instance.appointment: status_mapping = { 'WAITING': 'CHECKED_IN', 'CALLED': 'CHECKED_IN', 'IN_SERVICE': 'IN_PROGRESS', 'COMPLETED': 'COMPLETED', 'NO_SHOW': 'NO_SHOW', 'CANCELLED': 'CANCELLED' } if new_status in status_mapping: instance.appointment.status = status_mapping[new_status] instance.appointment.save() @receiver(post_save, sender=TelemedicineSession) def handle_telemedicine_session(sender, instance, created, **kwargs): """ Handle telemedicine session creation and updates. This signal handler: - Sends session invitations - Logs session events - Updates appointment status """ if created: # Log session creation AuditLogger.log_event( tenant=instance.appointment.tenant, event_type='CREATE', event_category='TELEMEDICINE', action='Telemedicine Session Created', description=f'Telemedicine session created for appointment {instance.appointment.pk}', content_object=instance ) # Send session invitation if hasattr(settings, 'SEND_TELEMEDICINE_INVITATIONS') and settings.SEND_TELEMEDICINE_INVITATIONS: send_telemedicine_invitation(instance) # Check if session started if instance.status == 'IN_PROGRESS' and instance.actual_start: telemedicine_session_started.send( sender=sender, instance=instance ) @receiver(telemedicine_session_started) def handle_telemedicine_session_start(sender, instance, **kwargs): """ Handle telemedicine session start. This signal handler: - Updates appointment status - Logs session start - Sends notifications """ # Update appointment status if instance.appointment: instance.appointment.status = 'IN_PROGRESS' instance.appointment.actual_start_time = instance.actual_start instance.appointment.save() # Log session start AuditLogger.log_event( tenant=instance.appointment.tenant, event_type='UPDATE', event_category='TELEMEDICINE', action='Telemedicine Session Started', description=f'Telemedicine session started at {instance.actual_start}', content_object=instance ) @receiver(post_save, sender=WaitingList) def handle_waiting_list_update(sender, instance, created, **kwargs): """ Handle waiting list entry creation and updates. This signal handler: - Updates positions - Sends notifications - Logs changes """ if created: # Log waiting list entry creation AuditLogger.log_event( tenant=instance.tenant, event_type='CREATE', event_category='WAITING_LIST', action='Patient Added to Waiting List', description=f'{instance.patient} added to waiting list for {instance.specialty}', content_object=instance ) # Update position instance.update_position() # Send confirmation notification if hasattr(settings, 'SEND_WAITLIST_CONFIRMATIONS') and settings.SEND_WAITLIST_CONFIRMATIONS: send_waitlist_confirmation(instance) @receiver(post_delete, sender=AppointmentRequest) def handle_appointment_deletion(sender, instance, **kwargs): """ Handle appointment deletion. This signal handler: - Logs deletion - Cleans up related records """ # Log deletion AuditLogger.log_event( tenant=instance.tenant, event_type='DELETE', event_category='APPOINTMENT_MANAGEMENT', action='Appointment Deleted', description=f'Appointment for {instance.patient} with {instance.provider} was deleted', content_object=None ) # Helper functions for notifications def send_appointment_confirmation(appointment): """Send appointment confirmation notification.""" if appointment.patient.email: subject = f'Appointment Confirmation - {appointment.scheduled_datetime.strftime("%B %d, %Y")}' message = f""" Dear {appointment.patient.get_full_name()}, Your appointment has been confirmed: Date: {appointment.scheduled_datetime.strftime("%B %d, %Y")} Time: {appointment.scheduled_datetime.strftime("%I:%M %p")} Provider: {appointment.provider.get_full_name()} Type: {appointment.get_appointment_type_display()} Location: {appointment.location or 'To be determined'} Please arrive 15 minutes early for check-in. If you need to reschedule or cancel, please contact us at least 24 hours in advance. Thank you, {appointment.tenant.name if appointment.tenant else 'Hospital Management'} """ try: send_mail( subject, message, settings.DEFAULT_FROM_EMAIL, [appointment.patient.email], fail_silently=True ) except Exception as e: # Log error but don't raise print(f"Error sending appointment confirmation: {e}") def send_status_notification(appointment, status): """Send appointment status change notification.""" if appointment.patient.email: status_messages = { 'CONFIRMED': 'Your appointment has been confirmed.', 'RESCHEDULED': 'Your appointment has been rescheduled.', 'CANCELLED': 'Your appointment has been cancelled.' } subject = f'Appointment {status.title()} - {appointment.scheduled_datetime.strftime("%B %d, %Y")}' message = f""" Dear {appointment.patient.get_full_name()}, {status_messages.get(status, 'Your appointment status has been updated.')} Appointment Details: Date: {appointment.scheduled_datetime.strftime("%B %d, %Y")} Time: {appointment.scheduled_datetime.strftime("%I:%M %p")} Provider: {appointment.provider.get_full_name()} If you have any questions, please contact us. Thank you, {appointment.tenant.name if appointment.tenant else 'Hospital Management'} """ try: send_mail( subject, message, settings.DEFAULT_FROM_EMAIL, [appointment.patient.email], fail_silently=True ) except Exception as e: print(f"Error sending status notification: {e}") def send_queue_notification(queue_entry): """Send queue position notification.""" if queue_entry.patient.email: subject = f'Queue Position - {queue_entry.queue.name}' message = f""" Dear {queue_entry.patient.get_full_name()}, You have been added to the queue: {queue_entry.queue.name} Your position: {queue_entry.queue_position} Estimated wait time: {queue_entry.estimated_service_time or 'To be determined'} We will notify you when it's your turn. Thank you for your patience, {queue_entry.queue.tenant.name if queue_entry.queue.tenant else 'Hospital Management'} """ try: send_mail( subject, message, settings.DEFAULT_FROM_EMAIL, [queue_entry.patient.email], fail_silently=True ) except Exception as e: print(f"Error sending queue notification: {e}") def send_telemedicine_invitation(session): """Send telemedicine session invitation.""" if session.appointment.patient.email: subject = f'Telemedicine Appointment - {session.scheduled_start.strftime("%B %d, %Y")}' message = f""" Dear {session.appointment.patient.get_full_name()}, Your telemedicine appointment is scheduled: Date: {session.scheduled_start.strftime("%B %d, %Y")} Time: {session.scheduled_start.strftime("%I:%M %p")} Provider: {session.appointment.provider.get_full_name()} Platform: {session.get_platform_display()} Meeting Link: {session.meeting_url} Meeting ID: {session.meeting_id} Password: {session.meeting_password if session.meeting_password else 'Not required'} Please test your connection 10 minutes before the appointment. Thank you, {session.appointment.tenant.name if session.appointment.tenant else 'Hospital Management'} """ try: send_mail( subject, message, settings.DEFAULT_FROM_EMAIL, [session.appointment.patient.email], fail_silently=True ) except Exception as e: print(f"Error sending telemedicine invitation: {e}") def send_waitlist_confirmation(waiting_list_entry): """Send waiting list confirmation notification.""" if waiting_list_entry.contact_email: subject = f'Waiting List Confirmation - {waiting_list_entry.specialty}' message = f""" Dear {waiting_list_entry.patient.get_full_name()}, You have been added to the waiting list for {waiting_list_entry.specialty}. Priority: {waiting_list_entry.get_priority_display()} Position: {waiting_list_entry.position} Estimated wait time: {waiting_list_entry.estimated_wait_time or 'To be determined'} We will contact you when an appointment becomes available. Contact method: {waiting_list_entry.get_contact_method_display()} Thank you for your patience, {waiting_list_entry.tenant.name if waiting_list_entry.tenant else 'Hospital Management'} """ try: send_mail( subject, message, settings.DEFAULT_FROM_EMAIL, [waiting_list_entry.contact_email], fail_silently=True ) except Exception as e: print(f"Error sending waitlist confirmation: {e}") def update_queue_positions(queue): """Update positions for all entries in a queue.""" entries = QueueEntry.objects.filter( queue=queue, status='WAITING' ).order_by('priority_score', 'joined_at') for index, entry in enumerate(entries, start=1): if entry.queue_position != index: entry.queue_position = index entry.save(update_fields=['queue_position'])