v1.3.0 — pannello admin completo, auth localStorage, Baileys WA, customers, calendario, paginazione, dashboard 7gg
This commit is contained in:
113
app/routers/public.py
Normal file
113
app/routers/public.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Endpoint pubblici — no auth. Usati dal widget di prenotazione."""
|
||||
from datetime import date, datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from app.database import get_db
|
||||
from app.models import Service, Booking, ProviderService
|
||||
from app.schemas import ServiceOut, BookingCreate, BookingOut, TimeSlot
|
||||
from app.services.availability import get_available_slots
|
||||
from app.services.notifications import notify_booking_confirmed, notify_operator
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["public"])
|
||||
TZ = ZoneInfo("Europe/Rome")
|
||||
|
||||
|
||||
@router.get("/services", response_model=list[ServiceOut])
|
||||
def list_services(db: Session = Depends(get_db)):
|
||||
"""Lista servizi attivi."""
|
||||
return (
|
||||
db.query(Service)
|
||||
.filter(Service.active == True)
|
||||
.order_by(Service.sort_order, Service.name)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
@router.get("/services/{service_id}/slots")
|
||||
def get_slots(service_id: int, date: date, db: Session = Depends(get_db)):
|
||||
"""Slot disponibili per un servizio in una data."""
|
||||
service = db.query(Service).filter(Service.id == service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(404, "Servizio non trovato")
|
||||
|
||||
slots = get_available_slots(db, service_id, date, service.duration_min)
|
||||
return {"service": service.name, "date": str(date), "duration_min": service.duration_min, "slots": slots}
|
||||
|
||||
|
||||
@router.post("/bookings", response_model=BookingOut)
|
||||
async def create_booking(data: BookingCreate, db: Session = Depends(get_db)):
|
||||
"""Crea una prenotazione. No login richiesto."""
|
||||
# Verifica servizio
|
||||
service = db.query(Service).filter(Service.id == data.service_id).first()
|
||||
if not service:
|
||||
raise HTTPException(404, "Servizio non trovato")
|
||||
|
||||
# Verifica che lo slot sia ancora libero
|
||||
slots = get_available_slots(db, data.service_id, data.start_at.date(), service.duration_min)
|
||||
slot_time = data.start_at.astimezone(TZ).strftime("%H:%M")
|
||||
available = [s for s in slots if s.start == slot_time and s.provider_id == data.provider_id]
|
||||
if not available:
|
||||
raise HTTPException(409, "Lo slot non è più disponibile")
|
||||
|
||||
# Crea prenotazione
|
||||
end_at = data.start_at + timedelta(minutes=service.duration_min)
|
||||
booking = Booking(
|
||||
service_id=data.service_id,
|
||||
provider_id=data.provider_id,
|
||||
customer_name=data.customer_name,
|
||||
customer_phone=data.customer_phone,
|
||||
customer_email=data.customer_email,
|
||||
start_at=data.start_at,
|
||||
end_at=end_at,
|
||||
notes=data.notes,
|
||||
)
|
||||
db.add(booking)
|
||||
db.commit()
|
||||
db.refresh(booking)
|
||||
|
||||
# Carica relazioni per la risposta
|
||||
booking = db.query(Booking).options(
|
||||
joinedload(Booking.service), joinedload(Booking.provider)
|
||||
).filter(Booking.id == booking.id).first()
|
||||
|
||||
# Notifiche async
|
||||
await notify_booking_confirmed(booking, service.name, booking.provider.name)
|
||||
if booking.provider.email:
|
||||
await notify_operator(booking, service.name, booking.provider.email)
|
||||
|
||||
return booking
|
||||
|
||||
|
||||
@router.get("/bookings/my")
|
||||
def my_bookings(phone: str, db: Session = Depends(get_db)):
|
||||
"""Le prenotazioni di un numero di telefono (per check 'ho già prenotato?')."""
|
||||
clean = phone.replace("+", "").replace(" ", "").replace("-", "")
|
||||
bookings = (
|
||||
db.query(Booking)
|
||||
.options(joinedload(Booking.service), joinedload(Booking.provider))
|
||||
.filter(
|
||||
Booking.customer_phone.contains(clean),
|
||||
Booking.status == "confirmed",
|
||||
Booking.start_at >= datetime.now(TZ),
|
||||
)
|
||||
.order_by(Booking.start_at)
|
||||
.all()
|
||||
)
|
||||
return bookings
|
||||
|
||||
|
||||
@router.delete("/bookings/{booking_id}")
|
||||
def cancel_booking(booking_id: int, phone: str, db: Session = Depends(get_db)):
|
||||
"""Cancella una prenotazione (verifica phone per sicurezza)."""
|
||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if not booking:
|
||||
raise HTTPException(404, "Prenotazione non trovata")
|
||||
|
||||
clean = phone.replace("+", "").replace(" ", "").replace("-", "")
|
||||
if clean not in booking.customer_phone:
|
||||
raise HTTPException(403, "Numero non corrispondente")
|
||||
|
||||
booking.status = "cancelled"
|
||||
db.commit()
|
||||
return {"ok": True, "message": "Prenotazione cancellata"}
|
||||
Reference in New Issue
Block a user