259 lines
10 KiB
Python
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(),
|
|
}
|