"""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.ai_session import AISession 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 # --- Completed session minutes today (active session time computed client-side) --- duration_expr = func.extract( "epoch", Session.completed_at - 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.completed_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() ] # --- Escalation count (team/account-wide pending escalations) --- escalation_count = 0 if current_user.team_id: esc_scope = AISession.team_id == current_user.team_id elif current_user.account_id: esc_scope = AISession.account_id == current_user.account_id else: esc_scope = None if esc_scope is not None: esc_result = await db.execute( select(func.count()).where( and_( esc_scope, AISession.status == "requesting_escalation", ) ) ) escalation_count = esc_result.scalar() or 0 # --- 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, escalation_count=escalation_count, tree_counts=SidebarTreeCounts( total=tc.total, troubleshooting=tc.troubleshooting, procedural=tc.procedural, maintenance=tc.maintenance, ), active_sessions=active_sessions, recent_completions=recent_completions, )