94 lines
4.4 KiB
Python
94 lines
4.4 KiB
Python
from datetime import datetime, timedelta, date, time
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import select
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
from app.models.ServiceModel import ServiceCatalog, ServiceAppointment, ServiceJobAssignment, ServiceJob
|
|
from app.models.TechnicianModel import TechnicianProfile, TechnicianSkill, TechnicianWorkingHours, TechnicianLeave
|
|
from app.repositories.ServiceRepository import ServiceRepository
|
|
from app.repositories.TechnicianRepository import TechnicianRepository
|
|
|
|
class SlotAllocationService:
|
|
def __init__(self):
|
|
self.service_repo = ServiceRepository()
|
|
self.tech_repo = TechnicianRepository()
|
|
|
|
def get_available_slots(self, db: Session, target_date: date, service_id: str, duration_override: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
duration = duration_override or 60
|
|
if service_id and service_id != "OTHER_SERVICE":
|
|
service = self.service_repo.get_catalog_service(db, service_id)
|
|
if service and not duration_override:
|
|
duration = service.estimated_duration_minutes
|
|
|
|
weekday = target_date.weekday() # 0-6 (Mon-Sun)
|
|
|
|
# 1. Fetch technicians
|
|
from app.models.TechnicianModel import TechnicianProfile
|
|
technicians = db.execute(select(TechnicianProfile).where(TechnicianProfile.active == True)).scalars().all()
|
|
available_slots = []
|
|
|
|
for tech in technicians:
|
|
# 2. Check if technician is on leave on this date
|
|
day_start = datetime.combine(target_date, time(0, 0))
|
|
day_end = datetime.combine(target_date, time(23, 59, 59))
|
|
if self.tech_repo.is_technician_on_leave_during(db, tech.technician_id, day_start, day_end):
|
|
continue
|
|
|
|
# 3. Fetch technician's working hours for this weekday
|
|
stmt = select(TechnicianWorkingHours).where(
|
|
TechnicianWorkingHours.technician_id == tech.technician_id,
|
|
TechnicianWorkingHours.day_of_week == weekday
|
|
)
|
|
working_hours = db.execute(stmt).scalars().all()
|
|
if not working_hours:
|
|
continue
|
|
|
|
# 4. Fetch technician's existing appointments for this day
|
|
booked_stmt = (
|
|
select(ServiceAppointment)
|
|
.join(ServiceJobAssignment, ServiceAppointment.appointment_id == ServiceJobAssignment.appointment_id)
|
|
.where(
|
|
ServiceJobAssignment.technician_id == tech.technician_id,
|
|
ServiceJobAssignment.unassigned_at == None,
|
|
ServiceAppointment.scheduled_start < day_end,
|
|
ServiceAppointment.scheduled_end > day_start,
|
|
ServiceAppointment.status.notin_(["CANCELLED", "EXPIRED"])
|
|
)
|
|
)
|
|
booked_appointments = db.execute(booked_stmt).scalars().all()
|
|
|
|
# 5. Generate potential slot windows (30 min increments) inside working hours
|
|
for wh in working_hours:
|
|
try:
|
|
start_h, start_m = map(int, wh.start_time.split(":"))
|
|
end_h, end_m = map(int, wh.end_time.split(":"))
|
|
except ValueError:
|
|
continue
|
|
|
|
work_start = datetime.combine(target_date, time(start_h, start_m))
|
|
work_end = datetime.combine(target_date, time(end_h, end_m))
|
|
|
|
current_time = work_start
|
|
while current_time + timedelta(minutes=duration) <= work_end:
|
|
slot_start = current_time
|
|
slot_end = current_time + timedelta(minutes=duration)
|
|
|
|
# Check overlap with existing appointments
|
|
overlap = False
|
|
for appt in booked_appointments:
|
|
if slot_start < appt.scheduled_end and slot_end > appt.scheduled_start:
|
|
overlap = True
|
|
break
|
|
|
|
if not overlap:
|
|
available_slots.append({
|
|
"start_time": slot_start.isoformat(),
|
|
"end_time": slot_end.isoformat(),
|
|
"technician_id": tech.technician_id
|
|
})
|
|
|
|
current_time += timedelta(minutes=30)
|
|
|
|
# Sort and return unique start times
|
|
available_slots.sort(key=lambda s: s["start_time"])
|
|
return available_slots
|