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