05/07 Phase 3: initial codes
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
"""
|
||||
app/tenant/dashboard/routes.py — Tenant dashboard (placeholder for Phase 3 KPIs).
|
||||
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__)
|
||||
|
||||
|
||||
@@ -16,8 +22,72 @@ dashboard_bp = Blueprint("dashboard", __name__)
|
||||
@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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user