162 lines
7.0 KiB
Python
162 lines
7.0 KiB
Python
"""Endpoint clienti e dashboard avanzata."""
|
|
from datetime import datetime, timedelta
|
|
from zoneinfo import ZoneInfo
|
|
from fastapi import APIRouter, Depends, HTTPException, Header
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text, func
|
|
from app.database import get_db
|
|
from app.models import Booking, Service, Provider
|
|
from app.routers.admin import verify_api_key
|
|
|
|
router = APIRouter(prefix="/api/admin", tags=["customers"])
|
|
TZ = ZoneInfo("Europe/Rome")
|
|
|
|
|
|
# === Customers ===
|
|
|
|
@router.get("/customers", dependencies=[Depends(verify_api_key)])
|
|
def list_customers(q: str = "", page: int = 1, per_page: int = 20, db: Session = Depends(get_db)):
|
|
"""Lista clienti con ricerca e paginazione."""
|
|
where = ""
|
|
params = {}
|
|
if q:
|
|
where = " WHERE name ILIKE :q OR phone ILIKE :q OR email ILIKE :q"
|
|
params["q"] = f"%{q}%"
|
|
total = db.execute(text("SELECT COUNT(*) FROM customers" + where), params).scalar()
|
|
sql = "SELECT id, name, phone, email, notes, total_visits, last_visit_at, first_visit_at, created_at FROM customers" + where
|
|
sql += " ORDER BY total_visits DESC, last_visit_at DESC NULLS LAST"
|
|
sql += f" LIMIT {per_page} OFFSET {(page-1)*per_page}"
|
|
rows = db.execute(text(sql), params).fetchall()
|
|
return {"items": [dict(r._mapping) for r in rows], "total": total, "page": page, "per_page": per_page, "pages": max(1,(total + per_page - 1) // per_page)}
|
|
|
|
|
|
@router.get("/customers/{customer_id}", dependencies=[Depends(verify_api_key)])
|
|
def customer_detail(customer_id: int, db: Session = Depends(get_db)):
|
|
"""Dettaglio cliente con storico prenotazioni."""
|
|
c = db.execute(text("SELECT * FROM customers WHERE id = :id"), {"id": customer_id}).fetchone()
|
|
if not c:
|
|
raise HTTPException(404)
|
|
customer = dict(c._mapping)
|
|
|
|
# Storico prenotazioni
|
|
bookings = db.execute(text("""
|
|
SELECT b.id, b.start_at, b.end_at, b.status, b.notes,
|
|
s.name as service, p.name as provider
|
|
FROM bookings b
|
|
LEFT JOIN services s ON s.id = b.service_id
|
|
LEFT JOIN providers p ON p.id = b.provider_id
|
|
WHERE b.customer_id = :cid
|
|
ORDER BY b.start_at DESC
|
|
"""), {"cid": customer_id}).fetchall()
|
|
customer["bookings"] = [dict(r._mapping) for r in bookings]
|
|
|
|
# Servizi più usati
|
|
top_svcs = db.execute(text("""
|
|
SELECT s.name, COUNT(*) as cnt
|
|
FROM bookings b JOIN services s ON s.id = b.service_id
|
|
WHERE b.customer_id = :cid
|
|
GROUP BY s.name ORDER BY cnt DESC LIMIT 5
|
|
"""), {"cid": customer_id}).fetchall()
|
|
customer["top_services"] = [{"name": r[0], "count": r[1]} for r in top_svcs]
|
|
|
|
return customer
|
|
|
|
|
|
@router.put("/customers/{customer_id}", dependencies=[Depends(verify_api_key)])
|
|
def update_customer(customer_id: int, data: dict, db: Session = Depends(get_db)):
|
|
"""Aggiorna note cliente."""
|
|
db.execute(text("UPDATE customers SET notes = :notes WHERE id = :id"),
|
|
{"notes": data.get("notes", ""), "id": customer_id})
|
|
db.commit()
|
|
return {"ok": True}
|
|
|
|
|
|
# === Dashboard avanzata ===
|
|
|
|
@router.get("/dashboard", dependencies=[Depends(verify_api_key)])
|
|
def dashboard_data(db: Session = Depends(get_db)):
|
|
"""Dati completi per la dashboard."""
|
|
now = datetime.now(TZ)
|
|
today = now.date()
|
|
today_start = datetime.combine(today, datetime.min.time(), tzinfo=TZ)
|
|
next7_end = today_start + timedelta(days=7)
|
|
|
|
# KPI base
|
|
today_count = db.execute(text(
|
|
"SELECT COUNT(*) FROM bookings WHERE start_at >= :s AND start_at < :e AND status = 'confirmed'"
|
|
), {"s": today_start, "e": today_start + timedelta(days=1)}).scalar()
|
|
|
|
week_count = db.execute(text(
|
|
"SELECT COUNT(*) FROM bookings WHERE start_at >= :s AND start_at < :e AND status IN ('confirmed','completed')"
|
|
), {"s": today_start, "e": next7_end}).scalar()
|
|
|
|
# Domani
|
|
tomorrow_start = today_start + timedelta(days=1)
|
|
tomorrow_count = db.execute(text(
|
|
"SELECT COUNT(*) FROM bookings WHERE start_at >= :s AND start_at < :e AND status = 'confirmed'"
|
|
), {"s": tomorrow_start, "e": tomorrow_start + timedelta(days=1)}).scalar()
|
|
|
|
total = db.execute(text("SELECT COUNT(*) FROM bookings WHERE status IN ('confirmed','completed')")).scalar()
|
|
no_shows = db.execute(text("SELECT COUNT(*) FROM bookings WHERE status = 'no_show'")).scalar()
|
|
total_customers = db.execute(text("SELECT COUNT(*) FROM customers")).scalar()
|
|
|
|
# Prenotazioni per giorno questa settimana (per chart)
|
|
week_daily = []
|
|
for d in range(7):
|
|
day = today_start + timedelta(days=d)
|
|
day_end = day + timedelta(days=1)
|
|
cnt = db.execute(text(
|
|
"SELECT COUNT(*) FROM bookings WHERE start_at >= :s AND start_at < :e AND status IN ('confirmed','completed')"
|
|
), {"s": day, "e": day_end}).scalar()
|
|
weekday_idx = (today.weekday() + d) % 7
|
|
week_daily.append({"day": ['Lun','Mar','Mer','Gio','Ven','Sab','Dom'][weekday_idx], "date": day.strftime("%Y-%m-%d"), "count": cnt, "is_today": d == 0})
|
|
|
|
# Top servizi (ultimi 30gg)
|
|
top_services = db.execute(text("""
|
|
SELECT s.name, COUNT(*) as cnt FROM bookings b
|
|
JOIN services s ON s.id = b.service_id
|
|
WHERE b.start_at >= :s AND b.status IN ('confirmed','completed')
|
|
GROUP BY s.name ORDER BY cnt DESC LIMIT 5
|
|
"""), {"s": today_start - timedelta(days=30)}).fetchall()
|
|
|
|
# Top operatori (ultimi 30gg)
|
|
top_providers = db.execute(text("""
|
|
SELECT p.name, COUNT(*) as cnt FROM bookings b
|
|
JOIN providers p ON p.id = b.provider_id
|
|
WHERE b.start_at >= :s AND b.status IN ('confirmed','completed')
|
|
GROUP BY p.name ORDER BY cnt DESC LIMIT 5
|
|
"""), {"s": today_start - timedelta(days=30)}).fetchall()
|
|
|
|
# Prossime prenotazioni (prossimi 7 giorni)
|
|
upcoming = db.execute(text("""
|
|
SELECT b.id, b.start_at, b.customer_name, s.name as service, p.name as provider, b.status
|
|
FROM bookings b
|
|
LEFT JOIN services s ON s.id = b.service_id
|
|
LEFT JOIN providers p ON p.id = b.provider_id
|
|
WHERE b.start_at >= :now AND b.start_at < :week_end AND b.status = 'confirmed'
|
|
ORDER BY b.start_at LIMIT 8
|
|
"""), {"now": now, "week_end": today_start + timedelta(days=7)}).fetchall()
|
|
|
|
# Attività recenti (ultime 10 prenotazioni create)
|
|
recent = db.execute(text("""
|
|
SELECT b.id, b.created_at, b.customer_name, s.name as service, b.status
|
|
FROM bookings b LEFT JOIN services s ON s.id = b.service_id
|
|
ORDER BY b.created_at DESC LIMIT 8
|
|
""")).fetchall()
|
|
|
|
return {
|
|
"kpi": {
|
|
"today": today_count,
|
|
"week": week_count,
|
|
"tomorrow": tomorrow_count,
|
|
"total": total,
|
|
"no_shows": no_shows,
|
|
"customers": total_customers,
|
|
},
|
|
"week_chart": week_daily,
|
|
"top_services": [{"name": r[0], "count": r[1]} for r in top_services],
|
|
"top_providers": [{"name": r[0], "count": r[1]} for r in top_providers],
|
|
"upcoming": [dict(r._mapping) for r in upcoming],
|
|
"recent": [dict(r._mapping) for r in recent],
|
|
}
|