178 lines
5.8 KiB
Python
178 lines
5.8 KiB
Python
"""Sidebar stats and activity feed endpoint."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from sqlalchemy import func, select, and_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.database import get_db
|
|
from app.api.deps import get_current_active_user
|
|
from app.models.session import Session
|
|
from app.models.tree import Tree
|
|
from app.models.user import User
|
|
from app.schemas.sidebar import (
|
|
SidebarActiveSession,
|
|
SidebarRecentSession,
|
|
SidebarStatsResponse,
|
|
SidebarTreeCounts,
|
|
)
|
|
|
|
router = APIRouter(prefix="/sessions", tags=["sidebar"])
|
|
|
|
|
|
@router.get("/sidebar-stats", response_model=SidebarStatsResponse)
|
|
async def get_sidebar_stats(
|
|
tz_offset: int = Query(..., description="Client UTC offset in minutes (e.g. -300 for EST)"),
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
) -> SidebarStatsResponse:
|
|
"""Get sidebar stats and activity feed for the current user.
|
|
|
|
Returns daily stats (resolved count, active count, time in session)
|
|
and lists of active + recently completed sessions.
|
|
"""
|
|
# Compute "today" start in user's timezone, then convert to UTC
|
|
now_utc = datetime.now(timezone.utc)
|
|
user_tz = timezone(timedelta(minutes=-tz_offset))
|
|
now_local = now_utc.astimezone(user_tz)
|
|
today_start_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
today_start_utc = today_start_local.astimezone(timezone.utc)
|
|
|
|
user_filter = Session.user_id == current_user.id
|
|
|
|
# --- Resolved today ---
|
|
resolved_result = await db.execute(
|
|
select(func.count()).where(
|
|
and_(
|
|
user_filter,
|
|
Session.completed_at >= today_start_utc,
|
|
Session.outcome == "resolved",
|
|
)
|
|
)
|
|
)
|
|
resolved_today = resolved_result.scalar() or 0
|
|
|
|
# --- Active count (all time, not just today) ---
|
|
active_result = await db.execute(
|
|
select(func.count()).where(
|
|
and_(
|
|
user_filter,
|
|
Session.started_at.isnot(None),
|
|
Session.completed_at.is_(None),
|
|
)
|
|
)
|
|
)
|
|
active_count = active_result.scalar() or 0
|
|
|
|
# --- Total session minutes today ---
|
|
duration_expr = func.extract(
|
|
"epoch",
|
|
func.coalesce(Session.completed_at, now_utc) - Session.started_at,
|
|
) / 60.0
|
|
duration_result = await db.execute(
|
|
select(func.coalesce(func.sum(duration_expr), 0)).where(
|
|
and_(
|
|
user_filter,
|
|
Session.started_at.isnot(None),
|
|
Session.started_at >= today_start_utc,
|
|
)
|
|
)
|
|
)
|
|
total_minutes = int(duration_result.scalar() or 0)
|
|
|
|
# --- Active sessions (max 5, most recent first) ---
|
|
active_sessions_result = await db.execute(
|
|
select(
|
|
Session.id,
|
|
Session.tree_id,
|
|
Session.started_at,
|
|
Session.ticket_number,
|
|
Session.psa_ticket_id,
|
|
Session.tree_snapshot["name"].as_string().label("tree_name"),
|
|
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
|
|
)
|
|
.where(
|
|
and_(
|
|
user_filter,
|
|
Session.started_at.isnot(None),
|
|
Session.completed_at.is_(None),
|
|
)
|
|
)
|
|
.order_by(Session.started_at.desc())
|
|
.limit(5)
|
|
)
|
|
active_sessions = [
|
|
SidebarActiveSession(
|
|
session_id=row.id,
|
|
tree_name=row.tree_name or "Unknown Flow",
|
|
tree_id=row.tree_id,
|
|
tree_type=row.tree_type or "troubleshooting",
|
|
started_at=row.started_at,
|
|
ticket_number=row.ticket_number,
|
|
psa_ticket_id=row.psa_ticket_id,
|
|
)
|
|
for row in active_sessions_result.all()
|
|
]
|
|
|
|
# --- Recent completions (max 3, completed today, most recent first) ---
|
|
recent_result = await db.execute(
|
|
select(
|
|
Session.id,
|
|
Session.tree_id,
|
|
Session.completed_at,
|
|
Session.tree_snapshot["name"].as_string().label("tree_name"),
|
|
Session.tree_snapshot["tree_type"].as_string().label("tree_type"),
|
|
)
|
|
.where(
|
|
and_(
|
|
user_filter,
|
|
Session.completed_at.isnot(None),
|
|
Session.completed_at >= today_start_utc,
|
|
)
|
|
)
|
|
.order_by(Session.completed_at.desc())
|
|
.limit(3)
|
|
)
|
|
recent_completions = [
|
|
SidebarRecentSession(
|
|
session_id=row.id,
|
|
tree_name=row.tree_name or "Unknown Flow",
|
|
tree_id=row.tree_id,
|
|
tree_type=row.tree_type or "troubleshooting",
|
|
completed_at=row.completed_at,
|
|
)
|
|
for row in recent_result.all()
|
|
]
|
|
|
|
# --- Tree counts (for All Flows sub-items) ---
|
|
tree_counts_result = await db.execute(
|
|
select(
|
|
func.count().label("total"),
|
|
func.count().filter(Tree.tree_type == "troubleshooting").label("troubleshooting"),
|
|
func.count().filter(Tree.tree_type == "procedural").label("procedural"),
|
|
func.count().filter(Tree.tree_type == "maintenance").label("maintenance"),
|
|
).where(
|
|
and_(
|
|
Tree.account_id == current_user.account_id,
|
|
Tree.is_active.is_(True),
|
|
Tree.deleted_at.is_(None),
|
|
)
|
|
)
|
|
)
|
|
tc = tree_counts_result.one()
|
|
|
|
return SidebarStatsResponse(
|
|
resolved_today=resolved_today,
|
|
active_count=active_count,
|
|
total_session_minutes_today=total_minutes,
|
|
tree_counts=SidebarTreeCounts(
|
|
total=tc.total,
|
|
troubleshooting=tc.troubleshooting,
|
|
procedural=tc.procedural,
|
|
maintenance=tc.maintenance,
|
|
),
|
|
active_sessions=active_sessions,
|
|
recent_completions=recent_completions,
|
|
)
|