ifixkart-backend/Backend/app/services/SlotAllocationService.py

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