380 lines
13 KiB
Python
380 lines
13 KiB
Python
"""
|
|
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
|