Files
booking-service/app/routers/admin.py

259 lines
10 KiB
Python

"""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(),
}