"""Calcolo slot disponibili — modello SALA UNICA. La sala dei servizi è unica: nessun overlap a livello globale, indipendentemente da provider o servizio. Le availability_rules sono per coppia (provider,service) e servono a definire le fasce orarie in cui un certo servizio può essere prenotato. Lo slot è confermato libero solo se nessun altro booking confirmed/completed lo intersecano. """ 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]: """Slot liberi per service_id in target_date, considerando la sala unica.""" ps_list = ( db.query(ProviderService) .filter(ProviderService.service_id == service_id) .all() ) if not ps_list: return [] weekday = target_date.weekday() # 0=lunedì # 1. Genero candidati: per ogni provider attivo, secondo le regole del giorno # Mappa: datetime → primo (provider_id, provider_name) che lo offre candidate_provider = {} for ps in ps_list: if not ps.provider or not ps.provider.active: continue rules = ps.availability_rules or [] for rule in [r for r in rules if r.get("weekday") == weekday]: sh, sm = map(int, rule["start"].split(":")) eh, em = map(int, rule["end"].split(":")) t = datetime.combine(target_date, time(sh, sm), tzinfo=TZ) limit = datetime.combine(target_date, time(eh, em), tzinfo=TZ) while t + timedelta(minutes=duration_min) <= limit: candidate_provider.setdefault(t, (ps.provider_id, ps.provider.name)) t += timedelta(minutes=duration_min) if not candidate_provider: return [] # 2. Tutte le prenotazioni del giorno con stato che blocca la sala (SALA UNICA) day_start = datetime.combine(target_date, time(0, 0), tzinfo=TZ) day_end = day_start + timedelta(days=1) busy_rows = ( db.query(Booking.start_at, Booking.end_at) .filter( Booking.start_at >= day_start, Booking.start_at < day_end, Booking.status.in_(["confirmed", "completed"]), ) .all() ) busy = [(s, e) for s, e in busy_rows] # 3. Tengo i candidati non in conflitto slots: list[TimeSlot] = [] for slot_start in sorted(candidate_provider.keys()): slot_end = slot_start + timedelta(minutes=duration_min) if any(slot_start < be and slot_end > bs for bs, be in busy): continue pid, pname = candidate_provider[slot_start] slots.append(TimeSlot( start=slot_start.strftime("%H:%M"), end=slot_end.strftime("%H:%M"), provider_id=pid, provider_name=pname, )) # 4. Se oggi, scarto orari già passati now = datetime.now(TZ) if target_date == now.date(): cur = now.strftime("%H:%M") slots = [s for s in slots if s.start > cur] return slots