94 lines
3.5 KiB
Python
94 lines
3.5 KiB
Python
"""
|
|
app/tenant/dashboard/routes.py
|
|
Dashboard with Phase 3 KPI widgets scoped to active location.
|
|
"""
|
|
import logging
|
|
from datetime import datetime, timezone, timedelta, date
|
|
from decimal import Decimal
|
|
from flask import Blueprint, render_template, g
|
|
from flask_login import login_required
|
|
from app.extensions import db
|
|
from app.models.salon import (
|
|
Appointment, Transaction, Staff, CheckinQueue, StaffClocking,
|
|
)
|
|
from app.decorators import require_role
|
|
from sqlalchemy import func
|
|
|
|
logger = logging.getLogger(__name__)
|
|
dashboard_bp = Blueprint("dashboard", __name__)
|
|
|
|
|
|
@dashboard_bp.route("/")
|
|
@login_required
|
|
@require_role("tenant_admin", "tenant_manager")
|
|
def index():
|
|
today = date.today()
|
|
day_start = datetime.combine(today, datetime.min.time()).replace(tzinfo=timezone.utc)
|
|
day_end = day_start + timedelta(days=1)
|
|
location_id = g.location.id if g.location else None
|
|
|
|
# ── Today's revenue ──────────────────────────────────────
|
|
revenue_row = db.session.query(
|
|
func.sum(Transaction.total)
|
|
).filter_by(
|
|
tenant_id=g.tenant.id, location_id=location_id
|
|
).filter(
|
|
Transaction.created_at >= day_start,
|
|
Transaction.created_at < day_end,
|
|
Transaction.voided_at.is_(None),
|
|
).scalar()
|
|
today_revenue = float(revenue_row or 0)
|
|
|
|
# ── Today's appointments ─────────────────────────────────
|
|
appt_counts = dict(
|
|
db.session.query(Appointment.status, func.count(Appointment.id))
|
|
.filter_by(tenant_id=g.tenant.id, location_id=location_id)
|
|
.filter(
|
|
Appointment.start_time >= day_start,
|
|
Appointment.start_time < day_end,
|
|
)
|
|
.group_by(Appointment.status)
|
|
.all()
|
|
)
|
|
total_appts = sum(appt_counts.values())
|
|
completed_appts = appt_counts.get("completed", 0)
|
|
pending_appts = appt_counts.get("pending", 0) + appt_counts.get("confirmed", 0)
|
|
|
|
# ── Staff on shift ────────────────────────────────────────
|
|
staff_on_shift = StaffClocking.query.filter_by(
|
|
tenant_id=g.tenant.id, location_id=location_id
|
|
).filter(StaffClocking.clocked_out_at.is_(None)).count()
|
|
|
|
# ── Check-in queue ─────────────────────────────────────────
|
|
queue_count = CheckinQueue.query.filter_by(
|
|
tenant_id=g.tenant.id, location_id=location_id, status="waiting"
|
|
).count()
|
|
|
|
# ── Upcoming appointments today (next 3) ──────────────────
|
|
now = datetime.now(timezone.utc)
|
|
upcoming = Appointment.query.filter_by(
|
|
tenant_id=g.tenant.id, location_id=location_id
|
|
).filter(
|
|
Appointment.start_time >= now,
|
|
Appointment.start_time < day_end,
|
|
Appointment.status.in_(["pending", "confirmed"]),
|
|
).order_by(Appointment.start_time).limit(5).all()
|
|
|
|
kpis = {
|
|
"today_revenue": today_revenue,
|
|
"total_appointments": total_appts,
|
|
"completed_appointments": completed_appts,
|
|
"pending_appointments": pending_appts,
|
|
"staff_on_shift": staff_on_shift,
|
|
"queue_count": queue_count,
|
|
}
|
|
|
|
return render_template(
|
|
"tenant/dashboard/index.html",
|
|
tenant=g.tenant,
|
|
location=g.location,
|
|
kpis=kpis,
|
|
upcoming_appointments=upcoming,
|
|
today=today,
|
|
)
|