""" Room Conflict Detection Service. This service prevents multiple therapists from booking the same room at overlapping times, ensuring physical space availability. """ from datetime import datetime, timedelta from django.db.models import Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ from .models import Appointment, Room class RoomConflictError(Exception): """Raised when a room conflict is detected.""" pass class RoomAvailabilityService: """ Service for checking and managing room availability. Prevents double-booking of physical spaces. """ @staticmethod def check_room_availability(room, scheduled_date, scheduled_time, duration, exclude_appointment=None): """ Check if a room is available at the specified time. Args: room: Room instance scheduled_date: Date of the appointment scheduled_time: Time of the appointment duration: Duration in minutes exclude_appointment: Appointment to exclude from check (for rescheduling) Returns: tuple: (is_available: bool, conflicting_appointments: QuerySet) Raises: RoomConflictError: If room is not available """ if not room: return True, Appointment.objects.none() # Calculate end time start_datetime = datetime.combine(scheduled_date, scheduled_time) end_datetime = start_datetime + timedelta(minutes=duration) # Find overlapping appointments in the same room conflicting_appointments = Appointment.objects.filter( room=room, scheduled_date=scheduled_date, status__in=[ Appointment.Status.BOOKED, Appointment.Status.CONFIRMED, Appointment.Status.ARRIVED, Appointment.Status.IN_PROGRESS, ] ) # Exclude current appointment if rescheduling if exclude_appointment: conflicting_appointments = conflicting_appointments.exclude(id=exclude_appointment.id) # Check for time overlaps conflicts = [] for apt in conflicting_appointments: apt_start = datetime.combine(apt.scheduled_date, apt.scheduled_time) apt_end = apt_start + timedelta(minutes=apt.duration) # Check if times overlap if (start_datetime < apt_end and end_datetime > apt_start): conflicts.append(apt) is_available = len(conflicts) == 0 return is_available, conflicts @staticmethod def validate_room_availability(room, scheduled_date, scheduled_time, duration, exclude_appointment=None): """ Validate room availability and raise exception if not available. Args: room: Room instance scheduled_date: Date of the appointment scheduled_time: Time of the appointment duration: Duration in minutes exclude_appointment: Appointment to exclude from check Raises: RoomConflictError: If room is not available """ is_available, conflicts = RoomAvailabilityService.check_room_availability( room, scheduled_date, scheduled_time, duration, exclude_appointment ) if not is_available: conflict_details = [] for apt in conflicts: conflict_details.append( f"{apt.scheduled_time.strftime('%H:%M')} - " f"{apt.provider.user.get_full_name()} - " f"{apt.patient}" ) raise RoomConflictError( f"Room {room.room_number} is not available at {scheduled_time.strftime('%H:%M')} " f"on {scheduled_date.strftime('%Y-%m-%d')}. " f"Conflicting appointments: {', '.join(conflict_details)}" ) @staticmethod def get_available_rooms(clinic, scheduled_date, scheduled_time, duration, tenant): """ Get list of available rooms for a clinic at the specified time. Args: clinic: Clinic instance scheduled_date: Date of the appointment scheduled_time: Time of the appointment duration: Duration in minutes tenant: Tenant instance Returns: QuerySet: Available rooms """ all_rooms = Room.objects.filter( clinic=clinic, tenant=tenant, is_available=True ) available_rooms = [] for room in all_rooms: is_available, _ = RoomAvailabilityService.check_room_availability( room, scheduled_date, scheduled_time, duration ) if is_available: available_rooms.append(room.id) return Room.objects.filter(id__in=available_rooms) @staticmethod def get_room_schedule(room, date): """ Get all appointments for a room on a specific date. Args: room: Room instance date: Date to check Returns: QuerySet: Appointments ordered by time """ return Appointment.objects.filter( room=room, scheduled_date=date, status__in=[ Appointment.Status.BOOKED, Appointment.Status.CONFIRMED, Appointment.Status.ARRIVED, Appointment.Status.IN_PROGRESS, Appointment.Status.COMPLETED, ] ).order_by('scheduled_time') @staticmethod def get_room_utilization(room, start_date, end_date): """ Calculate room utilization percentage for a date range. Args: room: Room instance start_date: Start date end_date: End date Returns: dict: Utilization statistics """ appointments = Appointment.objects.filter( room=room, scheduled_date__range=[start_date, end_date], status__in=[ Appointment.Status.COMPLETED, Appointment.Status.IN_PROGRESS, ] ) total_minutes = sum(apt.duration for apt in appointments) # Calculate available hours (assuming 8-hour workday) days = (end_date - start_date).days + 1 available_minutes = days * 8 * 60 # 8 hours per day utilization_percentage = (total_minutes / available_minutes * 100) if available_minutes > 0 else 0 return { 'total_appointments': appointments.count(), 'total_minutes': total_minutes, 'total_hours': total_minutes / 60, 'available_minutes': available_minutes, 'utilization_percentage': round(utilization_percentage, 2), } @staticmethod def find_next_available_slot(room, start_date, duration, max_days=7): """ Find the next available time slot for a room. Args: room: Room instance start_date: Date to start searching from duration: Required duration in minutes max_days: Maximum days to search ahead Returns: tuple: (date, time) of next available slot, or (None, None) if not found """ from datetime import time # Define working hours (8 AM to 6 PM) work_start = time(8, 0) work_end = time(18, 0) slot_duration = 30 # Check in 30-minute increments current_date = start_date end_search_date = start_date + timedelta(days=max_days) while current_date <= end_search_date: current_time = work_start while current_time < work_end: # Check if this slot is available is_available, _ = RoomAvailabilityService.check_room_availability( room, current_date, current_time, duration ) if is_available: return current_date, current_time # Move to next slot current_datetime = datetime.combine(current_date, current_time) next_datetime = current_datetime + timedelta(minutes=slot_duration) current_time = next_datetime.time() # Move to next day current_date += timedelta(days=1) return None, None @staticmethod def get_conflict_summary(room, scheduled_date): """ Get a summary of all appointments in a room for a specific date. Args: room: Room instance scheduled_date: Date to check Returns: list: List of appointment summaries """ appointments = RoomAvailabilityService.get_room_schedule(room, scheduled_date) summary = [] for apt in appointments: start_time = apt.scheduled_time end_time = ( datetime.combine(scheduled_date, start_time) + timedelta(minutes=apt.duration) ).time() summary.append({ 'appointment_number': apt.appointment_number, 'patient': str(apt.patient), 'provider': apt.provider.user.get_full_name(), 'start_time': start_time.strftime('%H:%M'), 'end_time': end_time.strftime('%H:%M'), 'duration': apt.duration, 'status': apt.get_status_display(), 'service_type': apt.service_type, }) return summary class MultiProviderRoomChecker: """ Specialized checker for rooms shared by multiple providers. Ensures no overlapping bookings across different therapists. """ @staticmethod def get_shared_rooms(clinic, tenant): """ Get rooms that are shared by multiple providers. Args: clinic: Clinic instance tenant: Tenant instance Returns: QuerySet: Rooms with multiple providers """ from django.db.models import Count # Get rooms that have appointments from multiple providers rooms_with_multiple_providers = Appointment.objects.filter( clinic=clinic, tenant=tenant, room__isnull=False ).values('room').annotate( provider_count=Count('provider', distinct=True) ).filter(provider_count__gt=1).values_list('room', flat=True) return Room.objects.filter(id__in=rooms_with_multiple_providers) @staticmethod def validate_multi_provider_booking(room, provider, scheduled_date, scheduled_time, duration, exclude_appointment=None): """ Validate booking for a room shared by multiple providers. Args: room: Room instance provider: Provider instance scheduled_date: Date of the appointment scheduled_time: Time of the appointment duration: Duration in minutes exclude_appointment: Appointment to exclude from check Raises: RoomConflictError: If room is not available """ # Use the standard room availability check # This works for both single and multi-provider rooms RoomAvailabilityService.validate_room_availability( room, scheduled_date, scheduled_time, duration, exclude_appointment ) @staticmethod def get_room_provider_schedule(room, scheduled_date): """ Get schedule showing which providers are using the room on a specific date. Args: room: Room instance scheduled_date: Date to check Returns: dict: Provider schedules """ appointments = RoomAvailabilityService.get_room_schedule(room, scheduled_date) provider_schedules = {} for apt in appointments: provider_name = apt.provider.user.get_full_name() if provider_name not in provider_schedules: provider_schedules[provider_name] = [] start_time = apt.scheduled_time end_time = ( datetime.combine(scheduled_date, start_time) + timedelta(minutes=apt.duration) ).time() provider_schedules[provider_name].append({ 'start_time': start_time.strftime('%H:%M'), 'end_time': end_time.strftime('%H:%M'), 'patient': str(apt.patient), 'service_type': apt.service_type, 'status': apt.get_status_display(), }) return provider_schedules