"""Endpoint admin — protetti con API key. Vista operatore.""" from datetime import date, datetime, timedelta from zoneinfo import ZoneInfo from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy.orm import Session, joinedload from sqlalchemy import text from app.database import get_db from app.models import Service, Provider, ProviderService, Booking from app.schemas import ServiceCreate, ServiceOut, ProviderCreate, ProviderOut, BookingOut, BookingUpdate from app.config import get_settings router = APIRouter(prefix="/api/admin", tags=["admin"]) settings = get_settings() TZ = ZoneInfo("Europe/Rome") DAYS = ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'] def verify_api_key(x_api_key: str = Header(...)): if x_api_key != settings.api_key: raise HTTPException(401, "API key non valida") return True # === Bookings === @router.get("/bookings", dependencies=[Depends(verify_api_key)]) def list_bookings( date: date | None = None, from_date: date | None = None, to_date: date | None = None, status: str | None = None, provider_id: int | None = None, page: int = 1, per_page: int = 20, db: Session = Depends(get_db), ): q = db.query(Booking).options(joinedload(Booking.service), joinedload(Booking.provider)) if date: day_start = datetime.combine(date, datetime.min.time(), tzinfo=TZ) q = q.filter(Booking.start_at >= day_start, Booking.start_at < day_start + timedelta(days=1)) elif from_date and to_date: q = q.filter( Booking.start_at >= datetime.combine(from_date, datetime.min.time(), tzinfo=TZ), Booking.start_at < datetime.combine(to_date + timedelta(days=1), datetime.min.time(), tzinfo=TZ), ) if status: q = q.filter(Booking.status == status) if provider_id: q = q.filter(Booking.provider_id == provider_id) total = q.count() items = q.order_by(Booking.start_at.desc()).offset((page-1)*per_page).limit(per_page).all() return {"items": [BookingOut.model_validate(b) for b in items], "total": total, "page": page, "per_page": per_page, "pages": (total + per_page - 1) // per_page} @router.put("/bookings/{booking_id}", response_model=BookingOut, dependencies=[Depends(verify_api_key)]) def update_booking(booking_id: int, data: BookingUpdate, db: Session = Depends(get_db)): booking = db.query(Booking).filter(Booking.id == booking_id).first() if not booking: raise HTTPException(404) if data.status: booking.status = data.status if data.notes is not None: booking.notes = data.notes db.commit() db.refresh(booking) return db.query(Booking).options( joinedload(Booking.service), joinedload(Booking.provider) ).filter(Booking.id == booking_id).first() # === Services CRUD === @router.get("/services", response_model=list[ServiceOut], dependencies=[Depends(verify_api_key)]) def admin_list_services(db: Session = Depends(get_db)): return db.query(Service).order_by(Service.sort_order, Service.name).all() @router.post("/services", response_model=ServiceOut, dependencies=[Depends(verify_api_key)]) def create_service(data: ServiceCreate, db: Session = Depends(get_db)): s = Service(**data.model_dump()) db.add(s) db.commit() db.refresh(s) return s @router.put("/services/{service_id}", response_model=ServiceOut, dependencies=[Depends(verify_api_key)]) def update_service(service_id: int, data: ServiceCreate, db: Session = Depends(get_db)): s = db.query(Service).filter(Service.id == service_id).first() if not s: raise HTTPException(404) for k, v in data.model_dump().items(): setattr(s, k, v) db.commit() db.refresh(s) return s @router.delete("/services/{service_id}", dependencies=[Depends(verify_api_key)]) def delete_service(service_id: int, db: Session = Depends(get_db)): s = db.query(Service).filter(Service.id == service_id).first() if not s: raise HTTPException(404) db.delete(s) db.commit() return {"ok": True} # === Providers CRUD === @router.get("/providers", response_model=list[ProviderOut], dependencies=[Depends(verify_api_key)]) def admin_list_providers(db: Session = Depends(get_db)): return db.query(Provider).filter(Provider.active == True).all() @router.post("/providers", dependencies=[Depends(verify_api_key)]) def create_provider(data: ProviderCreate, db: Session = Depends(get_db)): p = Provider(**data.model_dump()) db.add(p) db.commit() db.refresh(p) return {"id": p.id, "name": p.name} # === Provider detail with service assignments === @router.get("/providers/detail", dependencies=[Depends(verify_api_key)]) def providers_detail(db: Session = Depends(get_db)): """Tutti i provider con i loro servizi assegnati e regole orarie.""" providers = db.query(Provider).filter(Provider.active == True).order_by(Provider.id).all() result = [] for p in providers: assignments = db.query(ProviderService).filter(ProviderService.provider_id == p.id).all() services = [] for a in assignments: svc = db.query(Service).filter(Service.id == a.service_id).first() if svc: services.append({ "assignment_id": a.id, "service_id": svc.id, "service_name": svc.name, "service_slug": svc.slug, "duration_min": svc.duration_min, "category": svc.category, "availability_rules": a.availability_rules or [], }) result.append({ "id": p.id, "name": p.name, "email": p.email, "phone": p.phone, "services": services, }) return result # === Provider-Service assignment CRUD === @router.post("/providers/{provider_id}/services/{service_id}", dependencies=[Depends(verify_api_key)]) def assign_service(provider_id: int, service_id: int, rules: list[dict], db: Session = Depends(get_db)): """Assegna servizio a operatore con regole orarie. Body: [{"weekday":0,"start":"09:00","end":"13:00"}, ...] """ existing = db.query(ProviderService).filter( ProviderService.provider_id == provider_id, ProviderService.service_id == service_id ).first() if existing: existing.availability_rules = rules else: ps = ProviderService(provider_id=provider_id, service_id=service_id, availability_rules=rules) db.add(ps) db.commit() return {"ok": True} @router.put("/providers/{provider_id}/services/{service_id}", dependencies=[Depends(verify_api_key)]) def update_assignment(provider_id: int, service_id: int, rules: list[dict], db: Session = Depends(get_db)): """Aggiorna le regole orarie di un'assegnazione.""" ps = db.query(ProviderService).filter( ProviderService.provider_id == provider_id, ProviderService.service_id == service_id ).first() if not ps: raise HTTPException(404, "Assegnazione non trovata") ps.availability_rules = rules db.commit() return {"ok": True} @router.delete("/providers/{provider_id}/services/{service_id}", dependencies=[Depends(verify_api_key)]) def remove_assignment(provider_id: int, service_id: int, db: Session = Depends(get_db)): """Rimuovi assegnazione servizio da operatore.""" ps = db.query(ProviderService).filter( ProviderService.provider_id == provider_id, ProviderService.service_id == service_id ).first() if not ps: raise HTTPException(404) db.delete(ps) db.commit() return {"ok": True} # === Calendar view === @router.get("/calendar", dependencies=[Depends(verify_api_key)]) def calendar_view( from_date: date, to_date: date, provider_id: int | None = None, db: Session = Depends(get_db), ): """Prenotazioni per vista calendario, raggruppate per provider e giorno.""" q = db.query(Booking).options(joinedload(Booking.service), joinedload(Booking.provider)).filter( Booking.start_at >= datetime.combine(from_date, datetime.min.time(), tzinfo=TZ), Booking.start_at < datetime.combine(to_date + timedelta(days=1), datetime.min.time(), tzinfo=TZ), Booking.status.in_(["confirmed", "completed"]), ) if provider_id: q = q.filter(Booking.provider_id == provider_id) bookings = q.order_by(Booking.start_at).all() # Group by date then provider cal = {} for b in bookings: day = b.start_at.astimezone(TZ).strftime("%Y-%m-%d") if day not in cal: cal[day] = [] cal[day].append({ "id": b.id, "start": b.start_at.astimezone(TZ).strftime("%H:%M"), "end": b.end_at.astimezone(TZ).strftime("%H:%M"), "service": b.service.name if b.service else "—", "service_slug": b.service.slug if b.service else "", "category": b.service.category if b.service else "", "provider_id": b.provider_id, "provider": b.provider.name if b.provider else "—", "customer": b.customer_name, "phone": b.customer_phone, "status": b.status, "notes": b.notes, }) return cal # === Stats === @router.get("/stats", dependencies=[Depends(verify_api_key)]) def booking_stats(db: Session = Depends(get_db)): today_start = datetime.combine(datetime.now(TZ).date(), datetime.min.time(), tzinfo=TZ) today_end = today_start + timedelta(days=1) week_start = today_start - timedelta(days=today_start.weekday()) week_end = week_start + timedelta(days=7) return { "today": db.query(Booking).filter(Booking.start_at >= today_start, Booking.start_at < today_end, Booking.status == "confirmed").count(), "this_week": db.query(Booking).filter(Booking.start_at >= week_start, Booking.start_at < week_end, Booking.status == "confirmed").count(), "total_confirmed": db.query(Booking).filter(Booking.status == "confirmed").count(), "total_no_shows": db.query(Booking).filter(Booking.status == "no_show").count(), }