"""Calcolo slot disponibili per un servizio in una data. Logica: regole orario provider - prenotazioni esistenti - busy Google Calendar. Google Calendar è opzionale (fase 2). """ from datetime import datetime, date, timedelta, time from zoneinfo import ZoneInfo from sqlalchemy.orm import Session from app.models import ProviderService, Booking from app.schemas import TimeSlot TZ = ZoneInfo("Europe/Rome") def get_available_slots( db: Session, service_id: int, target_date: date, duration_min: int, ) -> list[TimeSlot]: """Restituisce tutti gli slot liberi per un servizio in una data.""" # 1. Trova tutti i provider che erogano questo servizio ps_list = ( db.query(ProviderService) .filter(ProviderService.service_id == service_id) .all() ) if not ps_list: return [] weekday = target_date.weekday() # 0=lunedì all_slots = [] for ps in ps_list: if not ps.provider.active: continue # 2. Filtra le regole di disponibilità per il giorno della settimana rules = ps.availability_rules or [] day_rules = [r for r in rules if r.get("weekday") == weekday] if not day_rules: continue # 3. Genera tutti gli slot possibili dalle regole raw_slots = [] for rule in day_rules: start_h, start_m = map(int, rule["start"].split(":")) end_h, end_m = map(int, rule["end"].split(":")) slot_start = datetime.combine(target_date, time(start_h, start_m), tzinfo=TZ) slot_end_limit = datetime.combine(target_date, time(end_h, end_m), tzinfo=TZ) while slot_start + timedelta(minutes=duration_min) <= slot_end_limit: raw_slots.append(slot_start) slot_start += timedelta(minutes=duration_min) # 4. Filtra via le prenotazioni esistenti (confirmed) day_start = datetime.combine(target_date, time(0, 0), tzinfo=TZ) day_end = day_start + timedelta(days=1) existing = ( db.query(Booking) .filter( Booking.provider_id == ps.provider_id, Booking.start_at >= day_start, Booking.start_at < day_end, Booking.status == "confirmed", ) .all() ) busy_ranges = [(b.start_at, b.end_at) for b in existing] for slot_start in raw_slots: slot_end = slot_start + timedelta(minutes=duration_min) conflict = any( slot_start < busy_end and slot_end > busy_start for busy_start, busy_end in busy_ranges ) if not conflict: all_slots.append( TimeSlot( start=slot_start.strftime("%H:%M"), end=slot_end.strftime("%H:%M"), provider_id=ps.provider_id, provider_name=ps.provider.name, ) ) # 5. Ordina per orario all_slots.sort(key=lambda s: s.start) # 6. Filtra slot nel passato se target_date è oggi now = datetime.now(TZ) if target_date == now.date(): all_slots = [s for s in all_slots if s.start > now.strftime("%H:%M")] return all_slots